前言
学习目标
- 了解three.js中灯光的基本用法
- 可以游刃有余的使用不同灯光渲染场景
知识点
- AmbientLight
- HemisphereLight
- RectAreaLight
- DirectionalLight
- PointLight
- SpotLight
- 投影
- LightProbe
- Lensflare
1-灯光
灯光的基类是Light对象,而Light对象又继承自Object3D对象,所以灯光对象都可以进行Object3D相关的操作。
Light对象的属性:
- color:灯光颜色。
- intensity:光照强度。
- isLight:灯光类型。
Light对象的方法:
- dispose():销毁灯光,在不需要灯光的时候可以用此方法销毁灯光,释放内存。
- copy(source : Light):拷贝灯光数据。
- toJSON(meta : Object):将灯光数据转化到json文件中。
three.js 内置的灯光可以根据有没有投影分成两类:
-
没有投影
- AmbientLight 泛光灯
- HemisphereLight 半球灯
- RectAreaLight 矩形光
-
有投影
- DirectionalLight 平行光
- PointLight 点光源
- SpotLight 锥形灯
除此之外,还有一种特殊的灯-LightProbe,它可以将贴图或周围的环境变成换环境光。
上面的大部分灯光会拥有相应的Helper对象,将灯光显示出来:
- HemisphereLightHelper
- DirectionalLightHelper
- PointLightHelper
- SpotLightHelper
- LightProbeHelper
接下来我们简单说一下这些灯光。
1-1-AmbientLight
泛光灯,全方位无死角的照射物体,可以快速提亮物体,但也容易降低物体的明暗层次和对比度。
构造函数
AmbientLight( color : Integer, intensity : Float )
- color:灯光颜色,默认 0xffffff。
- intensity:光照强度,以流明为单位,默认1。
在辐射度量学中,intensity是光源在单位时间、单位立体角中的能量,它是以坎德拉(candela,简写cd)为单位的。
当然,我们也可以用流明(lumen,简写lm)或瓦特(watt,简写W)来表示intensity,因为intensity就是单位时间的光能(power)除以立体角。
这其中的一些具体原理,我先不细说,因为这涉及辐射度量学,我需要另起一篇文章来说。
示例
const light = new THREE.AmbientLight( 0x404040,1);
scene.add( light );
1-2-HemisphereLight
半球灯,位于场景正上方的光源,颜色从天空颜色渐变为地面颜色。
构造函数
HemisphereLight( skyColor : Integer, groundColor : Integer, intensity : Float )
- skyColor:天空颜色。默认白色(0xffffff)。
- groundColor:地面颜色,默认白色(0xffffff)。
- intensity:光照强度,默认1。
solar intensity与一些现实场景的对应关系如下:
- 无月夜:0.0001
- 夜间辉光:0.002
- 满月:0.5,
- 城市黄昏:3.4
- 客厅:50
- 非常多云:100
- 办公室:350
- 日出/日落:400
- 超速:1000
- 日光:18000
- 阳光直射:50000
属性
color:天空颜色,默认白色(0xffffff)。
groundColor:地面颜色,默认白色(0xffffff)。
position:位置,默认为(0, 1, 0),这样灯光会自上往下照射。
示例
const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 );
scene.add( light );
1-3-RectAreaLight
矩形光,在一个矩形平面上均匀地发射光线。
这种光源类型可用于模拟明亮的窗户或条形照明。
仅支持MeshStandardMaterial和MeshPhysicalMaterial。
构造函数
RectAreaLight( color : Color, intensity : Float, width : Float, height : Float )
- color:灯光颜色,默认0xffffff。
- intensity:光照强度,默认1。
- width:灯光宽度,默认 10。
- height:灯光高度,默认 10。
属性
color:灯光颜色。
intensity:光照强度。
width:灯光宽度。
height:灯光高度。
power:灯光功率。
因为power是光源在单位时间的能量,intensity是power在单位立体角中的能量,所以当power发生改变时,intensity也会随之改变。
示例
/* 快速初始化项目 */
const stage = new Stage(0, 1, -6)
const { scene, renderer } = stage
renderer.outputEncoding = sRGBEncoding
/* 灯光 */
const data = [ [-1.5, 0xff0000],
[0, 0x00ff00],
[1.5, 0x0000ff],
]
data.forEach((ele) => {
const rectLight = new RectAreaLight(ele[1], 1, 1, 2)
rectLight.position.set(ele[0], 1, 0)
scene.add(rectLight)
const rectLightHelper = new RectAreaLightHelper(rectLight)
rectLight.add(rectLightHelper)
})
/* 地面 */
{
const geometry = new PlaneGeometry(2000, 2000)
geometry.rotateX(-Math.PI / 2)
const material = new MeshStandardMaterial({
color: 0xeeeeee,
roughness: 0.1,
metalness: 0,
})
const plane = new Mesh(geometry, material)
plane.receiveShadow = true
scene.add(plane)
}
效果如下:
1-4-DirectionalLight
平行光,从某一位置向某一方向平行发射光线,照射面积无限大。
可用于模型无限远的光,比如日光。
构造函数
DirectionalLight( color : Color, intensity : Float )
- color:灯光颜色,默认0xffffff。
- intensity:光照强度,默认 1。
属性
castShadow:是否投射阴影
position:光源位置,
shadow:DirectionalLightShadow类型的投影对象,此shadow的camera为OrthographicCamera。
target:光源看向的目标对象,Object3D类型。
DirectionalLight默认会从position看向零点,若需要使用target,需将其添加到场景scene中.
scene.add( light.target );
这是为了让目标的 matrixWorld 在每一帧自动更新。
也可以将目标设置为场景中的其他对象(任意拥有 position 属性的对象),如:
const targetObject = new THREE.Object3D();
scene.add(targetObject);
light.target = targetObject;
通过上述操作,光源就可以追踪目标对象了。
示例
const directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 );
scene.add( directionalLight );
1-5-PointLight
点光源,从一个点向所有方向发射的光,比如灯泡发出的光。
构造函数
PointLight( color : Color, intensity : Float, distance : Number, decay : Float )
- color:灯光颜色,默认0xffffff。
- intensity:光照强度,默认1。
- distance:从光源发出光的最大距离,默认0,无限远。
- decay:光线沿着光线的距离的衰减量,默认2,这是基于物理的正确性定义。
属性
- color:灯光颜色,默认0xffffff。
- intensity:光照强度,默认1。
- distance:从光源发出光的最大距离,默认0,无限远。
- decay:光线沿着光线的距离的衰减量,默认2,这是基于物理的正确性定义。
- power:灯光功率。
- shadow:PointLightShadow类型的投影对象。
此shadow的camera为PerspectiveCamera: fov 值为90度、aspect 值为 1、 near 值为 0、far 值为 500。
示例
const light = new THREE.PointLight( 0xff0000, 1, 100 );
light.position.set( 50, 50, 50 );
scene.add( light );
1-6-SpotLight
聚光灯,可以理解为从点光源中截取了一个圆锥。
构造函数
SpotLight( color : Integer, intensity : Float, distance : Float, angle : Radians, penumbra : Float, decay : Float )
- color:灯光颜色,默认0xffffff。
- intensity:光照强度,默认1。
- distance:从光源发出光的最大距离,默认0,无限远。
- angle:散射角度,默认Math.PI/3
- penumbra:半影衰减,值域[0,1],默认值为0。
- decay:光线沿着光线的距离的衰减量,默认2。
属性
color:灯光颜色,默认0xffffff。
intensity:光照强度,默认1。
distance:从光源发出光的最大距离,其强度根据光源的距离线性衰减
angle:散射角度,默认Math.PI/3。
penumbra:半影衰减。取值介于0和1之间,默认值为0。
decay:光线沿着光线的距离的衰减量,默认2。
power:光源的功率,默认Math.PI。
target:光源看向的目标对象,Object3D类型。
map:用于调节光线颜色的纹理(Texture),聚光灯颜色会与该纹理的RGB值混合,其比例与其alpha值相对应。如果 castShadow 值为 false 时,map 不可用。
示例
const light = new SpotLight(0x00acec)
light.position.set(2, 5, 3)
light.castShadow = true
light.angle = 0.3
light.penumbra = 0.5
light.decay = 2
light.distance = 50
// 投影
const {
shadow: { mapSize, camera },
} = light
mapSize.width = 512 // default
mapSize.height = 512 // default
camera.near = 1 // default
camera.far = 10 // default
scene.add(light)
效果如下:
light.angle会影响light.shadow.camera.fov,而fov属性会影响投影贴图的覆盖范围,投影贴图的覆盖范围则会影响投影的清晰度。
2-投影
投影的实现是需要投影贴图的,而投影贴图则是把光源当成了相机拍出来的。
因此,我们要控制投影,就要控制这个相机和其拍摄出的投影贴图的尺寸。
投影中的camera和mapSize属性便是我们平时用到最多的。
说到相机,大家应该会想到透视相机和正交相机,在投影中也存在着两种相机的差异。
- DirectionalLight 的投影用的是正交相机,因此其投影相对于光源来说不会近大远小;
- PointLight 和SpotLight 的投影用的是透视相机,所以投影相对于光源来说会近大远小。
与此同时,因为相机是有可视区域的,这就注定相机只能拍出其可视区域内的投影贴图。
这就需要我们在场景过大的时候,适度调整相机的可视区域。
这里说适度调整,是因为相机的可视区域小了不行,太大也不行。
物体投影的清晰度是与投影贴图的可视区域成反比的,即在你投影贴图的像素尺寸mapSize不变的前提下,你的视野越大,那其中每个物体在投影贴图内所占的像素就越少,因此计算出的投影就越模糊。
如果你不想通过调整相机可视区域提高投影的清晰度,可以让投影贴图的尺寸mapSize变大。
mapSize的值必须是2的n次幂,其宽度和高度不必相同,默认(512,512)。
mapSize尺寸越大,需要的渲染时间越多。
在正交相机和透视相机中,其可视区域的定义是不同的。
-
正交相机
- near,far 近裁剪距离和远裁剪距离
- top,bottom,left,right 视口的上下左右边界
-
透视相机
- near,far 近裁剪距离和远裁剪距离
- fov 是椎体的垂直夹角
- aspect 视口宽高比
接下来我们先以DirectionalLight为例看一下投影的设置。
2-1-DirectionalLightShadow
示例
/* 快速初始化项目 */
const stage = new Stage(8, 10, 15)
const { scene, renderer } = stage
renderer.shadowMap.enabled = true
/* 灯光 */
{
const light = new DirectionalLight()
light.position.set(0, 8, 12)
light.castShadow = true
// 投影
const {
shadow: { mapSize, camera },
} = light
mapSize.width = 512 // default
mapSize.height = 512 // default
camera.near = 0.5 // default
camera.far = 50 // default
camera.left = -5 // default
camera.right = 5 // default
camera.top = 5 // default
camera.bottom = -5 // default
scene.add(light)
}
/* 柱子 */
{
const mat = new MeshStandardMaterial({
color: 0xeeeeee,
})
const geo = new BoxGeometry(1, 4, 1)
const mesh = new Mesh(geo, mat)
mesh.position.y = 2
mesh.castShadow = true
scene.add(mesh)
}
/* 地面 */
{
const geometry = new PlaneGeometry(2000, 2000)
geometry.rotateX(-Math.PI / 2)
const material = new MeshStandardMaterial()
const plane = new Mesh(geometry, material)
plane.receiveShadow = true
scene.add(plane)
}
效果如下:
当前的投影相对于灯光而言,并没有发生透视变化。
从上面的代码可以看出,要绘制物体的投影需要以下基本操作:
1.在渲染器中开启投影。
renderer.shadowMap.enabled = true
2.让灯光透视投影。
light.castShadow = true
3.让需要透视投影的物体投射投影。
mesh.castShadow = true
4.让需要接收阴影的物体接收投影。
plane.receiveShadow = true
当场景或场景中的物体较大时,我们并不能把投影完全显示出来。
比如我把上面的几何体尺寸变大:
const geo = new BoxGeometry(15, 4, 15)
效果如下:
这时候我们就需要把灯光的视野变大:
// camera.left = -5 // default
// camera.right = 5 // default
// camera.top = 5 // default
// camera.bottom = -5 // default
camera.left = -10
camera.right = 10
camera.top = 10
camera.bottom = -10
效果如下:
有时候,当相机的可视区域过大时,会影响投影的清晰度会降低。
比如,我把相机可视区域的边界调大:
camera.left = -100
camera.right = 100
camera.top = 100
camera.bottom = -100
投影就会模糊:
将mapSize变大可以提高其清晰度:
mapSize.width = 5120
mapSize.height = 5120
效果如下:
接下来,我们在看一下点光源的阴影。
2-2-PointLightShadow
PointLight的投影是朝四面八方投射的,所以PointLight的相机虽然是透视相机,但其fov属性是不需要的。
示例
/* 快速初始化项目 */
const stage = new Stage(0, 25, 0)
const { scene, renderer } = stage
renderer.shadowMap.enabled = true
/* 灯光 */
{
const light = new PointLight()
light.position.set(0, 6, 0)
light.castShadow = true
// 投影
const {
shadow: { mapSize, camera },
} = light
mapSize.width = 512 // default
mapSize.height = 512 // default
camera.near = 0.5 // default
camera.far = 50 // default
scene.add(light)
}
/* 柱子 */
for (let z = 0; z < 2; z++) {
for (let x = 0; x < 2; x++) {
const mat = new MeshStandardMaterial({
color: 0xeeeeee,
})
const geo = new BoxGeometry(1, 4, 1)
const mesh = new Mesh(geo, mat)
mesh.position.y = 2
mesh.position.x = x * 4 - 2
mesh.position.z = z * 4 - 2
mesh.castShadow = true
scene.add(mesh)
}
}
/* 地面 */
{
const geometry = new PlaneGeometry(2000, 2000)
geometry.rotateX(-Math.PI / 2)
const material = new MeshStandardMaterial()
const plane = new Mesh(geometry, material)
plane.receiveShadow = true
scene.add(plane)
}
其俯视效果如下:
2-3-SpotLightShadow
SpotLight可视之为PointLight中的一个椎体,其投影也只会在这个椎体中生效。
在SpotLight的投影相机中,其fov会随SpotLight的散射角度而变。
示例
/* 快速初始化项目 */
const stage = new Stage(40, 25, 35)
const { scene, renderer } = stage
renderer.shadowMap.enabled = true
/* 灯光 */
{
const light = new SpotLight()
light.position.set(0, 6, 2)
light.castShadow = true
// 投影
const {
shadow: { mapSize, camera },
} = light
mapSize.width = 512 // default
mapSize.height = 512 // default
camera.near = 0.5 // default
camera.far = 50 // default
scene.add(light)
}
/* 柱子 */
for (let z = 0; z < 2; z++) {
for (let x = 0; x < 2; x++) {
const mat = new MeshStandardMaterial({
color: 0xeeeeee,
})
const geo = new BoxGeometry(1, 4, 1)
const mesh = new Mesh(geo, mat)
mesh.position.y = 2
mesh.position.x = x * 4 - 2
mesh.position.z = z * 4 - 2
mesh.castShadow = true
scene.add(mesh)
}
}
/* 地面 */
{
const geometry = new PlaneGeometry(2000, 2000)
geometry.rotateX(-Math.PI / 2)
const material = new MeshStandardMaterial()
const plane = new Mesh(geometry, material)
plane.receiveShadow = true
scene.add(plane)
}
效果如下:
3-LightProbe
LightProbe 是一种存储光线数据的对象,它可以将cubeTexture或周围的环境变成光线数据。
我们可以使用LightProbeGenerator对象,基于cubeTexture或者cubeRenderTarget生成LightProbe对象。
3-1-基于cubeTexture生成LightProbe
示例
/* 快速初始化项目 */
const stage = new Stage(0, 0, 6)
const { scene, renderer } = stage
/* probe*/
const lightProbe = new LightProbe()
scene.add(lightProbe)
/* 贴图 */
const genCubeUrls = function (prefix: string, postfix: string) {
return [
prefix + 'px' + postfix,
prefix + 'nx' + postfix,
prefix + 'py' + postfix,
prefix + 'ny' + postfix,
prefix + 'pz' + postfix,
prefix + 'nz' + postfix,
]
}
const urls = genCubeUrls('textures/pisa/', '.png')
new CubeTextureLoader().load(urls, function (cubeTexture) {
cubeTexture.encoding = sRGBEncoding
scene.background = cubeTexture
lightProbe.copy(LightProbeGenerator.fromCubeTexture(cubeTexture))
/* 材质 */
const mat = new MeshStandardMaterial({
color: 0xffffff,
roughness: 0,
envMap: cubeTexture,
envMapIntensity: 2,
})
/* 几何体 */
const sphereGeometry = new SphereGeometry(1, 36, 36)
//球体
scene.add(new Mesh(sphereGeometry, mat))
})
效果如下:
上面的LightProbeGenerator.fromCubeTexture(cubeTexture) 便是基于cubeTexture生成LightProbe的方法。
LightProbe的运行原理有点类似于存储了光照数据的hdr。
3-2-基于cubeRenderTarget生成LightProbe
示例
/* 快速初始化项目 */
const stage = new Stage(0, 0, 6)
const { scene, renderer } = stage
// probe
const lightProbe = new LightProbe()
scene.add(lightProbe)
// cube渲染目标
const cubeRenderTarget = new WebGLCubeRenderTarget(256)
// cube相机
const cubeCamera = new CubeCamera(1, 1000, cubeRenderTarget)
/* 贴图 */
const genCubeUrls = function (prefix: string, postfix: string) {
return [
prefix + 'px' + postfix,
prefix + 'nx' + postfix,
prefix + 'py' + postfix,
prefix + 'ny' + postfix,
prefix + 'pz' + postfix,
prefix + 'nz' + postfix,
]
}
const urls = genCubeUrls('textures/pisa/', '.png')
new CubeTextureLoader().load(urls, function (cubeTexture) {
cubeTexture.encoding = sRGBEncoding
scene.background = cubeTexture
cubeCamera.update(renderer, scene)
lightProbe.copy(
LightProbeGenerator.fromCubeRenderTarget(renderer, cubeRenderTarget)
)
const mat = new MeshStandardMaterial({
color: 0xffffff,
roughness: 0,
envMap: cubeTexture,
})
const sphereGeometry = new SphereGeometry(1, 36, 36)
scene.add(new Mesh(sphereGeometry, mat))
})
{
const mat = new MeshBasicMaterial({
color: 0xffff00,
})
const sphereGeometry = new SphereGeometry(1, 9, 9)
const mesh = new Mesh(sphereGeometry, mat)
mesh.position.x = 2
scene.add(mesh)
}
效果如下:
通过上面的效果可以看出,周围的环境和黄色的圆球都影响到了左侧的球体,这就有了色溢的感觉。
说一下其实现过程。
1.准备一个CubeCamera对象和WebGLCubeRenderTarget对象。
const cubeRenderTarget = new WebGLCubeRenderTarget(256)
const cubeCamera = new CubeCamera(1, 1000, cubeRenderTarget)
CubeCamera用于拍摄周围的场景。
WebGLCubeRenderTarget 对象用于存储拍摄结果。
2.使用cubeCamera拍摄场景。
cubeCamera.update(renderer, scene)
当前的场景是scene,这个场景通过renderer渲染,渲染的结果会存储在cubeRenderTarget中。
3.基于renderer和 cubeRenderTarget 生成LightProbe对象。
lightProbe.copy(
LightProbeGenerator.fromCubeRenderTarget(renderer, cubeRenderTarget)
)
4-Lensflare
Lensflare继承自Mesh对象,可以为灯光创建光晕。
使用Lensflare时,需要将WebGLRenderer的alpha为true。
Lensflare中可以包含了多个LensflareElement对象。
LensflareElement( texture : Texture, size : Float, distance : Float, color : Color )
- texture:flare纹理
- size:像素尺寸
- distance:光源距离,值域[0,1]
- color:flare颜色
示例
/* 快速初始化项目 */
const stage = new Stage(0, 0, 6)
const { scene, renderer } = stage
const textureLoader = new TextureLoader()
const textureFlare0 = textureLoader.load('/textures/lensflare/lensflare0.png')
const textureFlare3 = textureLoader.load('/textures/lensflare/lensflare3.png')
const light = new PointLight(0x00acec)
light.position.set(0, 0, 2)
scene.add(light)
const lensflare = new Lensflare()
lensflare.addElement(new LensflareElement(textureFlare0, 700, 0, light.color))
lensflare.addElement(new LensflareElement(textureFlare3, 60, 0.6))
lensflare.addElement(new LensflareElement(textureFlare3, 70, 0.7))
lensflare.addElement(new LensflareElement(textureFlare3, 120, 0.9))
lensflare.addElement(new LensflareElement(textureFlare3, 70, 1))
light.add(lensflare)
效果如下:
总结
本章只是把灯光的基本用法走了一遍,下一章我会将上一章的材质和本章的灯光合一个案例出来。