Three.js 笔记文档(一)

224 阅读30分钟

Three.js学习

1-1 使用Parcel搭建three.js开发环境

parcel:极速零配置Web应用打包工具

  • 安装:npm install parcel-bundler --save-dev

  • 配置运行命令:

    {
        "script": {
            ## your entry file:一般是src/index.html
            "dev": "parcel <your entry file>",
            "build": "parcel build <your entry file>"
        }
    }
    
  • 运行:npm run dev

  • 安装three.js:npm i three --save

  • 文件结构

1720410800833.png

  • index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Document</title>
      <link rel="stylesheet" href="./assets/css/style.css">
    </head>
    <body>
      
    </body>
    <script src="./mian/main.js"></script>
    </html>
    
  • css/style.css

    * {
      padding: 0;
      margin: 0;
    }
    ​
    body {
      background: skyblue;
    }
    
  • main/main.js

    import * as THREE from "three";
    ​
    console.log(THREE);
    

1720411361615.png

1-2 使用three.js渲染第一个场景和物体

  • 使用:相机+场景+渲染器

  • 目标:了解three.js的基础内容

  • 透视相机:PerspectiveCamera(fov: Number, aspect: Number, near: Number, far: Number)

    • fov — 摄像机视椎体垂直视野角度
    • aspect — 摄像机视椎体长宽比
    • near — 摄像机视椎体进端面
    • far — 摄像机视椎体远端面

    这些参数一起定义了摄像机的viewing frustum(视椎体)

  • 完整渲染场景的案例

import * as THREE from "three";
​
// 1 创建场景
const scene = new THREE.Scene();
​
// 2 创建相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
​
// 3 设置相机位置
camera.position.set(0, 0, 10);
​
// 4 将相机添加到场景当中
scene.add(camera);
​
// 5 添加物体
// 创建几何体对象(BoxGeometry: 立方体)
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
​
// 设置几何体的材质(MeshBasicMaterial: 基础网格材质)
const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
​
// 根据几何体和材质创建物体(Mesh: 网格)
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
​
// 添加几何体至场景中
scene.add(cube);
​
// 6 渲染
// 初始化渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染器的尺寸和大小
renderer.setSize(window.innerWidth, window.innerHeight);
​
// 7 将webgl渲染的canvas内容添加到body当中
document.body.appendChild(renderer.domElement);
​
// 8 使用渲染器通过相机将场景渲染
renderer.render(scene, camera);
  • 结果

    1720438642582.png

1-3 如何处理运行搭建Three环境出现的问题

1720438740566.png

由于版本更新造成的。

1-将 .cache .parcel-caceh node_modules yarn.lock package-json.lock 等都删除

2-查看parcel版本

yarn add --dev -arcel

1-4 结合vue开发three.js

1 vue create threeapp
2 npm i three / yarn add three
3 启动项目

APP.vue文件

<template>
  <div></div>
</template><script setup>
import * as THREE from "three";
​
// console.log(THREE);// 目标:了解three.js最基本的内容// 1、创建场景
const scene = new THREE.Scene();
​
// 2、创建相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
​
// 设置相机位置
camera.position.set(0, 0, 10);
scene.add(camera);
​
// 添加物体
// 创建几何体
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
// 根据几何体和材质创建物体
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
// 将几何体添加到场景中
scene.add(cube);
​
// 初始化渲染器
const renderer = new THREE.WebGLRenderer();
// 设置渲染的尺寸大小
renderer.setSize(window.innerWidth, window.innerHeight);
// console.log(renderer);
// 将webgl渲染的canvas内容添加到body
document.body.appendChild(renderer.domElement);
​
// 使用渲染器,通过相机将场景渲染进来
renderer.render(scene, camera);
</script><style>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}
​
canvas {
  width: 100vw;
  height: 100vh;
  position: fixed;
  left: 0;
  top: 0;
}
</style>
  • 运行项目即可

2-1 轨道控制器(OrbitControls)查看物体

上次实现的是一个平面的,现在开始进行立体的学习。可以使用轨道控制器。

  • 轨道控制器:使得 相机 围绕 目标 进行轨道运动

  • 目标:使用控制器查看3D物体

  • 代码实现-鼠标可以操作物体

    // 1 导入轨道控制器
    import { OrbitControls } from "three/examples/jsm/controls/orbitcontrols";
    ​
    // 2 创建轨道控制器
    const controls = new OrbitControls(camera, renderer.domElement);
    ​
    // 3 设置渲染函数
    function render() {
      renderer.render(scene, camera);
      // 渲染下一帧的时候就会调用render函数
      requestAnimationFrame(render);
    }
    ​
    // 4 初始调用渲染函数
    render();
    

    1720440106383.png

    1720440174221.png

2-2 添加坐标轴辅助器

  • AxesHelper

    • 用于简单模拟3个坐标轴的对象
    • 红色代表X轴,绿色代表Y轴,蓝色代表Z轴
  • 示例

    const axesHelper = new THREE.AxesHelper(5);
    scene.add(axesHelper);
    
  • 构造函数

    • AxesHelper(size: Number)
    • size -- (可选的)表示坐标轴的线段长度,默认为1

1720440498990.png

2-3 设置物体移动

  • 目标:控制3D物体移动
console.log(cube);

1720440773291.png

可以通过position修改物体的位置

  • position:Vector3(三维向量对象)。默认值(0, 0, 0)

  • set(x: Float, y: Float, z: Float): this

    设置该向量的x、y和z分量

  • 代码示例

    // 将cube的位置修改到(5, 0, 0)
    // 1 使用set
    cube.position.set(5, 0, 0);
    ​
    // 2 直接修改position的x值
    cube.position.x = 5;
    
  • 利用render函数使物体自动以每一步0.01的距离移动,当超过最大值5的时候,将其重置为0

    // 渲染函数
    function render() {
      cube.position.x += 0.01;
      if (cube.position.x > 5) {
        cube.position.x = 0;
      }
      renderer.render(scene, camera);
      // 渲染下一帧的时候就会调用render函数
      requestAnimationFrame(render);
    }
    

2-4 物体的缩放与旋转

设置物体的缩放

1720441698198.png

  • scale:Vector3

    // 缩放
    cube.scale.set(3, 2, 1);
    ​
    // 或者
    cube.scale.x = 3;
    
设置物体的旋转

1720441966672.png

  • Rotation:Euler(欧拉角对象)

    • Euler(x: Float, y: Float, z: Float, order: String)

    • 物体的局部旋转,以弧度来表示

    • 属性(均为可选):

      • order:String
      • 设置旋转顺序:XYZ(默认)、ZXY、YXZ等等,必须是大写字母
      • x:Float
      • y:Float
      • z:Float
    • 通过set方式直接修改:

      • set(x: Float, y: Float, z: Float, order: String)
    // 旋转
    cube.rotation.set(Math.PI / 4, 0, 0, "XYZ");
    ​
    // 或者
    cube.rotation.x = Math.PI / 4;
    ​
    // 也可以在render函数中形成动画
    // 渲染函数
    // 从坐标轴看旋转是逆时针,从原点看是顺时针
    function render() {
      cube.position.x += 0.01;
      cube.rotation.x += 0.01;
      if (cube.position.x > 5) {
        cube.position.x = 0;
      }
      renderer.render(scene, camera);
      // 渲染下一帧的时候就会调用render函数
      requestAnimationFrame(render);
    }
    

    微信截图_20240709120357.png

2-5 应用requestAnimationFrame正确处理动画运动

优化性能,渲染速度以及时间间隔是否一致?

// 调用render函数会自动传一个time,打印一下time
function render(time) {
  console.log(time);
  cube.position.x += 0.01;
  if (cube.position.x > 5) {
    cube.position.x = 0;
  }
  renderer.render(scene, camera);
  // 渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}
  • 打印结果

    1720498336640.png

上面打印的是每一帧渲染的毫秒数,每一帧之间间隔的时间不一致,这样就渲染的时快时慢

可以根据时间去走:这里我的理解是,过了多少时间,按时间的数量走距离

function render(time) {
  // 时间一直在走,大于最大距离5的时候,对时间取余,距离也就从0开始
  let t = (time / 1000) % 5;
  cube.position.x = t * 1;
  
  renderer.render(scene, camera);
  // 渲染下一帧的时候就会调用render函数
  requestAnimationFrame(render);
}

2-6 通过Clock跟踪时间处理动画

  • Clock对象:用于跟踪时间

    • Clock(autoStart: Boolean)

      autoStart——(可选)是否要自动开启时钟。默认true

  • 属性

    • autoStart: Boolean

      true:在第一次update是开启时钟,默认true

    • startTime: Float

      存储时钟最后一次调用start方法的时间

    • oldTime: Float

      存储时钟最后一次调用start, getElapsedTime或getDelta方法的时间

    • elapsedTime: Float

      保存时钟运行的总时长

    • running: Boolean

      判断时钟是否在运行

  • 方法

    • start(): null

      启动时钟。同时将startTime和oldTime设置为当前时间。设置elapsedTime为0,且设置running为true

    • stop(): null

      停止时钟。同时将oldTime设置为当前时间

    • getElapsedTime(): Float

      获取自时钟启动后的秒数,同时将oldTime设置为当前时间

      如果autoStart设置为true且时钟未运行,则该方法同时启动时钟

    • getDelta(): Float

      获取自oldTime设置后到当前的秒数。同时将oldTime设置为当前时间

      如果autoStart设置为true且时钟未运行,则该方法同时启动时钟

  • 获取一下时间(打印的都是秒数)

    const clock = new THREE.Clock();
    ​
    // 渲染函数
    function render() {
      // 获取时钟运行的总时长
      // let time = clock.getElapsedTime();
      // 获取时间间隔
      let delta = clock.getDelta();
      // console.log(time);
      console.log('获取时间间隔:', delta);
    ​
      // let t = (time / 1000) % 5;
      // cube.position.x = t * 1;
    ​
      renderer.render(scene, camera);
      // 渲染下一帧的时候就会调用render函数
      requestAnimationFrame(render);
    }
    

    1720501904485.png

  • 实现上一个案例实现的效果

    const clock = new THREE.Clock();
    ​
    // 渲染函数
    function render() {
      // 获取时钟运行的总时长(秒)
      let time = clock.getElapsedTime();
      let t = time % 5;
      cube.position.x = t * 1;
    ​
      renderer.render(scene, camera);
      // 渲染下一帧的时候就会调用render函数
      requestAnimationFrame(render);
    }
    

2-7 Gsap动画库基本使用与原理(Gsap-补间动画)

  • 目标:掌握各种补间动画效果

  • 安装

    npm install gsap
    
  • 使用动画库

    // 1 导入gsap
    import gsap from 'gsap';
    ​
    // 2 设置动画
    // cube 由0移动到5,持续5秒
    gsap.to(
        cube.position, // 动画目标
        { 
            x: 5, // 动画目标的属性
            duration: 5, // 动画持续时长
            ease: "power1.inOut" // 动画运行速率 慢-快-慢
        }
    );
    // cube 旋转360度,持续5秒
    gsap.to(
        cube.rotation, 
        { 
            x: 2 * Math.PI, 
            duration: 5, 
            ease: "power1.inOut" 
        }
    );
    

2-8 Gsap控制动画属性与方法

  • Gsap控制动画的属性

    • duration:动画持续时间
    • ease:动画运行速率
    • repeat:动画重复次数(-1为无数次)
    • yoyo:往返运动
    • delay:延迟时长
  • 方法

    • onStart:动画开始时执行
    • onComplete:动画结束时执行
  • 代码实现

    // 设置cube的运动
    gsap.to(cube.position, {
      x: 5,
      duration: 5,
      ease: "power1.inOut",
      repeat: -1,
      yoyo: true,
      delay: 2,
    ​
      onStart: () => {
        console.log("动画开始");
      },
      onComplete: () => {
        console.log("动画完成");
      },
    });
    
  • 实现鼠标双击暂停和恢复动画

    var animation1 = gsap.to(……);
    ​
    window.addEventListener("dblclick", () => {
      // 如果动画正在运行,则停止 isActive() 获取动画当前状态
      if (animation1.isActive()) {
        animation1.pause(); // pause()方法可以停止动画
      } else {
        animation1.resume(); // resume()方法可以恢复动画
      }
    });
    

2-9 根据尺寸变化实现自适应画面

  • 设置控制器的阻尼效果

    • enableDamping: Boolean

      将其设置为true以启用阻尼(惯性),这将给控制器带来重量感。默认值false。

      请注意,如果该值被启用,将必须在动画循环中调用.update()。

    // 1 设置控制器属性
    // (之前写的)9 创建轨道控制器
    const controls = new OrbitControls(camera, renderer.domElement);
    // 设置控制器阻尼,使控制器更有真实的效果
    controls.enableDamping = true;
    ​
    // 2 在动画循环中调用
    // 渲染函数
    function render() {
      controls.update(); // 调用控制器的更新
      renderer.render(scene, camera);
      // 渲染下一帧的时候就会调用render函数
      requestAnimationFrame(render);
    }
    
  • 实现屏幕尺寸变化自适应

    • 不是自适应屏幕大小变化的时候

    1720581697450.png

    • 实现屏幕自适应
    // 监听画面变化,更新渲染画面
    window.addEventListener("resize", () => {
      // 1 更新摄像头-视椎体的长宽比
      camera.aspect = window.innerWidth / window.innerHeight;
    ​
      // 2 更新摄像机的投影矩阵
      camera.updateProjectionMatrix();
    ​
      // 3 更新渲染器
      renderer.setSize(window.innerWidth, window.innerHeight);
    ​
      // 4 设置渲染器的像素比例
      renderer.setPixelRatio(window.devicePixelRatio);
    });
    

    这样就不会出现上面的问题

    Camera在大多数属性发生改变后,需要调用.updateProjectionMatrix使得这些改变生效

2-10 调用js接口控制画布全屏和退出全屏

// 双击全屏
window.addEventListener("dblclick", () => {
    const fullScreenElement = document.fullScreenElement;
    if (fullScreenElement) {
        // 让画布对象进入全屏
        renderer.domElement.requestFullscreen();
    } else {
        // 直接使用文档元素退出全屏
        domElement.exitFullscreen();
    }
});

2-11 应用图形用户界面更改变量

(1)安装gui库

npm install --save dat.gui

(2)导入gui

import * as dat from "dat.gui";

(3)初始化gui界面

const gui = new dat.GUI();

(4)创建gui设置

/** 
 * 给cube的位置的x坐标设置gui
 * (1)设置最小值为0
 * (2)设置最大值为5
 * (3)设置步进为0.01
 * (4)设置名称
 * (5)值被修改时的回调函数
 */
gui
    .add(cube.position, "x")
    .min(0)
    .max(5)
    .step(0.01)
    .name("移动x轴坐标")
    .onChange((value)=> {
        console.log("值被修改为:", value);
    }).onFinishChange((value) => {
        console.log("完全停下来", value)
    });
​
// 修改物体的颜色
const params = {
    color: "#ffff00",
    fn: () => {
        // 让立方体运动起来
        gsap.to(cube.position, {x: 5}, duration: 2, yoyo: true, repeat: -1);
    }
}
gui.addColor(params, "color").onChange((value) => {
    console.log("值被修改", value);
    cube.material.color.set(value);
});
​
// 设置选项框
gui.add(cube, "visible").name("是否显示");
​
// 点击触发某个事件
// gui.add(params, "fn").name("立方体运动");// 设置文件夹
var folder = gui.addFolder("设置立方体");
folder.add(cube.material, "wireframe");
folder.add(params, "fn").name("立方体运动");

3-1 掌握几何体顶点_UV_法向属性

  • 几何体(以BoxGeometry为例)

    • 打印几何体的信息
    console.log(cubeGeometry);
    console.log(cube);
    

    1720583220646.png

    以上可以看到:

    BoxGeometry对象也可以通过cube.geometry来获取

    BoxGeometry有一些attributes(属性)

  • 几何体的属性

    • position

      1720583763229.png

    • uv

      1720584032689.png

      1720584295093.png

    • normal 法向

      在光打过来的时候,我们需要知道立方体面得朝向,法向表述的就是面得朝向

    • 细分程度

      1720584510328.png

      1720584522956.png

      3个顶点可以组成一个面,立方体都是由多个三角形组成的,可以将面细分成多个三角形,然后根据三角形的顶点,可以选择让某个面去凹陷

3-2 BufferGeometry设置顶点创建矩形

// 添加物体
const geometry = new THREE.BufferGeometry();
​
// 创建顶点(用两个三角形组成一个面)
const vertices = new Float32Array([
  -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0,
  1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0,
]);
​
// 将创建的顶点设置为物体的位置属性
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
​
// 设置几何体的材质
const Material = new THREE.MeshBasicMaterial({color: 0xffff00});
​
// 设置物体
const mesh = new THREE.Mesh(geometry, Material);
​
// 将物体添加至场景中
scene.add(mesh);

1720598522157.png

3-3 生成酷炫自定义三角形科技物体

将创建三角形面的操作放在循环中

for (let i = 0; i < 50; i++) {
  // 每一个三角形需要3个顶点,每个顶点需要3个值
  const geometry = new THREE.BufferGeometry();
  // 每一个三角形面的顶点数据有9个值,Float32Array的参数必须要设置为9,代表数组长度为9,否则设置不上
  const positionArray = new Float32Array(9);
  for (let j = 0; j < 9; j++) {
    // 设置图形的横跨区域为-5~5
    positionArray[j] = Math.random() * 10 - 5;
  }
  // 为创建的三角形面设置位置属性
  geometry.setAttribute(
    "position",
    new THREE.BufferAttribute(positionArray, 3)
  );
  // 创建颜色对象,为材质设置颜色,设为随机颜色
  let color = new THREE.Color(Math.random(), Math.random(), Math.random());
  // 设置几何体的材质(MeshBasicMaterial: 基础网格材质)
  const material = new THREE.MeshBasicMaterial({
    color: color,
    // 设置透明度时,transparent的值必须为true
    transparent: true,
    // 透明度的值
    opacity: 0.5,
  });
  // 创建物体
  const mesh = new THREE.Mesh(geometry, material);
  // 添加物体至场景
  scene.add(mesh);
}

1720603625404.png

3-4 常用网格几何体

管网查看:threejs.org/docs/#api/z…

4-1 初识材质与纹理

  • 基础网格材质(MeshBasicMaterial)

  • 立方体有可能是一个包装盒,一个烟盒等等,他们表面的材质纹理是不一样的

  • 纹理图片

    1720607525111.png

  • 为立方体设置材质与纹理

    // 6 创建纹理加载器
    const textureLoader = new THREE.TextureLoader();
    // 7 加载纹理文件
    const doorColorTexture = textureLoader.load("./textures/door/color.jpg");
    ​
    // 1 添加物体
    const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
    // 2 将物体设置为基础网格材质
    const basicMaterial = new THREE.MeshBasicMaterial({
      color: "#ffff00",
      // 8 将加载的纹理设置到材质上
      map: doorColorTexture,
    });
    // 3 根据立方体和材质创建物体
    const mesh = new THREE.Mesh(cubeGeometry, basicMaterial);
    // 4 将物体添加至场景中
    scene.add(mesh);
    
  • 实现效果

    1720607559525.png

4-2 纹理_偏移_旋转_重复

  • 偏移

    doorColorTexture.offset.x = 0.5;
    doorColorTexture.offset.y = 0.5;
    ​
    // 也可写为
    doorColorTexture.offset.set(0.5, 0.5);
    

    1720607949933.png

  • 旋转

    // 设置旋转的原点为中心位置
    doorColorTexture.center.set(0.5, 0.5);doorColorTexture.rotation = Math.PI / 4;
    
    • 旋转

      1720667715123.png

    • 中心位置旋转

      1720667753425.png

  • 重复

    • repeat: Vector2

      决定纹理在表面的重复次数,两个方向分别表示U和V

    • 代码实现

      // 设置重复
      doorColorTexture.repeat.set(2, 3);
      

      1720668647223.png

    • 如果repeat的重复次数在任何方向上设置了超过1的数值, 对应的Wrap需要设置为THREE.RepeatWrapping或者THREE.MirroredRepeatWrapping来 达到想要的平铺效果

      • wrapS: number

        定义了纹理贴图在水平方向上将如何包裹,在UV映射中对应U默认值THREE.ClampToEdgeWrapping,即纹理边缘将被推到外部边缘的纹素

        其它两个值分别为THREE.RepeatWrapping(重复) THREE.MirroredRepeatWrapping(镜像重复)

      • wrapT: number

        定义了纹理贴图在垂直方向上将如何包裹,在UV映射中对应于V

      // 设置重复
      doorColorTexture.repeat.set(2, 3);
      // 设置纹理重复的模式
      doorColorTexture.wrapS = THREE.RepeatWrapping;
      doorColorTexture.wrapT = THREE.RepeatWrapping;
      

      1720668921689.png

4-3 设置纹理显示算法与mipmap

  • 设置纹理效果

    const texture = textureLoader.load("./textures/minecraft.png");
    ​
    const basicMaterial = new THREE.MeshBasicMaterial({
      color: "#ffff00",
      map: texture,
    });
    

    1720683338701.png

  • magFilter: number 当一个纹素覆盖大于一个像素时,贴图将如何采样

    默认值为THREE.LinearFilter, 它将获取四个最接近的纹素,并在他们之间进行双线性插值

    THREE.NearestFilter,它将使用最接近的纹素的值

  • minFilter: number 当一个纹素覆盖小于一个像素时,贴图将如何采样

    默认值为THREE.LinearMipmapLinearFilter, 它将使用mipmapping以及三次线性滤镜

  • 将纹理的采样值均换成 THREE.NearestFilter

    texture.minFilter = THREE.NearestFilter;
    texture.magFilter = THREE.NearestFilter;
    

    1720683418733.png

4-4 透明材质与透明纹理

  • 为4-1中的门设置透明区域

    下图(alpha.jpg)白色区域为不透明,黑色区域为透明,灰色的为半透明

alpha.jpg

  • 设置透明材质(个人理解:类似于设计中所说的蒙版,白色的为蒙版区域)

    • alphaMap: Texture

      alpha贴图是一张灰度纹理,用于控制整个表面的不透明度。(黑色:完全透明;白色:完全不透明)。 默认值为null

    • transparent: Boolean

      定义此材质是否透明。这对渲染有影响,因为透明对象需要特殊处理,并在非透明对象之后渲染 设置为true时,通过设置材质的opacity属性来控制材质透明的程度

      设置为true时,透明材质才会起效果

    // 加载透明材料
    const doorAlphaTexture = textureLoader.load("./textures/door/alpha.jpg");
    // 为材质添加透明设置
    const basicMaterial = new THREE.MeshBasicMaterial({
      color: "#ffff00",
      map: doorColorTexture,
      alphaMap: doorAlphaTexture,
      transparent: true
    });
    ​
    // 创建一个平面
    const plane = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), basicMaterial);
    plane.position.set(3, 0, 0);
    scene.add(plane);
    

    1720750363016.png

  • 设置side

    • side: Integer

      定义将要渲染哪一面 - 正面,背面或两者

      THREE.FrontSide 正面(默认)

      THREE.BackSide 背面

      THREE.DoubleSide 双面

    // 加载透明材料
    const doorAlphaTexture = textureLoader.load("./textures/door/alpha.jpg");
    // 为材质添加透明设置
    const basicMaterial = new THREE.MeshBasicMaterial({
      color: "#ffff00",
      map: doorColorTexture,
      alphaMap: doorAlphaTexture,
      side: THREE.DoubleSide, // 设置为双面的
      transparent: true
    });
    ​
    // 创建一个平面
    const plane = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), basicMaterial);
    plane.position.set(3, 0, 0);
    scene.add(plane);
    

    1720750392282.png

    设置side等属性,也可单独设置。以side为例:

    basicMaterial.side = THREE.DoubleSide;
    

4-5 环境遮挡贴图与强度

  • 使用图片

    ambientOcclusion.jpg

    和透明材质的原理差不多,只不过白色是透明的,黑色是不透明的。

    • 代码实现

      // 1 导入环境遮挡贴图
      const doorAoTexture = textureLoader.load(
        "./textures/door/ambientOcclusion.jpg"
      );
      ​
      // 材质
      const basicMaterial = new THREE.MeshBasicMaterial({
        color: "#ffff00",
        map: doorColorTexture,
        alphaMap: doorAlphaTexture,
        side: THREE.DoubleSide,
        transparent: true,
        aoMap: doorAoTexture, // 2 环境遮挡贴图
        aoMapIntensity: 1, // 2 环境遮挡贴图强度
      });
      ​
      const cube = new THREE.Mesh(cubeGeometry, basicMaterial);
      scene.add(cube);
      // 3 为cube设置第二组UV
      cubeGeometry.setAttribute(
        "uv2",
        new THREE.BufferAttribute(cubeGeometry.attributes.uv.array, 2)
      );
      

      1721009417097.png

4-6 详解PBR物理渲染

1 什么是PBR
  • 基于物理渲染
  • 以前的渲染是在模仿灯光的外观
  • 现在是模仿光的实际行为
  • 使图形看起来更真实
2 PBR的组成部分

1721010430532.png

  • 灯光属性:直接照明、间接照明、直接高光、间接高光、阴影、环境光闭塞
  • 表面属性:基础色、法线、高光、粗糙度、金属度
3 灯光属性
1、光线类型

1721010715592.png

  • 入射光

    • 直接照明:直接从光源反射阴影物体表面的光
    • 间接照明:环境光和直接光经过反弹第二次进入的光
  • 反射光

    • 镜面光:在经过表面反射聚焦在同一方向上进入人眼的高亮光
    • 漫反射:光被散射并沿着各个方向离开表面
2、光与表面相互作用类型

1721014223434.png

  • 直接漫反射:从源头到四面八方散发出来的直接高光
  • 直接高光:直接来自光源并被集中反射的光
  • 间接漫反射:来自环境的光被表面散射的光
  • 间接高光:来自环境光并被集中反射的光

(1)间接漫反射

  • 1721014993803.png
    • 直接来自光源的光
    • 撞击表面后散落在各个方向
    • 在着色器中使用简单的数学计算

(2)直接高光

  • 1721015134989.png
    • 直接来自光源的光

    • 反射在一个更集中的方向上

    • 在着色器中使用简单的数学计算

      直接镜面反射的计算成本比漫反射低很多

(3)间接漫反射

  • 1721015244646.png
    • 来自环境中各个方向的光
    • 撞击表面后散落在各个方向
    • 因为计算昂贵,所以引擎的全局照明解决方案通常胡离线渲染,并被烘焙成灯光地图

(4)镜面反射

  • 1721015347571.png
    • 来自环境中各个方向的光
    • 反射在一个更集中的方向上
    • 引擎中使用反射探头,平面反射,SSR,或射线追踪计算
4 灯光属性
1、基础色
  • 1721015503040.png

    • 定义表面的漫反射颜色
    • 真实世界的材料不会比20暗或比240 sRGB亮
    • 粗糙表面具有更高的最低 ~ 50 sRGB
    • 超出范围的值不能正确发光,所以保持在范围内是至关重要的
  • 1721015639652.png

    • 基础色贴图制作注意点:

      • 不包括任何照明或阴影
      • 基本颜色纹理看起来应该非常平坦
      • 使用真实世界的度量或获取最佳结果的数据
2、法线
  • 1721015943674.png
    • 定义曲面的形状每个像素代表一个矢量
    • 该矢量指示表面所面对的方向即使网格是完全平坦的
    • 法线贴图会使表面显得凹凸不平
    • 用于添加表面形状的细节,这里的三角形是实现不了的
    • 因为它们表示矢量数据,所以法线贴图是无法手工绘制的
3、镜面
  • 1721016158206.png

    • 用于直接和间接镜面照明的叠加
    • 当直视表面时,定义反射率
    • 非金属表面反射约4%的光
    • 0.5代表4%的反射
    • 1.0代表8%的反射但对于大多数物体来说太高了
    • 在掠射角下,所有表面都是100%反射的,内置于引擎中的菲涅耳项
  • 1721016350181.png

    • 镜面贴图制作注意点:

      • 高光贴图应该大多在0.5
      • 使用深色的阴影来遮盖不应该反光的裂缝
      • 一个裂缝贴图乘以0.5就是一个很好的高光贴图
4、粗糙度
  • 1721016498390.png
    • 粗糙度贴图制作注意点:

      • 没有技术限制-完全艺术的选择
      • 艺术家可以使用这张地图来定义表面的“特征”,并展示它的历史
      • 考虑一下被打磨光滑、磨损或老化的表面
5、金属度
  • 1721016533634.png

    • 两个不同的着色器通过金属度混合他们
    • 基本色变成高光色而不是漫反射颜色
    • 金属漫反射是黑色的
    • 在底色下,镜面范围可达100%
    • 大多数金属的反光性在60%到100%之间
    • 确保对金属颜色值使用真实世界的测量值,并保持它们明亮
    • 当金属为1时,镜面输入将被忽略

    1721017028123.png

    粗糙度贴图制作注意点:

    • 将着色器切换到金属模式
    • 灰度值会很奇怪,最好使用纯白色或黑色
    • 当金属色为白色时,请确保使用正确的金属底色值
    • 没有黑暗金属这回事
    • 所有金属均为180srgb或更亮
5 金属和非金属
  • 1721017144670.png 非金属

  • 基础颜色=漫反射

  • 镜面反射=0-8%

金属

  • 基础颜色=0-100%的镜面反射
  • 镜面=0%
  • 漫反射总是黑色的
6 总结

1.PBR是基于物理渲染的着色模型,PBR着色模型分为材质灯光 两个属性

2.材质部分由: 基础色法线高光粗糙度金属度 来定义材质表面属性的

3.灯光部分是由: 直接照明间接照明直接高光间接高光阴影环境光闭塞 来定义照明属性的

4.通常我们写材质的时候只需要关注材质部分的属性即可,灯光属性都是引擎定义好的直接使用即可

5.PBR渲染模型不但指的是PBR材质,还有灯光,两者缺一不可

4-7 标准网格材质与光照物理效果

(1)MeshStandardMaterial - 标准网格材质

  • 特点

    • 之前 MeshBasicMaterial(基础网格材质)用到的属性均有
    • 必须要有光照才能显示,否则显示的是黑色的
  • 将之前的门改为标准网格材质

    // 标准材质
    const material = new THREE.MeshStandardMaterial({
      color: "#ffff00",
      map: doorColorTexture,
      alphaMap: doorAlphaTexture,
      side: THREE.DoubleSide,
      transparent: true,
      aoMap: doorAoTexture,
      aoMapIntensity: 1,
    });
    ​
    // 添加物体
    const cube = new THREE.Mesh(cubeGeometry, material);
    scene.add(cube);
    

    1721026574702.png

    能看到黑色的影子,但是看不清是什么

(2)DirectionalLight - 直线光/平行光

  • 特点:

    • 平行光是沿着特定方向发射的光
    • 第二个参数是光的强度,默认是1
    • 光的表现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光的效果
    • 只能从一个方向照射,从其它方向看可能就看不见光
    • position:设置平行光的方向
  • 为门设置平行光

    // 创建平行光
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
    // 设置平行光位置
    directionalLight.position.set(10, 10, 10);
    // 添加平行光
    scene.add(directionalLight);
    
    • 正面

      1721026942515.png

    • 背面

      1721026960702.png

(3)Ambient - 环境光

  • 特点

    • 会均匀的照亮场景中的所有物体
    • 环境光不能用来投射阴影,因为它没有方向
  • 为门创建环境光

    // 设置环境光
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);
    
    • 正面

      1721027158996.png

    • 背面

      1721027170123.png

4-8 置换贴图与顶点细分设置

  • displacementMap:Texture

    • 位移贴图会影响网格顶点的位置,与仅影响材质的光照和阴影的其他贴图不同,移位的顶点可以投射阴影,阻挡其他对象, 以及充当真实的几何体。位移纹理是指:网格的所有顶点被映射为图像中每个像素的值(白色是最高的),并且被重定位
    • 本案例中用于设置门表面的高低不平
  • displacementScale:Float

    • 位移贴图对网格的影响程度(黑色是无位移,白色是最大位移)
    • 如果没有设置位移贴图,则不会应用此值。默认值为1
  • 为门设置置换贴图

    // 1 导入置换纹理
    const doorHeightTexture = textureLoader.load("./textures/door/height.jpg");
    ​
    // 3 设置物体的细分程度
    const cubeGeometry = new THREE.BoxGeometry(1, 1, 1, 100, 100, 100);
    ​
    // 材质
    const material = new THREE.MeshStandardMaterial({
      color: "#ffff00",
      map: doorColorTexture,
      alphaMap: doorAlphaTexture,
      side: THREE.DoubleSide,
      transparent: true,
      aoMap: doorAoTexture,
      aoMapIntensity: 1,
      displacementMap: doorHeightTexture, // 2 设置置换贴图(只设置这个顶点不会凸起,门还是平的)
      displacementScale: 0.1, // 4 设置位移贴图对网格的影响程度(默认是1,因为顶点有100份,所以设置0.1)
    });
    

    1721028619162.png

4-9 设置粗糙度与粗糙度贴图

  • roughness: Float

    • 材质的粗糙程度
    • 0.0表示平滑的镜面反射,1.0表示完全漫反射。默认值为1.0
    • 如果还提供roughnessMap,则两个值相乘
  • roughnessMap: Float

  • 该纹理的绿色通道用于改变材质的粗糙度

  • 为门设置粗糙度贴图

    // 1 导入粗糙度纹理
    const roughnessTexture = textureLoader.load("./textures/door/roughness.jpg");
    ​
    const material = new THREE.MeshStandardMaterial({
      color: "#ffff00",
      map: doorColorTexture,
      alphaMap: doorAlphaTexture,
      side: THREE.DoubleSide,
      transparent: true,
      aoMap: doorAoTexture,
      aoMapIntensity: 1,
      displacementMap: doorHeightTexture,
      displacementScale: 0.1,
      roughness: 1, // 2 设置粗糙程度
      roughnessMap: roughnessTexture, // 3 设置粗糙度纹理
    });
    

    1721029511464.png

    门反射光,金属锁扣部分反射光

4-10 设置金属度与金属贴图

  • metalness: Float

    • 材质与金属的相似度
    • 非金属材质,如木材或石材,使用0.0,金属使用1.0,通常没有中间值。 默认值为0.0
    • 0.0到1.0之间的值可用于生锈金属的外观。如果还提供了metalnessMap,则两个值相乘
  • metalnessMap: Texture

    • 该纹理的蓝色通道用于改变材质的金属度
  • 设置金属贴图

    // 1 导入金属纹理
    const metalnessTexture = textureLoader.load("./textures/door/metalness.jpg");
    ​
    const material = new THREE.MeshStandardMaterial({
      color: "#ffff00",
      map: doorColorTexture,
      alphaMap: doorAlphaTexture,
      side: THREE.DoubleSide,
      transparent: true,
      aoMap: doorAoTexture,
      aoMapIntensity: 1,
      displacementMap: doorHeightTexture,
      displacementScale: 0.1,
      roughness: 1,
      roughnessMap: roughnessTexture,
      metalness: 1, // 2 设置金属度
      metalnessMap: metalnessTexture, // 3 设置金属贴图
    });
    

    1721030100835.png

4-11 法线贴图应用

  • normalMap: Texture

    • 创建法线贴图的纹理
    • RGB值会影响每个像素片段的曲面法线,并更改颜色照亮的方式
    • 法线贴图不会改变曲面的实际形状,只会改变光照
  • 设置法线贴图

    // 1 导入法线贴图文件
    const normalTexture = textureLoader.load("./textures/door/normal.jpg");
    ​
    const material = new THREE.MeshStandardMaterial({
      color: "#ffff00",
      map: doorColorTexture,
      alphaMap: doorAlphaTexture,
      side: THREE.DoubleSide,
      transparent: true,
      aoMap: doorAoTexture,
      aoMapIntensity: 1,
      displacementMap: doorHeightTexture,
      displacementScale: 0.1,
      roughness: 1,
      roughnessMap: roughnessTexture,
      metalness: 1,
      metalnessMap: metalnessTexture,
      normalMap: normalTexture, // 2 设置发现纹理
    });
    

    1721033810955.png

4-12 如何获取各种类型纹理贴图

poliigon.com

需要用clash加速:3dtextures.me

需要打开pigcha:arroway-textures.ch

Quixel Bridge:需要注册虚幻引擎账号,被游戏公司收购,有虚幻引擎账号,资源免费试用

4-13 纹理加载进度情况

  • 单张纹理加载进度

    const doorColorTexture = textureLoader.load(
      "./textures/door/color.jpg",
      (onload = () => {
        console.log("加载完成");
      }),
      (onprogress = (e) => {
        console.log(e);
        console.log("开始加载");
      }),
      (onerror = (e) => {
        console.log(e);
        console.log("加载失败");
      })
    );
    // 或者
    const event = {};
    event.onload = () => {
      console.log("加载完成");
    };
    event.onprogress = () => {
      console.log("加载中");
    };
    event.onerror = () => {
      console.log("加载失败");
    };
    const doorColorTexture = textureLoader.load(
      "./textures/door/color.jpg",
      event.onload,
      event.onprogress,
      event.onerror
    );
    

    由于单张纹理加载得太快了,所以加载中显示不出来

  • 多张纹理加载进度

    • 加载管理器(LoadingManager)

    • 代码实现

      const event = {};
      event.onload = () => {
        console.log();
        console.log("加载完成");
      };
      event.onprogress = (e) => {
        console.log(e);
        console.log("加载中");
      };
      event.onerror = (e) => {
        console.log(e);
        console.log("加载失败");
      };
      ​
      // 设置加载管理器
      const loadingManager = new THREE.LoadingManager(
        event.onload,
        event.onprogress,
        event.onerror
      );
      ​
      // 将加载管理器设置给纹理加载器,可以检测到多个纹理的加载进度
      const textureLoader = new THREE.TextureLoader(loadingManager);
      

      1721095642874.png

4-14 详解环境贴图

  • CubeTextureLoader - 立方体纹理加载器

    • 纹理设置要设置6个面的,px,py,pz,nx,ny,nz(p正面方向,n负面方向)
    • 加载的 CubeTexture 采用 sRGB 色彩空间。这意味着 colorSpace 属性默认设置为 THREE.SRGBColorSpace
  • 实现环境贴图

    之前的代码,除了基础设置(场景、相机、灯光、渲染器、轨道控制器、坐标轴辅助器、渲染函数、监听页面变化)外,均删除

    // 1 设置cube纹理加载器
    const cubeTextureLoader = new THREE.CubeTextureLoader();
    // 2 加载纹理
    const envMapTexture = cubeTextureLoader.load([
      "./textures/environmentMaps/1/px.jpg",
      "./textures/environmentMaps/1/nx.jpg",
      "./textures/environmentMaps/1/py.jpg",
      "./textures/environmentMaps/1/ny.jpg",
      "./textures/environmentMaps/1/pz.jpg",
      "./textures/environmentMaps/1/nz.jpg",
    ]);
    ​
    // 3 设置物体(球)
    const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
    const material = new THREE.MeshStandardMaterial({
      metalness: 1,
      roughness: 0.1,
      envMap: envMapTexture,
    });
    const sphere = new THREE.Mesh(sphereGeometry, material);
    scene.add(sphere);
    

    1721097302177.png

4-15 经纬线映射贴图

  • 设置环境背景/给场景添加背景

    // 给场景添加背景
    scene.background = envMapTexture;
    ​
    // 给场景所有的物体添加默认的环境贴图
    scene.enviornment = envMapTexture;
    ​
    const material = new THREE.MeshStandardMaterial({
      metalness: 1,
      roughness: 0.1,
      // envMap: envMapTexture, // 给场景中所有的物体添加环境贴图时,可以不给材质单独设置环境贴图
    });
    

    1721380535850.png

  • HDR - 高动态范围显示技术

    HDR技术是一种改善动态对比度的技术,HDR就是高动态范围技术,如其名字一样,HDR技术增加了亮度范围,同时提升最亮和最暗画面的对比度,从而获得更广泛的色彩范围,除了明显改善灰阶,也带来了更黑或更白的颜色效果。这样用户就可以看到更多的细节,当然前提是你放映的片源也要支持HDR技术才可以,目前市面上使用HDR技术录制的视频还很少。

    • HDR处理的照片效果

      HDR技术使得图像看上去效果更好,图像充满活力,而不是洗白或偏色的图像,使得整体画质表现力有较大的提升。从技术角度来看,其的确对于用户来说是意义的,但是其实HDR技术和3D技术在某种意义上有着相同的尴尬,那就是这种技术到底能不能有用武之地。

    • HDR图一般比较大

    • 经纬线贴图

      EquirectangularReflectionMapping 和 EquirectangularRefractionMapping 用于等距圆柱投影的环境贴图,也被叫做经纬线映射贴图。等距圆柱投影贴图表示沿着其水平中线360°的视角,以及沿着其垂直轴向180°的视角。贴图顶部和底部的边缘分别对应于它所映射的球体的北极和南极。

    import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
    ​
    const rgbeLoader = new RGBELoader();
    rgbeLoader.loadAsync("textures/hdr/002.hdr").then((texture) => {
      texture.mapping = THREE.EquirectangularReflectionMapping;
      scene.background = texture;
      scene.environment = texture;
    });
    

    1721382199007.png

4-16 清除物体_几何体_材质_纹理_保证性能和内存不泄露

  • 在每一次渲染后清除内容,以便下一次渲染

    • dispose() 清除
    • 详情看创建canvas材质和render函数
    import * as THREE from "three";
    // 导入轨道控制器
    import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
    ​
    // 1 创建场景
    const scene = new THREE.Scene();
    ​
    // 2 创建相机
    const camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    );
    ​
    // 3 设置相机位置
    camera.position.set(0, 0, 10);
    ​
    // 4 将相机添加到场景当中
    scene.add(camera);
    ​
    // 设置物体
    const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
    const material = new THREE.MeshStandardMaterial({
      metalness: 1,
      roughness: 0.1,
      // envMap: envMapTexture,
    });
    const sphere = new THREE.Mesh(sphereGeometry, material);
    ​
    scene.add(sphere);
    ​
    // 追加灯光
    // 环境光
    const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
    scene.add(ambientLight);
    ​
    // 平行光
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
    // 设置位置
    directionalLight.position.set(10, 10, 10);
    scene.add(directionalLight);
    ​
    // 6 渲染
    // 初始化渲染器
    const renderer = new THREE.WebGLRenderer();
    // 设置渲染器的尺寸和大小
    renderer.setSize(window.innerWidth, window.innerHeight);
    ​
    // 7 将webgl渲染的canvas内容添加到body当中
    document.body.appendChild(renderer.domElement);
    ​
    // 8 使用渲染器通过相机将场景渲染
    renderer.render(scene, camera);
    ​
    // 9 创建轨道控制器
    const controls = new OrbitControls(camera, renderer.domElement);
    // 设置控制器阻尼,使控制器更有真实的效果
    controls.enableDamping = true;
    ​
    // 10 添加坐标轴辅助器
    const axesHelper = new THREE.AxesHelper(5);
    scene.add(axesHelper);
    ​
    window.addEventListener("dblclick", () => {});
    ​
    // 创建canvas材质
    function createImage() {
      const canvas = document.createElement("canvas");
      canvas.width = 256;
      canvas.height = 256;
      const ctx = canvas.getContext("2d");
      ctx.fillStyle = `rgb(${Math.random() * 255},${Math.random() * 255},${
        Math.random() * 255
      })`;
      ctx.fillRect(0, 0, 256, 256);
      return canvas;
    }
    ​
    function render() {
        // 创建物体
        const sphereGeometry = new THREE.SphereGeometry(
            1,
            Math.random() * 64,
            Math.random() * 32
        );
    ​
        // 创建canvas纹理贴图
        const texture = new THREE.CanvasTexture(createImage());
        const sphereMaterial = new THREE.MeshBasicMaterial({
            map: texture,
            // color: Math.random() * 0xffffff,
        });
    ​
        const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
    ​
        scene.add(sphere);
    ​
        controls.update();
        renderer.render(scene, camera);
        // 渲染下一帧的时候调用render函数
        requestAnimationFrame(render);
    ​
        // 清除场景中物体
        scene.remove(sphere);
    ​
        // 渲染完画面清除物体(几何体、材质、纹理贴图)
        sphereGeometry.dispose();
        sphereMaterial.dispose();
        texture.dispose();
    }
    ​
    render();
    ​
    // 监听画面变化,更新渲染画面
    window.addEventListener("resize", () => {
        // 更新摄像头
        camera.aspect = window.innerWidth / window.innerHeight;
        // 更新摄像机的投影矩阵
        camera.updateProjectionMatrix();
        // 更新渲染器
        renderer.setSize(window.innerWidth, window.innerHeight);
        // 设置渲染器的像素比
        renderer.setPixelRatio(window.devicePixelRatio);
    });

5-1 灯光与阴影的关系与设置

  • 阴影

    • 环境光不产生因阴影
    • 平行光会产生阴影(eg: 太阳光)
    • 点光源产生阴影
    • 聚光灯产生阴影
    • 平面光源不支持阴影(窗户的光)
  • 材质

    • 基础网格材质不受光照影响
    • 标准网格材质受光源影响,基于物理的渲染(PBR)——常用
    • Lambert网格材质是一种非光泽表面材质,没有镜面高光(未经处理过的木材或石材)
    • Phong网格材质是一种具有镜面高光的光泽表面的材质(漆面木材或石材)
    • 物理网格材质(比基础网格材质更逼真的物理)
    • MeshToonMaterial一种实现卡通着色的材质
  • 灯光阴影目标

    • 材质要满足能够对光照有反应
    • 设置渲染器开启引阴影的计算 renderer.shadowMap.enabled = true;
    • 设置光照投射阴影 directionalLight.castShadow = true;
    • 设置物体投射阴影sphere.castShadow = true;
    • 设置物体接收阴影 plane.receiveShadow = true;
    // 设置球体
    const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
    const material = new THREE.MeshStandardMaterial();
    const sphere = new THREE.Mesh(sphereGeometry, material);
    scene.add(sphere);
    sphere.castShadow = true; // 设置物体投射阴影
    ​
    // 创建平面
    const planeGeometry = new THREE.PlaneGeometry(10, 10);
    const plane = new THREE.Mesh(planeGeometry, material);
    plane.position.set(0, -1, 0);
    plane.rotation.x = -Math.PI / 2;
    plane.receiveShadow = true; // 设置物体接收阴影
    scene.add(plane);
    ​
    // 平行光
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
    directionalLight.position.set(5, 5, 5);
    scene.add(directionalLight);
    directionalLight.castShadow = true; // 设置光照投射阴影
    ​
    // 初始化渲染器
    const renderer = new THREE.WebGLRenderer();
    renderer.shadowMap.enabled = true; // 设置渲染器开启引阴影的计算
    

    1721449640624.png

5-2 平行光阴影属性与阴影相机原理

  • shadow.radius - 设置阴影模糊度(将此值设置为大于1的值将模糊阴影的边缘)

    directionalLight.shadow.radius = 20;
    

    1721450139107.png

  • mapSize: Vector2 阴影贴图的分辨率 默认512*512,设置较大,阴影会较细致

    directionalLight.shadow.mapSize.set(4096, 4096);
    

    1721725814892.png

  • 设置平行光投射相机的属性

    directionalLight.shadow.camera.near = 0.5;
    directionalLight.shadow.camera.far = 500;
    directionalLight.shadow.camera.top = 5;
    directionalLight.shadow.camera.bottom = -5;
    directionalLight.shadow.camera.left = -5;
    directionalLight.shadow.camera.right = 5;
    
  • updateProjectionMatrix:更新相机的投影矩阵

    // 使用gui查看阴影
    // 导入dat.gui
    import * as dat from "dat.gui";
    ​
    //创建gui对象
    const gui = new dat.GUI();
    ​
    gui
      .add(directionalLight.shadow.camera, "near")
      .min(0)
      .max(10)
      .step(0.1)
      .onChange(() => {
        directionalLight.shadow.camera.updateProjectionMatrix();
      });
    

    1721728109174.png

5-3 详解聚光灯各种属性与应用

  • SpotLight:聚光灯

    • color:十六进制光照颜色
    • intensity:光照强度
    • distance:从光源发出光的最大距离,其强度根据光源的距离线性衰减 0为不衰减
    • angle:光线散射角度,最大为Math.PI/2。默认值为Math.PI/3
    • penumbra:聚光锥的半影衰减百分比。在0和1之间的值。默认为0
    • decay:沿着光照距离的衰减量
    // 创建点光源并进行基本设置
    const spotLight = new THREE.SpotLight(0xffffff, 0.5);
    // 设置
    spotLight.position.set(5, 5, 5);
    spotLight.castShadow = true;spotLight.shadow.radius = 20;
    spotLight.shadow.mapSize.set(4096, 4096);
    ​
    scene.add(spotLight);
    

    1721728655241.png

  • 移动聚光灯的位置看效果

    // 将之前创建的平面面积改大一点
    const planeGeometry = new THREE.PlaneBufferGeometry(50, 50); // 大小设置为50, 50// 使用gui监听position的x
    gui.add(sphere.position, "x").min(-5).max(5).step(0.1);
    

    1721729079504.png

  • 设置聚光灯的角度并监听

    spotLight.angle = Math.PI / 6; // 30度
    ​
    gui
      .add(spotLight, "angle")
      .min(0)
      .max(Math.PI / 2)
      .step(0.01);
    

    1721729267298.png

    聚光灯的效果类似于透视相机,即可以设置camera的属性和透视相机一样

  • 设置衰减效果

    spotLight.distance = 0;
    ​
    gui
      .add(spotLight, "distance")
      .min(0)
      .max(10)
      .step(0.01);
    

    1721729924182.png

  • 设置聚光锥的半影衰减百分比

    spotLight.penumbra = 0;
    ​
    gui
      .add(spotLight, "penumbra")
      .min(0)
      .max(1)
      .step(0.01);
    

    1721730105864.png

  • 设置decay

    spotLight.decay = 0;
    ​
    gui
      .add(spotLight, "decay")
      .min(0)
      .max(5)
      .step(0.01);
    ​
    // 这里需要设置渲染器为physicallyCorrect才能起效果
    renderer.physicallyCorrectLights = true;
    

    1721807209001.png

  • 调节亮度

    spotLight.intensity = 2;
    

5-4 详解点光源属性与应用

  • 设置点光源

    // 点光源
    const pointLight = new THREE.PointLight(0xff0000, 1);
    // 设置
    pointLight.position.set(2, 2, 2);
    pointLight.castShadow = true;pointLight.shadow.radius = 20;
    pointLight.shadow.mapSize.set(4096, 4096);
    pointLight.intensity = 2;
    ​
    scene.add(pointLight);
    ​
    gui.add(pointLight.position, "x").min(-5).max(5).step(0.1);
    gui.add(pointLight, "distance").min(0).max(10).step(0.01);
    gui.add(pointLight, "decay").min(0).max(5).step(0.01);
    

    1721808161711.png

  • 创建一个小球,模拟点光源的位置

    // 创建小球
    const smallBall = new THREE.Mesh(
      new THREE.SphereGeometry(0.1, 20, 20),
      new THREE.MeshBasicMaterial({ color: 0xff0000 })
    );
    smallBall.position.set(2, 2, 2); // 将小球设置到点光源的位置// 将点光源的位置设置 和 添加点光源至场景的操作 给注释掉
    // pointLight.position.set(2, 2, 2);
    // scene.add(pointLight);// 将点光源作为子元素添加到小球上
    smallBall.add(pointLight);
    // 将小球添加到场景中
    scene.add(smallBall);
    

    1721808903339.png

  • 让小球围绕着物体转,并且上下运动

    // 设置时钟
    const clock = new THREE.Clock();
    ​
    function render() {
      let time = clock.getElapsedTime();
      smallBall.position.x = Math.sin(time) * 3; // 设置小球左右来回移动
      smallBall.position.z = Math.cos(time) * 3; // 设置小球绕着物体转圈
      smallBall.position.y = Math.abs(Math.sin(time * 10)) * 2; // 设置小球上下运动
    ​
      controls.update();
      renderer.render(scene, camera);
    ​
      // 渲染下一帧的时候调用render函数
      requestAnimationFrame(render);
    }
    

    1721809654778.png