three.js HemisphereLight 半球光详解:天空—地面渐变环境光照的配置与渲染原理
半球光(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:十六进制整数,如
0xffffbb、0x080820; - 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 );
从 构造函数实现 可以看到,构造器做了三件关键事情:
- 调用
super( skyColor, intensity ),由 Light 基类 初始化继承自 Object3D 的this.color与this.intensity; - 设置类型标记
this.isHemisphereLight = true与this.type = 'HemisphereLight',并把位置设为Object3D.DEFAULT_UP(即(0, 1, 0),指向世界 +Y 上方),随后updateMatrix(),这正是"置于场景正上方"的实现来源; - 以第二个参数创建
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(),更稳健)。
两处容易混淆的特性
- 无距离衰减:半球光不携带 position/distance/decay 等点光源字段,着色器公式中完全没有距离项。它均匀影响整个场景中所有朝向匹配的表面——这是它与 AmbientLight 最本质的区别(AmbientLight 是对所有表面施加单一常量色,而半球光对每个表面按法线朝向差异化混色)。
- 不能投射阴影:文档明确说明 "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.html、misc_controls_pointerlock.html 等文件均可搜索到 new THREE.HemisphereLight(...) 的实际用法,可作为参考范例。
通过地面色控制环境冷暖倾向
在需要偏冷或偏暖的氛围时,单独调节 groundColor 即可改变阴影面与水平面的颜色倾向,无需整体改动天空光。地面色常用深蓝紫(如 0x080820、0x000022)以形成"日落后地面反射冷光"的视觉逻辑,这与示例 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 依据 type 与 data.groundColor 还原。同理,copy( source, recursive )(src/lights/HemisphereLight.js)会额外复制 groundColor,保证对象复制的完整性。
使用注意事项小结
- HemisphereLight 均匀作用于全场景,没有位置衰减与范围,不适合表现局部近距离光源;
- 无法投射阴影,需要阴影请另加 DirectionalLight/SpotLight 等直接光源;
- 默认 position 为
(0, 1, 0);要调整渐变轴请旋转光或用位于单位球面上的 position,而非随手赋一个大数坐标; groundColor是 HemisphereLight 独有的、决定暗部冷暖的关键参数,值得像调color一样认真调节;- 文档所给的默认参数(skyColor
0xffffff、groundColor0xffffff、intensity1)均可在构造时省略;无参构造会被单元测试覆盖,见 test/unit/src/lights/HemisphereLight.tests.js。
参考资源
- 官方文档:docs/pages/HemisphereLight.html.md(本文内容骨架来源)
- 类实现:src/lights/HemisphereLight.js、基类 src/lights/Light.js
- GLSL 光照公式:src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js、src/renderers/shaders/ShaderChunk/lights_fragment_begin.glsl.js
- uniform 上传:src/renderers/webgl/WebGLLights.js
- TSL 节点实现:src/nodes/lighting/HemisphereLightNode.js
- 可视化辅助:src/helpers/HemisphereLightHelper.js
- 序列化还原:src/loaders/ObjectLoader.js
- 单元测试:test/unit/src/lights/HemisphereLight.tests.js、test/unit/src/helpers/HemisphereLightHelper.tests.js
- 实际用例如 examples/css3d_mixed.html、examples/misc_controls_pointerlock.html 等
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00