环境光照
物理材质不仅响应场景灯光,也接收周围环境的光:光亮表面映出周围景象,哑光表面受各方向入射光染色,玻璃表面透出周围环境。这些都来自环境贴图,即天空、房间或户外地点等周围环境的图像。
PlayCanvas 可以从场景或材质本身取得环境贴图。本页解释环境为材质提供什么、来源有哪些、何时使用哪个来源,以及哪些设置影响结果。也介绍平面反射,让地板、镜面和水面精确反射周围场景。
环境提供的内容
**反射。**表面映出环境。光滑表面呈现清晰图像,粗糙表面呈现柔和模糊图像,金属会用自身颜色给反射染色。材质的 Glossiness、Metalness 和 Reflectivity 控制反射强度和清晰度。
**环境光。**从各方向到达表面、而非来自特定灯光实体的光。有环境贴图时,环境光随方向变化:上方天空更亮、更蓝,下方地面更暗、更暖。这使对象自然融入周围环境。没有环境贴图时,环境光就是场景设置中统一的 Ambient Color(环境色)。
**折射。**启用 Refraction 但未启用 Dynamic Refractions 的材质,会透过表面显示按折射率弯曲后的环境。动态折射则显示表面后方的实际对象,不使用环境贴图。
**背景。**在所有内容后方绘制的天空。只有场景环境会绘制为背景;材质环境贴图只影响其所属材质。
场景环境
场景环境在 Scene Settings → Rendering(场景设置 → 渲染)的 Skybox(天空盒)中设置,代码中对应 app.scene.skybox 和 app.scene.envAtlas。它既作为背景绘制,也照亮所有未自带环境的材质。
要为材质提供光照,天空盒立方体贴图必须预滤波:立方体贴图资源的 Prefilter 按钮会预先计算粗糙反射和环境光所需的模糊版本。未预滤波的立方体贴图仅绘制为背景。制作和预滤波环境贴图参见基于图像的光照。
Scene Settings → Rendering 中以下设置仅适用于场景环境:
| 设置 | 作用 |
|---|---|
| Intensity(强度) | 同时提亮或压暗天空及其照亮的所有内容,包括反射、环境光和折射。可用于匹配灯光曝光。 |
| Rotation(旋转) | 围绕场景旋转环境,反射和环境光随之旋转。 |
| Mip | 仅模糊背景,材质仍保留清晰反射。 |
| Ambient Color(环境色) | 统一环境光,仅供不从环境贴图接收环境光的材质使用。 |
材质环境
材质可在材质检查器的 Environment(环境)部分,或通过代码中的对应属性提供独立环境。典型用途是从对象所在位置渲染房间立方体贴图,使反射与周围墙壁、地板和窗户匹配。
| 设置 | 作用 |
|---|---|
Cube Map(立方体贴图)(cubeMap) | 对象周围环境的立方体贴图。预滤波后,材质从中取得反射、环境光和折射,与使用场景天空盒相同。未预滤波时,无论光泽度如何,反射都像镜子一样清晰,环境光回退到统一环境色。 |
Sphere Map(球面贴图)(sphereMap) | 从镜面球体看到的周围环境的单张二维图像。开销最低,只提供反射,且相对于相机固定,因此相机环绕对象时反射不移动。同时设置时 Cube Map 优先。球面贴图为兼容现有内容而保留,计划在未来主要版本移除。 |
Use Skybox(使用天空盒)(useSkybox) | 材质没有独立环境贴图时,是否允许使用场景环境。对于完全不应反射天空的材质应关闭。对具有独立环境贴图的材质无影响。 |
Projection(投影)(cubeMapProjection) | 环境在对象周围的放置方式。Normal 将环境视为无限远,适合天空。Box 根据指定位置和尺寸映射到盒体,使反射与房间墙壁、地板和天花板对齐。参见盒式投影映射。 |
Reflectivity(反射率)(reflectivity) | 缩放该材质的环境反射强度,与环境来源无关。 |
Ambient(环境色)(ambient) | 为该材质的环境光染色,与环境来源无关。 |
代码中,预滤波光照是一张称为环境图集(envAtlas)的纹理,由 EnvLighting 辅助方法从立方体贴图或等距柱状图像生成。设置到材质后提供反射、环境光和折射;同时设置立方体贴图,可为极光滑表面增加镜面般清晰的反射。
使用哪个环境
规则很简单:具有独立环境贴图的材质只使用自己的环境;其他材质使用场景环境。
| 材质环境 | Use Skybox | 反射和折射 | 环境光 |
|---|---|---|---|
| 无 | 开启(默认) | 场景天空盒 | 场景天空盒 |
| 无 | 关闭 | 无 | 环境色 |
| 预滤波立方体贴图 | 任意 | 材质立方体贴图 | 材质立方体贴图 |
| 普通立方体贴图或球面贴图 | 任意 | 材质贴图 | 环境色 |
材质环境完全替换场景环境。场景的强度和旋转不影响它,场景也不会为普通立方体贴图或球面贴图补充环境光。若希望具有独立环境的材质获得方向性环境光,应在编辑器中预滤波立方体贴图,或在代码中生成环境图集。
平面反射是例外:它仅替换材质反射,环境光仍按上表取得。
在代码中配置
// Scene environment from an HDR image: a sharp sky for the background and mirror-like
// reflections, plus a prefiltered lighting atlas for ambient light and rough reflections
const skybox = EnvLighting.generateSkyboxCubemap(hdrTexture);
const lighting = EnvLighting.generateLightingSource(hdrTexture);
const envAtlas = EnvLighting.generateAtlas(lighting);
lighting.destroy();
app.scene.skybox = skybox;
app.scene.envAtlas = envAtlas;
app.scene.skyboxIntensity = 1.5;
// Material environment: this material ignores the scene skybox and reflects its own surroundings
const material = new StandardMaterial();
material.envAtlas = roomAtlas; // prefiltered lighting: reflections, ambient light and refraction
material.cubeMap = roomCubemap; // optional: mirror-sharp reflections on very glossy surfaces
material.update();
平面反射
环境贴图显示的是从单个点观察的周围景象,无法反射地板上的对象或表现其接触位置。抛光地板、镜面和水面等平面可改用平面反射:由第二台相机每帧向纹理渲染场景,相机按表面所在平面镜像。
PlanarRenderer 脚本(scripts/esm/planar-renderer.mjs)创建相机和纹理,每帧随主相机移动。材质反射 planarReflectionMap 指定的纹理,并用 planarReflectionMatrix 指定的相机视图投影矩阵进行投影。
- 平面反射只替换反射。材质灯光、阴影和菲涅耳不变,环境光仍来自环境,参见使用哪个环境。
- 粗糙表面通过纹理 mipmap 模糊反射,因此除完美镜面外,都应启用脚本的
mipmaps。脚本用高斯滤波生成,确保相机移动时模糊反射保持平滑。将mipmapFilter设为'box'可降低开销,但模糊更有块状感。 - 法线贴图会扭曲反射,类似凹凸或潮湿表面的反射。
- 启用脚本的
hdr,使反射中的亮光源与场景中同样明亮。此时纹理存储线性颜色,由渲染材质的相机进行色调映射。 - 主相机使用 TAA 时,反射相机跟随其抖动,因此反射也获得抗锯齿。
- 具有平面反射的材质若需折射,必须启用动态折射,因为该纹理没有可透过表面显示的环境。
- 将反射表面放入反射相机不渲染的图层,因为表面不能出现在自身反射中。
- 反射每帧额外渲染场景一次。可降低脚本的
scale,按屏幕分辨率的一部分渲染,并只让反射相机包含值得反射的图层。完美镜面每像素读取纹理一次,粗糙表面为平滑模糊会多读取几次。
在代码中配置平面反射:
import { PlanarRenderer } from 'playcanvas/scripts/esm/planar-renderer.mjs';
// The reflection camera renders the reflected layers before the main camera
const reflectionCamera = new Entity('ReflectionCamera');
reflectionCamera.addComponent('camera', {
layers: [worldLayer.id, skyboxLayer.id],
priority: -1
});
reflectionCamera.addComponent('script');
const planarRenderer = reflectionCamera.script.create(PlanarRenderer, {
properties: {
sceneCameraEntity: cameraEntity,
planePoint: new Vec3(0, 0, 0),
planeNormal: new Vec3(0, 1, 0),
mipmaps: true, // blurred reflections on rough surfaces
hdr: true // bright lights stay bright in the reflection
}
});
app.root.addChild(reflectionCamera);
// The floor is on its own layer, rendered by the main camera but not by the reflection camera
floorEntity.render.layers = [floorLayer.id];
// Each frame, after the main camera has moved, the floor reflects the updated texture
app.on('update', () => {
const texture = planarRenderer.frameUpdate();
floorMaterial.planarReflectionMap = texture;
floorMaterial.planarReflectionMatrix = planarRenderer.viewProjectionMatrix;
floorMaterial.update();
});