概念性的知识
- threejs是一个让用户通过javascript入手进入搭建WebGL项目的类库;
- WebGL 使得网页在支持 HTML <canvas> 标签的浏览器中,不需要使用任何插件,便可以使用基于 OpenGL ES 2.0 的 API 在 canvas 中进行 3D 渲染;
- OpenGL在图形学、电子游戏领域使用很长时间,WebGL是Web版的OpenGL;
- OpenGL学习笔记 -
https://juejin.cn/post/6995037193114746894
- (逐章说明,优先看)
安装
- threejs安装
https://threejs.org/docs/#manual/zh/introduction/Installation
- npm 安装
npm install three
//-- 方式 1: 导入整个 three.js核心库
import * as THREE from 'three';
const scene = new THREE.Scene();
//-- 方式 2: 仅导入你所需要的部分
import { Scene } from 'three';
const scene = new Scene();
- 从CDN或静态主机安装
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@<version>/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
const scene = new THREE.Scene();
</script>
- 核心库外的组件使用
如:控制器(control)、加载器(loader)以及后期处理效果(post-processing effect)
示例无需被单独地安装,但需要被单独地导入。
※ 如果 three.js 是使用 npm 来安装的,你可以使用如下代码来加载轨道控制器OrbitControls
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls( camera, renderer.domElement );
※ 如果 three.js 安装自一个 CDN ,请使用相同的 CDN 来安装其他组件:
<script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@<version>/build/three.module.js",
"three/addons/": "https://unpkg.com/three@<version>/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls( camera, renderer.domElement );
</script>
特别注意:
所有文件使用相同版本是十分重要的
请勿从不同版本导入不同的示例,也不要使用与 three.js 本身版本不一致的示例
- threejs中文手册
https://threejs.org/docs/index.html#manual/zh/introduction/Creating-a-scene
- 官方示例下载
https://github.com/mrdoob/three.js/archive/refs/heads/dev.zip
- 国外视频课程中文笔记 -- (优先!) -- (独立且完整的章节项目)
https://juejin.cn/post/7152438555246067719
- 国内文章的博客地址 -- (图文代码罗列很清晰,比官方文档好) -- (基础)
http://www.webgl3d.cn/pages/aac9ab/
- 国内博主的threejs相关
- threejs基础图文教程 -- (优先看)
- 免费素材、threejs基础图文、其他课程
- 公开的视频列表
https://www.three3d.cn/
http://www.cpengx.cn/
https://www.ixigua.com/home/10552573903/?list_entrance=search
- 博主文章 - (涉及很多知识点 - 逐章 - 讲述代码齐全)
https://juejin.cn/user/4485631602599495/posts
文档以及示例学习
为了真正能够让你的场景借助three.js来进行显示,我们需要以下几个对象,这样我们就能透过摄像机渲染出场景:
场景、相机、渲染器场景、相机、渲染器 均为threejs内置库
import * as THREE from 'https://unpkg.com/three/build/three.module.js';
※ 渲染器、场景、模型、灯光
场景 - scene
场景能够让你在什么地方、摆放什么东西来交给three.js来渲染,这是你放置物体、灯光和摄像机的地方
可以在场景内添加模型(scene.add(模型)),场景是模型的载体
属性与方法:
//-- 场景背景色
scene.background = new THREE.Color( 0xa0a0a0 );
//-- 场景添加雾气
scene.fog = new THREE.Fog( 0xa0a0a0, 200, 1000 );
//-- 添加一个模型、光源
scene.add( xxx )
//-- 删除一个模型、光源
scene.remove( xxx );
//-- 获取所有场景内的模型
scene.children
代码:
const scene = new THREE.Scene();
实例:
- 场景内的模型看向xxx的位置,官方示例-
https://threejs.org/examples/misc_lookat.html
for( let i = 1, l = scene.children.length; i < l; i ++ ){
scene.children[ i ].lookAt( xxx.position );
}
- 删除场景内的网格模型
示例:https://threejs.org/examples/#misc_exporter_obj
for ( let i = 0; i < scene.children.length; i ++ ) {
const child = scene.children[ i ];
if ( child.isMesh || child.isPoints ) {
child.geometry.dispose();
scene.remove( child );
i --;
}
}
光源
- 半球光 - HemisphereLight
光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色。
注:
半球光不能投射阴影!!!!
HemisphereLight( skyColor : Integer, groundColor : Integer, intensity : Float )
- skyColor - (可选参数) 天空中发出光线的颜色。 缺省值 0xffffff
- groundColor - (可选参数) 地面发出光线的颜色。 缺省值 0xffffff
- intensity - (可选参数) 光照强度。 缺省值 1
代码:
const hemiLight = new THREE.HemisphereLight( 0xffffff, 0x444444 );
hemiLight.position.set( 0, 200, 0 );
scene.add( hemiLight );
- 平行光 - DirectionalLight
平行光是沿着特定方向发射的光
这种光的表现像是无限远,从它发出的光线都是平行的
使用场景:
常用平行光来模拟太阳光 的效果
DirectionalLight( color : Integer, intensity : Float )
- color - (可选参数) 16进制表示光的颜色。 缺省值为 0xffffff (白色)
- intensity - (可选参数) 光照的强度。缺省值为1
属性:
- castShadow - 如果设置为 true 该平行光会产生动态阴影,默认为 false
- shadow.camera.zoom - 设置阴影的大小, 数字类型,如实例 dirLight.shadow.camera.zoom = 1
警告:
这样做的代价比较高而且需要一直调整到阴影看起来正确
注:
平行光可以投射阴影!!!!
代码:
const dirLight = new THREE.DirectionalLight( 0xffffff );
dirLight.position.set( 0, 200, 100 );
//-- 如果设置为 true 该平行光会产生动态阴影。 警告: 代价比较高,需要一直调整到阴影看起来正确.
dirLight.castShadow = true;
//-- 设置阴影的各种参数
dirLight.shadow.camera.top = 180;
dirLight.shadow.camera.bottom = - 100;
dirLight.shadow.camera.left = - 120;
dirLight.shadow.camera.right = 120;
light.shadow.mapSize.width = 512; // default
light.shadow.mapSize.height = 512; // default
light.shadow.camera.near = 0.5; // default
light.shadow.camera.far = 500; // default
scene.add( dirLight );
※ 模型在(球形光、平行光)中的不同效果下的差异
- 点光源 - PointLight
自一个点向各个方向发射的光源
注意:
模型如果材质使用(网格材质MeshLambertMaterial - color颜色),不给颜色,则会渲染为黑色
使用场景:
模拟一个灯泡发出的光
PointLight( color : Integer, intensity : Float, distance : Number, decay : Float )
- color - (可选参数)) 十六进制光照颜色。 缺省值 0xffffff (白色)
- intensity - (可选参数) 光照强度。 缺省值 1
- distance - 这个距离表示从光源到光照强度为0的位置。 当设置为0时,光永远不会消失(距离无穷大)。缺省值 0
- decay - 沿着光照距离的衰退量。缺省值 2
代码:
const light = new THREE.PointLight( 0xff0000, 1, 100 );
light.position.set( 50, 50, 50 );
scene.add( light );
- 环境光 - AmbientLight
环境光会均匀的照亮场景中的所有物体
环境光不能用来投射阴影,因为它没有方向
使用场景:
没有阴影要求情况下,要看清模型所有的面
AmbientLight( color : Integer, intensity : Float )
- color - (参数可选)颜色的rgb数值。缺省值为 0xffffff
- intensity - (参数可选)光照的强度。缺省值为 1
代码:
//-- 柔和的白光
const light = new THREE.AmbientLight( 0x404040 );
scene.add( light );
三维物体 - Object3D - 模型
说明:
这是Three.js中大部分对象的基类,
提供了一系列的属性和方法来对三维空间中的物体进行操纵
关于阴影的参数
- castShadow - 对象是否被渲染到阴影贴图中。默认值为false
- receiveShadow - 材质是否接收阴影。默认值为false
- position - 表示对象局部位置的Vector3。默认值为(0, 0, 0)
- lookAt ( vector : Vector3 ) - 朝向三维坐标点
- lookAt ( x : Float, y : Float, z : Float ) - 朝向三维坐标点
- rotateX ( rad : Float ) - 旋转 参数弧度
- Mesh - 创建一个网格模型
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshBasicMaterial( { color: 0xffff00 } );
const box = new THREE.Mesh( geometry, material );
scene.add( box );
- InstancedMesh - 创建大量具有相同几何体与材质网格模型
方法:
- setMatrixAt( i, matrix ) - 针对第i个模型,设置坐标
- setColorAt( i, color.setHex( 0xffffff * Math.random() ) ) - 针对第i个模型,设置颜色
示例代码:
const geometry = new THREE.BoxGeometry( 1, 1, 1 );
const material = new THREE.MeshLambertMaterial();
//-- 创建10个网格模型
const boxes = new THREE.InstancedMesh( geometry, material, 10 );
//-- 每帧更新一次,提高渲染效率
boxes.instanceMatrix.setUsage( THREE.DynamicDrawUsage );
scene.add( boxes );
//-- 额外方法
const matrix = new THREE.Matrix4();
for ( let i = 0; i < boxes.count; i ++ ) {
//-- 设置坐标
matrix.setPosition( Math.random() - 0.5, Math.random() * 2, Math.random() - 0.5 );
//-- 通过 .setMatrixAt() 来修改实例数据
boxes.setMatrixAt( i, matrix );
//-- 将颜色设置为
boxes.setColorAt( i, color.setHex( 0xffffff * Math.random() ) );
}
相机 - camera
通过摄像机,我们可以看到场景内的模型,摄像机可以调整视野角度、宽高比等
- PerspectiveCamera - 透视摄像机
功能说明:
这一投影模式被用来模拟人眼所看到的景象,它是3D场景的渲染中使用得最普遍的投影模式
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
camera.position.z = 5;
参数说明:
- 设置摄像机位置 - camera.position.set( - 5, 3, 10 );
- 设置摄像机朝向- camera.lookAt( 0, 2, 0 );
- 设置设备像素比 - renderer.setPixelRatio( window.devicePixelRatio );
- 将输出canvas的大小调整为(width, height)并考虑设备像素比 - renderer.setSize( window.innerWidth, window.innerHeight )
- 摄像机视锥体长宽比 - camera.aspect - 通常是使用画布的宽/画布的高。默认值是1(正方形画布)
- 如果使用,它包含阴影贴图的引用, 允许在场景中使用阴影贴图,默认是 false -
renderer.shadowMap.enabled = true;
- 更新摄像机投影矩阵 -
camera.updateProjectionMatrix();
- 在任何参数被改变以后必须被调用 - 第一人称视角旋转BUG修复 -
camera.rotation.order='YXZ'
-http://threejs.org/examples/games_fps.html
摄像机位置camera.position.set( x, y, z );
PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
参数说明:
参数1 - 视野角度(FOV):
视野角度就是无论在什么时候,你所能在显示器上看到的场景的范围,它的单位是角度(与弧度区分开)
参数2 - 长宽比(aspect ratio):
一个物体的宽除以它的高的值,相当于电影幕布的比例
参数3 - 长宽比(near):
摄像机视锥体近端面
参数4 - 长宽比(far):
摄像机视锥体远端面
- CameraHelper - 摄像机椎体
用于模拟相机视锥体的辅助对象.它使用LineSegments来模拟相机视锥体.
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const helper = new THREE.CameraHelper( camera );
scene.add( helper );
- 正交相机 - OrthographicCamera
这一摄像机使用orthographic projection(正交投影)来进行投影。
正交相机使用摄像机椎体
const camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 );
cameraOrthoHelper = new THREE.CameraHelper( camera );
scene.add( cameraOrthoHelper );
- 摄像机阵列 - ArrayCamera
功能说明:
ArrayCamera 用于更加高效地使用一组已经预定义的摄像机来渲染一个场景
这将能够更好地提升VR场景的渲染性能
例子:https://threejs.org/examples/webgl_camera_array.html
渲染器 - renderer
WebGLRender 用WebGL渲染出你精心制作的场景
参数:
- antialias - 是否执行抗锯齿, 默认为false
- alpha - 隐藏背景,默认false 不隐藏
- setPixelRatio - 设置设备像素比。通常用于避免HiDPI设备上绘图模糊
- setSize - 将输出canvas的大小调整为(width, height)并考虑设备像素比
- shadowMap.enabled - 允许在场景中使用阴影贴图,默认是 false
- outputEncoding - 默认THREE.LinearEncoding - 颜色偏暗,
THREE.sRGBEncoding - 颜色会亮一些 - 推荐
- setClearColor - 设置颜色以及透明度
const renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setPixelRatio( window.devicePixelRatio );
//-- 将输出canvas的大小调整为(width, height)并考虑设备像素比
renderer.setSize( window.innerWidth, window.innerHeight );
//-- 一个canvas,渲染器在其上绘制输出
document.body.appendChild( renderer.domElement );
- 渲染场景
完成上述基础操作后,不会看到任何东西,这是因为我们还没有对它进行真正的渲染。
需要借助下面两种渲染方式来实现
“渲染循环”(render loop)
“动画循环”(animate loop)
渲染循环
function animate() {
//-- 渲染循环
requestAnimationFrame( animate );
//-- 使模型旋转起来
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
//-- 渲染器渲染 场景与摄像机
//-- 在大多数屏幕上,刷新率一般是60次/秒
renderer.render( scene, camera );
}
animate();
requestAnimationFrame相较于setInterval有很多的优点:
setInterval并非能做到均匀间隔执行,某些情况下会略过本应执行的代码
最重要的一点或许就是当用户切换到其它的标签页时,它会暂停,因此不会浪费用户宝贵的处理器资源,也不会损耗电池的使用寿命
画面调整尺寸时候,重置渲染器
代码:
window.addEventListener( 'resize', onWindowResize );
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
//-- 更新摄像机投影矩阵。在任何参数被改变以后必须被调用。
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
}
轨道控制器 - 操控屏幕移动、旋转等
- 轨道控制器 - OrbitControls
给场场景加上鼠标拖拽旋转控制
更多参数说明: https://threejs.org/docs/index.html?q=OrbitControls#examples/zh/controls/OrbitControls
代码:
//-- OrbitControls 是一个附加组件,必须显式导入
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
const controls = new OrbitControls( camera, renderer.domElement );
//-- 为指定的DOM元素添加按键监听
controls.listenToKeyEvents( window );
//-- 将其设置为true以启用阻尼(惯性),这将给控制器带来重量感。默认值为false。
controls.enableDamping = true;
//-- 阻尼惯性值(默认0.05)
controls.dampingFactor = 0.05;
//-- 定义当平移的时候摄像机的位置将如何移动。如果为true,摄像机将在屏幕空间内平移。 否则,摄像机将在与摄像机向上方向垂直的平面中平移。
controls.screenSpacePanning = false;
//-- 你能够将相机向内移动多少(默认值为0)
controls.minDistance = 100;
//-- 你能够将相机向外移动多少(默认值为Infinity无穷大)
controls.maxDistance = 500;
//-- 你能够垂直旋转的角度的上限,范围是0到Math.PI (默认值为Math.PI)
controls.maxPolarAngle = Math.PI * 0.5;
//-- 你能够水平旋转的角度下限 - (默认值为无穷大)
controls.minAzimuthAngle = - Math.PI / 2;
//-- 你能够水平旋转的角度上限 - (默认值为无穷大)
controls.maxAzimuthAngle = Math.PI / 2;
//-- 启用或禁用摄像机平移,默认为true。
controls.enablePan = false;
//-- 必须在对摄影机进行变换后,手动调用controls.update()
controls.target.set( 0, 0.5, 0 );
controls.target.x = 1;
controls.target.y = 1;
controls.target.z = 1;
controls.update();
function animate() {
requestAnimationFrame( animate );
controls.update();
renderer.render( scene, camera );
}
※ 限制轨道控制器,只能看到模型的左右两耳之间,以及平视到头顶范围
//-- 你能够垂直旋转的角度的上限,范围是0到Math.PI (默认值为Math.PI)
controls.maxPolarAngle = Math.PI / 2;
//-- 你能够水平旋转的角度下限 - (默认值为无穷大)
controls.minAzimuthAngle = - Math.PI / 2;
//-- 你能够水平旋转的角度上限 - (默认值为无穷大)
controls.maxAzimuthAngle = Math.PI / 2;
- 第一人称视角轨道控制器 - FirstPersonControls
import { FirstPersonControls } from 'three/addons/controls/FirstPersonControls.js';
let controls, camera, renderer;
init();
animate();
function init() {
...
controls = new FirstPersonControls( camera, renderer.domElement );
//-- 移动速度
controls.movementSpeed = 500;
//-- 转向速度
controls.lookSpeed = 0.1;
window.addEventListener( 'resize', onWindowResize );
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize( window.innerWidth, window.innerHeight );
controls.handleResize();
}
function animate() {
requestAnimationFrame( animate );
const delta = clock.getDelta();
controls.update( delta );
renderer.render( scene, camera );
stats.update();
}
- TransformControls - 变换控制器
功能说明:
对模型进行交互的方式,来在3D空间中变换物体
与其他控制器不同的是,变换控制器不倾向于对场景摄像机的变换进行改变
代码:
import { TransformControls } from 'three/addons/controls/TransformControls.js';
transformControls = new TransformControls( camera, renderer.domElement );
//-- 手柄UI(轴/平面)的大小,默认为1。
transformControls.size = .75;
//-- x轴手柄是否显示
transformControls.showX = false;
//-- 定义了在哪种坐标空间中进行变换,可选值有"world" 和 "local",默认为world
transformControls.space = 'world';
//-- 设置应当变换的3D对象
transformControls.attach( OOI.target_hand_l );
scene.add( transformControls );
//-- 当使用变换控制器时候,禁用轨道控制器,防止冲突
transformControls.addEventListener( 'mouseDown', () => orbitControls.enabled = false );
transformControls.addEventListener( 'mouseUp', () => orbitControls.enabled = true );
- PointerLockControls - 指针锁定控制器
功能说明:
该类的实现是基于Pointer Lock API的
对于第一人称3D游戏来说, PointerLockControls 是一个非常完美的选择
语法说明:
创建 - PointerLockControls( camera : Camera, domElement : HTMLDOMElement )
- camera: 渲染场景的摄像机
- domElement: 用于事件监听的HTML元素
方法:
- lock - 当指针状态为 “locked” (即鼠标被捕获)时触发
- unlock - 当指针状态为 “unlocked” (即鼠标不再被捕获)时触发
代码:
import { PointerLockControls } from 'three/addons/controls/PointerLockControls.js';
let moveForward = false;
let moveBackward = false;
let moveLeft = false;
let moveRight = false;
let canJump = false;
//-- 该类表示的是一个三维向量3d vector
const velocity = new THREE.Vector3();
//-- 创建指针锁定控制器
let controls = new PointerLockControls( camera, document.body );
//-- 点击介绍浮层后,
instructions.addEventListener( 'click', function () {
//-- 当指针状态为 “locked” (即鼠标被捕获)时触发
controls.lock();
instructions.style.display = 'none';
});
//-- 将控制器添加到场景里
scene.add( controls.getObject() );
const onKeyDown = function ( event ) {
switch ( event.code ) {
case 'Space':
if ( canJump === true ) velocity.y += 350;
canJump = false;
break;
}
};
document.addEventListener( 'keydown', onKeyDown );
//-- 渲染场景
function animate() {
requestAnimationFrame( animate );
//--左右移动
controls.moveRight(xxx)
//--前后移动
controls.moveForward(xxx)
//--上下移动
controls.getObject().position.y += xxx
//-- 关于跳跃
if ( onObject === true ) {
velocity.y = Math.max( 0, velocity.y );
canJump = true;
}
if ( controls.getObject().position.y < 10 ) {
velocity.y = 0;
controls.getObject().position.y = 10;
canJump = true;
}
}
示例链接: https://threejs.org/examples/misc_controls_pointerlock.html
三维向量 - Vector3
功能说明:
该类表示的是一个三维向量(3D vector)
一个三维向量表示的是一个有顺序的、三个为一组的数字组合(标记为x、y和z),可被用来表示很多事物,
例如:
- 一个位于三维空间中的点
- 一个在三维空间中的方向与长度的定义
- 任意的、有顺序的、三个为一组的数字组合
方法:
- clone - 返回一个新的Vector3
- copy - 将所传入Vector3的x、y和z属性复制给这一Vector3,相当于遍历复制值
- random - 将该向量的每个分量(x、y、z)设置为介于 0 和 1 之间的伪随机数
- randomDirection - 将该向量的每个分量(x、y、z)设置为介于 -1 和 +1 之间的伪随机数
- length - 计算从(0, 0, 0) 到 (x, y, z)的欧几里得长度
- setLength - 向量方向不变,x、y、z坐标进行缩放,达到预期距离(0,0,0)的长度
- multiplyScalar - 接收一个参数,将x、y、z值与这个参数数字相乘
语法说明:
创建 - Vector3( x : Float, y : Float, z : Float )
- x - 向量的x值,默认为0
- y - 向量的y值,默认为0
- z - 向量的z值,默认为0
代码:
const a = new THREE.Vector3( 0, 1, 0 );
//-- 没有参数,那么x,y,z值均为 0
const b = new THREE.Vector3( );
示例链接: https://threejs.org/examples/misc_controls_pointerlock.html
动画混合器 - AnimationMixer
动画混合器是用于场景中特定对象的动画的播放器。
当场景中的多个对象独立动画时,每个对象都可以使用同一个动画混合器。
- 播放模型指定的动画(如模型角色,有站立,行走,奔跑),这个可以实现某个动画的执行
- 动画需要混合加载器去更新 mixer.update( delta )
- delta是通过跟踪时间对象Clock来获取--(
const delta = clock.getDelta();
)
代码:
- 基本运作
let mixer;
//-- 该对象用于跟踪时间(时间获取工具)
const clock = new THREE.Clock();
//-- 模型解压缩配置
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' );
//-- gltf格式模型加载器
const loader = new GLTFLoader();
loader.setDRACOLoader( dracoLoader );
//-- 加载模型
loader.load( 'models/gltf/LittlestTokyo.glb', function ( gltf ) {
//-- 添加模型至场景
const model = gltf.scene;
model.position.set( 1, 1, 0 );
model.scale.set( 0.01, 0.01, 0.01 );
scene.add( model );
//-- 创建一个动画混合器,将模型传入
mixer = new THREE.AnimationMixer( model );
//-- 动画混合器播放 模型的某一个动画片段( 动画片段为动画名组成的数组 )
mixer.clipAction( gltf.animations[ 0 ] ).play();
//-- 进行(循环或动画)渲染
animate();
}, undefined, function ( e ) {
console.error( e );
});
function animate() {
requestAnimationFrame( animate );
//-- 渲染模型的动画
//-- 获取自.oldTime设置后到当前的秒数。 同时将.oldTime设置为当前时间
const delta = clock.getDelta();
//-- 推进混合器时间并更新动画
mixer.update( delta );
controls.update();
renderer.render( scene, camera );
}
- 线性动画
完成一些指定的动画 (位置、大小、颜色、透明度)
示例:https://threejs.org/examples/misc_animation_keys.html
init();
animate();
function init(){
...
//-- 添加一个网格模型盒子
const geometry = new THREE.BoxGeometry( 5, 5, 5 );
const material = new THREE.MeshBasicMaterial( { color: 0xffffff, transparent: true } );
const mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
//-- 位置变化 - 向量关键帧值的轨迹
const positionKF = new THREE.VectorKeyframeTrack( '.position', [ 0, 1, 2 ], [ 0, 0, 0, 30, 0, 0, 0, 0, 0 ] );
//-- 大小变化 - 向量关键帧值的轨迹
const scaleKF = new THREE.VectorKeyframeTrack( '.scale', [ 0, 1, 2 ], [ 1, 1, 1, 2, 2, 2, 1, 1, 1 ] );
//-- 颜色变化 - 表示颜色更改的关键帧值的轨迹
const colorKF = new THREE.ColorKeyframeTrack( '.material.color', [ 0, 1, 2 ], [ 1, 0, 0, 0, 1, 0, 0, 0, 1 ], THREE.InterpolateDiscrete );
//-- 透明度变化 - 数字关键帧值的轨迹
const opacityKF = new THREE.NumberKeyframeTrack( '.material.opacity', [ 0, 1, 2 ], [ 1, 0, 1 ] );
//-- 创建动画混合器
mixer = new THREE.AnimationMixer( mesh );
//-- 建动画序列
const clip = new THREE.AnimationClip( 'Action', 3, [ scaleKF, positionKF, colorKF, opacityKF ] );
//-- 播放动画
const clipAction = mixer.clipAction( clip );
clipAction.play();
}
function animate() {
requestAnimationFrame( animate );
//-- 更新动画
const delta = clock.getDelta();
if ( mixer ) {
mixer.update( delta );
}
renderer.render( scene, camera );
}
对象用于跟踪时间 - Clock
优先使用performance.now
,如果没有则使用,略欠精准的Date.now
来实现
- 判断时钟是否在运行,默认值是 false (属性) - running
- 获取自时钟启动后的秒数(方法) - getElapsedTime
- 获取循环渲染两次间隔(方法) - getDelta
代码:
function animate() {
requestAnimationFrame( animate );
//精准获取循环渲染animate两次调用的时间间隔,以供动画混合器更新动画使用
const delta = clock.getDelta();
mixer.update( delta );
renderer.render( scene, camera );
}
animate();
资源的加载器 - Draco加载器
用Draco库压缩的几何图形加载器
- dracoLoader.setDecoderPath - 设置 包含JS和WASM解码器库的文件夹的路径
代码:
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' );
const loader = new GLTFLoader();
loader.setDRACOLoader( dracoLoader );
loader.load( 'models/gltf/LittlestTokyo.glb', function ( gltf ) {
....
}
资源的加载器 - GLTFLoader、DRACOLoader
功能说明:
threejs推荐使用glTF(gl传输格式)
.GLB和.GLTF是这种格式的这两种不同版本, 都可以被很好地支持
由于glTF这种格式是专注于在程序运行时呈现三维物体的,所以它的传输效率非常高,且加载速度非常快
功能方面则包括了网格、材质、纹理、皮肤、骨骼、变形目标、动画、灯光和摄像机
- loader.setDRACOLoader( dracoLoader ) - THREE.DRACOLoader的实例,用于解码使用KHR_draco_mesh_compression扩展压缩过的文件
GLTFLoader - 加载gltf格式模型
DRACOLoader - Draco 是一种库,用于压缩和解压缩 3D 几何网格(geometric mesh)和点云(point cloud)
代码:
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath( 'jsm/libs/draco/gltf/' );
const loader = new GLTFLoader();
loader.setDRACOLoader( dracoLoader );
loader.load( 'models/gltf/LittlestTokyo.glb', function ( gltf ) {
//-- 针对加载模型的一些操作...
let model = gltf.scene;
//-- 设置模型位置
model.position.set(0,1,0);
//-- 设置模型大小
model.scale.set( 0.1,0.1,0.1 );
scene.add(model);
}, undefined, function ( e ) {
console.error( e );
} );
Stats - 添加帧频显示,查看渲染卡顿情况
import Stats from 'three/addons/libs/stats.module.js';
const container = document.getElementById( 'container' );
const stats = new Stats();
container.appendChild( stats.dom );
function animate() {
requestAnimationFrame( animate );
stats.update();
renderer.render( scene, camera );
}
GUI - 可视化调试工具
调试threejs的三方可视化,选项配置插件 - 需要自己安装
- 具体调试界面效果见-官方示例
https://threejs.org/examples/webgl_animation_skinning_morph.html
代码:
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
let gui;
const loader = new GLTFLoader();
loader.load( 'models/gltf/RobotExpressive/RobotExpressive.glb', function ( gltf ) {
model = gltf.scene;
scene.add( model );
//threejs调试工具
createGUI( model, gltf.animations );
}, undefined, function ( e ) {
console.error( e );
});
//创建调试项目
function createGUI( model, animations ) {
const states = [ 'Idle', 'Walking', 'Running', 'Dance', 'Death', 'Sitting', 'Standing' ];
const emotes = [ 'Jump', 'Yes', 'No', 'Wave', 'Punch', 'ThumbsUp' ];
gui = new GUI();
mixer = new THREE.AnimationMixer( model );
actions = {};
for ( let i = 0; i < animations.length; i ++ ) {
const clip = animations[ i ];
const action = mixer.clipAction( clip );
actions[ clip.name ] = action;
if ( emotes.indexOf( clip.name ) >= 0 || states.indexOf( clip.name ) >= 4 ) {
action.clampWhenFinished = true;
action.loop = THREE.LoopOnce;
}
}
// states
const statesFolder = gui.addFolder( 'States' );
const clipCtrl = statesFolder.add( api, 'state' ).options( states );
clipCtrl.onChange( function () {
fadeToAction( api.state, 0.5 );
} );
statesFolder.open();
// emotes
const emoteFolder = gui.addFolder( 'Emotes' );
function createEmoteCallback( name ) {
api[ name ] = function () {
fadeToAction( name, 0.2 );
mixer.addEventListener( 'finished', restoreState );
};
emoteFolder.add( api, name );
}
function restoreState() {
mixer.removeEventListener( 'finished', restoreState );
fadeToAction( api.state, 0.2 );
}
for ( let i = 0; i < emotes.length; i ++ ) {
createEmoteCallback( emotes[ i ] );
}
emoteFolder.open();
// expressions
face = model.getObjectByName( 'Head_4' );
const expressions = Object.keys( face.morphTargetDictionary );
const expressionFolder = gui.addFolder( 'Expressions' );
for ( let i = 0; i < expressions.length; i ++ ) {
expressionFolder.add( face.morphTargetInfluences, i, 0, 1, 0.01 ).name( expressions[ i ] );
}
activeAction = actions[ 'Walking' ];
activeAction.play();
expressionFolder.open();
}
Audio - 音频的使用
- 全局音频
- Audio - 创建一个( 全局 ) audio对象
- AudioListener - 用一个虚拟的listener表示在场景中所有的位置和非位置相关的音效
- AudioLoader - 音频资源加载器
// 创建一个 AudioListener 并将其添加到 camera 中
const listener = new THREE.AudioListener();
camera.add( listener );
// 创建一个全局 audio 源
const sound = new THREE.Audio( listener );
// 加载一个 sound 并将其设置为 Audio 对象的缓冲区
const audioLoader = new THREE.AudioLoader();
audioLoader.load( 'sounds/ambient.ogg', function( buffer ) {
sound.setBuffer( buffer );
sound.setLoop( true );
sound.setVolume( 0.5 );
sound.play();
});
- 位置音频 - PositionalAudio
应用场景:
收音机,原理它声音小,接近声音大
代码:
//-- 创建一个音频监听器,并添加到相机
const listener = new THREE.AudioListener();
camera.add( listener );
//-- 创建一个位置音频,并将监听器传入
const sound = new THREE.PositionalAudio( listener );
//--加载音频文件,完成后,通过位置音频来播放
const audioLoader = new THREE.AudioLoader();
audioLoader.load( 'sounds/song.ogg', function( buffer ) {
sound.setBuffer( buffer );
sound.setRefDistance( 20 );
sound.play();
});
//-- 创建一个对象到场景
const sphere = new THREE.SphereGeometry( 20, 32, 16 );
const material = new THREE.MeshPhongMaterial( { color: 0xff2200 } );
const mesh = new THREE.Mesh( sphere, material );
scene.add( mesh );
//-- 将位置音频放到这个对象里
mesh.add( sound );
CSS 2D渲染器
功能说明:
CSS 3D渲染器 的简化版本,唯一支持的变换是位移
如果你希望将三维物体和基于HTML的标签相结合
,则这一渲染器将十分有用
各个DOM元素也被包含到一个CSS2DObject实例中,并被添加到场景图中
※※※※※※※ css ※※※※※※※
.label {
color: #FFF;
font-family: sans-serif;
padding: 2px;
background: rgba( 0, 0, 0, .6 );
}
※※※※※※※ script ※※※※※※※
//-- 引入CSS2DRenderer渲染器以便将DOM元素和模型相结合
//-- 引入CSS2DObject构造函数,将dom变化为2d对象
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
let camera, scene, renderer, labelRenderer;
init();
animate();
function init() {
....
//-- 创建一个球形状,并添加贴图,命名为月亮,添加到场景里
const moonGeometry = new THREE.SphereGeometry( MOON_RADIUS, 16, 16 );
const moonMaterial = new THREE.MeshPhongMaterial( {
shininess: 5,
map: textureLoader.load( 'textures/planets/moon_1024.jpg' )
} );
moon = new THREE.Mesh( moonGeometry, moonMaterial );
scene.add( moon );
//-- 月亮所在层级显示
moon.layers.enableAll();
//-- 创建一个dom元素,内容为月亮的描述文字
const moonDiv = document.createElement( 'div' );
moonDiv.className = 'label';
moonDiv.textContent = 'Moon';
moonDiv.style.marginTop = '-1em';
//-- 将这个dom通过CSS2DObject实例化
const moonLabel = new CSS2DObject( moonDiv );
moonLabel.position.set( 0, 0.27, 0 );
//-- 将2d对象添加到月亮模型内
moon.add( moonLabel );
//-- 设置层级为0
moonLabel.layers.set(0);
...
}
//-- 显示或隐藏月亮的描述文字
camera.layers.toggle(0);
function animate() {
requestAnimationFrame( animate );
//-- 获取自时钟启动后的秒数
const elapsed = clock.getElapsedTime();
//-- 月亮旋转
moon.position.set( Math.sin( elapsed ) * 5, 0, Math.cos( elapsed ) * 5 );
renderer.render( scene, camera );
//-- 2d文字描述重新渲染,保证朝向永远正面对着屏幕
labelRenderer.render( scene, camera );
}
EventDispatcher - 自定义事件
说明:
创建一个自定义事件,通过发布dispatchEvent,和订阅addEventListener自定义事件名(如:start),来触发
代码:
//-- 创建一个自定义对象
class Car extends EventDispatcher {
//-- 声明一个自定义事件
start(){
this.dispatchEvent({ type: 'start', message: 'vroom vroom!' });
};
}
//-- 实例化一个自定义对象
const car = new Car();
//-- 触发自定义事件
car.addEventListener('start',function( event ){
console.log( event.message );
});
car.start(); //-- vroom vroom!
Raycaster - 光线投射 - (用于鼠标与三维物体交互)
功能说明:
光线投射用于进行鼠标拾取,进行鼠标与模型的交互判断
在三维空间中计算出鼠标移过了什么物体
属性与方法:
- 通过摄像机和鼠标位置更新射线 - raycaster.setFromCamera( pointer, camera )
- 检查哪些相交的对象(可选参数是否检查所有的后代,默认true) - raycaster.intersectObjects( scene.children, false )
注:
光线投影,有多少模型,就可以穿透多少模型
代码:
let raycaster;
let INTERSECTED;
//-- 接收两个参数,x、y坐标,最好写个值,防止默认0,0 触发鼠标的滑过效果
const pointer = new THREE.Vector2(1,1);
init();
animate();
function init(){
...
raycaster = new THREE.Raycaster();
document.addEventListener( 'pointermove', onPointerMove );
...
}
function onPointerMove( event ) {
//-- 让鼠标在屏幕(自左至右)移动时候,坐标取值为(-1 ~ 1)
pointer.x = ( event.clientX / window.innerWidth ) * 2 - 1;
//-- 让鼠标在屏幕(自下而上)移动时候,坐标取值为(-1 ~ 1)
pointer.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
}
function animate(){
requestAnimationFrame( animate );
//-- 通过摄像机和鼠标位置更新射线
raycaster.setFromCamera( pointer, camera );
//-- 获取与鼠标相交的模型数组
const intersects = raycaster.intersectObjects( scene.children, false );
if ( intersects.length > 0 ) {
//--※※※※※※※ 相当于DOM操作的mouseenter ※※※※※※※
//-- 存在鼠标相交的模型
if ( INTERSECTED != intersects[ 0 ].object ) {
//-- 记录上一次的相交临时对象不为空时候
if ( INTERSECTED ){
//-- 将上一次的相交对象的颜色,设置为模型原有颜色
INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );
}
//-- 上一次临时相交对象设置为目前鼠标触碰的对象
INTERSECTED = intersects[ 0 ].object;
//-- 将这个触碰对象的颜色,存储在临时相交对象的自定义属性currentHex下
INTERSECTED.currentHex = INTERSECTED.material.emissive.getHex();
//-- 将这个触碰对象的颜色,修改为自己喜欢的高亮色
INTERSECTED.material.emissive.setHex( 0xff0000 );
};
} else {
//--※※※※※※※ 相当于DOM操作的mouseleave ※※※※※※※
//-- 鼠标没有相交任何模型
if ( INTERSECTED ){
//-- 鼠标移出上一个对象,且有颜色记录值,把上一个对象的颜色值,恢复为它原有的颜色
INTERSECTED.material.emissive.setHex( INTERSECTED.currentHex );
}
//-- 将临时相交对象,设置为null
INTERSECTED = null;
}
renderer.render( scene, camera );
}
layers - 图层属性
说明:
创建一个新的图层对象
Layers 对象为 Object3D 分配 1个到 32 个图层 (32个图层从 0 到 31 编号标记)
默认所有 Object3D 对象都存储在第 0 个图层上
- Object3D - 这是Three.js中大部分对象的基类,提供了一系列的属性和方法来对三维空间中的物体进行操纵
- Object3D.layers - 通过操作 三维物体的layers方法来实现一系列操作
方法:
- 显示层级所在模型 - xxxx.layers.enable
- 显示所在层级模型 - xxxx.layers.enableAll
- 隐藏层级所在模型 - xxxx.layers.disable
- 隐藏所在层级模型 - xxxx.layers.disableAll
- 设置模型所在层级 - xxxx.layers.set(0 ~ 31 的整数)
- 测试模型层级对错 - xxxx.layers.test(0 ~ 31 的整数)
GLTFExporter - gltf模型导出
说明:
glTF(GL传输格式)是一种用于高效传输和加载3D内容的开放格式规范。可以以JSON(.gltf)或二进制(.glb)格式提供资产;
glTF资产可以提供一个或多个场景,包括网格、材质、纹理、皮肤、骨架、变形目标、动画、灯光和/或相机;
代码:
import { GLTFExporter } from 'three/addons/exporters/GLTFExporter.js';
let scene1
const params = {
trs: false,
onlyVisible: true,
binary: false,
maxTextureSize: 4096
};
scene1 = new THREE.Scene();
scene1.name = 'Scene1';
//-- 添加网络地面
gridHelper = new THREE.GridHelper( 2000, 20, 0x888888, 0x444444 );
gridHelper.position.y = - 50;
gridHelper.name = 'Grid';
scene1.add( gridHelper );
//-- 添加三维坐标提示
//-- 一个轴对象,以一种简单的方式显示3个轴,X轴是红色的、Y轴是绿色的、Z轴为蓝色。
const axes = new THREE.AxesHelper( 500 );
axes.name = 'AxesHelper';
scene1.add( axes );
exportGLTF( scene1 );
function exportGLTF( input ) {
const gltfExporter = new GLTFExporter();
const options = {
trs: params.trs,
onlyVisible: params.onlyVisible,
binary: params.binary,
maxTextureSize: params.maxTextureSize
};
gltfExporter.parse(
input,
function ( result ) {
if ( result instanceof ArrayBuffer ) {
saveArrayBuffer( result, 'scene.glb' );
} else {
const output = JSON.stringify( result, null, 2 );
console.log( output );
saveString( output, 'scene.gltf' );
}
},
function ( error ) {
console.log( 'An error happened during parsing', error );
},
options
);
}
OimoPhysics - 插件
说明:
一种物理引擎
方法:
- 开启物理引擎 - 异步函数 直接调用 - await OimoPhysics();
- 添加模型到物理世界 - physics.addMesh( 网格模型, 是否参与运动, 默认为0 - 单纯刚体, 1 - 参与运动 );
- 渲染模型新位置 - physics.setMeshPosition( 网格模型, Vector3三维坐标, 网格模型的索引 - 第一个为0 );
示例 - https://threejs.org/examples/physics_oimo_instancing.html
代码:
import { OimoPhysics } from 'three/addons/physics/OimoPhysics.js';
let physics, position;
let boxes;
init();
animate();
async function init() {
//-- 物理引擎为异步函数,需要async await调用
physics = await OimoPhysics();
//-- 三维向量(Vector3)
position = new THREE.Vector3();
//-- 添加一个地面 --(第二个参数不写,表示为刚体)
...
scene.add( floor );
physics.addMesh( floor );
//-- 批量创建模型,数量为10
boxes = new THREE.InstancedMesh( geometryBox, material, 10 );
...
scene.add( boxes );
physics.addMesh( boxes, 1 );(第二个参数写,表示运动物体)
}
function animate(){
//--
position.set( 0, 1, 0 );
index = Math.floor( Math.random() * boxes.count );
//-- 设置随机的box,将位置拉回到初始点
physics.setMeshPosition( boxes, position, index );
}
引擎OimoPhysics.js - 源码结构图
- 存储网格数据信息的实例 meshMap
- 通过new WeakMap()创建
- 通过meshMap.set( mesh, bodies );
import * as OIMO from '../libs/OimoPhysics/index.js';
async function OimoPhysics() {
//-- 一秒60帧的频率进行数据变化
const frameRate = 60;
//-- 创建一个物理世界,重力负的9.8
const world = new OIMO.World( 2, new OIMO.Vec3( 0, - 9.8, 0 ) );
//-- 网格弱映射--存储模型数据使用
const meshMap = new WeakMap();
const meshes = [];
//-- 刷新数据
function step() {
const time = performance.now();
if ( lastTime > 0 ) {
world.step( 1 / frameRate );
}
lastTime = time;
for ( let i = 0, l = meshes.length; i < l; i ++ ) {
const mesh = meshes[ i ];
if ( mesh.isInstancedMesh ) {
//-- 一组网格模型的数据信息
const array = mesh.instanceMatrix.array;
const bodies = meshMap.get( mesh );
for ( let j = 0; j < bodies.length; j ++ ) {
const body = bodies[ j ];
compose( body.getPosition(), body.getOrientation(), array, j * 16 );
}
//-- 修改实例数据,你必须将它的needsUpdate标识为 true
mesh.instanceMatrix.needsUpdate = true;
} else if ( mesh.isMesh ) {
//-- 单个网格模型
const body = meshMap.get( mesh );
mesh.position.copy( body.getPosition() );
mesh.quaternion.copy( body.getOrientation() );
}
}
}
//-- animate
setInterval( step, 1000 / frameRate );
return {
addMesh: addMesh,
setMeshPosition: setMeshPosition
// addCompoundMesh
};
}
//-- 针对一组通过new THREE.InstancedMesh创建的网格处理数据
function compose( position, quaternion, array, index ) {
//-- 获取下一帧的位置、方向、
}
export { OimoPhysics };
AmmoPhysics - 插件
说明:
一种物理引擎
方法:
- 开启物理引擎 - 异步函数 直接调用 - await AmmoPhysics();
- 添加模型到物理世界 - physics.addMesh( 网格模型, 是否参与运动, 默认为0 - 单纯刚体, 1 - 参与运动 );
- 渲染模型新位置 - physics.setMeshPosition( 网格模型, Vector3三维坐标, 网格模型的索引 - 第一个为0 );
示例 - https://threejs.org/examples/physics_ammo_instancing.html
LoadingManager - 预加载器
说明:
通过这个工具,可以实现threejs资源预加载
const manager = new THREE.LoadingManager();
manager.onStart = function ( url, itemsLoaded, itemsTotal ) {
console.log( 'Started loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' );
};
manager.onLoad = function ( ) {
console.log( 'Loading complete!');
};
manager.onProgress = function ( url, itemsLoaded, itemsTotal ) {
console.log( 'Loading file: ' + url + '.\nLoaded ' + itemsLoaded + ' of ' + itemsTotal + ' files.' );
};
manager.onError = function ( url ) {
console.log( 'There was an error loading ' + url );
};
const loader = new THREE.OBJLoader( manager );
loader.load( 'file.obj', function ( object ) {
//
} );
//-- 待完成....
练习笔记
- 调用模型的某一个动画
在html中通过threejs,引入一个模型,并调用这个模型的某个动画
- 在建模软件中制作动画;
- 导出GLTF模型;
- 在WebGL引擎中导入GLTF模型并调用动画;
threejs官方中文文档 - 播放某一个模型动画
https://threejs.org/docs/index.html#manual/zh/introduction/Creating-a-scene
播放导出模型中的某一个动画
https://threejs.org/docs/index.html#manual/zh/introduction/Animation-system
- 添加一个临时测试地面
const mesh = new THREE.Mesh( new THREE.PlaneGeometry( 2000, 2000 ), new THREE.MeshPhongMaterial( { color: 0x999999, depthWrite: false } ) );
mesh.rotation.x = - Math.PI / 2;
mesh.receiveShadow = true;
scene.add( mesh );
const grid = new THREE.GridHelper( 2000, 20, 0x000000, 0x000000 );
grid.material.opacity = 0.2;
grid.material.transparent = true;
scene.add( grid );
2-2. 添加三维坐标提示轴
一个轴对象,以一种简单的方式显示3个轴,X轴是红色的、Y轴是绿色的、Z轴为蓝色
const axes = new THREE.AxesHelper( 500 ); //-- 500标识轴线长度
axes.name = 'AxesHelper';
scene1.add( axes );
- 添加雾
这个类中的参数定义了线性雾。
Fog( color : Integer, near : Float, far : Float )
参数:
- near - 开始应用雾的最小距离,默认1
- far - 结束计算、应用雾的最大距离,默认1000
代码:
const scene = new THREE.Scene();
scene.fog = new THREE.Fog( 0xa0a0a0, 200, 1000 );
- 海浪的制作
官方的海浪参考:https://threejs.org/examples/webgl_geometry_dynamic.html
代码:
init()
function init(){
//-- 创建一个平面几何体PlaneGeometry
geometry = new THREE.PlaneGeometry( 20000, 20000, worldWidth - 1, worldDepth - 1 );
geometry.rotateX( - Math.PI / 2 );
mesh = new THREE.Mesh( geometry, material );
scene.add( mesh );
}
function animate() {
requestAnimationFrame( animate );
render();
}
function render() {
const delta = clock.getDelta();
//-- 获取累积时长
const time = clock.getElapsedTime() * 10;
//-- 获取平面上的点
const position = geometry.attributes.position;
//-- 将所有点进行遍历
for ( let i = 0; i < position.count; i ++ ) {
const y = 35 * Math.sin( i / 5 + ( time + i ) / 7 );
//-- 将遍历的点,Y坐标进行变化
position.setY( i, y );
}
position.needsUpdate = true;
renderer.render( scene, camera );
}
- 显示隐藏某个模型,通过camera的layers操作来实现
- 文档 -
https://threejs.org/docs/index.html?q=layers#api/zh/core/Layers
- 关于layers的说明:
layers 对象为 Object3D 分配 1个到 32 个图层
32个图层从 0 到 31 编号标记。
在内部实现上,每个图层对象被存储为一个 bit mask, 默认的,所有 Object3D 对象都存储在第 0 个图层上
- 关于切换模型显示隐藏,示例:
camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 10000 );
//-- 摄像机层级 0、1、2 都激活
camera.layers.enable( 0 );
camera.layers.enable( 1 );
camera.layers.enable( 2 );
//-- 新建一个带颜色的立方体模型
const geometry = new THREE.BoxGeometry( 20, 20, 20 );
const object = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial( { color: '0xff0000' } ) );
//-- 设置模型的层级
object.layers.set(0);
//-- 查看模型是否显示,显示返回true
object.layers.isEnabled
//-- 将该模型添加到场景中
scene.add( object );
//-- 摄像机层级0切换显示与隐层
camera.layers.toggle(0);
//-- 摄像机所有层级均激活
camera.layers.enableAll();
//-- 摄像机所有层级均隐藏
camera.layers.disableAll();
6-2. 通过visible同样,可以显示模型,显示隐藏
sphereInter.visible = true;
sphereInter.visible = false;
- 镜头光晕
代码:
//-- 导入工具
import { Lensflare } from 'three/addons/objects/Lensflare.js';
//-- 创建一个点光源,插入到场景中去
const light = new THREE.PointLight( 0xffffff, 1.5, 2000 );
scene.add( light );
//-- 通过材质加载器,加载三张光晕图片资源
const textureLoader = new THREE.TextureLoader();
const textureFlare0 = textureLoader.load( "textures/lensflare/lensflare0.png" );
const textureFlare1 = textureLoader.load( "textures/lensflare/lensflare2.png" );
const textureFlare2 = textureLoader.load( "textures/lensflare/lensflare3.png" );
//-- 通过光晕工具,添加三个光晕资源,添加到创建的光晕中
const lensflare = new Lensflare();
lensflare.addElement( new LensflareElement( textureFlare0, 512, 0 ) );
lensflare.addElement( new LensflareElement( textureFlare1, 512, 0 ) );
lensflare.addElement( new LensflareElement( textureFlare2, 60, 0.6 ) );
//-- 将光晕追加到点光源中去
light.add( lensflare );
- 操纵摄像机围绕中心点移动,并聚焦中心点
代码:
let theta = 0;
...
animate();
function animate() {
requestAnimationFrame( animate );
theta += 0.1;
camera.position.x = radius * Math.sin( THREE.MathUtils.degToRad( theta ) );
camera.position.y = radius * Math.sin( THREE.MathUtils.degToRad( theta ) );
camera.position.z = radius * Math.cos( THREE.MathUtils.degToRad( theta ) );
camera.lookAt( scene.position );
//-- 请注意,在大多数属性发生改变之后,你将需要调用.updateProjectionMatrix来使得这些改变生效
camera.updateMatrixWorld();
renderer.render( scene, camera );
}
- 设置鼠标触碰模型的位置与朝向
// -- 看看从相机到世界的光线是否击中了我们的一个网格
const intersects = raycaster.intersectObject( mesh );
//-- 设置位置与朝向
xxx.position.set( 0, 0, 0 );
xxx.lookAt( intersects[ 0 ].face.normal );
- 获取模型的不同部位,以及控制这些部位移动变化
官方示例 -https://threejs.org/examples/#webgl_animation_skinning_ik
代码:
//-- 一个位于三维空间中的点
const v0 = new THREE.Vector3();
//-- 模型数据临时存贮位置
const OOI = {};
//-- 加载模型
const gltf = await gltfLoader.loadAsync( 'models/gltf/kira.glb' );
init()
animation()
function init(){
//-- 遍历模型中的不同部位
gltf.scene.traverse( n => {
//-- 通过blender模型中位置命名,将模型数据存到临时对象OOI中去
if ( n.name === 'head' ) OOI.head = n;
if ( n.name === 'lowerarm_l' ) OOI.lowerarm_l = n;
if ( n.name === 'Upperarm_l' ) OOI.Upperarm_l = n;
if ( n.name === 'hand_l' ) OOI.hand_l = n;
if ( n.name === 'target_hand_l' ) OOI.target_hand_l = n;
if ( n.name === 'boule' ) OOI.sphere = n;
if ( n.name === 'Kira_Shirt_left' ) OOI.kira = n;
});
scene.add( gltf.scene );
//-- 轨道控制朝向球体
orbitControls.target.copy( OOI.sphere.position );
//-- 模型手部绑定水晶球
OOI.hand_l.attach( OOI.sphere );
}
function animation(){
//-- 人物转头看向水晶球
//-- 获取水晶球世界坐标
OOI.sphere.getWorldPosition( v0 );
//-- 修改模型头部朝向水晶球坐标
OOI.head.lookAt( v0 );
//-- 旋转模型头部
OOI.head.rotation.set( OOI.head.rotation.x, OOI.head.rotation.y + Math.PI, OOI.head.rotation.z );
requestAnimationFrame( animate );
}
- 平行光照射物体,物体添加阴影
- 平行光 - DirectionalLight - castShadow
- 模型 - Mesh - castShadow
- 平台 - PlaneGeometry - receiveShadow
init();
animate();
function init(){
//-- 1. 创建平行光
const directionalLight = new THREE.DirectionalLight( 0xffffff, 0.7 );
directionalLight.position.set( 0, 5, 5 );
scene.add( directionalLight );
const d = 5;
//-- 开启平行光阴影
directionalLight.castShadow = true;
//-- 平行光阴影参数
directionalLight.shadow.camera.left = - d;
directionalLight.shadow.camera.right = d;
directionalLight.shadow.camera.top = d;
directionalLight.shadow.camera.bottom = - d;
directionalLight.shadow.camera.near = 1;
directionalLight.shadow.camera.far = 20;
directionalLight.shadow.mapSize.x = 1024;
directionalLight.shadow.mapSize.y = 1024;
//-- 2. 创建一个足球
const ball = new THREE.Mesh( ballGeometry, ballMaterial );
//-- 开启平行光阴影
ball.castShadow = true;
ball.position.x = radius * Math.cos( s );
ball.position.z = radius * Math.sin( s );
scene.add( ball );
//-- 3. 创建一个平台
const floorGeometry = new THREE.PlaneGeometry( 10, 10 );
const floorMaterial = new THREE.MeshLambertMaterial( { color: 0x4676b6 } );
const floor = new THREE.Mesh( floorGeometry, floorMaterial );
floor.rotation.x = Math.PI * - 0.5;
floor.receiveShadow = true;
scene.add( floor );
}
function animate() {
requestAnimationFrame( animate );
//-- ...渲染球移动的动画
renderer.render( scene, camera );
}
- 通过集合(Group)来操作一类模型的行为
- 通过group.add来添加模型
- 通过group.traverse来遍历集合(Group)中的模型
- 通过 模型.userData.xxx来设置自定义属性
- 通过 模型.userData.xxx来获取自定义属性
let group;
init();
animate();
function init(){
group = new THREE.Group();
scene.add( group );
for ( let i = 0; i < 20; i ++ ) {
const sphere = new THREE.Mesh( geometry, material );
//-- 添加一个自定义属性phase,值为Math.random() * Math.PI
sphere.userData.phase = Math.random() * Math.PI;
...
//-- 添加模型到集合中去
group.add( sphere );
}
}
function animate() {
const time = performance.now() / 1000;
group.traverse( function ( child ) {
if ( 'phase' in child.userData ) {
//-- 通过模型的自定义属性phase,来进行最大高度差异化的,跳动
child.position.y = Math.abs( Math.sin( time + child.userData.phase ) ) * 4 + 0.3;
}
});
renderer.render( scene, camera );
requestAnimationFrame( animate );
}
- 给3d空间内添加标记点Sprite
- Sprite - 精灵是一个总是面朝着摄像机的平面,通常含有使用一个半透明的纹理。
- 应用: 汽车模型内饰说明,3d看房切换房间
//-- 添加一个标记点
createSprite() {
const url = '材质地址'
const texture = new THREE.TextureLoader().load(url)
const material = new THREE.SpriteMaterial({ map: texture })
const sprite = new THREE.Sprite(material)
// 设置大小、位置、内容
sprite.scale.set(0.5, 0.5, 0.5)
sprite.position.set(0.4, 0, -4.5)
// 加入场景中
scene.add(sprite)
}
//-- 切换场景
GoToRoom(e) {
e.preventDefault()
const { clientX, clientY } = e
const dom = renderer.domElement
// 拿到canvas画布到屏幕的距离
const domRect = dom.getBoundingClientRect()
// 计算标准设备坐标 - 归一化设备坐标
const x = ((clientX - domRect.left) / dom.clientWidth) * 2 - 1
const y = -((clientY - domRect.top) / dom.clientHeight) * 2 + 1
const vector = new THREE.Vector3(x, y)
// 转世界坐标
const worldVector = vector.unproject(camera)
console.log('世界坐标', worldVector)
// 向量相减,并获取单位向量
const ray = worldVector.sub(camera.position).normalize()
// 射线投射对象, 第一个参数是射线原点 第二个参数是射线方向
const raycaster = new THREE.Raycaster(camera.position, ray)
raycaster.camera = camera
//返回射线选中的对象 //第一个参数是检测的目标对象 第二个参数是目标对象的子元素
const intersects= raycaster.intersectObjects(scene.children)
if (intersects.length > 0) {
console.log("捕获到对象", intersects);
const intersect = intersects[0]
//-- 通过精灵的类型,以及内容来
if (intersect.object?.type == "Sprite" && intersect.object?.content?.isComeAround) {
//-- 删除房间精灵,删除标记点
scene.remove(sphere)
scene.remove(sprite)
if (intersect.object?.content?.to == '小明家') {
//-- 添加另一个精灵
cube = cube || this.createCube()
scene.add(cube)
}
}
}else{
console.log("没捕获到对象");
}
},
initEvent() {
window.addEventListener('click', this.GoToRoom)
}
- 给一个场景内的模型添加热区用于跳转链接
实现思路:
- 添加一个平面覆盖再模型的某个位置,如: 手机模型的屏幕
- 给屏幕添加一个自定义属性URL到userData
//-- 用于交互的对象盒子
let rayCheckObjArr = [];
...
phoneHotMesh.userData.URL = 'https://www.baidu.com';
scene.add(phoneHotMesh);
rayCheckObjArr.push(phoneHotMesh);
- 热区添加鼠标滑入滑出额的手型标记
document.addEventListener('mousemove', mouseLinkHover, false);
function mouseLinkHover(event) {
event.preventDefault();
var mouse = new THREE.Vector2();
//-- 修改鼠标坐标系中间为(0,0)点,两侧极限范围(-1~+1)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
var raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(rayCheckObjArr);
//-- 划入热区时候为html的body标签,添加手型图标
if (intersects.length > 0) {
document.getElementById('documentBody').style.cursor = 'pointer';
} else {
document.getElementById('documentBody').style.cursor = 'default';
}
}
- 点击热区完成 跳转操作 (当然也可做其他操作,如改变材质颜色等)
document.addEventListener('click', mouseLinkClick, false);
function mouseLinkClick(event) {
event.preventDefault();
var mouse = new THREE.Vector2();
//-- 修改鼠标坐标系中间为(0,0)点,两侧极限范围(-1~+1)
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
var raycaster = new THREE.Raycaster();
raycaster.setFromCamera(mouse, camera);
var intersects = raycaster.intersectObjects(rayCheckObjArr);
//-- 划入热区时候为html的body标签,添加手型图标
if (intersects.length > 0) {
const pickedObject = intersectedObjects[0].object;
if (intersectedObjects[0].object.userData.URL)
window.open(intersectedObjects[0].object.userData.URL);
else {
return;
}
}
}
注:
当然为了定制化,也可以对添加鼠标镜头射线检测的物体mesh添加name,intersects长度不为0时候,判断name再进行后续操作,增加一层定制的逻辑,也可以判断mesh的type类型来增加判断条件种类
键盘(操控干)控制移动
....待完成视差动画与模型动画结合
包含: 粒子创建、摄像机移动、移动端适配
文章地址:
https://juejin.cn/post/7118746934000484365/
demo地址:
https://gaohaoyang.github.io/threeJourney/21-scrollBasedAnimation/
微信小程序中使用threejs库
https://gitee.com/threejs/three-weixin#%E4%BB%8B%E7%BB%8D
https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx5d6376b4fc730db9
https://mp.weixin.qq.com/wxopen/plugindevdoc?appid=wx74ea3ef7e1e72753&token=&lang=zh_CN#-uni-app-
- 二维图片实现伪三维动画
说明:
将图片,通过网站计算出前景后景,分离出深度图,然后将深度图按照深色移动慢,浅色移动快,来完成伪3D动画
资源网址
https://convert.leiapix.com/
https://www.bilibili.com/video/BV1Gg411X7FY?p=61&vd_source=948598a2d43866b02bf79d40339f8de5
- 粒子效果
参考帖子:https://juejin.cn/post/7155278132806123557
- 使用THREE.Sprite创建粒子
const createParticlesBySprite = () => {
for (let x = -15; x < 15; x++) {
for(let y = -10; y < 10; y++) {
let material = new THREE.SpriteMaterial({
color: Math.random() * 0xffffff
});
let sprite = new THREE.Sprite(material);
sprite.position.set(x * 4, y * 4, 0);
scene.add(sprite);
}
}
}
- 使用THREE.Points创建粒子
const createParticlesByPoints = () => {
const geom = new THREE.BufferGeometry();
const material = new THREE.PointsMaterial({
size: 3,
vertexColors: true,
color: 0xffffff
});
let veticsFloat32Array = []
let veticsColors = []
for (let x = -15; x < 15; x++) {
for (let y = -10; y < 10; y++) {
veticsFloat32Array.push(x * 4, y * 4, 0);
const randomColor = new THREE.Color(Math.random() * 0xffffff);
veticsColors.push(randomColor.r, randomColor.g, randomColor.b);
}
}
const vertices = new THREE.Float32BufferAttribute(veticsFloat32Array, 3);
const colors = new THREE.Float32BufferAttribute(veticsColors, 3);
geom.attributes.position = vertices;
geom.attributes.color = colors;
const particles = new THREE.Points(geom, material);
scene.add(particles);
- 粒子的运动或者动画
...
const particleSystem = new THREE.Points(geom, material);
scene.add(particleSystem);
...
//-- 更新粒子位置
const updateParticles = () => {
// 粒子系统旋转动画
particleSystem.position.x = 0.2 * Math.cos(t);
particleSystem.position.y = 0.2 * Math.cos(t);
particleSystem.rotation.z += 0.015;
camera.lookAt(particleSystem.position);
// 粒子系统由近到远动画
for (let i = 0; i < veticsFloat32Array.length; i++) {
//-- 每间隔三个值,表示一个粒子三维坐标,如果(i + 1) % 3==0,则表示该粒子的Z轴值
if ((i + 1) % 3 === 0) {
const dist = veticsFloat32Array[i] - camera.position.z;
//-- 如果接近摄像机,则将该粒子拉到远离粒子的位置(-500~-1000)的位置上
if (dist >= 0) veticsFloat32Array[i] = rand(-1000, -500);
//-- 向着摄像机移动的速度
veticsFloat32Array[i] += 2.5;
//-- Float32BufferAttribute:第一个参数为数据,第二个参数为分隔数,坐标(x,y,z)所以采用3
const _vertices = new THREE.Float32BufferAttribute(veticsFloat32Array, 3);
geom.attributes.position = _vertices;
}
}
particleSystem.geometry.verticesNeedUpdate = true;
}
//-- 获取两个值之间的随机数字
const rand = (min, max) => min + Math.random() * (max - min);
//-- 更新场景相机
const updateRenderer = () => {
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
}
//-- 渲染函数
const tick = () => {
//-- 更新粒子位置
updateParticles();
//-- 更新场景相机
updateRenderer();
renderer.render(scene, camera);
requestAnimationFrame(tick);
t += 0.01;
}
待学习
http://threejs.org/examples/misc_boxselection.html
- 通过圈选的方式,获取模型
https://threejs.org/examples/webgl_shaders_sky.html
- 模拟一天的阳光变化
https://threejs.org/examples/webgl_shaders_ocean.html
- 模拟大海
https://threejs.org/examples/#webgl_lights_physical
- 室内光源对材质的影响
https://threejs.org/examples/#webgl_loader_collada_kinematics
- 机械臂运动
https://threejs.org/examples/#games_fps
- 在一个场景内移动
https://jesse-zhou.com/
- 完整的3d模型(模型、灯光、交互、视频)
https://www.kodeclubs.com/
- 第三人称视角,度假小岛(模型、灯光、交互)
未完待续...