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 -
文件结构
-
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);
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);
-
结果
1-3 如何处理运行搭建Three环境出现的问题
由于版本更新造成的。
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();
2-2 添加坐标轴辅助器
-
AxesHelper
- 用于简单模拟3个坐标轴的对象
- 红色代表X轴,绿色代表Y轴,蓝色代表Z轴
-
示例
const axesHelper = new THREE.AxesHelper(5); scene.add(axesHelper); -
构造函数
- AxesHelper(size: Number)
- size -- (可选的)表示坐标轴的线段长度,默认为1
2-3 设置物体移动
- 目标:控制3D物体移动
console.log(cube);
可以通过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 物体的缩放与旋转
设置物体的缩放
-
scale:Vector3
// 缩放 cube.scale.set(3, 2, 1); // 或者 cube.scale.x = 3;
设置物体的旋转
-
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); } -
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);
}
-
打印结果
上面打印的是每一帧渲染的毫秒数,每一帧之间间隔的时间不一致,这样就渲染的时快时慢
可以根据时间去走:这里我的理解是,过了多少时间,按时间的数量走距离
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); } -
实现上一个案例实现的效果
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); } -
-
实现屏幕尺寸变化自适应
- 不是自适应屏幕大小变化的时候
- 实现屏幕自适应
// 监听画面变化,更新渲染画面 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);以上可以看到:
BoxGeometry对象也可以通过cube.geometry来获取
BoxGeometry有一些attributes(属性)
-
几何体的属性
-
position
-
uv
-
normal 法向
在光打过来的时候,我们需要知道立方体面得朝向,法向表述的就是面得朝向
-
细分程度
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);
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);
}
3-4 常用网格几何体
4-1 初识材质与纹理
-
基础网格材质(MeshBasicMaterial)
-
立方体有可能是一个包装盒,一个烟盒等等,他们表面的
材质与纹理是不一样的 -
纹理图片
-
为立方体设置材质与纹理
// 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); -
实现效果
4-2 纹理_偏移_旋转_重复
-
偏移
doorColorTexture.offset.x = 0.5; doorColorTexture.offset.y = 0.5; // 也可写为 doorColorTexture.offset.set(0.5, 0.5); -
旋转
// 设置旋转的原点为中心位置 doorColorTexture.center.set(0.5, 0.5); doorColorTexture.rotation = Math.PI / 4;-
旋转
-
中心位置旋转
-
-
重复
-
repeat: Vector2
决定纹理在表面的重复次数,两个方向分别表示U和V
-
代码实现
// 设置重复 doorColorTexture.repeat.set(2, 3); -
如果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; -
-
4-3 设置纹理显示算法与mipmap
-
设置纹理效果
const texture = textureLoader.load("./textures/minecraft.png"); const basicMaterial = new THREE.MeshBasicMaterial({ color: "#ffff00", map: texture, }); -
magFilter: number 当一个纹素覆盖大于一个像素时,贴图将如何采样
默认值为THREE.LinearFilter, 它将获取四个最接近的纹素,并在他们之间进行双线性插值
THREE.NearestFilter,它将使用最接近的纹素的值
-
minFilter: number 当一个纹素覆盖小于一个像素时,贴图将如何采样
默认值为THREE.LinearMipmapLinearFilter, 它将使用mipmapping以及三次线性滤镜
-
将纹理的采样值均换成
THREE.NearestFiltertexture.minFilter = THREE.NearestFilter; texture.magFilter = THREE.NearestFilter;
4-4 透明材质与透明纹理
-
为4-1中的门设置透明区域
下图(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); -
-
设置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);设置side等属性,也可单独设置。以side为例:
basicMaterial.side = THREE.DoubleSide; -
4-5 环境遮挡贴图与强度
-
使用图片
和透明材质的原理差不多,只不过白色是透明的,黑色是不透明的。
-
代码实现
// 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) );
-
4-6 详解PBR物理渲染
1 什么是PBR
- 基于物理渲染
- 以前的渲染是在模仿灯光的外观
- 现在是模仿光的实际行为
- 使图形看起来更真实
2 PBR的组成部分
- 灯光属性:直接照明、间接照明、直接高光、间接高光、阴影、环境光闭塞
- 表面属性:基础色、法线、高光、粗糙度、金属度
3 灯光属性
1、光线类型
-
入射光
- 直接照明:直接从光源反射阴影物体表面的光
- 间接照明:环境光和直接光经过反弹第二次进入的光
-
反射光
- 镜面光:在经过表面反射聚焦在同一方向上进入人眼的高亮光
- 漫反射:光被散射并沿着各个方向离开表面
2、光与表面相互作用类型
- 直接漫反射:从源头到四面八方散发出来的直接高光
- 直接高光:直接来自光源并被集中反射的光
- 间接漫反射:来自环境的光被表面散射的光
- 间接高光:来自环境光并被集中反射的光
(1)间接漫反射
- 直接来自光源的光
- 撞击表面后散落在各个方向
- 在着色器中使用简单的数学计算
(2)直接高光
-
直接来自光源的光
-
反射在一个更集中的方向上
-
在着色器中使用简单的数学计算
直接镜面反射的计算成本比漫反射低很多
-
(3)间接漫反射
- 来自环境中各个方向的光
- 撞击表面后散落在各个方向
- 因为计算昂贵,所以引擎的全局照明解决方案通常胡离线渲染,并被烘焙成灯光地图
(4)镜面反射
- 来自环境中各个方向的光
- 反射在一个更集中的方向上
- 引擎中使用反射探头,平面反射,SSR,或射线追踪计算
4 灯光属性
1、基础色
-
- 定义表面的漫反射颜色
- 真实世界的材料不会比20暗或比240 sRGB亮
- 粗糙表面具有更高的最低 ~ 50 sRGB
- 超出范围的值不能正确发光,所以保持在范围内是至关重要的
-
-
基础色贴图制作注意点:
- 不包括任何照明或阴影
- 基本颜色纹理看起来应该非常平坦
- 使用真实世界的度量或获取最佳结果的数据
-
2、法线
- 定义曲面的形状每个像素代表一个矢量
- 该矢量指示表面所面对的方向即使网格是完全平坦的
- 法线贴图会使表面显得凹凸不平
- 用于添加表面形状的细节,这里的三角形是实现不了的
- 因为它们表示矢量数据,所以法线贴图是无法手工绘制的
3、镜面
-
- 用于直接和间接镜面照明的叠加
- 当直视表面时,定义反射率
- 非金属表面反射约4%的光
- 0.5代表4%的反射
- 1.0代表8%的反射但对于大多数物体来说太高了
- 在掠射角下,所有表面都是100%反射的,内置于引擎中的菲涅耳项
-
-
镜面贴图制作注意点:
- 高光贴图应该大多在0.5
- 使用深色的阴影来遮盖不应该反光的裂缝
- 一个裂缝贴图乘以0.5就是一个很好的高光贴图
-
4、粗糙度
-
粗糙度贴图制作注意点:
- 没有技术限制-完全艺术的选择
- 艺术家可以使用这张地图来定义表面的“特征”,并展示它的历史
- 考虑一下被打磨光滑、磨损或老化的表面
-
5、金属度
-
- 两个不同的着色器通过金属度混合他们
- 基本色变成高光色而不是漫反射颜色
- 金属漫反射是黑色的
- 在底色下,镜面范围可达100%
- 大多数金属的反光性在60%到100%之间
- 确保对金属颜色值使用真实世界的测量值,并保持它们明亮
- 当金属为1时,镜面输入将被忽略
粗糙度贴图制作注意点:
- 将着色器切换到金属模式
- 灰度值会很奇怪,最好使用纯白色或黑色
- 当金属色为白色时,请确保使用正确的金属底色值
- 没有黑暗金属这回事
- 所有金属均为180srgb或更亮
5 金属和非金属
-
非金属
-
基础颜色=漫反射
-
镜面反射=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);能看到黑色的影子,但是看不清是什么
(2)DirectionalLight - 直线光/平行光
-
特点:
- 平行光是沿着特定方向发射的光
- 第二个参数是光的强度,默认是1
- 光的表现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光的效果
- 只能从一个方向照射,从其它方向看可能就看不见光
- position:设置平行光的方向
-
为门设置平行光
// 创建平行光 const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5); // 设置平行光位置 directionalLight.position.set(10, 10, 10); // 添加平行光 scene.add(directionalLight);-
正面
-
背面
-
(3)Ambient - 环境光
-
特点
- 会均匀的照亮场景中的所有物体
- 环境光不能用来投射阴影,因为它没有方向
-
为门创建环境光
// 设置环境光 const ambientLight = new THREE.AmbientLight(0xffffff, 0.5); scene.add(ambientLight);-
正面
-
背面
-
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) });
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 设置粗糙度纹理 });门反射光,金属锁扣部分反射光
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 设置金属贴图 });
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 设置发现纹理 });
4-12 如何获取各种类型纹理贴图
需要用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);
-
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);
4-15 经纬线映射贴图
-
设置环境背景/给场景添加背景
// 给场景添加背景 scene.background = envMapTexture; // 给场景所有的物体添加默认的环境贴图 scene.enviornment = envMapTexture; const material = new THREE.MeshStandardMaterial({ metalness: 1, roughness: 0.1, // envMap: envMapTexture, // 给场景中所有的物体添加环境贴图时,可以不给材质单独设置环境贴图 }); -
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; }); -
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; // 设置渲染器开启引阴影的计算
5-2 平行光阴影属性与阴影相机原理
-
shadow.radius - 设置阴影模糊度(将此值设置为大于1的值将模糊阴影的边缘)
directionalLight.shadow.radius = 20; -
mapSize: Vector2 阴影贴图的分辨率 默认512*512,设置较大,阴影会较细致
directionalLight.shadow.mapSize.set(4096, 4096); -
设置平行光投射相机的属性
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(); });
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); -
移动聚光灯的位置看效果
// 将之前创建的平面面积改大一点 const planeGeometry = new THREE.PlaneBufferGeometry(50, 50); // 大小设置为50, 50 // 使用gui监听position的x gui.add(sphere.position, "x").min(-5).max(5).step(0.1); -
设置聚光灯的角度并监听
spotLight.angle = Math.PI / 6; // 30度 gui .add(spotLight, "angle") .min(0) .max(Math.PI / 2) .step(0.01);聚光灯的效果类似于透视相机,即可以设置camera的属性和透视相机一样
-
设置衰减效果
spotLight.distance = 0; gui .add(spotLight, "distance") .min(0) .max(10) .step(0.01); -
设置聚光锥的半影衰减百分比
spotLight.penumbra = 0; gui .add(spotLight, "penumbra") .min(0) .max(1) .step(0.01); -
设置decay
spotLight.decay = 0; gui .add(spotLight, "decay") .min(0) .max(5) .step(0.01); // 这里需要设置渲染器为physicallyCorrect才能起效果 renderer.physicallyCorrectLights = true;
-
调节亮度
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);
-
创建一个小球,模拟点光源的位置
// 创建小球 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);
-
让小球围绕着物体转,并且上下运动
// 设置时钟 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); }