three.js是一个应用于web端,基于webGl的3D可视化开发的js库,本文档整理记录一下threejs的常用方法。
我用threejs是和vue3.0一起使用的,但基本运用到的是threejs的api和js代码,vue3.0的语法相对用的较少,所以跟vue关系不大。
创建一个场景
引入threejs:
我们可以使用npm安装threejs库,并使用模块重命名引入整个库:
npm install three
import * as THREE from 'three';
因为我大部分项目使用的是ts,所以可能还要安装threejs的types:
npm install @types/three
创建一个基本的场景需要以下变量,请注意他们是必选的
# 渲染各种物体的场景类,相当于舞台
const scene = new THREE.Scene();
# 相机,用于看到场景内物体的类,这里我们使用的是PerspectiveCamera(透视相机)
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
# 光,没了他场景会一片乌漆嘛黑,看不见场景内的物体,这里我们使用的是AmbientLight(环境光)
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
# 渲染器,用于渲染场景和相机
const renderer = new THREE.WebGlRenderer({
antialias: true, # 是否开启抗锯齿
alpha: true # 是否开启背景透明
})
# 用于显示渲染器中的canvas的父元素,需要把canvas添加到这个父元素中显示在html
const home = ref<HTMLElement | null>(null);
这里设置threejs的常量不用vue3.0的ref()函数,是因为如果使用ref()的话,常量会变成一个响应式数据对象,变量.value是一个Proxy对象,导致拿不到真正的Scene对象。
const scene = ref<THREE.Scene | null>(null);
scene.value = new THREE.Scene();
scene.add(camera);
# console TypeError: scene.add is not a function
创建好这几个常量后,我们通过一个函数初始化这些常量:
<template>
<div class="home" ref="home"></div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import * as THREE from 'three';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
const renderer = new THREE.WebGlRenderer({
antialias: true,
alpha: true
})
const home = ref<HTMLElement | null>(null);
# 初始化
const initThree = () => {
if(!home.value){
return false;
}
# 相机的z轴设置1000
camera.position.z = 1000;
# 场景添加相机和环境光源
scene.add(camera);
scene.add(ambientLight);
# 设置渲染器的范围,一般是浏览器可视区的宽高
renderer.setSize(window.innerWidth, window.innerHeight);
# 设置渲染器的像素密度,不设置可能会出现视图模糊的情况
renderer.setPixelRatio(window.devicePixelRatio);
# 允许渲染器渲染阴影
renderer.shadowMap.enabled = true;
# 把渲染器的canvas添加到要显示的元素中
home.value.appendChild(renderer.domElement);
}
# 在生命周期内调用该函数
onMounted(() => {
initThree();
})
</script>
一个基本的渲染初始化就完成了,但我们需要让场景实时渲染,所以需要使用 requestAnimationFrame 函数来循环调用渲染函数,更新帧动画。
const animate = () => {
# 更新帧动画
requestAnimationFrame(animate);
# 使用渲染器渲染场景和相机
renderer.render(scene, camera);
}
onMounted(() => {
animate();
})
这里使用requestAnimationFrame而不使用setInterval的原因是,虽然这两个函数都是实现循环触发事件,但setInterval是基于时间的,requestAnimationFrame是基于帧数的,requestAnimationFrame的自适应能力强,并且在切到后台的时候会自动停止函数,详情可以了解以下两个api的文档介绍: requestAnimationFrame api文档 setInterval api文档
我们可以使用window事件监听器,监听窗口变化并重置视图
const initResize = () => {
# 监听窗口变化
window.addEventListener('resize', () => {
# 重设相机宽高比例
camera.aspect = window.innerWidth / window.innerHeight;
# 更新相机投影矩阵
camera.updateProjectionMatrix();
# 重设渲染器渲染范围
renderer.setSize(window.innerWidth, window.innerHeight);
})
}
# 在生命周期里调用
onMounted(() => {
initResize();
})
场景创建好了,怎么在场景里添加物体呢?
threejs内置了许多几何类,我们可以通过创建不同类来实现创建几何体
一个完整的几何体包括“结构(Geometry)”、“材质(Material)”,创建这两个变量后,通过new THREE.Mesh()合并成几何体实例。
# 定义一个全局变量 用来存储物体实例
let boxMesh;
# 创建一个正方体
const initBox = () => {
# 创建结构,这里使用的是立方体类,创建一个长宽高为1的立方体
const boxGeo = new THREE.boxGeometry(1, 1, 1);
# 创建材质,这里使用金属类材质,颜色为白色
const boxMate = new THREE.MeshPhongMaterial({
color: 0xffffff
});
# 将结构和材质合并生成实例
boxMesh = new THREE.Mesh(boxGeo, boxMate);
# 将实例添加到场景中
scene.add(boxMesh);
}
# 在生命周期里调用
onMounted(() => {
initBox();
})
threejs提供了主流3D模型文件的加载器,当我们需要导入一个模型时,我们只需要引入不同的模型加载器就能加载模型文件。
考虑到需要在程序上运行的3D模型,threejs推荐的模型格式为glTF,当然如果是其他的文件格式,只需要引入不同的加载器就可以了。
# 在pubilc里新建一个model文件夹,将模型文件放在public/model/文件夹里面,这里是静态文件夹
# public/model/shibuya 这里是本项目存放模型的地方,是一个广告牌模型
# 所有的模型加载器都在'three/examples/jsm/loaders/'里,按照文件类型的不同来引入不同的loader,每个loader的语法上可能不同,这里引入的是GLTF文件加载器。
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
# 定义一个全局变量 用来存储模型的实例
let model;
# 初始化模型
const initModel = () => {
# 实例化加载器
const loader = new GLTFLoader();
# 加载模型文件
# 模型文件加载成功会返回一个模型文件对象resource,resource.scene为模型实例
loader.load('/model/shibuya/scene.gltf', (gltf) => {
# 将实例赋值在全局变量
model = gltf.scene;
# 缩小模型至相对于模型自身的40%
model.scale.multiplyScalar(0.4);
# 微调一下模型的位置
model.position.y = -200;
model.position.x = -400;
# 让模型能产生阴影和接收阴影
model.castShadow = true;
model.receiveShadow = true;
# 在场景里添加模型实例
scene.add(model)
})
}
直接加载进去的模型可能不太尽人意,比如本来是钢板的地方,加载进去却像塑料,有灯的地方没有灯光照射出来等等,于是我们需要遍历并逐个调整模型中的细节。
# 获取模型内所有子节点
model.traverse((child) => {
# 获取子节点的name,通过name区分操作
const modelName = child.name;
switch (modelName) {
case 'billboard': {
# 右侧大广告牌 children[0]是背面 children[1]是正面
# 背面换成金属材质
child.children[0]['material'] = new THREE.MeshPhongMaterial({
color: new THREE.Color(0xcccccc), # 材质色改为灰色
shininess: 100 # 金属材质的高亮程度,这里设置100%
})
# 因为正面材质中存在贴图,所以需要定义一个变量保存这个贴图
const map = child.children[1]['material'].emissiveMap;
# 正面换成金属材质
child.children[1]['material'] = new THREE.MeshPhongMaterial({
map: map, # 将贴图重新赋予材质中
emissiveMap: map, # 设置放射的贴图
shininess: 100,
emissive: new THREE.Color(0xffffff), # 自发光的颜色,设置为白色
emissiveIntensity: 0.5 # 自发光的程度,设置为0.5
})
# 将该实例分至第1层
child.children[1].layers.enable(1);
break;
}
case 'billboard-wireframe': {
# 右侧大广告牌的黑色框架
# 微调y轴,使框架突出,不然跟广告牌重叠会导致广告牌花屏
child.children[0].position.y = -0.1
#将框架替换为金属材质
child.children[0]['material'] = new THREE.MeshPhongMaterial({
color: 0x000000,
shininess: 100
})
break;
}
case 'lamp': case 'lamp001': case 'lamp002': {
# 右侧大广告牌上面的三个灯
# 创建聚光灯实例,这里使用的是THREE.PointLight
const light = new THREE.PointLight(0xffff00, 1);
# 使聚光灯能够产生阴影和接受阴影
light.castShadow = true;
light.receiveShadow = true;
# 微调灯的位置
light.position.x = 2;
# 灯泡实例,把聚光灯添加到灯泡里
child.children[2].add(light);
# 将灯杆和灯罩替换为金属材质
child.children[0]['material'] = new THREE.MeshPhongMaterial({
color: 0x222222,
emissive: 0x222222,
shininess: 100
})
child.children[1]['material'] = new THREE.MeshPhongMaterial({
color: 0x222222,
emissive: 0x222222,
shininess: 100
})
break;
}
case 'cube-letter':
case 'cube-letter001':
case 'cube-letter002':
case 'cube-letter003':
case 'cube-letter004':
case 'cube-letter005':
case 'cube-letter007':
{
# 七个led广告牌灯
# 将灯面替换为金属材质
child.children[1]['material'] = new THREE.MeshPhongMaterial({
color: 0xffffff,
emissive: 0xffffff,
emissiveIntensity: 1
})
# 将该实例分至第1层
child.children[1].layers.enable(1);
break;
}
case 'takoyaki-ya': case 'takoyaky-ta': case 'takoyaki-ko': case 'takoyaki-ki': {
# 大广告牌旁边四个小字灯的字 正面
# 微调字的位置
child.position.z += 10
break;
}
case '2-takoyaki-ya': case '2-takoyaki-ta': case '2-takoyaki-ko': case '2-takoyaki-ki': {
# 大广告牌旁边四个小字灯的字 背面
# 微调字的位置
child.position.z -= 10
break;
}
case 'donkey': case 'jote': {
# donkey广告牌上的字
# 微调字的位置
child.position.z += 10;
# 将该实例分至第1层
child.children[0].layers.enable(1);
break;
}
case 'cube-letter006': {
# donkey广告牌的边框
# 将该实例分至第1层
child.children[0].layers.enable(1);
break;
}
case 'text-shibuya':
case 'text-shibuya001':
case 'text-shibuya003':
{
# 涉谷109的led字
# 微调字的位置
child.position.x -= 100;
# 将该实例分至第1层
child.children[0].layers.enable(1);
break;
}
case 'text-shibuya002': {
# TOMA CAFE的led字
# 将该实例分至第1层
child.children[0].layers.enable(1);
break;
}
default:
break;
}
})
这是经过微调后的模型,是不是感觉好多了,发光的发光,反光的反光。
在座细心的各位会发现我把某些实例分层在第一层,这是为什么呢?
既然这是个广告牌,还那么多霓虹灯,就该有灯的样子,我印象中的霓虹灯。。。灯红酒绿。。。有种雾里的感觉,特别梦幻,现在的模型没有一种特别梦幻的感觉,反而像玩具。。。
所以滤镜加起来!
threejs封装了很多后期处理的工具库,我们可以按需引入
# libs/shaderData.ts
# 创建一个存放顶点着色器和片段着色器内容的ts文件,并暴露出去
# 官方例子是把顶点着色器和片段着色器存放在两个script标签里,在vue项目中我们可以使用es6的模板语法来存放,效果一样的
# 顶点着色器和片段着色器用GLSL着色器语言编写,这个暂且先了解一下就行
# 顶点着色器
const bloomVertexShader = `varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}`
# 片段着色器
const bloomFragmentshader = `uniform sampler2D baseTexture;
uniform sampler2D bloomTexture;
varying vec2 vUv;
void main() {
gl_FragColor = ( texture2D( baseTexture, vUv ) + vec4( 0.5 ) * texture2D( bloomTexture, vUv ) );
}`
export default {
bloomVertexShader,
bloomFragmentshader,
}
# ---------------------------------
# home.vue
# 引入效果组合器
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer'
# 引入渲染通道
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'
# 引入自定义着色器
import { ShaderPass } from 'three/examples/jsm/postprocessing/ShaderPass'
# 引入辉光通道
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass'
# 定义两个效果组合器,一个作为渲染不需要发光的材质的组合器,一个作为渲染发光的材质的组合器
let bloomComposer = new EffectComposer(renderer);
let finalComposer = new EffectComposer(renderer);
# 初始化
const initComposer = () => {
# 辉光通道的相关参数
const params = {
bloomStrength: 1, # 辉光的强度,值越大明亮的区域越亮
bloomThreshold: 0, # 光照的强度阈值,如果照在物体上的光照强度大于该值就会产生辉光
bloomRadius: 0 # 发光散光的范围半径
}
# 定义渲染通道实例
const renderScene = new RenderPass(scene, camera);
# 定义一个辉光效果通道实例
# 设置通道的辉光覆盖范围(一般取可视屏幕的可视范围),强度,强度阈值,范围,这个靠自己目测看着来调就行了
const bloomPass = new UnrealBloomPass(new THREE.Vector2(window.innerWidth, window.innerHeight), 1.5, 0.4, 0.85);
bloomPass.threshold = params.bloomThreshold;
bloomPass.strength = params.bloomStrength;
bloomPass.radius = params.bloomRadius;
# 最终过程是否被渲染到屏幕
# bloomComposer中不需要渲染到屏幕
bloomComposer.renderToScreen = false;
# 设置效果组合器的范围和像素密度
bloomComposer.setSize(window.innerWidth, window.innerHeight);
bloomComposer.setPixelRatio(window.devicePixelRatio);
# 将渲染通道和辉光通道添加到通道组合器内
bloomComposer.addPass(renderScene);
bloomComposer.addPass(bloomPass);
# 定义一个着色器,用于后期效果渲染
const finalPass = new ShaderPass(
new THREE.ShaderMaterial({
uniforms: {
baseTexture: {
value: null
},
bloomTexture: {
value: bloomComposer.renderTarget2.texture
}
},
vertexShader: shaderData.bloomVertexShader, # 顶点着色器
fragmentShader: shaderData.bloomFragmentshader, # 片段着色器
defines: {}
}),
'baseTexture'
)
# 官方文档中找不到这个属性的解释
# 打开源码搜索了一下终于找到了一句注释
# if set to true, the pass indicates to swap read and write buffer after rendering
# 意思是如果这个属性设置为true,则过程指示在渲染后交换读写缓冲区
# 我寻思可能是对于一些大型的效果又很炫酷的项目开启这个属性,在渲染的时候会把效果存储在缓冲区,后面用到时再重复调用,在显示效果的性能上会好一点,这个先了解一下就行
# 反正在这个项目中我搁这不管设置为true或者false,最终显示效果都没啥区别-_-||
finalPass.needsSwap = true;
# 设置效果组合器的范围和像素密度
finalComposer.setSize(window.innerWidth, window.innerHeight);
finalComposer.setPixelRatio(window.devicePixelRatio);
# 将渲染通道和辉光通道添加到通道组合器内
finalComposer.addPass(renderScene);
finalComposer.addPass(finalPass);
}
# 在生命周期内调用
onMounted(() => {
initComposer();
})
这里我们定义了一组辉光通道处理效果,我们接下来需要调用并渲染它
先定义一些需要用到的全局变量
# 定义一个层阶为1的视图层,用来后续做判断渲染
const bloomLayer = new THREE.Layers();
bloomLayer.set(1);
# 定义一个黑色普通材质实例,用于后续辉光渲染使用
const darkMaterial = new THREE.MeshBasicMaterial({
color: 0x000000
})
# 临时存放材质实例对象合集
let materialsArr = {};
添加了后期效果通道后,我们需要修改之前的渲染方式,不能依靠renderer.render()
来渲染了
# 定义一个将所有子节点转为黑色材质的回调函数
const darkenNonBloomed = (obj: THREE.Object3D<THREE.Event>) => {
# 我们不需要渲染场景的背景色后期效果,所以scene需要做特殊判断处理
if (obj instanceof THREE.Scene) {
materialsArr['scene'] = obj.background;
obj.background = null;
return;
}
# 将不属于第1层的节点材质改为黑色
# 改材质是为了渲染辉光的时候原本材质不受污染
if (obj['isMesh'] && bloomLayer.test(obj.layers) === false) {
# 把材质储存在对象中做统一管理
materialsArr[obj.uuid] = obj['material'];
obj['material'] = darkMaterial;
}
}
# 定义一个将所有子节点还原材质的回调函数
const restoreMaterial = (obj: THREE.Object3D<THREE.Event>) => {
# 场景做特殊处理,还原scene并删除对象中的scene
if (obj instanceof THREE.Scene) {
obj.background = materialsArr['scene'];
delete materialsArr['scene'];
}
# 将之前的节点材质从黑色材质改为原来的材质
if (materialsArr[obj.uuid]) {
obj['material'] = materialsArr[obj.uuid];
# 删除对象中对应的材质
delete materialsArr[obj.uuid];
}
}
# 通道渲染函数
const renderBloom = () => {
# 先把不需要的材质赋黑色材质
scene.traverse(darkenNonBloomed);
# bloomComposer组合器渲染
bloomComposer.render();
# 还原材质
scene.traverse(restoreMaterial);
# finalComposer组合器渲染
finalComposer.render();
}
const animate = () => {
# 更新帧动画
requestAnimationFrame(animate);
# 以前的渲染方式就不要了,改为通道渲染
renderBloom();
# renderer.render(scene, camera);
}
最终效果是这样的,是不是有一种霓虹灯该有的梦幻的感觉?
这个辉光效果的代码借鉴于这里
官方也有现成的例子可以参考(白嫖)
以下是我通过开发demo用到的一些比较常用的api用法
创建一个用于放置物体、模型、通道的场景,是构成整个threejs实例必要的类
用法:
const scene = new THREE.Scene()
修改scene的背景颜色
# scene.background可以接受THREE.Color()类和THREE.Texture类,根据自身需求进行选择
# 使用THREE.Color进行纯色背景填充
scene.background = new THREE.Color(0xffffff);
# 使用THREE.Texture进行图片背景填充
const loader = new THREE.TextureLoader();
loader.load('./xxx.png', (texture) => {
scene.background = texture
})
相机类,用于查看场景的元素,可以理解为你的浏览器窗口就是相机的摄像头,是组成threejs实例必要的类。
相机大致分为 透视相机(PerspectiveCamera) 和 正交相机(OrthographicCamera)
- 透视相机(PerspectiveCamera),在threejs比较常用且普遍的相机,用来模拟人的眼球所看到的景象。
# 初始化一个透视相机
# 透视相机接收四个参数
# fov — 摄像机视锥体垂直视野角度,该值数字越小,场景里的物体离相机的距离越近,反之越远
# aspect — 摄像机视锥体长宽比,一般使用 窗口文档显示区宽度 / 窗口文档显示区高度 的值
# near — 摄像机视锥体近端面,该值越大,场景里的物体能靠近相机的距离越短,反之越长
# far — 摄像机视锥体远端面,该值越大,场景里的物体能远离相机的距离越长,反之越短
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 10000);
- 正交相机,在正交相机里不管物体距离相机的距离是远还是近,他们的大小都不变,该相机类常用于2D场景较多的项目中。
# 初始化一个正交相机
# 正交相机接受六个参数
# left — 摄像机视锥体左侧面,一般为窗口文档显示区宽度 / -2。
# right — 摄像机视锥体右侧面,一般为窗口文档显示区宽度 / 2。
# top — 摄像机视锥体上侧面,一般为窗口文档显示区高度 / 2。
# bottom — 摄像机视锥体下侧面,一般为窗口文档显示区高度 / -2。
# near — 摄像机视锥体近端面,该值越大,场景里的物体能靠近相机的距离越短,反之越长
# far — 摄像机视锥体远端面,该值越大,场景里的物体能远离相机的距离越长,反之越短
const camera = new THREE.OrthographicCamera(window.innerWidth / -2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / -2, 0.1, 10000)
# 相机是一个THREE.object3D类,我们可以让相机做该类能做的事情,比如移动位置:
camera.position.set(100, 100, 100);
# 比如我们可以让相机一直“盯着”某个位置,或某个物体,即使我们改变了相机本身的位置
camera.lookAt(new THREE.Vector3(100, 100, 100));
const position = scene.position;
camera.lookAt(position);
# 应该还有其他比较骚的操作,但可惜我自己不是那么骚,想不出来。。。
灯光类,用于给场景加载灯光,提供光源,是组成threejs实例必要的类。
如果你初始化了一个场景,但是窗口一片乌漆嘛黑,请检查是否在场景内添加了光源。
除了环境光,其他光源均可以产生阴影,环境光无法产生阴影是因为这货没有方向。
常用的灯光类包括环境光(AmbientLight)、平行光(DirectionalLight)、点光源(PointLight)、聚光灯(SpotLight)
- 环境光,该光源会均匀照亮场景中的所有物体(雨露均沾。。。)。
# 初始化一个环境光
# 环境光类可接受两个参数
# color - (可选参)光的颜色,使用rgb色值,默认值为0xffffff
# intersity - (可选参)光的强度,该值越大,光线越强,默认为1
const light = new THREE.AmbientLight(0xffffff, 1)
- 平行光,是沿着特定方向发射的光,这种光理论可以无限远,在现实中,太阳光就是一种平行光。
# 初始化一个平行光
# 平行光类可接受两个参数
# color - (可选参)光的颜色,使用rgb色值,默认值为0xffffff
# intersity - (可选参)光的强度,该值越大,光线越强,默认为1
const light = new THREE.DirectionalLight(0xffffff, 1);
- 点光源,是一个在固定点位放射的光源,类似于夜空中的萤火虫。
# 初始化一个点光源
# 点光源类可接受三个参数
# color - (可选参)光的颜色,使用rgb色值,默认值为0xffffff
# intersity - (可选参)光的强度,该值越大,光线越强,默认为1
# distance - (可选参)光的衰退值,如果该值不为0,光就会从当前值衰减到0,默认为0
const light = new THREE.PointLight(0xffffff, 1, 0);
- 聚光灯,是一个在固定点沿着圆锥体照射的光源,可以想象在黑暗中打开一个手电筒。
# 初始化一个聚光灯源
# 聚光灯类可接受六个参数
# color - (可选参)光的颜色,使用rgb色值,默认值为0xffffff
# intersity - (可选参)光的强度,该值越大,光线越强,默认为1
# distance - (可选参)光的衰退值,如果该值不为0,光就会从当前值衰减到0,默认为0
# angle - (可选参)光的散射角度,最大为Math.PI / 2,默认为Math.PI / 3
# penumbra - (可选参)光锥的半影衰减百分比,可选值为0到1,默认为0
# decay - (可选参)光照距离的衰减值
const light = new THREE.SpotLight(0xffffff);
同样我们能够对光源做一些骚操作,比如:
# 放一个小球在点光源中,让光源具有实体
let light;
# 实例化一个小球
const ballGeo = new THREE.SphereGeometry(0.5, 16, 8);
const ballMate = new THREE.MeshBasicMaterial({
color: 0xffffff
})
const ballMesh = new THREE.Mesh(ballGeo, ballMate);
# 实例化一个点光源
light = new THREE.pointLight(0xffffff, 1, 50);
# 将小球添加到光源中,这样我们可以得到一个看起来会发光的球
light.add(ballMesh);
# 让小球动起来
const animate = () => {
requestAnimationFrame(animate);
# 让光源x轴随帧率刷新而向x轴右方移动
light.position.x += 0.0001;
}
要让光源能够产生阴影,需要将光源的 castShadow = true
。
# 实例化一个平行光
const light = new THREE.DirectionalLight(0xffffff, 1);
# 让这个光源发射的光能够产生阴影
light.castShadow = true;
# 光源产生的阴影像素密度,值越大,像素密度越高,阴影越清晰
light.shadow.mapSize.width = 1024;
light.shadow.mapSize.height = 1024;
# 调整光源产生的阴影贴图的属性
# 如果发现物体的阴影有缺失或者不正常,请尝试调整这些属性值
light.shadow.camera.near = 500;
light.shadow.camera.far = 4000;
light.shadow.camera.fov = 30;
渲染器,负责渲染场景及相机,并且生成一个canvas元素,该canvas是显示整个场景交互的元素,一般使用的是WebGlRenderer渲染器。
# 初始化一个渲染器
const renderer = new THREE.WebGlRenderer({
antialias: true, # 是否开启抗锯齿
alpha: true # 是否开启背景透明
});
# 设置渲染器的宽高
renderer.setSize(window.innerWidth, window.innerHeight);
# 设置渲染器的像素密度
renderer.setPixelRatio(window.devicePixelRatio);
# 设置渲染帧缓冲区,如果遇到画布像素叠加的问题,可以试着将该属性设置为false
renderer.autoClear = false;
# 允许渲染阴影
# ***如果想让场景内的物体产生阴影,则必须设置该属性为true***
renderer.shadowMap.enabled = true;
# 可以替换阴影材质包,自己看哪种阴影更符合自身的需求
# - THREE.BasicShadowMap
# - THREE.PCFShadowMap (默认)
# - THREE.PCFSoftShadowMap
# - THREE.VSMShadowMap
renderer.shadowMap.type = THREE.basicShadowMap;
场景中的物体离不开几何体,像我们日常生活中的高楼大厦,车水马龙,映射在threejs的场景世界中,就是由一个个几何体组合而成。
threejs给开发者提供了很多常见的几何体类,供我们便捷高效的开发项目。
threejs中一个完整的几何体包括“结构(Geometry)”、“材质(Material)”,创建这两个变量后,通过new THREE.Mesh()合并成几何体实例。
结构类是定义几何体形状的基本类,threejs已经封装了许多常见的几何体类供我们开箱即用,不过本质都是BufferGeometry;
这里以一个盒体类作为例子:
# 创建一个长宽高为1的正方体
const geo = new THREE.boxGeometry(1, 1, 1);
假如threejs自带封装的几何体类无法满足自己的开发需求,我们可以使用构造器(ShapeGeometry)以及形状(Shape)来进行自定义几何体。
# 封装一个创建圆角矩形函数
# - shape 需要进行创建的shape
# - opts 圆角矩形的相关配置,x:矩形的x轴坐标点,y:矩形的y轴坐标点,width:矩形的宽度,height:矩形的高度,radius:矩形的圆角度数
# - returns shape 创建好的矩形
const roundedRect = (shape: THREE.Shape, opts: {
x: number,
y: number,
width: number,
height: number,
radius: number
}) => {
shape.moveTo(opts.x, opts.y + opts.radius);
shape.lineTo(opts.x, opts.y + opts.height - opts.radius);
shape.quadraticCurveTo(opts.x, opts.y + opts.height, opts.x + opts.radius, opts.y + opts.height);
shape.lineTo(opts.x + opts.width - opts.radius, opts.y + opts.height);
shape.quadraticCurveTo(opts.x + opts.width, opts.y + opts.height, opts.x + opts.width, opts.y + opts.height - opts.radius);
shape.lineTo(opts.x + opts.width, opts.y + opts.radius);
shape.quadraticCurveTo(opts.x + opts.width, opts.y, opts.x + opts.width - opts.radius, opts.y);
shape.lineTo(opts.x + opts.radius, opts.y);
shape.quadraticCurveTo(opts.x, opts.y, opts.x, opts.y + opts.radius);
return shape;
}
# 创建一个形状类
const shape = new THREE.shape();
# 将形状类和属性参传入封装函数中
const roundedRectShape = roundedRect(shape, { 0, 0, 1, 1, 0.2 });
# 使用构造器进行构造
const geo = new THREE.ShapeGeometry(roundedRectShape);
材质是决定几何体表面外观的基本类,常用的材质分别为:
-
基础网格材质(MeshBasicMaterial),一种只有简单着色的几何体材质,存在该材质的物体无法产生阴影。
-
高光网格材质(MeshPhongMaterial),一种具有金属高光的几何体材质,多用于金属类,陶瓷类具有反光性质的几何体材质。
-
非高光网格材质(MeshLambertMaterial),一种不具备光泽度的几何体材质,没有高光,可以相当于磨砂材质,一般用于
差不多就用过这么几种,在日常开发中其实已经够用了,threejs官网还提供了很多其他特殊的材质,可以移步到这 —> 文档地址
这里以一个高光材质作为例子:
# 创建一个颜色为白色的高光网格材质
const mate = new THREE.MeshPhongMaterial({
color: 0xffffff,
});
用于将结构和材质结合,生成物体实例的类,同时也作为其他类的基类。
# 将结构和材质结合,生成物体实例
# 该类可接收两个参数
# - geometry(可选参) 接收一个BufferGeometry的结构实例,默认值是一个新的BufferGeometry。
# - material(可选参) 接收一个Material的材质实例,默认是一个新的MeshBasicMaterial。
const mesh = new THREE.Mesh(geo, mate);
# 将物体添加到场景内
scene.add(mesh);
用于在材质上使用贴图,自定义材质外观的类,使用loader加载。
# 创建一个纹理,使用TextureLoader()加载器
# 先定义一个加载器
const loader = new THREE.TextureLoader();
# 使用加载器加载贴图,加载器允许接收一个图片url,返回一个texture纹理
# 可以有两种加载写法
# 1.不带回调的写法
const texture = loader.load('xxx.png');
# 将纹理添加到材质中
mate.map = texture;
# 2.带有回调的写法
loader.load('xxx.png', (texture) => {
# 将纹理添加到材质中
mate.map = texture;
}, (progress) => {
# 纹理加载中的回调,返回当前纹理加载的进度和字节
console.log(progress)
}, (err) => {
# 纹理加载错误的回调
console.log(err)
})
用于将多个Object3D对象合并的类,如果你想将场景中的多个物体进行统一管理,可以使用该类。
# 创建一个组
const group = new THREE.Group();
# 将需要统一管理的物体添加到组内
# 添加两个几何体,一个灯光和一个模型
group.add(mesh1);
group.add(mesh2)
group.add(light);
group.add(model);
# 将组添加到场景中
scene.add(group);
# *** 渲染函数中 ***
const animate = () => {
requestAnimationFrame(animate);
# 使组进行x轴的旋转运动
# 那么组内的物体都会相对于组的中心点进行x轴的旋转运动
group.rotation.x += 0.001;
}
一个用于显示点的类,一般用于制作粒子特效,用法跟new THREE.Mesh()
大致相同。
# 实例化一个点类
# 该类可接收两个参数
# - geometry (可选参)接收一个BufferGeometry的结构实例,默认值是一个新的BufferGeometry。
# - material (可选参)接收一个对象,默认是一个新的pointMaterial。
const point = new THREE.Points();
# 将点添加到场景中
scene.add(point)
一个适用于点类的材质。
# 实例化一个点材质
const pointMate = new THREE.pointsMaterial({
color: 0xffffff
});
# 将点材质添加到点类中
point.material = pointMate;
辅助器可以将坐标轴、光线方向、物体骨骼等三维线性的虚拟对象进行可视化,这将对开发提供友好的帮助。
这里举例几个我常用的辅助器。
THREE.AxesHelper用于简单模拟并可视化整个场景的原点坐标轴x、y、z轴的对象,可帮助开发者快速定位场景中的原点。
# 创建一个坐标轴辅助器
# 该类接受1个参数
# - size (可选)辅助线的线段长度,默认为1
const axesHelper = new THREE.AxesHelper(100);
scene.add(axesHelper);
THREE.BoxHelper用于将场景中某个几何体或模型等3D对象进行外包围盒进行可视化操作,可帮助开发者快速查看3D对象的外包围骨骼。
想要进行外包围辅助器,该3D对象中必须包含BufferGeometry(缓存几何),没包含BufferGeometry的对象将无法使用外包围辅助器。
# 创建一个正方体对象
const geo = new THREE.BoxGeometry(1, 1, 1);
const mate = new THREE.MeshBasicMaterial({ color: 0xffffff });
const mesh = new THREE.Mesh(geo, mate);
# 创建一个包围盒辅助器
# 该类接受2个参数
# - object3D THREE.Object3D<THREE.Event> 需要进行可视化包围盒的3D对象
# - color (可选)THREE.Color 包围盒线框的颜色
const boxHelper = new THREE.BoxHelper(mesh, 0xffffff);
scene.add(boxHelper);
灯光辅助器可以将各种灯光类的光线朝向进行可视化渲染,并通过箭头和线条进行显示,当添加了灯光辅助器之后,我们可以直观的看到灯光光线的朝向和灯光范围等。
不同的灯光类需要匹配不同的灯光辅助器。
因为AmbientLight环境光类没有方向,所以该光类无辅助器。
# 创建一个平行光类
const directionalLight = new THREE.DirectionalLight(0xffffff);
# 创建一个平行光辅助器类
# 该类接受3个参数
# - directionalLight THREE.DirectionalLight 需要进行辅助的平行光类
# - size (可选)number 辅助线的尺寸,默认为1
# - color (可选)THREE.Color 辅助线的颜色,如该参数为空则使用灯源的颜色
const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight, 100, 0xffffff)
scene.add(directionalLightHelper);
# 创建一个点光源类
const pointLight = new THREE.PointLight(0xffffff, 1);
# 创建一个点光源辅助器类
# 该类接受3个参数
# - pointLight THREE.PointLight 需要进行辅助的点光源类
# - size (可选)number 辅助线的尺寸,默认为1
# - color (可选)THREE.Color 辅助线的颜色,如该参数为空则使用灯源的颜色
const pointLightHelper = new THREE.PointLightHelper(pointLight, 100, 0xffffff);
scene.add(pointLightHelper);
# 创建一个聚光灯类
const spotLight = new THREE.SpotLight(0xffffff, 1);
# 创建开一个聚光灯辅助器类
# 该类接受2个参数
# - spotLight THREE.SpotLight 需要进行辅助的聚光灯类
# - color (可选)THREE.Color 辅助线的颜色,如该参数为空则使用灯源的颜色
const spotLightHelper = new THREE.SpotLightHelper(spotLight, 0xffffff);
scene.add(spotLightHelper);
效果组合器用于在threejs中实现各种后期特效效果,该类管理了最终后期特效效果的过程出历链,我们可将各种通道渲染添加至效果组合器中,统一管理并且渲染他。
你必须额外在官方npm包中引入它才能够正常使用效果组合器
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer';
# 实例化一个效果组合器
# 该类允许接收两个参数
# - renderer 用于渲染场景的渲染器
# - renderTarget (可选参)一个预先配置的渲染目标,内部由EffectComposer使用(这个参数暂且没用过,先了解一下)
const effectComposer = new EffectComposer(renderer);
renderPass通常位于EffectComposer过程链的最上层,这个通道会渲染场景,但不会将渲染结果输出到屏幕上。
你必须额外在官方npm包中引入它才能够正常使用RenderPass
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass'
# 实例化一个renderPass通道
# 该类允许接收六个参数
# - scene 将要渲染的场景实例
# - camera 将要渲染的相机实例
# - overrideMaterial (可选参)等同于scene.overrideMaterial, 强制将场景里的物体使用该参数作为材质
# - clearColor (可选参)等同于renderer.setClearColor(),设置renderer的颜色,合法参数是一个THREE.Color()函数
# - clearAlpha (可选参)等同于renderer.setClearAlpha(),设置renderer的透明度,合法参数是一个0.0到1.0之间的浮点数
# - clearDepth (可选参)等同于renderer.clearDepth,清除深度缓存
const renderPass = new RenderPass(scene, camera);
再添加renderPass通道后,添加接下来的动效通道。
threejs封装了很多常用的动效通道,这里引用一个“故障风格”的动效通道。
你必须额外在官方npm包中引入它才能够正常使用threejs封装好的动效通道
import { GlitchPass } from 'three/examples/jsm/postprocessing/GlitchPass.js';
# 实例化一个故障风格的动效通道
const glitchPass = new GlitchPass();
定义了这两个通道后,将通道添加进合成器中,再进行渲染。
# 将通道添加到合成器中
effectComposer.add(renderPass);
effectComposer.add(glitchPass);
const animate = () => {
requestAnimationFrame(animate);
# 这里摒弃掉renderer.render()的渲染方式
# 改用合成器的渲染方式
effectComposer.render();
}
该函数可以将两个或多个THREE.Vector3()
三维坐标点合并创建成一条平滑的三维曲线。
# 定义一条三维曲线
# 该函数接受四个参数
# - points THREE.Vector3[] 三维坐标点数组,数组中至少有两个点。
# - closed boolean 是否闭合曲线 如该值为true,则曲线会闭合,默认为false。
# - curveType string 可选值 'centripetal' | 'chordal' | 'catmullrom', 默认为'catmullrom',看了下源码应该是关于曲线张力的,如果该值为'centripetal'则张力为0.5,如果该值为'chordal'则张力为0.25,如果该值为'catmullrom'则张力为参数tension的值。
# - tension number 曲线张力,如果curveType = 'catmullrom',则曲线张力会设置为该值,该值越大,曲线的弧度越大,默认为0.5。
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 0, 100),
new THREE.Vector3(0, 0, 0)
], true)
# .getPoints(number) 将曲线分成设置值等分的三维坐标点数组,这里将曲线进行100等分。
const curvePoints = curve.getPoints(100)
# 创建一个缓存几何,设置该几何的点队列
const lineGeo = new THREE.BufferGeometry().setFromPoints(curvePoints)
# 创建一个颜色为红色的基础线型材质
const lineMate = new THREE.lineBasicMaterial({
color: 'red'
})
# 创建一条线
const line = new THREE.Line(lineGeo, lineMate)
# 将线添加到场景内
scene.add(line)
THREE.Vector2(二维向量)可以代表二维平面中的的一个坐标点,通常该函数用x轴,y轴即可代表一个确切坐标点,是一个常在threejs中运用到的函数。
# 创建一个二维向量
# 该类接受两个参数
# - x (可选)坐标点的x轴,默认为0
# - y (可选)坐标点的y轴,默认为0
let vector2 = new THREE.Vector2(1, 1);
# 我们可以随时修改坐标点内的x,y轴
vector2.x = 2;
vector2.y = 2;
THREE.Vector3(三维向量)可以代表三维平面中的一个坐标点,通常该函数用x轴,y轴,z轴即可代表一个确切坐标点,是一个常在threejs中运用到的函数。
# 创建一个三维向量
# 该类接受三个参数
# - x (可选)坐标点的x轴,默认为0
# - y (可选)坐标点的y轴,默认为0
# - z (可选)坐标点的z轴,默认为0
let vector3 = new THREE.Vector3(1, 1, 1);
# 我们可以随时修改坐标点内的x,y,z轴
vector3.x = 2;
vector3.y = 2;
vector3.z = 2;
THREE.Raycaster(光线投射)可以在某个坐标点对threejs场景中投射一道光线,返回该射线照射到的物体,一般用于鼠标在threejs中的交互,计算出鼠标在场景中移动到了哪个物体。
# 创建一个光线投射
const raycaster = new THREE.Raycaster();
# 创建一个二维向量
const pointer = new THREE.Vector2(0, 0);
# 通过相机更新二维向量所对应的三维世界的坐标点
# .setFromCamera()接受两个参数
# - pointer 需要投射的二维向量
# - camera 场景所对应的相机类
raycaster.setFromCamera(pointer, camera)
# 计算射线和物体的焦点
# 返回射线所照射的物体
# intersects返回一个object3D[]数组,你可以通过遍历操作光线所照射的物体
# 光线是“穿透”的,光线不会因为物体位置的前后而被遮挡,所以它会返回一条射线所穿透的所有物体
const intersects = raycaster.intersectObjects(scene.children)
网页离不开交互,如果想要在threejs中做点击事件、hover事件等交互事件,通常需要监听整个document,然后通过公式获取到鼠标映射到的模型,让模型做出相应的响应。
# 监听函数
const mouseMoveHandler = (e: MouseEvent) => {
# 取消默认操作
e.preventDefault();
# 创建一个光线投射
const raycaster = new THREE.Raycaster();
# 创建一个用于保存鼠标坐标的二维向量
const mouse = new THREE.Vector2();
# --重点--
# 将鼠标位置通过公式转换为二维向量
# x轴 = (鼠标在窗口中的x轴 / 窗口宽度) * 2 - 1
# y轴 = -(鼠标在窗口中的y轴 / 窗口高度) * 2 + 1
mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
# 根据鼠标二维向量,通过相机射出射线
raycaster.setFromCamera(mouse, camera);
# 定义射出射线所照射的物体
# 拿到intersects后我们可以通过该值获取到鼠标所接触到的物体,并自行开发物体的交互操作
const intersects = raycaster.intersectObjects(scene.children);
}
onMounted(() => {
# 监听鼠标滑动事件
document.addEventListener('mousemove', mouseMoveHandler)
})
未完待续。。。