一 基本对象及属性
1 物体
由几何体(Geometry)与材质(Material)混合生成物体(Mesh)
2 材质(Material)
1.1 纹理(texture)
Three.js提供TextureLoader以加载纹理(Texture)对象,内部使用ImageLoader来加载文件
const textureLoader = new THREE.TextureLoader(); // 初始化纹理加载器
const logoTexture = textureLoader.load('./logo.png'); // 加载纹理贴图
最终得到的是一个纹理(Texture)对象,可将其应用到一个表面,或者作为反射/折射贴图
常用参数:
名称 | 类型 | 说明 |
---|---|---|
offset | Vector2 | 贴图单次重复中的起始偏移量,分别表示U和V。 一般范围是由0.0 到1.0 |
rotation | number | 纹理将围绕中心点旋转多少度,单位为弧度(rad)。正值为逆时针方向旋转,默认值为0 |
center | Vector2 | 旋转中心点。(0.5, 0.5)对应纹理的正中心。默认值为(0,0),即左下角 |
repeat | Vector2 | 决定纹理在表面的重复次数,两个方向分别表示U和V,如果重复次数在任何方向上设置了超过1的数值, 对应的Wrap需要设置为THREE.RepeatWrapping或者THREE.MirroredRepeatWrapping来 达到想要的平铺效果 |
wrapS | number | 这个值定义了纹理贴图在水平方向上将如何包裹,在UV映射中对应于U。默认值是THREE.ClampToEdgeWrapping,即纹理边缘将被推到外部边缘的纹素。 其它的两个选项分别是THREE.RepeatWrapping和THREE.MirroredRepeatWrapping |
wrapT | number | 这个值定义了纹理贴图在垂直方向上将如何包裹,在UV映射中对应于V,其默认值与选项与wrapS相同 |
magFilter | number | 当一个纹素覆盖大于一个像素时,贴图将如何采样。默认值为THREE.LinearFilter, 它将获取四个最接近的纹素,并在他们之间进行双线性插值。 另一个选项是THREE.NearestFilter,它将使用最接近的纹素的值 |
minFilter | number | 当一个纹素覆盖小于一个像素时,贴图将如何采样,默认值为THREE.LinearMipmapLinearFilter,它将使用mipmapping以及三次线性滤镜 |
side | Integer | 定义将要渲染哪一面 - 正面,背面或两者。默认为THREE.FrontSide。其他选项有THREE.BackSide 和 THREE.DoubleSide |
transparent | Boolean | 定义此材质是否透明。这对渲染有影响,因为透明对象需要特殊处理,并在非透明对象之后渲染。设置为true时,材质的opacity与alphaMap才会生效 |
opacity | Float | 在0.0 - 1.0的范围内的浮点数,表明材质的透明度。值0.0表示完全透明,1.0表示完全不透明。 如果材质的transparent属性未设置为true,则材质将保持完全不透明,此值仅影响其颜色。 默认值为1.0 |
注:
- 关于纹理相关常量请参阅texture constants以了解详细信息
- 通过设置magFilter与magFilter可以快速优化渲染性能
1.2 纹理贴图
颜色贴图 map
作用在物体上,显示图片的本身效果,是最基本的贴图
const texture = textureLoader.load('./texture.png');
const material = new THREE.MeshBasicMaterial({
map: texture
});
透明度贴图 alphaMap
alphaMap是一张灰度纹理,用于控制整个表面的不透明度,黑色为完全透明,白色为完全不透明
const alphaTexture = textureLoader.load('./alpha.png');
const material = new THREE.MeshBasicMaterial({
alphaMap: alphaTexture,
transparent: true // 允许透明
});
只有在transparent
设置为true时,材质的alphaMap才会生效
环境遮挡贴图 .aoMap
用于遮挡环境光,白色为不遮挡,黑色为全遮挡
使用aoMap
需要为物体设置第二组UV,如:
const geometry = new THREE.BoxGeometry(1, 1, 1);
geometry.setAttribute('uv2', new THREE.BufferAttribute(geometry.attributes.uv.array, 2));
const aoTexture = textureLoader.load('./ao.png');
const material = new THREE.MeshBasicMaterial({
aoMap: aoTexture,
aoMapIntensity: 0.5
});
可通过aoMapIntensity属性设置aoMap
的强度,默认值为1,0是完全不遮挡效果
凹凸贴图 bumpMap
bumpMap用于创建凹凸贴图的纹理。黑色和白色值映射到与光照相关的感知深度。
凹凸实际上不会影响对象的几何形状,只影响光照。如果定义了法线贴图,则将忽略该贴图
可使用bumpScale设置凹凸贴图会对材质产生多大影响,典型范围是0-1,默认值为1
const bumpTexture = textureLoader.load('./bump.png');
const material = new THREE.MeshBasicMaterial({
bumpMap: bumpTexture,
bumpScale: 0.5
});
法线贴图 normalMap
normalMap用于创建法线贴图的纹理。RGB值会影响每个像素片段的曲面法线,并更改颜色照亮的方式。
法线贴图不会改变曲面的实际形状,只会以改变光照的方式使其拥有立体感
可使用normalScale以设置法线贴图对材质的影响程度。典型范围是0-1。默认值是Vector2设置为(1,1)
const normalTexture = textureLoader.load('./normal.png');
const material = new THREE.MeshBasicMaterial({
normalMap: normalTexture
});
material.normalScale.set(1, 1);
粗糙度贴图 roughnessMap
该纹理的绿色通道用于改变材质的粗糙度
可同时设置roughness以改变粗糙度,0.0表示平滑的镜面反射,1.0表示完全漫反射,默认值为1.0。材质的粗糙程度将由两个值相乘得出
const roughnessTexture = textureLoader.load('./roughness.png');
const material = new THREE.MeshBasicMaterial({
roughnessMap: roughnessTexture,
roughness: 0.5
});
金属度贴图 metalnessMap
该纹理的蓝色通道用于改变材质的金属度
可使用metalness设置材质整体的金属度,非金属材质,如木材或石材,使用0.0,金属使用1.0,通常没有中间值,默认值为0.0
0.0到1.0之间的值可用于生锈金属的外观,如果同时提供metalnessMap与metalness,则两个值相乘
const metalnessTexture = textureLoader.load('./metalness.png');
const material = new THREE.MeshBasicMaterial({
metalnessMap: metalnessTexture,
metalness: 0.5
});
位移贴图(置换贴图) displacementMap
位移贴图会影响网格顶点的位置,因此如果希望displacementMap生效,物体需要有足够多的顶点
与仅影响材质的光照和阴影的其他贴图不同,移位的顶点可以投射阴影,阻挡其他对象,以及充当真实的几何体
const dispTexture = textureLoader.load('./disp.png');
const material = new THREE.MeshBasicMaterial({
displacementMap: dispTexture,
displacementScale: 0.5
});
1.3 环境贴图
envMap
创建envMap纹理需要使用CubeTextureLoader,这个类会创建出一个CubeTexture对象
创建环境贴图,需要使用六张图片,这六张图片分别为:朝前的(pz)、朝后的(nz)、朝上的(py)、朝下的(np)、朝右的(px)和朝左的(nx),Three.js 会将这些图片整合到一起来创建一个无缝的环境贴图
六张图片必须等宽等高,否则贴图将无法加载
const cubeTextureLoader = new THREE.CubeTextureLoader();
const envMapTexture = cubeTextureLoader
.setPath('PATH/')
.load([
'px.jpg',
'nx.jpg',
'py.jpg',
'ny.jpg',
'pz.jpg',
'nz.jpg',
])
// 将环境贴图作为场景的背景图
const scene = new THREE.Scene();
scene.background = envMapTexture;
// 创建一个物体
const sphereGeometry = new THREE.SphereGeometry(1, 20, 20);
const material = new THREE.MeshStandardMaterial({
envMap: envMapTexture, // 设置环境贴图
metalness: 1,
roughness: 0,
});
const sphere = new THREE.Mesh(sphereGeometry, material);
scene.add(sphere);
当物体较多时,可以设置场景(scene)对象的environment属性,environment属性在不为null时,将被设为场景中所有物理材质的默认环境贴图,environment
的优先级低于MeshStandardMaterial.envMap
const cubeTextureLoader = new THREE.CubeTextureLoader();
const envMapTexture = cubeTextureLoader
.setPath('PATH/')
.load([
'px.jpg',
'nx.jpg',
'py.jpg',
'ny.jpg',
'pz.jpg',
'nz.jpg',
])
const scene = new Scene();
scene.environment = envMapTexture; // 设置环境(即通用环境贴图)
HDR贴图
HDR(High-Dynamic Range) ,中文“高动态范围”,简单来说就是一种提高影像亮度和对比度的处理技术,它可以将每个暗部的细节变亮,暗的地方更暗,丰富更多细节色彩,让电影,图片都能呈现出极佳的效果
Three.js提供了用于解析HDR贴图的插件RGBELoader
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
const scene = new THREE.Scene();
new RGBELoader()
.load('./hdr/brown_photostudio_01_2k.hdr', function (texture) {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;
scene.background = texture;
render();
})
但由于HDR贴图一般体积较大,所以多数情况下会选择异步加载
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
const scene = new THREE.Scene();
new RGBELoader()
.loadAsync('./hdr/brown_photostudio_01_2k.hdr')
.then(function (texture) {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;
scene.background = texture;
render();
})
1.4 加载管理器 LoadingManager
LoadingManager可以跟踪处理已加载和待加载的数据,如果未手动设置加强管理器,则会为加载器创建和使用默认全局实例加载器管理器 - 请参阅 DefaultLoadingManager
一般来说,默认的加载管理器已足够使用了,但有时候也需要设置单独的加载器 - 例如,如果你想为对象和纹理显示单独的加载条
const loadingManager = new THREE.LoadingManager(
() => {
console.log('onLoadHandler');
console.log('--------------------------');
},
(url, itemsLoaded, itemsTotal) => {
console.log('onProgressHandler');
console.log('url', url); // 被加载的项的url
console.log('itemsLoaded', itemsLoaded); // 目前已加载项的个数
console.log('itemsTotal', itemsTotal); // 需要加载项的总个数
console.log('--------------------------');
},
(errorUrl) => {
console.log('onErrorHandler');
console.log('errorUrl', errorUrl); // 加载发生错误的项目的url
}
)
const textureLoader = new THREE.TextureLoader(loadingManager)
二 Three.js中的PBR物理渲染
PBR(physically-based rendering)算法通过使用更真实的材质属性、光照计算和环境地图来模拟材料与现实世界之间的相互作用。这使得渲染的结果更加逼真,同时也更容易管理
1 Three.js的PBR材质
我们的想法是,不是在特定照明下调整材质以使其看起来很好,而是可以创建一种材质,能够“正确”地应对所有光照场景。——Three.js官方文档
基于物理的明暗处理意味着我们估算光线的实际情况,而不是估算我们 凭直觉认为它应该发生的情况。最终结果是,这样可产生更准确并且通常更加自然的外观。基于物理 的材质在所有照明环境中都可以同样完美地工作。另外,材质值可以 不那么复杂,相互依赖也可以少一些,从而产生更加直观的界面。——虚幻引擎文档
Three.js提供了两个基于PRB标准的材质,分别是MeshStandardMaterial与MeshPhysicalMaterial,MeshPhysicalMaterial是MeshStandardMaterial材质的扩展,它提供更高级的API,但性能开销也更大
整体上看,材质的渲染表现能力与其性能开销成正比
- 渲染表现能力 MeshBasicMaterial < MeshLambertMaterial < MeshPhongMaterial < MeshStandardMaterial < MeshPhysicalMaterial
- 硬件资源占用 MeshBasicMaterial < MeshLambertMaterial < MeshPhongMaterial < MeshStandardMaterial < MeshPhysicalMaterial
使用Three.js的PBR很简单,只需切换使用的材质并添加光源即可
2 属性
颜色(color)、金属度(metalness)、粗糙度(roughness)、折射率(refractionRatio)、透明度和透明度映射、环境映射(environmentMap)
材质由基础色、法线、高光、粗糙度、金属度定义
金属度(metalness)与粗糙度(roughness)
金属度控制表面在多大程度上"像金属",其取值介于0到1之间。对于纯表面,例如纯金属、纯石头、纯塑料等等,此值将是0或1,而不是任何介于它们之间的值。大多数情况下,仅当创建受腐蚀、落满灰尘或生锈金属之类的混合表面时,可能会需要介于0与1之间的值。严格来说,只有值 0.0 或 1.0 具有物理意义,因此应尽量少使用或不使用其他值
在物理意义上,金属度代表了有多少光子是直接被反射出去, 有多少光子会进入物体内部形成了漫反射
粗糙度控制材质表面的粗糙或平滑程度
与平滑的材质相比,粗糙的材质将向更多方向散射所反射的光线,这决定了反射的模糊或清晰度(或者镜面反射高光的广度或密集度)
粗糙度0是镜面反射,而粗糙度1是漫射(或无光)表面
高光(specular)
在编辑非金属表面材质时,可以通过控制specular来调整它反射光线的能力。specular取值范围介于0到1之间,漫反射材质对应的值为0,默认值为0.0
Three.js中的MeshPhysicalMaterial材质提供了specular的相关属性,包括用于控制高光强度的specularIntensity与specularIntensityMap,和控制高光颜色的specularColor与specularColorMap,在Phong网格材质(MeshPhongMaterial)中也提供了specular属性,但MeshPhongMaterial材质并不是一种PBR材质
对于漫射度非常大的材质,您可能倾向于将此值设置为零。请忍住,不要这样做!所有材质 都具有镜面反射,请参阅此帖子以获取示例。对于漫射度非常大的材质,您真正想做的是 使它们粗糙。
——虚幻引擎文档
三 光照与阴影
1 光照
环境光
环境光(AmbientLight)没有方向,会均匀地照亮场景中的所有物体,它只能设置光强与颜色两种属性
const light = new THREE.AmbientLight(0x0070FF, 1);
scene.add(light);
平行光
平行光(DirectionalLight)是沿着特定方向发射的光,它发出的光线是平行的,距离为无限远,常用于模拟阳光效果
平行光可以指定起点(位置)与指向方向,默认值分别为(0, 1, 0)与(0, 0, 0)
平行光可以投射阴影
const directionalLight = new THREE.DirectionalLight(0xffffff, 3);
// 设置光源位置
directionalLight.position.set(-5, 55, -50)
// 设置其指向方向
const targetObject = new THREE.Object3D(); // 平行光的target属性取值为一个Object3D对象
scene.add(targetObject);
targetObject.position.set(0, 50, 0)
directionalLight.target = targetObject;
scene.add(directionalLight);
点光源
点光源(PointLight)是从一个点向各个方向发射的光源
const light = new THREE.PointLight(0xff0000, 1, 100, 2);
light.position.set(50, 50, 50);
scene.add(light);
PointLight拥有四个参数,分别为颜色、强度、最大距离、衰减量
聚光灯
聚光灯(SpotLight)将光线从一个点沿一个方向射出,射出的光线呈圆锥体,其尺寸随距离变远而放大
聚光灯可以投射阴影
2 阴影
开启阴影需要进行以下设置:
- 渲染器允许使用阴影贴图
- 灯光投射阴影
- 物体A投射阴影
- 物体B接收阴影
const scene = new THREE.Scene()
const renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true; // 渲染器阴影贴图设置,允许在场景中使用阴影贴图
const light = new THREE.PointLight(0xffffff, 1);
light.castShadow = true; // 灯光将投射阴影
scene.add(light);
const sphere = new THREE.Mesh(new THREE.SphereGeometry(15, 32, 16), material); // 球
sphere.castShadow = true; // 物体将投射阴影
scene.add(sphere);
const plane = new THREE.Mesh(new THREE.PlaneGeometry(300, 300, 90, 90), material); // 平面
plane.receiveShadow = true; // 物体将接收阴影
scene.add(plane);
四 动画
1 实现原理
three.js基于requestAnimationFrame实现,建议使用动画库(如GSAP、anime.js)控制
requestAnimationFrame
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
回调函数执行次数通常是每秒60次,但实际运行时间会随客户端性能而存在波动
requestAnimationFrame
是一次性的,所以如果希望实现连续的动画,需要在回调函数中再次调用requestAnimationFrame
const render = (time) => {
console.log(time);
// DO sth
requestAnimationFrame(render);
}
requestAnimationFrame
会默认传递给回调函数一个参数,这个参数代表着动画开始运行至本次调用时所经过的时间
注:
为了提高性能和电池寿命,在大多数浏览器里,当requestAnimationFrame()
运行在后台标签页或者隐藏的iframe里时,requestAnimationFrame
会被暂停调用
五 相关插件及第三方库
dat.gui
dat.gui
是一个用于在JavaScript中更改变量的轻量级图形用户界面
借助dat.gui
可以轻松地修改与控制物体的位置、大小等变量,也可以控制函数的执行,并提供一系列相应的事件
import * as dat from 'dat.gui';
gui.add(cube.position, 'x').min(0).max(15).step(0.1).name('cube.position.x');
gui.add(cube.position, 'y').min(0).max(15).step(0.1).name('cube.position.y');
gui.add(cube.position, 'z').min(0).max(15).step(0.1).name('cube.position.z');