首页
/ three.js HemisphereLight 半球光详解:天空—地面渐变环境光照的配置与渲染原理

three.js HemisphereLight 半球光详解:天空—地面渐变环境光照的配置与渲染原理

2026-09-07 10:17:45作者:殷蕙予

半球光(HemisphereLight)是 three.js 提供的一种"无方向衰减、无阴影"的模拟天空环境光照:它把光源固定在场景上方,让表面颜色从天空色(skyColor)平滑过渡到地面色(groundColor),用于为室内外场景补充柔和的冷暖渐变基调。本文以官方 API 文档为基础,结合当前仓库的 实现源码、着色器逻辑、辅助对象与单元测试,讲解 HemisphereLight 的构造参数、属性、底层光照模型、典型用法与使用限制,帮助你在场景布光时正确选用与调校半球光。

HemisphereLight 是什么

按照文档与源码 JSDoc 的定位(src/lights/HemisphereLight.js):

A light source positioned directly above the scene, with color fading from the sky color to the ground color. This light cannot be used to cast shadows.

即:光源位于场景正上方,颜色由天空色渐变至地面色,且不能用于投射阴影。它并不模拟一个具体的点状光源,而是一种基于物体表面法线方向的"渐变辐照度"模型:朝上的表面被天空色照亮,朝下的表面被地面色照亮,介于两者之间的倾斜表面得到两色的平滑插值。因此它常与 DirectionalLight(太阳直射光)配合,充当模拟天光漫反射的环境填充光。

继承链为 EventDispatcher → Object3D → Light → HemisphereLight,这意味着它同时具备 Object3D 的变换能力(position/rotation/quaternion/scale)和 Light 的 color/intensity 属性。

构造函数与参数

构造函数签名:

new HemisphereLight( skyColor, groundColor, intensity )
参数 类型 说明 默认值
skyColor number / Color / string 天空色(光的主色调) 0xffffff
groundColor number / Color / string 地面色(与天空色渐变的另一端) 0xffffff
intensity number 光的强度 1

三个参数均为可选。颜色值三种写法等价:

  • number:十六进制整数,如 0xffffbb0x080820
  • Color 实例:如 new THREE.Color( 0.6, 0.4, 0.2 )
  • string:CSS 颜色字符串,如 'skyblue''#ffffbb''rgb(255, 255, 187)'

最小的可运行示例(与文档 Code Example 一致):

const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );
scene.add( light );

构造函数实现 可以看到,构造器做了三件关键事情:

  1. 调用 super( skyColor, intensity ),由 Light 基类 初始化继承自 Object3D 的 this.colorthis.intensity
  2. 设置类型标记 this.isHemisphereLight = truethis.type = 'HemisphereLight',并把位置设为 Object3D.DEFAULT_UP(即 (0, 1, 0),指向世界 +Y 上方),随后 updateMatrix(),这正是"置于场景正上方"的实现来源;
  3. 以第二个参数创建 this.groundColor = new Color( groundColor ) —— 这是 HemisphereLight 区别于其他灯光、在基类之外新增的唯一字段。

属性说明

.color : Color(继承自 Light)

光的颜色,即"天空色"。可通过 light.color.setHex( 0xffeedd )light.color.setStyle( 'skyblue' ) 等方式随时修改。渲染时它对应着色器中的 skyColor

.groundColor : Color

地面色,是 HemisphereLight 独有属性(其余灯光均没有)。着色器中它与天空色按表面朝向做插值,因此修改它会影响所有朝下或倾斜表面的染色。

.intensity : number(继承自 Light,默认 1)

光的强度。注意其作用方式与点光、聚光不同:在 WebGLLights.js 的 setup 阶段 中,intensity 会同时乘以天空色和地面色:

uniforms.skyColor.copy( light.color ).multiplyScalar( intensity );
uniforms.groundColor.copy( light.groundColor ).multiplyScalar( intensity );

.isHemisphereLight : boolean(只读)

类型测试标记,恒为 true。内部渲染器正是通过 light.isHemisphereLight 分支把光归入半球光统一列表 state.hemi,而非遍历显式类型名。这也是文档所述"该标记可用于类型检测"的实际用途。

.type : string

'HemisphereLight'type 会被写入 JSON 序列化结果中,供 ObjectLoader 反序列化时据此重建正确类型:

case 'HemisphereLight':
    object = new HemisphereLight( data.color, data.groundColor, data.intensity );
    break;

.position : Vector3(继承自 Object3D)

默认被设为 (0, 1, 0)(见 src/lights/HemisphereLight.js),即指向 +Y 的天空方向。对 HemisphereLight 来说,position 的意义是确定渐变轴方向(见下文着色器原理),而非确定衰减中心。

源码级原理:半球光如何在着色器中工作

核心光照公式

在 WebGL 渲染路径中,半球光在片段着色器阶段作为**间接漫反射(irradiance)**贡献叠加到材质上,核心实现位于 lights_fragment_begin.glsl.js

#pragma unroll_loop_start
for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {
    irradiance += getHemisphereLightIrradiance( hemisphereLights[ i ], geometryNormal );
}
#pragma unroll_loop_end

逐光辐照度函数定义在 lights_pars_begin.glsl.js

vec3 getHemisphereLightIrradiance( const in HemisphereLight hemiLight, const in vec3 normal ) {
    float dotNL = dot( normal, hemiLight.direction );
    float hemiDiffuseWeight = 0.5 * dotNL + 0.5;
    vec3 irradiance = mix( hemiLight.groundColor, hemiLight.skyColor, hemiDiffuseWeight );
    return irradiance;
}

这条公式直观解释了"天空到地面的颜色渐变":

  • 当表面法线 normal 与光的 direction(默认 +Y 天顶)完全一致时,dotNL = 1,权重 hemiDiffuseWeight = 1,辐照度取天空色
  • 当表面法线朝下(dotNL = -1)时,权重为 0,辐照度取地面色
  • 垂直墙面(dotNL = 0)恰好得到两色各半的 mix 结果。

天空色与地面色在 CPU 侧被合并进半球光 uniform 结构 HemisphereLight { vec3 direction; vec3 skyColor; vec3 groundColor; },数量受 NUM_HEMI_LIGHTS 宏控制,可与场景中的多个 HemisphereLight 一起逐光累加。

方向从何而来

WebGLLights.js 的 setupView 阶段 中,半球光的 direction uniform 由世界矩阵位置提取,再变换到视图空间:

uniforms.direction.setFromMatrixPosition( light.matrixWorld );
uniforms.direction.transformDirection( viewMatrix );

也就是说,光的 position 实质是方向向量。默认 (0, 1, 0) 使渐变轴竖直;若旋转光或改变其位置,渐变方向会随之倾斜(例如模拟夕阳从侧面照亮的天空暖光)。从着色器中的点积 dot(normal, hemiLight.direction) 可以推断,该方向向量应保持单位长度——因此若手动移动半球光,建议让 position 落在以场景为中心的"单位球面"上,避免权重被非归一化向量拉偏(在 WebGPU/TSL 渲染路径中 HemisphereLightNode 会对位置做 .normalize(),更稳健)。

两处容易混淆的特性

  1. 无距离衰减:半球光不携带 position/distance/decay 等点光源字段,着色器公式中完全没有距离项。它均匀影响整个场景中所有朝向匹配的表面——这是它与 AmbientLight 最本质的区别(AmbientLight 是对所有表面施加单一常量色,而半球光对每个表面按法线朝向差异化混色)。
  2. 不能投射阴影:文档明确说明 "This light cannot be used to cast shadows"。在 WebGLLights.js 中,各阴影类光均需从 ShadowUniformsCache 取得阴影 uniform 并写入 shadow 状态,而 HemisphereLight 分支不生成任何 shadow uniform;其光照也只作为间接漫反射分量存在,不具备直接光照的阴影采样路径。

WebGPU / TSL 节点化渲染对应实现

在使用 WebGPURenderer + NodeMaterial 的新渲染管线中,同一光照模型由 TSL 节点 HemisphereLightNode 等价实现,其 setup 方法 用节点语法复刻了 GLSL 公式:

const dotNL = normalWorld.dot( lightDirectionNode );
const hemiDiffuseWeight = dotNL.mul( 0.5 ).add( 0.5 );
const irradiance = mix( groundColorNode, colorNode, hemiDiffuseWeight );
builder.context.irradiance.addAssign( irradiance );

同时其 update 方法 与 WebGL 路径保持一致,把 groundColor * intensity 写入 uniform,保证两条渲染路径光照行为一致。

典型使用场景

作为天光环境填充,配合太阳直射光

最典型的用法是"一暖一冷、一直接一间接"双灯布光:DirectionalLight 提供清晰的太阳直射与阴影,HemisphereLight 提供柔和的冷暖渐变环境光,避免背光面死黑:

// 太阳直射光
const sun = new THREE.DirectionalLight( 0xffffff, 2.5 );
sun.position.set( 50, 80, 30 );
scene.add( sun );

// 天空暖色 + 地面冷色,模拟天光反弹的环境渐变
const hemi = new THREE.HemisphereLight( 0xffeeb1, 0x080820, 1 );
scene.add( hemi );

同样的布光模式在仓库众多示例中反复出现,例如 webgl_lightprobe_cubecamera.htmlmisc_controls_pointerlock.html 等文件均可搜索到 new THREE.HemisphereLight(...) 的实际用法,可作为参考范例。

通过地面色控制环境冷暖倾向

在需要偏冷或偏暖的氛围时,单独调节 groundColor 即可改变阴影面与水平面的颜色倾向,无需整体改动天空光。地面色常用深蓝紫(如 0x0808200x000022)以形成"日落后地面反射冷光"的视觉逻辑,这与示例 0xffffbb, 0x080820 的经典搭配一致。

需要时可叠加多个半球光

着色器循环按 NUM_HEMI_LIGHTS 累加所有半球光的辐照度,因此多个 HemisphereLight 会线性叠加。可借此实现"主环境渐变 + 局部染色"的复合效果,但需留意叠加后强度会成倍增加,建议相应调低各自的 intensity

用 HemisphereLightHelper 可视化光的方向与颜色

需要在实际场景中观察半球光的渐变轴方向时,可使用辅助对象 HemisphereLightHelper

const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );
const helper = new THREE.HemisphereLightHelper( light, 5 );
scene.add( helper );

该辅助对象用一个 OctahedronGeometry 八面体线框表示光的方向与色彩:未显式指定 color 参数时,其顶点色会将光的上半部分(前一半顶点)染为 light.color、下半部分染为 light.groundColor(见 update()),直观呈现两端的渐变色。需要注意:

  • 默认 size = 1,即八面体的半径(示例传入 5 表示半径 5 单位);
  • 当光的 transform 或颜色改变后,需调用 helper.update() 同步,并调用 helper.matrixWorldNeedsUpdate = true
  • 不再需要时调用 helper.dispose() 释放内部网格的 geometry 与 material(src/helpers/HemisphereLightHelper.js)。

序列化与反序列化

HemisphereLight 覆写了 toJSON,把独有的地面色写入对象数据(src/lights/HemisphereLight.js):

toJSON( meta ) {
    const data = super.toJSON( meta );   // 含 type、color、intensity 与 Object3D 变换
    data.object.groundColor = this.groundColor.getHex();
    return data;
}

产出的 JSON 形如:

{
  "type": "HemisphereLight",
  "color": 16773051,
  "groundColor": 526368,
  "intensity": 1,
  ...
}

反序列化时由 ObjectLoader 依据 typedata.groundColor 还原。同理,copy( source, recursive )src/lights/HemisphereLight.js)会额外复制 groundColor,保证对象复制的完整性。

使用注意事项小结

  • HemisphereLight 均匀作用于全场景,没有位置衰减与范围,不适合表现局部近距离光源;
  • 无法投射阴影,需要阴影请另加 DirectionalLight/SpotLight 等直接光源;
  • 默认 position 为 (0, 1, 0);要调整渐变轴请旋转光或用位于单位球面上的 position,而非随手赋一个大数坐标;
  • groundColor 是 HemisphereLight 独有的、决定暗部冷暖的关键参数,值得像调 color 一样认真调节;
  • 文档所给的默认参数(skyColor 0xffffff、groundColor 0xffffff、intensity 1)均可在构造时省略;无参构造会被单元测试覆盖,见 test/unit/src/lights/HemisphereLight.tests.js

参考资源

登录后查看全文
热门项目推荐
相关项目推荐