11.Three.js基础之灯光

11 Three.js基础之灯光

灯光的种类

在场景中添加灯光后,灯光照射在物体上产生明暗、光亮和阴影,从而让物体显得更加立体有光泽。

在有些文档或教程中,会把 灯光 称呼为 光源,这只是对light这个单词的不同翻译而已
如果我在本系列文章中,有时候使用 “灯光”,有时候使用 “光源”,请勿见怪。

向场景中添加灯光 这个行为,在传统3D软件制作中通常被称为 “打灯”

若场景中的物体由非反光材质构成,即使场景中没有任何光源,渲染后依然可以看见该物体。

非反光材质为MeshBasicMaterial

若场景中的物体由反光材质构成,假设场景中没有任何光源,渲染出的结果将是一片漆黑,什么物体都看不见。

反光材质为MeshPhongMaterial

灯光的种类

Three.js中,有6种基础类型的灯光,他们都继承于Three.Light

灯光类型(都继承于Light) 灯光名称 是否支持阴影 是否作用于全局(无处不在) 是否有照射目标
AmbientLight 环境光、氛围光
DirectionalLight 平行光
HemisphereLight 半球光源、户外光源
PointLight 点光源
RectAreaLight 矩形面光源
SpotLight 聚光灯光源

补充说明1:环境光

有个别文档或教程中,会把HemisphereLight也称呼为 “环境光”。
事实上AmbientLightHemisphereLight的作用都是提供环境光,只是HemisphereLight的环境光更加真实,当然渲染所需性能也更多。

补充说明2:是否支持阴影

**所有的光照射到物体上后,都会产生阴影。**但 这里说的不是 是否会产生阴影,而是说 “是否支持阴影”

一共有3种光不支持阴影:AmbientLight、HemisphereLight、RectAreaLight

其他种类的光,都支持阴影:DirectionalLight、PointLight、SpotLight

关于Three.js中的 阴影(LightShadown、DirectionalLightShadown、PointLightShadown、SpotLightShadown),我们会在后面单独开辟一节来讲解。

关于“阴影”的进一步补充:

在之前所有的示例中,当场景上有反光物体且有灯光时,物体会产生明暗,但是请注意:

  1. 这个“物体显示出的明暗”并不是真正的“阴影”。

    Three.js中 有真正的阴影对象,这个会在后面的 “Three.js基础之阴影” 一文中有详细说明。

  2. 这个“物体显示出的明暗”并不是完全符合我们日常的“光影明暗”。

    这是因为我们目前所有示例都使用的是“简单光照模型”,也就是说光照射在物体上后并不进行漫反射,所以渲染出的“明暗”并不完全自然合理。

  3. 默认渲染器并不会渲染阴影、默认支持阴影的灯光也不会投射阴影,若想产生真正的阴影,还需开启阴影渲染和投射。

    具体如何开启,也会在后面的 “Three.js基础之阴影” 一文中有详细说明。

补充说明3:是否作用域全局

只有环境光(AmbientLightHemisphereLight)作用于全局,其他光则照耀范围都是有限的。

补充说明4:什么叫 “是否有照射目标

答:就是这个光除了光源本身之外,还包含一个target属性,并且可以通过设置target.position的位置。对于有照射目标的灯光,在场景中不光要添加灯光本身,还可以添加 灯光照射目标。

注意是 “可以添加” 灯光目标,而不是说 “必须也要添加” 灯光目标。

请注意上面表格中,关于 “是否产生阴影” 和 “是否有照射目标”,这2项 是完全相同的,也就是说,只有包含照射目标的光,才会产生阴影。

Three.js中,有3种 光探针 类型的环境光。

灯光类型 灯光名称
LightProbe 光探针
AmbientLightProbe(继承于LightProbe) 环境光探针
HemisphereLightProbe(继承于LightProbe) 半球光探针

关于 光探针,是另外一套比较复杂的光的算法方式。

根据我网上搜索到的一些信息,大概可以描述为:

传统的 环境光(AmbientLightHemisphereLight)渲染时需要的计算量比较大,对于渲染静止物体来说还可以,但是渲染 运动类型 的物体时所消耗的性能过高,而 光探针类型的环境光(AmbientLightProbeHemisphereLightProbe)则更加适合 运动类型 的物体。

光的辅助对象

场景中的光本身是不可见的,为了让我们方便观测光源,Three.js提供了 光的辅助对象:DirectionalLightHelper、HemisphereLightHelper、PointLightHelper、RectAreaLightHelper、SpotLightHelper

所谓光的辅助对象,就是在渲染后出现的一些白色细线,这些白色细线指示出光源的位置、大小、以及光发射的方向。

光的辅助对象用法非常简单,3步骤:

  1. 先创建 光 的实例
  2. 将创建好的光实例作为 辅助对象构造函数的参数
  3. 场景中添加 辅助对象即可

例如:

//创建并设置平行光
const directionalLight = new Three.DirectionalLight(0xFFFFFF, 1)
directionalLight.position.set(0, 10, 0);
directionalLight.target.position.set(-5, 0, 0)

//将平行光添加到场景中
scene.add(directionalLight)
scene.add(directionalLight.target)

//根据平行光实例,创建对应的辅助对象,并将辅助对象添加到场景中
const directionalLightHelper = new Three.DirectionalLightHelper(directionalLight)
scene.add(directionalLightHelper)

灯光作用与特点

AmbientLight(环境光、氛围光)

通常仅作为基础光线,一般需要与其他灯光配合使用。不能产生阴影、也无需指定坐标位置,仅需设置颜色和强度。

注意:不支持阴影

DirectionalLight(平行光)

最为经常使用的光源,光纤(光芒)都是平行向着一个方向发射。

经常使用DirectionalLight来模拟太阳光照射到某个物体上的光照效果。

HemisphereLight(半球光源)

相对ambientLighthemisphereLight更加真实的模拟自然光源,提供 天空 和 地面 漫反射光线。

一共接收3个参数:

  • 1个参数:天空光线颜色
  • 2个参数:地面反射光颜色
  • 3个参数:光的反射强度

注意:不支持阴影

PointLight(点光源)

类似生活中的灯泡,光纤(光芒)没有固定方向,朝着四周散射。

注意:点光源对应的辅助对象PointLightHelper只有一个 菱形的光源形状,并没有 光 的发射线条。

RectAreaLight(矩形面光源)

DirectionalLight模拟太阳光不同,RectAreaLight光源形状为一个矩形,可以模拟出明亮的窗口或矩形照明光源。

注意:

  1. 不支持阴影

  2. 只有MeshStandardMaterialMeshPhysicalMaterial材料才支持RectAreaLight光源

  3. 按照官方文档描述,场景中必须加入RectAreaLightUniformsLib.init()

    目前我比较疑惑的是,经过试验发现,即使不添加RectAreaLightUniformsLib.init(),场景依然正常渲染,似乎看不出有任何差别

特别说明:

RectAreaLight对应的 辅助对象RectAreaLightArea引入方式和其他 光辅助对象 引入方式不同。

其他光辅助对象都是内置在three中的,使用之前无需引入,可以直接使用,例如:

const directionalLightHelper = new Three.DirectionalLightHelper(directionalLight)

但是RectAreaLightHelper在使用前需要引入才可以,引入代码:

import { RectAreaLightHelper } from 'three/examples/jsm/helpers/RectAreaLightHelper'

SpotLight(聚光灯)

类似生活中的聚光灯效果。

关于每个灯光的具体参数详情、属性用法,请参考官方文档:https://threejs.org/docs/index.html#api/zh/lights/Light

接下来,我们将通过创建一个HelloLight的例子,直观的观察不同类型灯光的效果。

这个示例中,会用到我们上节学习的 纹理 相关知识。

前期准备:OrbitControls讲解、制作纹理图片

OrbitControls的简介和用法

本示例中需要用到一个之前从未使用过的类:OrbitControls,先简单介绍一下这个类。

  1. orbit单词的翻译为:轨道
  2. controls单词的翻译为:控制权

OrbitControls是鼠标镜头轨道控件,可以通过鼠标来配置镜头的运动轨道,例如 缩放、平移、旋转。也就是说在不修改场景的前提下,可以通过鼠标来改变镜头,以便查看不同角度下的场景。

在手机端,不是鼠标,而是手指滑动

OrbitControls的用法

const controls = new OrbitControls(camera, canvas) //创建一个实例
controls.target.set(0, 5, 0) //controls.target 为镜头的坐标系统
//controls.target.set(0, 5, 0) 的意思是:设置原点 Y 轴的坐标(以高出5米的轨道运行)
controls.update() //使控件使用新目标

请注意,在上面代码中,OrbitControls的构造函数中第2个参数为DOM中的canvas节点,实际上当添加过OrbitControls之后,鼠标在canvas上的 拖拽、鼠标滚轴滚动 等操作都会被捕捉到,并且做出相对应的镜头画面切换。

说直白点,我们终于可以通过鼠标对3D场景进行不同角度,距离的切换操作了。

OrbitControlschange事件:

无论是通过 鼠标或键盘 来修改镜头轨道 都会触发OrbitControlschange事件。

我们可以通过添加 事件监听 来捕获该事件:

const handleChange = () => { ... }

const controls = new OrbitControls(camera, canvasRef.current)
controls.addEventListener('change',handleChange)

对于目前的我们来说,是没有必要使用该事件的,在后续的Three.js技巧篇 中,我们才会运用到change事件。

除此之外,我们还可以设置 禁止缩放、禁止旋转、禁止右键拖拽、设置可旋转角度范围等等一系列配置,具体的可查阅官方文档:https://threejs.org/docs/#examples/zh/controls/OrbitControls

特别注意:

OrbitControls并不是包含在three根目录下,而是位于three/examples/jsm/controls/OrbitControls中,因此引入代码为:

import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'

提醒:在OrbitControls.js中,分别导出有OrbitControlsMapControls,所以引入OribitControls是需要加 大括号{ }

补充说明:

严格意义上讲,OrbitControls并不是Three.js核心包含的代码内容,OrbitControls是将最常见的鼠标与Three.js场景交互内容的一个额外封装。

由于OrbitControls实在是过于频繁使用,最终Three.jsOrbitControls也包含到了Three.js代码包中,只不过不在默认的目录中,而是在Three.js示例目录中。

Three.js早期的版本中,代码包中并未包含OrbitControls,若想使用还需要yarn安装three-orbitcontrols这个包。只不过当后来Three.js包含了OrbitControls之后,才再也无需额外安装了。

补充说明2

原本OrbitControl除了鼠标拖拽可以改变场景视角,还支持键盘上的4个方向键来改变场景视角。

只不过React对于原生DOM事件支持度并不高,React更倾向于给组件添加onKeydown属性处理函数。

本质上 组件的onKeydown相当于React的合成事件。

Three.js的官方示例使用的是原生html + js,是完全支持原生键盘事件的。所以 官网的示例 使用键盘方向键控制场景没有问题,但是在React项目中却不太容易实现。

React中如果想让canvas拥有键盘事件监听,需要做以下2处设置:

  1. useEffect中,当第一次挂载完成,添加canvasRef.current.fouce(),让canvas自动获得焦点
  2. <canvas />中添加tabindex属性,属性值为-1、0、1都无所谓,例如:<canvas tabindex={0} />

只有满足以上2个条件后,canvas才会监听到键盘事件,但是一旦canvas失去焦点,那么就又监听不到了。

还是继续使用 鼠标拖拽 来修改查看场景视角吧

补充说明3

事实上如果你真的需要监听 鼠标键盘方向键 ,其实最简单的办法就是把OrbitControls监听对象修改为document.body上。

- const controls = new OrbitControls(camera, canvasRef.current)
+ const controls = new OrbitControls(camera, document.body)

纹理图片准备:制作纹理图片checker.png

本示例中需要用到一个类似3D场景地面黑白网格的纹理,因此我们需要提前准备好这个纹理图片。

PhotoShop中新建一个 宽高都为2像素的画布,然后:

  1. 左上角和右下角 的那1像素中填充一个较黑的颜色
  2. 右上角和左下角 的那1像素中填充一个较白的颜色
  3. 将图片导出为checker.png,并保存到src/assets/imgs/目录中

补充说明:

虽然制作的纹理图片非常小,只有2像素* 2像素,但是我们可以通过设置纹理的重复,来实现渲染出比较大的 黑白网格底盘。

  1. 设置纹理magFilter的属性值为Three.NearestFilter
  2. 设置纹理wrapSwrapT属性值为Three.RepeatWrapping
  3. 根据 黑白网格的尺寸,计算并设置纹理repeat重复次数

以上3点刚好都是上一节我们讲解 纹理 时学习到的知识点。

灯光示例:HelloLight

先回顾一下Three.js10个 光 的类型:

光的原型(Light) + 6种基础光(AmbientLight) +光探针原型(LightProbe) + 2种环境光探针(AmbientLightProbe、HemisphereLightProbe) = 1 + 6 + 1 + 2 = 10

示例目标:

  1. 使用并体验Three.js中除LightLightProbe之外的其他8种光类型
  2. 使用并体验 光的辅助对象( XxxLightHelper)

补充说明:

  1. 环境光AmbientLight是没有 辅助对象的、其他光都有辅助对象
  2. 矩形光RectAreaLight的辅助对象RectAreaLightHelper和其他光的辅助对象 引入方式不同

代码拆分与梳理:

1、create-scene.ts:创建基础场景

创建src/components/hello-light/create-scene.ts ,导出一个名为createScene的函数,用来专门负责创建基础的场景。

具体代码梳理:

  1. 该场景中包含1个黑白网格的地面、1个立方体、1个球体,但是该场景不包含任何光。

  2. createScene创建基础场景时接收一个参数typetype只能为以下2个值中的其中1种:MESH_PHONE_MATERIALMESH_STANDARD_MATERIAL

  3. type默认值为MESH_PHONE_MATERIAL

进一步解释:

由于RectAreaLight只作用在MeshStandardMaterialMeshPhysicalMaterial材料物体上,所以我们才设置type这个参数。

  1. 当使用RectAreaLight时,我们告知createScene,使用MeshStandardMaterial材质创建 地面、立方体、球体
  2. 当使用 其他 光时,我们告知createScene,使用MeshPhongMaterial材质创建 地面、立方体、球体

2、index.tsx:创建渲染器、镜头、以及不同种类的光

具体代码梳理:

  1. canvas DOM初始化后,创建 渲染器、镜头、镜头交互(OrbitControls)
  2. 创建8个按钮,每个按钮对应一种光
  3. 使用useState创建一个变量type,用来记录当前演示 光的类型
  4. 点击不同按钮后,修改 当前光的类型type的值,从而引发react重新渲染
  5. 在新一轮的渲染中,通过判断type类型,使用createScene创建一个新的场景 和 对应的 光

3、index.scss:设置相关样式

具体样式梳理:

  1. 设置canvas对应的样式
  2. 设置8个按钮对应的样式

具体的代码:

create-scene.ts:

import * as Three from 'three'

export enum MaterialType {
    MESH_PHONE_MATERIAL = 'MESH_PHONE_MATERIAL',
    MESH_STANDARD_MATERIAL = 'MESH_STANDARD_MATERIAL'
}

const createScene: (type?: keyof typeof MaterialType) => Three.Scene = (type = MaterialType.MESH_PHONE_MATERIAL) => {

    const scene = new Three.Scene()

    const planeSize = 40

    const loader = new Three.TextureLoader()
    const texture = loader.load(require('@/assets/imgs/checker.png').default)
    texture.wrapS = Three.RepeatWrapping
    texture.wrapT = Three.RepeatWrapping
    texture.magFilter = Three.NearestFilter
    texture.repeat.set(planeSize / 2, planeSize / 2)

    let planeMat: Three.Material
    let cubeMat: Three.Material
    let sphereMat: Three.Material
    switch (type) {
        case MaterialType.MESH_STANDARD_MATERIAL:
            planeMat = new Three.MeshStandardMaterial({
                map: texture,
                side: Three.DoubleSide
            })
            cubeMat = new Three.MeshStandardMaterial({ color: '#8AC' })
            sphereMat = new Three.MeshStandardMaterial({ color: '#CA8' })
            break
        default:
            planeMat = new Three.MeshPhongMaterial({
                map: texture,
                side: Three.DoubleSide
            })
            cubeMat = new Three.MeshPhongMaterial({ color: '#8AC' })
            sphereMat = new Three.MeshPhongMaterial({ color: '#8AC' })
    }

    const planeGeo = new Three.PlaneBufferGeometry(planeSize, planeSize)
    const mesh = new Three.Mesh(planeGeo, planeMat)
    mesh.rotation.x = Math.PI * -0.5
    scene.add(mesh)

    const cubeGeo = new Three.BoxBufferGeometry(4, 4, 4)
    const cubeMesh = new Three.Mesh(cubeGeo, cubeMat)
    cubeMesh.position.set(5, 2.5, 0)
    scene.add(cubeMesh)

    const sphereGeo = new Three.SphereBufferGeometry(3, 32, 16)
    const sphereMesh = new Three.Mesh(sphereGeo, sphereMat)
    sphereMesh.position.set(-4, 5, 0)
    scene.add(sphereMesh)

    return scene
}

export default createScene

index.tsx:

import { useEffect, useRef, useState } from "react";
import * as Three from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
//import { RectAreaLightUniformsLib } from 'three/examples/jsm/lights/RectAreaLightUniformsLib'
import { RectAreaLightHelper } from "three/examples/jsm/helpers/RectAreaLightHelper";

import createScene, { MaterialType } from "./create-scene";

import "./index.scss";

enum LightType {
  AmbientLight = "AmbientLight",
  AmbientLightProbe = "AmbientLightProbe",
  DirectionalLight = "DirectionalLight",
  HemisphereLight = "HemisphereLight",
  HemisphereLightProbe = "HemisphereLightProbe",
  PointLight = "PointLight",
  RectAreaLight = "RectAreaLight",
  SpotLight = "SpotLight",
}

const buttonLables = [
  LightType.AmbientLight,
  LightType.AmbientLightProbe,
  LightType.DirectionalLight,
  LightType.HemisphereLight,
  LightType.HemisphereLightProbe,
  LightType.PointLight,
  LightType.RectAreaLight,
  LightType.SpotLight,
];

const HelloLight = () => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const sceneRef = useRef<Three.Scene | null>(null);

  const [type, setType] = useState<LightType>(LightType.AmbientLight);

  useEffect(() => {
    if (canvasRef.current === null) {
      return;
    }

    const renderer = new Three.WebGLRenderer({
      canvas: canvasRef.current as HTMLCanvasElement,
    });

    const camera = new Three.PerspectiveCamera(45, 2, 0.1, 1000);
    camera.position.set(0, 10, 20);

    const controls = new OrbitControls(camera, canvasRef.current);
    controls.target.set(0, 5, 0);
    controls.update();

    const scene = createScene();
    sceneRef.current = scene;

    const render = () => {
      if (sceneRef.current) {
        renderer.render(sceneRef.current, camera);
      }
      window.requestAnimationFrame(render);
    };
    window.requestAnimationFrame(render);

    const handleResize = () => {
      const canvas = canvasRef.current;
      if (canvas === null) {
        return;
      }
      camera.aspect = canvas.clientWidth / canvas.clientHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(canvas.clientWidth, canvas.clientHeight, false);
    };
    handleResize();
    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, [canvasRef]);

  useEffect(() => {
    if (sceneRef.current === null) {
      return;
    }

    sceneRef.current = null;

    let newScene: Three.Scene;
    if (type === LightType.RectAreaLight) {
      newScene = createScene(MaterialType.MESH_STANDARD_MATERIAL);
    } else {
      newScene = createScene();
    }
    sceneRef.current = newScene;

    switch (type) {
      case LightType.AmbientLight:
        const ambientLight = new Three.AmbientLight(0xffffff, 1);
        newScene.add(ambientLight);
        break;
      case LightType.AmbientLightProbe:
        const ambientLightProbe = new Three.AmbientLightProbe(0xffffff, 1);
        newScene.add(ambientLightProbe);
        break;
      case LightType.DirectionalLight:
        const directionalLight = new Three.DirectionalLight(0xffffff, 1);
        directionalLight.position.set(0, 10, 0);
        directionalLight.target.position.set(-5, 0, 0);
        newScene.add(directionalLight);
        newScene.add(directionalLight.target);

        const directionalLightHelper = new Three.DirectionalLightHelper(
          directionalLight
        );
        newScene.add(directionalLightHelper);
        break;
      case LightType.HemisphereLight:
        const hemisphereLight = new Three.HemisphereLight(
          0xb1e1ff,
          0xb97a20,
          1
        );
        newScene.add(hemisphereLight);

        const hemisphereLightHelper = new Three.HemisphereLightHelper(
          hemisphereLight,
          5
        );
        newScene.add(hemisphereLightHelper);

        break;
      case LightType.HemisphereLightProbe:
        const hemisphereLightProbe = new Three.HemisphereLightProbe(
          0xb1e1ff,
          0xb97a20,
          1
        );
        newScene.add(hemisphereLightProbe);
        break;
      case LightType.PointLight:
        const pointLight = new Three.PointLight(0xffffff, 1);
        pointLight.position.set(0, 10, 0);
        newScene.add(pointLight);

        const pointLightHelper = new Three.PointLightHelper(pointLight);
        newScene.add(pointLightHelper);
        break;
      case LightType.RectAreaLight:
        //RectAreaLightUniformsLib.init() //实际测试时发现即使不添加这行代码,场景似乎也依然正常渲染,没有看出差异

        const rectAreaLight = new Three.RectAreaLight(0xffffff, 5, 12, 4);
        rectAreaLight.position.set(0, 10, 0);
        rectAreaLight.rotation.x = Three.MathUtils.degToRad(-90);
        newScene.add(rectAreaLight);

        const rectAreaLightHelper = new RectAreaLightHelper(rectAreaLight);
        newScene.add(rectAreaLightHelper);
        break;
      case LightType.SpotLight:
        const spotLight = new Three.SpotLight(0xffffff, 1);
        spotLight.position.set(0, 10, 0);
        spotLight.target.position.set(-5, 0, 0);
        newScene.add(spotLight);
        newScene.add(spotLight.target);

        const spotLightHelper = new Three.SpotLightHelper(spotLight);
        newScene.add(spotLightHelper);
        break;
      default:
        console.log("???");
        break;
    }
  }, [type]);

  return (
    <div className="full-screen">
      <div className="buttons">
        {buttonLables.map((label, index) => {
          return (
            <button
              className={label === type ? "button-selected" : ""}
              onClick={() => {
                setType(label);
              }}
              key={`button${index}`}
            >
              {label}
            </button>
          );
        })}
      </div>
      <canvas ref={canvasRef} />
    </div>
  );
};

export default HelloLight;

create-scene.tsindex.tsx的代码非常多,并且我也没有写注释。
但是如果之前章节中的示例你也都跟着 敲一遍,应该很容易看懂 这两个文件里的代码。

index.scss:

.full-screen, canvas {
    display: block;
    height: inherit;
    width: inherit;
}

.buttons {
    display: flex;
    justify-content: center;
    width: 100%;
    position: fixed;
    top: 30px;
}

.buttons button {
    width: 200px;
    height: 30px;
    margin-left: 20px;
    font-size: 18px;
    cursor: pointer;
}

.buttons button:first-child {
    margin-left: 0;
}

.button-selected{
    background-color:green;
    color: white;
}

若一切正常,实际运行后:

  1. 点击网页中不同顶部的按钮,可以切换不同光对应的场景效果
  2. 点击并拖动鼠标 或 滚动鼠标滚轴,可以切换场景视角

遗留的疑惑

1个疑惑:RectAreaLightUniformsLib

按照官方文档的说法,在使用RectAreaLight时,必须要执行RectAreaLightUniformsLib.init()的,但实际试验发现不执行这行代码也没有任何问题。

为什么会这样?

2个疑惑:RectAreaLightHelper

实际运行发现,RectAreaLightHelper所展示的光源的位置和方向与实际不符。

矩形面灯光的代码为:

const rectAreaLight = new Three.RectAreaLight(0xFFFFFF, 5, 12, 4)
rectAreaLight.position.set(0, 10, 0)
rectAreaLight.rotation.x = Three.MathUtils.degToRad(-90)
newScene.add(rectAreaLight)

const rectAreaLightHelper = new RectAreaLightHelper(rectAreaLight)
newScene.add(rectAreaLightHelper)

我们对rectAreaLight进行了position(位置)rotation(旋转)设置,难道 对应的辅助对象并不会跟随同步变化?

额外的一些唠叨话

我是一边学习Three.js官方教程,一边写本系列文章。

本片文章的源头,对应的原始教程是:https://threejsfundamentals.org/threejs/lessons/threejs-lights.html

因为之前刚好学过 场景,按照当时编写的示例代码:

  1. 月球围绕地球
  2. 地球围绕太阳
  3. 太阳自转

我当时得出了一个结论:每一个scene实例都是一个相对独立的 场景空间,我想当然得认为这里面的独立也包含 场景中的 光

所以最初我想实现 光 示例时这样的:

  1. 一个主场景
  2. 主场景内,23列 分布着6个子场景
  3. 6个子场景里,分别包含这6中基础光源

按照这个目标,我编写了代码,结果渲染后的场景画面,完全不是我预期的,实际结果是:6个子场景上的灯光,完全混合在一起,并不是相互独立的

当时我完全懵的状态。

经过1天的迷惑,查资料、QQ交流群里询问其他人,直至我最终查阅了scene.add()函数源码,才明白过来,我那个结论是错误的。

sceneB.add(lightB)
sceneA.add(sceneB)

以上代码最终真正的执行,会将lightB添加到sceneA中。

尤其假设lightB光的类型为 环境光,而环境光是无处不在的,那么lightB会影响到所有sceneA中的物体。

这也就解释了为什么 “6个子场景上的灯光,完全混合在一起,并不是相互独立的”。

或许你会疑惑?你说的这些不都已经在 Three.js基础之场景.md 一节中讲过了,怎么又说了一遍?

事实是我在进一步理解 场景、光 之后,又重新修改编辑了 之前章节的错误观点,所以你看到的时候都才是正确的。

我是一边学习,一边有新的知识领悟,然后再不断回头修改、补充之前文章中的相关知识点。

我唠叨的这些目的,其实想表达2个事情:

  1. 学习Three.js的 类、函数、方法、属性时候,最好去看一下three.js的源码,绝对会加深你的Three.js理解和功力。
  2. 学习Three.js真的挺难,需要不断打破已有认知,若有些地方暂时无法理解也没关系,只要继续加油,终会搞明白的。

唠叨结束。

本文学习了 光(Light),按道理接下来应该学习 阴影(LightShadown),但是下节我们先学习 镜头(Camera),学完之后再回过头学习 阴影。

明天是20201219日,阿里巴巴 前端D2技术分享大会开幕,19号、20号 为期2天的前端技术直播会议分享。

98元买的直播观看门票,不能浪费了,所以,换换脑子,未来2天暂停Three.js的学习。

上一页
下一页