three.js灯光

1,022 阅读12分钟

前言

学习目标

  • 了解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)
}

效果如下:

image-20230603004500371

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)

效果如下:

image-20230602221248434

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)
}

效果如下:

image-20230527220934044

当前的投影相对于灯光而言,并没有发生透视变化。

从上面的代码可以看出,要绘制物体的投影需要以下基本操作:

1.在渲染器中开启投影。

renderer.shadowMap.enabled = true

2.让灯光透视投影。

light.castShadow = true

3.让需要透视投影的物体投射投影。

mesh.castShadow = true

4.让需要接收阴影的物体接收投影。

plane.receiveShadow = true

当场景或场景中的物体较大时,我们并不能把投影完全显示出来。

比如我把上面的几何体尺寸变大:

const geo = new BoxGeometry(15, 4, 15)

效果如下:

image-20230527221240472

这时候我们就需要把灯光的视野变大:

// 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

效果如下:

image-20230527222836497

有时候,当相机的可视区域过大时,会影响投影的清晰度会降低。

比如,我把相机可视区域的边界调大:

camera.left = -100
camera.right = 100
camera.top = 100
camera.bottom = -100

投影就会模糊:

image-20230528092122158

将mapSize变大可以提高其清晰度:

mapSize.width = 5120 
mapSize.height = 5120

效果如下:

image-20230528092358705

接下来,我们在看一下点光源的阴影。

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)
}

其俯视效果如下:

image-20230529144855334

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)
}

效果如下:

image-20230529154511317

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))
})

效果如下:

image-20230529163950966

上面的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)
}

效果如下:

image-20230529165621113

通过上面的效果可以看出,周围的环境和黄色的圆球都影响到了左侧的球体,这就有了色溢的感觉。

说一下其实现过程。

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)

效果如下:

image-20230602193645415

总结

本章只是把灯光的基本用法走了一遍,下一章我会将上一章的材质和本章的灯光合一个案例出来。