ThreeJS学习笔记-1

Table of Contents

使用AxesHelper

使用AxesHelper可以直观地展示场景内的轴线,帮助定位物体位置

const axesHelper = new Three.AxesHelper(2)
scene.add(axesHelper)

转换对象

有四种属性(properties)用于转换对象

  1. position
  2. scale
  3. rotation
  4. quaternion – 四元数属性,类似与rotation旋转属性

所有继承自Object3D的类将拥有这些属性,例如PerspectiveCameraMesh,这些属性将被转化为矩阵,通过更新矩阵将对象渲染到画面。

Position

位置position属性继承自Vector3,可以移动对象位置,主要用到如下的几个属性:

  1. x
  2. y
  3. z

获取物体距离场景中心点的距离:

mesh.position.length()

获取两个物体之间的距离(以物体到相机的距离为例):

mesh.postion.distanceTo(camera.position)

归一化:会将物体的矢量长度减小为1

mesh.position.normalize()
// normalize后将输出1
console.log(mesh.position.length())

设置位置:

mesh.position.set(0.7, -0.6, 1)

Scale

缩放scale同样继承自Vector3,用于设置物体在各个轴线上的缩放刻度

mesh.scale.set(2, 0.5, 0.5)

旋转物体

可以通过rotationquaternion属性实现,两者将相互更新

rotation

rotation虽然也有xyz,但是他继承自Euler欧拉,用于物体旋转。

需要了解旋转时的轴向选择

通过Math.PI完成旋转,如果你想在在y轴上旋转一个半圆,则有

mesh.rotation.y = Math.PI * 0.5

gimbal lock

Euler通常会遇到轴旋转顺序的问题(gimbal lock)

通常rotation旋转是以x、y、z的顺序旋转,如果你不考虑旋转顺序,先旋转某个轴,再对另一个轴旋转,可能达到一个奇怪的结果。

例如你正在开发一款FPS游戏,你需要考虑人物先在Y轴进行左右旋转,然后在X轴上下旋转

为了解决这个问题,可以重新定义旋转轴的顺序,可以通过入如下代码实现:

mesh.rotation.reorder('YXZ')

quaternion

四元数quaternion是一种旋转的表示方法,但是其数学性更强,通过不同的公式或坐标轴能够得到相同的结果,但是更难想象。

lookAt

Object3D的实例拥有方法lookAt用于将物体旋转面向另一个坐标,这个物体的z轴将朝向你提供的坐标

camera.lookAt(new THREE.Vector(0, -1, 0))

组合变换

你可以任意顺序自由组合转换对象的四种属性position、scale、rotation、quaternion

Group

你可以将多个对象放入一个Group内,更新Group的四种转换对象属性,Group同样继承自Object3D

const group = new THREE.Group()

动画

请求动画帧

请求动画帧window.requestAnimationFrame的主要目的在于在下一帧调用一个函数,可以利用请求动画帧来实现动画效果

const tick = () => {
    mesh.rotation.y += 0.01
    renderer.render(scene, camera)
    window.requestAnimationFrame(tick)
}
tick()

适应帧频

帧数越高,旋转越快,为了适应帧频,使不同设备有相同的动画效果:

使用timestamp

let time = Date.now()

const tick = () => {
    const currentTime = Date.now()
    const deltaTime = currentTime - time
    time = currentTime

    mesh.rotation.y += 0.001 * deltaTime

    // ...
}

使用Clock

Clock是ThreeJS内置的解决方案,可以实现类似的适应帧频功能:

getElapsedTime为经过时间

const clock = new THREE.Clock()

const tick = () => {
    const elapsedTime = clock.getElapsedTime()

    mesh.rotation.y = elapsedTime

    // ...
}

让摄像机在旋转的同时,始终盯着物体的中心点

camera.position.y = Math.sin(elapsedTime)
camera.postion.x = Math.cos(elapsedTime)
camera.lookAt(mesh.position)

使用第三方依赖

如果需要对动画进行更多控制,创建补间动画、时间轴等,可以使用一些第三方依赖,例如GSAP

npm install --save [email protected]
import gsap from 'gsap'

实现简单的左右移动动画

gsap.to(mesh.position, { duraation: 1, delay: 1, x: 2 })
gsap.to(mesh.position, { duraation: 1, delay: 1, x: 0 })

摄像机

  • 阵列摄像机ArrayCamera在渲染的特定区域使用多个摄像机渲染场景
  • 立体摄像机StereoCamera通过两个摄像机来渲染场景,模拟眼睛为VR设备创建深度效果
  • 立方体摄像机CubeCamera将会进行六次渲染(前后左右上下),可以利用其制作环境效果图、地图等可用于反射、折射、阴影的东西
  • 正交摄像机OrthographicCamera用于创建场景渲染但不带透视。如果一个物体距离很远,但在摄像机前方同一物体大小相同,且距离摄像机相近。RTS游戏,帝国边缘
  • 透视摄像机PerspectiveCamera

透视摄像机

以下是构造透视摄像机的所有参数:

const camera = new THREE.PerspectiveCamera(75, size.width / sizes.height, 1, 1000)

对于透视摄像机来说,有如下几个参数:

  1. 视野(Field of view / fov),它是一个度数、垂直视角,是你能看到的范围。渲染幅度很大时,物体将会被挤压,建议在45~75之间
  2. 宽高比(Aspect ratio),通常为画布渲染宽度/高度
  3. 近参数(可选)
  4. 远参数(可选)

近参数和远参数:任何比参数更近或更远的物体将不会被展示。

近参数和远参数不要使用极值(例如0.0001和999999)来避免z-fighting的bug(场景中出现纹理的闪烁、不停地切换纹理),否则两个物体相近时,GPU将很难计算出哪个物体在前和在后,将出现渲染问题。

使用鼠标控制相机

编写鼠标监听时间

const cursor = {
    x: 0,
    y: 0
}
window.addEventListener('mousemove' event => {
    cursor.x = event.clientX / sizes.width - 0.5
    cursor.y = -(event.clientY / sizes.height - 0.5)
})

const tick = () => {
// 3为振幅
camera.position.x = curosr.x * 3
camera.position.y = cursor.y * 3
camera.lookAt(mesh.position)
// ...
}

但这样只能在正面观察物体,为了让摄像机能够绕物体一周,还需要如下处理:

camera.position.x = Math.sin(cursor.x * Math.PI * 2) * 3
camera.position.z = Math.cos(cursor.x * Math.PI * 2) * 3
camera.position.y = cursor.y * 5

使用内置组件

介绍几种ThreeJS支持的内置控制器:
DeviceOrientationControls在具有设备方向控制功能的智能手机上使用设备方向控制功能(陀螺仪等)
FlyControls飞行控制,可以进行移动、旋转等
FirstPersonControls第一人称控制,类似于飞行控制的移动,但无法进行视角旋转
PointerLockContols指针锁定控制
OrbitControls轨道控制
TrackballControls类似于轨道控制,但没有垂直视角的的限制
TransformControls编辑器
DragControls用来移动物体

轨道控制器

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

需要提供一个更新的相机和dom元素用于监听鼠标事件

const controls = new OrbitControls(camera, canvas)

如果需要修改控制器的坐标轴:

controls.target.y = 1
controls.update()

设置阻尼,避免相机旋转过快:

controls.enbleDamping = true

const tick = () => {
    // ...
    controls.update()
    // ...
}

正交摄像机

可以旋转、移动但不能缩放

不需要提供视野,只需要提供左右上下四个方向可以看见的距离和近远参数

const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0.1, 100)

正交摄像机渲染的物体大小将会受到画布宽高比的拉伸,因此为了使得宽高比展示正常还需要进行如下处理:

const aspectRatio = size.width / sizes.height
const camera = new THREE.OrthographicCamera(-1 * aspectRatio, 1 * aspectRatio, 1, -1, 0.1, 100)

全屏和调整大小

网页全屏

window.innerWidth
window.innerHeight

.webgl {
    position: fixed;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    outline: 0;
}

调整大小

更新相机的宽高比、更新投影矩阵,并设置渲染器的宽高

window.addEventListener('resize', () => {
    sizes.width = window.innerWidth
    sizes.height = window.innerHeight
    camera.aspect = sizes.width / sizes.height
    camera.updateProjectionMatrix()
    render.setSize(sizes.width, sizes.height)
})

像素比

有些人可能会看到模糊的图或物体边缘有阶梯一样的效果,这是因为屏幕的像素比大于1

像素比(Pixel ration):一个软件部分的像素单位在屏幕上有多少个物理像素

像素比越大的屏幕,在渲染物体时更容易模糊

可以通过window.devicePixelRatio查看当前设备的像素比

通过renderer.setPixelRatio可以设置渲染器渲染时使用的像素比:

而为了让渲染器不要使用过高的像素比(一般不超过2),可以使用:

renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))

在自适应画布大小的同时加入这段代码,当页面由一个窗口移动到另一个窗口时,也能应用正确的像素比

全屏

window.addEventListener('dbclick', () => {
    if (!document.fullscreenElement) {
        canvas.requestFullscreen()
    } else {
        document.exitFullscreen();
    }
})

也可以使用其他fullscreen相关的第三方依赖实现全屏,否则对Safari暂时不支持

几何体

几何体由顶点组成,可用于网格或例子,每个顶点都有位置、UV坐标和法线等属性

所有的几何体都继承自BufferGeometry几何图形类

BoxGeometry
PlaneGeometry
CircleGeometry
ConeGeometry 圆锥几何体
CylinderGemor 圆柱几何体
RingGeometry
TorusGeometry 环形几何体
TorusKnotGeometry 环节几何体
DodecahedronGeometry 十二面体
OctachedronGeometry 八面体
TetrahedronGeometry 四面体
IcosahedronGeometry
SphereGeometry 球体
ShapeGeometry 形状几何体,基于贝塞尔曲线
TubeGeometry 管道
ExtrudeGeometry
LatheGeometry
TextGeometry 文本

盒子案例

  • width x轴尺寸
  • height y轴尺寸
  • depth z轴尺寸
  • widthSegments 宽度分段
  • heightSegments 高度分段
  • depthSegments 深度分段

后三个参数用于控制每个面上三角形的数量

更多三角形的细分可用于更多细节,例如地形创建等

使用线框材质观察三角形构成每个面的细节

const geometry = new THREE.BoxGeometry(1, 1, 1, 2, 2, 2)
const material = new THREE.MeshBasicMaterial({
    color: 0xff0000,
    wireframe: true // here
})
const mesh = new THREE.Mesh(geometry, material)
scene.add(mesh)

创建自定义几何体

需要使用Float32Array来添加顶点位置,对于该数组有如下特点:

  1. 有类型的数组,只能用于存储浮点数
  2. 长度固定
  3. 对于计算机更易处理
const positionsArray = new Float32Array(9)

// 顶点1
positionsArray[0] = 0 // x
positionsArray[1] = 0 // y
positionsArray[2] = 0 // z
// 顶点2
positionsArray[3] = 0
positionsArray[4] = 1
positionsArray[5] = 0
// 顶点3
positionsArray[6] = 1
positionsArray[7] = 0
positionsArray[8] = 0

// 或
const positionsArray = new Float32Array([
    0, 0, 0, // 顶点1
    0, 1, 0, // 顶点2
    1, 0, 0  // 顶点3
])

然后,将数组转换为缓冲区属性BufferAttribute,其中3表示3个为一组作为顶点的坐标

const positionsAttribute = new THREE.BufferAttribute(positionsArrray, 3)

将缓冲区属性通过setAttribute传递给缓冲区几何体BufferGeometry

const geometry = new THREE.BufferGeometry()
geometry.setAttribute('position', positionsAttribute)

循环创建三角形测试

一些几何体的面的组成有共同的顶点

Debug UI

lil-gui

npm install lil-gui

Range
Color
Text
CheckBox
Select
Button

Range

import GUI from 'lil-gui'

const gui = new GUI()

gui.add(mesh.position, 'y')

但是这样仅仅只能进行数值输入,为了实现拖动控制,需要添加更多参数(最小值、最大值、步长)

gui.add(mesh.position, 'y', -3, 3, 0.01)

// 或
gui.add(mesh.position, 'y')
    .min(-3)
    .max(3)
    .step(0.01)
    .name('elevation')

对于自定义属性的调整,需要写成:

const myObject = {
    myVariable: 1337
}
gui.add(myObject, 'myVariable')

CheckBox

gui.add(mesh, 'visible')
gui.add(material 'wireframe')

Color

由于颜色不是数值、布尔变量,无法自动识别,因此需要使用addColor

gui.addColor(material, 'color')

但是你可能会遇到从Debug UI复制调试好的颜色到代码后,保存刷新却发现与调试颜色不一致的情况:这是因为Three.js应用了一些颜色管理来优化渲染效果(sRGB、色彩空间、色彩管理等)

解决方法1:使用getHexString获取正确的颜色值

gui.addColor(material, 'color')
    .onChange(val => {
        console.log(val.getHexString())
    })

解决方法2:处理传入ThreeJS的颜色

const debugObject = {}
debugObject.color = "#3a64a6"
// ...
const material = new THREE.MeshBasicMaterial({ color: debugObject.color })
// ...

gui.addColor(material, 'color')
    .onChange(val => {
        material.color.set(debugObject.color)
    })

Function/Button

通过按钮触发方法

debugObject.spin = () => {
    gsap.to(mesh.rotation, { y: mesh.rotation.y + Math.PI * 2 })
}
gui.add(debugObject, 'spin')

调整几何体其他属性

例如你想要调整几何体的分段,但是分段参数只有在初始化创建几何体时设置,并不是几何体的属性

因此当值变化时,我们需要销毁原有的几何体,并创建新的几何体

debugObject.subdivsion = 2
gui.add(debugObject, 'subdivision')
    .min(1)
    .max(20)
    .step(1)
    .onFinishChange(() => {
        mesh.geometry.dispose()
        mesh.geometry = new THREE.BoxGeometry(1, 1, 1, debugObject.subdivision, debugObject.subdivision, debugObject.subdivision)
    })

如果要完成一些类似于销毁重建的调试操作,建议使用onFinishChnage代替onChange

注意旧的几何体仍存在于内存中,容易引发内存泄漏,因此需要注意使用dispose正确销毁

Folder

使用addFolder为Debug UI参数创建分组,再将其他可调整选项添加到分组

GUI配置

const gui = new GUI({
    width: 300,
    title: "Nice debug Ui",
    closeFolders: false // 折叠所有分组
})
gui.close() // 折叠整个UI

gui内的hide方法是完全从界面隐藏Debug UI

使用show方法可以从隐藏状态显示,通过show(visible)传入visible参数可以显示/隐藏Debug UI

Textures纹理/贴图

door textures

常用的纹理类型:

  • Color 颜色贴图/反射率贴图:应用于几何体的颜色
  • Alpha 阿尔法贴图:灰度图像,白色部分清晰可见,黑色部分不可见,灰色则一半可见
  • Height 高度贴图/位移贴图:灰度图像,白色顶点上升,黑色定点下降,灰色则不移动
  • Normal 法线贴图:增加照明细节,顶点较少由较好的性能
  • Ambient Occlusion 环境光遮挡:灰度图像,在缝隙中添加假阴影(较深的颜色),以增加细节、对比度
  • Metalness 金属贴图:灰度图像,白色为金属色,黑色非金属,用于创建反射,金属能够看到背后的反射效果
  • Roughness 粗糙度贴图:灰度图像,通产看到金属感时也会看到粗糙感,与光消散有关,白色表示粗糙,黑色表示光滑

PBR 基于物理的渲染

这些纹理遵循PBR原则。PBR原则主要是金属度和粗糙度。通过接近实际计算获得更加真实的效果。

如何加载纹理

使用Image加载

const image = new Image()
const texrture = new THREE.Texture(image)
image.onload = ()  => {
    texture.needsUpdate = true
}
image.src = '/textures/door/color.jpg'

const material; = new THREE.MeshBasicMaterial({ map: texture })

使用TextureLoader纹理加载器

const textureLoader = new THREE.TextureLoader()
const texture = textureLoader.load('/textures/door/color.jpg')

一个纹理加载器可以加载多个纹理,同时在load方法提供了额外的三个回调方法参数

  • load:加载完成
  • progress:加载中
  • error:加载错误

使用LoadingManager加载管理器

可以使用加载管理器统一管理资源加载(贴图、字体、模型等)的回调,包括开始加载、已加载、正在加载、进度和错误等

const loadingManager = new THREE.LoadingManager()
const textureLoader = new THREE.textureLoader(loadingManager)

loadingManager.onStart = () => {}
loadingManager.onLoad = () => {}
loadingManager.onProgress = () => {}
loadingManager.onError = () => {}

UV unwrapping UV展开

将盒子几何体BoxGeometry替换为其他几何体,贴图应用于其他形状的几何体将变得怪异

这与纹理如何放置在几何体上有关

通过geometry.attributes.uv可以看到这些UV坐标,这些坐标有助于几何体上定位图片(纹理)

改变纹理

贴图重复

texture.repeat.x
texture.repeat.y

贴图重复THREE.MirroredRepeatWrapping,贴图镜像重复THREE.RepeatWrapping

texture.wrapS // 横向
texture.wrapT // 纵向

贴图偏移位置

texture.offset.x
texture.offset.y

让贴图在二维方向旋转一定弧度

texture.rotation = Math.PI /4

但是旋转、偏移、重复的中心点总是在左下角(0, 0的UV坐标),修改中心点坐标到中间

texture.center.x = 0.5
texture.center.y = 0.5

Filtering 和 Mipmapping 过滤和MIP映射

将相机放在立方体顶面的边缘,可以看到立方体的纹理是模糊的,这里发生的是过滤和MIP映射

GPU在绘制时将重复创建大小只有一半的纹理,直到得到一个1×1的纹理

有两种类型的算法将会发生这些MIP映射

Minification filter 缩小过滤器

当纹理的像素小于渲染的像素时(将带有纹理的几何体缩放到很小时)

如果你对模糊效果不满意的话,通过修改texture的minFilter可以更改获取像素的方式:

  • THREE.NearestFilter(试试这个)
  • THREE.LinearFilter
  • THREE.NearestMipmapNearestFilter
  • THREE.NearestMipmapLinearFilter
  • THREE.LinearMipmapNearestFilter
  • THREE.LinearMipmapLinearFilter(默认)

莫尔条纹

使用THREE.NearestFilter时,不需要MIP映射,可以停用

texture.generateMipmaps = false

Magnification filter 放大过滤器

当纹理太小将其渲染到较大的几何体上将变得模糊时

可以修改texture的magFilter

  • THREE.NearestFilter(试试这个,贴图风格将会变得锋利)
  • THREE.LinearFilter(默认)

如果你并不关系展示的结果,使用THREE.NearestFilter将获得更好的效果和性能

纹理格式和优化

用户在使用时需要下载所有纹理,因此在准备纹理素材时需要考虑三个因素:

  1. 文件体积:jpg有损压缩,文件较小;png无损,文件较大
  2. 图像大小(尺寸):调整纹理大小,尽可能小的纹理对于GPU友好。当使用MIP映射时,图像大小大约是额外的3/4倍(创建小版本)。
  3. 数据:如果使用jpg格式作为色彩贴图,可能还需要提供另一个的透明度贴图,如果希望使用一个文件解决,最好使用png格式;使用法线贴图时通常是png贴图,因为需要精确的坐标

困难在于找到纹理格式和分辨率的正确组合,否则将遇到性能问题

获得纹理资源的常用网站

Materials 材质

材质用于为几何体的每个可见像素添加颜色,决定材质颜色的程序称为着色器

MeshBasicMaterial 网格基础材质

  • map:颜色贴图
  • color:颜色
    const material = new THREE.MeshBaiscMaterial({ color: 'red' })
    material.color = new THREE.Color('red')
    material.color.set('red')
  • wireframe:线框材质
  • transparent和opacity/alphaMap:透明,使用透明时需要提前设置transparent为true
  • side:设置面可见,默认背面是不渲染的
    • THREE.FrontSide(默认)
    • THREE.BackSide
    • THREE.DoubleSide

MeshNormalMaterial 网格法线材质

法线:包含面外部方向的信息,可以被用于光照、反射、折射

相比于MeshBasicMaterial的常用属性,MeshNormalMaterial多了一个flatShading,设置为true时平面将不再光滑,有平坦的阴影

material.flatShading = true

MeshMatcapMaterial 网格材质捕捉材质

将通过相对于相机的法线使用正确的颜色

在场景没有灯光的情况下模拟灯光、阴影

const material = new THREE.MeshMatcapMaterial()
material.matcap = matcapTexture

更多的Matcap贴图:https://github.com/nidorx/matcaps

MeshDepthMaterial 网格深度材质

网格深度材质简单地将距离相机近的颜色渲染为白色,远的为黑色

const material = new THREE.MeshDepthMaterial()

可以用于雾和预处理

MeshLambertMaterial 网格Lambert材质

网格Lambert材质将会对光源做出反应,拥有与光源相关的属性

创建环境光

const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)

创建点光源

const pointLight = new THREE.PointLight(0xffffff, 0.5)
pointLight.position.x = 2
pointLight.position.y = 3
pointLight.position.z = 4
scene.add(pointLight)

MeshPhongMaterial

类似与网格Lambert材质,但是性能劣于网格Lambert材质,可以看到光的反射

shininess光泽,用于设置光的反射程度

material.shininess = 100

specular用于设置反射光的颜色

material.specular = new THREE.Color(0x1188ff)

MeshToonMaterial

类似与网格Lambert材质,但风格变得卡通化

可以通过gradientMap设置渐变贴图

material.gardientMap = gardientTexture

当渐变贴图很小,将不会看到明显的分离,因为存在mipmapping,可以通过设置minFilter和magFilter或关闭mipmapping处理(以重新获得卡通效果)

gradientTexture.minFilter = THREE.NearestFilter
gradientTexture.magFilter = THREE.NearestFilter
gradientTexture.generateMipmaps = false

MeshStandardMaterial

支持光线,有更好的接近于现实的算法和参数,例如粗糙度和金属度

material.metalness = 0.45
material.roughness = 0.65

也可以使用map为材质添加颜色贴图

使用aoMap(Ambient occlusion map)属性为材质添加阴影,但是需要另外提供一组UV坐标

material.aoMap = doorAmbientOcclusionTexture

//...
plane.geometry.setAttribute(
    'uv2',
    new THREE.BufferAttribute(plane.geometry.attributes.uv.array, 2)
)

可以通过aoMapIntensity来控制AO贴图的强度

将高度/位移贴图应用于材质

material.displacementMap = doorHeightTexture

缺少足够的顶点来展示位移 => 为几何体设置更大的细分参数

调整位移比例

material.displacementScale = 0.05

金属度和粗糙度贴图

material.matalnessMap = doorMatalnessTexture
material.roughnessMap = doorRoughnessTexture

金属度和粗糙度贴图将和金属度matalness和粗糙度roughness参数冲突,记得删除或将其设置为默认值

material.metalness = 0
material.roughness = 1

法线贴图:为几何体添加更多细节而不需要创建更多细分

material.normalMap = doorNormalTexture

可以设置法线贴图的强度(Vector2)

material.normalScale.set(0.5, 0.5)

alpha贴图:

material.alphaMap = doorAlphaTexture
material.transparent = true

MeshPhysicalMaterial

与MeshStandardMaterial相同,但是支持透明图层效果(clear coat effect)

https://threejs.org/examples/#webgl_materials_physical_clearcoat

如果你的物体拥有透明图层可以使用这种材质,否则将引起不必要的GPU计算

Points material

用于创建粒子

ShaderMaterial 和 RawShaderMaterial

用于创建自定义的材质

Environment map环境贴图

将周围的环境场景映射在物体上,注意金属度matalness和粗糙度roughness的值将影响效果

Three.js只支持立方体环境贴图

使用CubeTextureLoader(立方体纹理加载器),而不是Texture

const cubeTextureLoader = new THREE.CubeTextureLoader()
const environmentMapTexture = cubeTextureLoader.load({
    // ...六个方向的环境贴图
})
material.envMap = environmentMapTextujre

如何寻找更多环境贴图
https://polyhaven.com/

3D text 3D文字

使用TextBufferGeometry,并且需要使用typeface这种特定的字体格式

这个在线工具可以帮助你将已有的字体转换成typeface
Facetype.js

另一种方式是使用ThreeJS提供的字体,在/node_modules/three/examples/fonts目录可以找到

使用FontLoader加载字体,这里无法像textureLoader直接将load后的贴图复制给变量texture,必须使用回调函数

const fontLoader = new THREE.FontLoader()

fontLoader.load('...', font => {
    const textGeometry = new THREE.TextBufferGeometry(
        'Hello Three.js',
        {
            font: font,
            size: 0.5,
            heght: 0.2,
            curveSegments: 12,
            bevelEnabled: true,
            bevelThinkness: 0.03,
            bevelSize: 0.02,
            bevelOffset: 0,
            bevelSegments: 5
        }
    )
    const textMaterial = new THREE.MeshBasicMaterial()
    const text = new THREE.Mesh(textGeometry, textMaterial)
    scene.add(text)
})

curveSegments曲线分段、bevelSegments斜边分段(倒角分段数)

133后,TextLoader引入的方法有所调整,TextBufferGeometry也变为TextGeometry

文字居中

通过将AxesHelper添加到场景,可以发现文字并不是居中的,文字的左下角为默认的中心点

使用Bounding 边界

Bounding是几何体占据空间多少的一个信息,可以是盒状box或是球形sphere

frustum culling 视锥体剔除,为了禁用相机视野外的几何体的渲染器,ThreeJs将会使用这些盒状或球体的边界去计算,而不是使用几何体上的每一个顶点

默认情况下,ThreeJS使用球形边界

你需要重新计算Bounding

textGeometry.computeBoundingBox()
console.log(textGeometry.boundingBox)

如果没有计算边界盒子,textGeometry.boundingBox讲取得空值,否则将获得一个Box3的对象

使用translate移动几何体但不移动网格,移动几何体的所有顶点

textGeometry.translate(
    - textGeometry.boundingBox.max.x * 0.5,
    - textGeometry.boundingBox.max.y * 0.5
    - textGeometry.boundingBox.max.z * 0.5
)

看起来居中了,但是boundingBox.max和boundingBox.min对应的属性还不是完全相反的值

这是因为斜边bevel对几何形状和中心的改变也有影响,因此还需要进行处理

bevelSize将影响x和y,bevelThickness将影响z

textGeometry.translate(
    - (textGeometry.boundingBox.max.x - 0.02) * 0.5,
    - (textGeometry.boundingBox.max.y - 0.02) * 0.5
    -( textGeometry.boundingBox.max.z - 0.03) * 0.5
)

至此,文字几何体将完全居中

使用center

center()将让几何体基于边界盒居中

textGeometry.center()

尝试实现这样的效果:
使用Three.js实现神奇的3D文字悬浮效果

Lights 灯光

AmbientLight 环境光

网格的每一个位置将会以相同的方式被照亮

用于模拟光反射(bouncing),在各处应用一个小的昏暗的光

color
intensity 强度

const ambientLight = new THREE.AmbientLight(0xffffff, 0.5)
scene.add(ambientLight)

DirectionalLight 定向光

光线来自同一方向,且平行,将会仅照亮指定的部分

color
intensity 强度

const directionalLight = new THREE.directionalLight(0x00fffc, 0.3)
scene.add(directionalLight)

修改定向光的position

HemisphereLight半球光

color(or skyColor)
groundColor
intensity

const hemisphereLight = new THREE.HemisphereLight(0xff0000, 0x0000ff, 0.3)
scene.add(hemisphereLight)

场景顶部将是红色,底部将是蓝色,介于中间是两种颜色的混合,也就是紫色

PointLight 点光源

有一个点向各个方向照射光
color
intensity

const pointLight = new THREE.PointLight(0xff9000, 0.5)
scene.add(pointLight)

pointLight.position.set

点光源的contructor除了color和intensity,点光源还有其他参数

distance 灯光衰减的距离
decay 灯光衰减的速度

RectAreaLight 矩形区域光

color
intensity
width
height

const rectAreaLight = new THREE.RectAreaLight(0x4e00ff, 2, 1, 1)
scene.add(rectAreaLight)

矩形平面向一个方向照亮

矩形区域光只适用于MeshStandardMaterial和MeshPhysicalMaterial

可以修改矩形区域光的position和rotation,使用lookAt可以使光源面向对象

SpotLight 聚光灯

将会得到一个圆形的光照射区域,用途类似相机的闪光灯

color
intensity
distance 消失距离
angle 光源范围
penumbra 光源边缘变暗/模糊的程度,0不变暗,数值越大模糊程度越高
decay 通常为1

const spotLight = new THREE.SpotLight(0x78ff00, 0.5, 10, Math.PI * 0.1 0.25, 1)
spotLight.position.set(0, 2, 3)
scene.add(spotLight)

如果需要移动或旋转聚光灯,需要将spotLight.target添加到场景中,然后修改target

spotLight.target.position = -0.75
scene.add(spotLight.target)

灯光优化

添加尽可能少以及使用性能开销较少的灯光

开销最低的灯光:

  • AmbientLight
  • HemisphereLight

开销适中的灯光:

  • DirectionalLight
  • PointLight

开销较高的灯光:

  • SpotLight
  • RectAreaLight

Baking 灯光烘培

使用3D软件将灯光的结果烘焙到纹理中,但是缺点是无法移动光线

烘培的相关示例
将场景内的灯光及阴影烘焙到纹理上

Helpers 灯光助手

通常用于定位灯光的位置、方向和影响范围

当使用相应的灯光助手时们需要提供相应的灯光light及助手的大小size,然后将其添加到场景

const hemisphereLightHelper = new THREE.HemisphereLightHelper(hemisphereLight, 0.2)
scene.add(hemisphereLightHelper)

试一试:
DirectionalLightHelper
PointLightHepler

对于SpotLightHelper,没有size参数,当target变化后需要在下一帧执行update

const spotLightHelper = new THREE.SpotLightHelper(spotLight)
scene.add(spotLight)

//window.requestAnimationFrame(() => {
    //spotLightHelper.update()
//})

对于RectAreaLightHelper,引入的方式不同

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

const rectAreaLightHelper = new RectAreaLightHelper(rectAreaLight)
scene.add(rectAreaLiaghtHelper)

Shadow 阴影

在使用灯光后,物体的背面已经有了阴影,称为核心阴影/本影(core shadow);而物体投在其他物体上的阴影称为投影(drop shadow),物体在其他物体上的轮廓

激活阴影

激活渲染器的shadowMap

renderer.shadowMap.enabled = true

对于物体,需要允许其投影

sphere.castShadow = true

对于平面,需要允许其接受阴影

plane.receiveShadow = true

然后需要激活灯光上的阴影

directionalLight.castShadow = true

只有以下几种类型的灯光支持阴影

  • PointLight
  • DirectionalLight
  • SpotLight

ThreeJs投影的工作方式

  • 当进行一次渲染时,ThreeJS将对支持阴影的每个灯光做一次渲染
  • 将模拟灯光再相机中看到的样子
  • 渲染期间所有的材质将被替换为MeshDepthMaterial
  • 灯光渲染效果将被存储在纹理中(shadow maps)
  • 然后将所有的阴影纹理应用于每种应该接收阴影的材质并投影到相应的材质

示例:
https://threejs.org/examples/?q=shadow#webgl_shadowmap_viewer

调整directionalLight的阴影效果

渲染尺寸

shadowMap具有宽高,可以通过shadow访问灯光上的shadowMap

directionalLight.shadow

通过shadow.mapSize调整渲染尺寸

directionalLight.shadow.mapSize.width = 1024
directionalLight.shadow.mapSize..height = 1024

远近

可以控制渲染的远近

通过CameraHelper,帮助定位光相机shadow.camera

const directionalLightCameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera)
scene.add(directionalLightCameraHelper)

调整光相机的远近,在不影响阴影渲染效果的同时,减少渲染距离以优化性能

directionalLight.shadow.near = 1
directionalLight.shadow.far = 6

Amplitude 振幅/光范围

DirectionalLight使用了OrthographicCamera正交相机,通过CameraHelper可以发现其振幅/光范围较大

directionalLight.shadow.camera.top = 2
directionalLight.shadow.camera.right = 2
directionalLight.shadow.camera.bottom = -2
directionalLight.shadow.camera.left = -2

较小的值可能导致阴影被裁切

当对阴影调试后的效果满意时,可以隐藏CameraHelper

directionalLightCameraHelper.visible = false

模糊

通过radius控制阴影的模糊

directionalLight.shadow.radius = 10

阴影贴图算法

BasicShadowMap:基本阴影贴图,性能更高但会损失质量
PCFShadowMap(默认)
PCFSoftShadowMap
:PCF软阴影贴图,边界将会更柔和,但是性能稍微差点
VSMShadowMap:性能较低,约束较多,将会有意想不到的效果

为了改善贴图效果,通常会修改为PCFSoftShadowMap

renderer.shadowMap.type = THREE.PCFSoftShadowMap

radius属性不适用于PCFSoftShadowMap

调整spotLight的阴影效果

添加聚光灯

const spotLight = new THREE.SpotLight(0xffffff, 0.4, 10, Math.PI * 0.3)

spotLight.castShadow = true

spotLight.position.set(0, 2, 2)
scene.add(spotLight)
scene.add(spotLight.target)

添加相机助手

const spotLightCameraHelper = new THREE.CameraHelper(spotLight.shadow.camera)
scene.add(spotLightCameraHelper)

混合阴影在场景的展示中并不准确,最好不要有太多的灯光在同一个地方产生阴影

调整shadow.mapSize

spot使用透视相机作为光相机,通过调整fov属性来调整振幅/光范围

spotLight.shadow.camera.fov = 30

调整near和far

PointLight

点光源使用透视相机作为光相机,在六个方向上进行渲染,最后将光相机朝下

调整以下参数来控制点光源的投影
mapSize
near

烘焙阴影

在纹理中烘焙阴影

使用TextureLoader引入相应的纹理(静态阴影)

在ThreeJS 144版本后,直接关闭renderer的shadowMap来关闭阴影,而2024-11-29 00:19:50 Friday不调整灯光的castShadow参数可能会触发错误

也可以简单烘焙一个阴影,将其放置于物体下方切略高于平面(避免与平面重叠出现z-fighting)使得阴影可以随着物体的移动而移动,物体上升可以增加阴影的透明度,物体下降可以减少阴影的透明度

示例代码待补充

阶段示例 – 鬼屋

雾 Fog

color
near 雾化影响的最近距离
far 雾化影响的最远距离

const fog = new THREE.fog('#262837', 1, 15)
scene.fog = fog

将雾的颜色设置为渲染器的背景颜色,以隐藏边界

renderer.setClearColor('#262837')

示例代码待补充

发布者

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注