threejs 基础自学总结

概念性的知识

  • threejs是一个让用户通过javascript入手进入搭建WebGL项目的类库;
  • WebGL 使得网页在支持 HTML <canvas> 标签的浏览器中,不需要使用任何插件,便可以使用基于 OpenGL ES 2.0 的 API 在 canvas 中进行 3D 渲染;
  • OpenGL在图形学、电子游戏领域使用很长时间,WebGL是Web版的OpenGL;
  • OpenGL学习笔记 - https://juejin.cn/post/6995037193114746894 - (逐章说明,优先看)
摘取 WebGL编程指南 2014年第一版

摘录

摘录

OpenGL与WebGL关系

网页与WebGL网页的关系

安装

  1. 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 本身版本不一致的示例

  1. threejs中文手册
https://threejs.org/docs/index.html#manual/zh/introduction/Creating-a-scene
  1. 官方示例下载
https://github.com/mrdoob/three.js/archive/refs/heads/dev.zip
  1. 国外视频课程中文笔记 -- (优先!) -- (独立且完整的章节项目)
https://juejin.cn/post/7152438555246067719
更新中的课程笔记列表
  1. 国内文章的博客地址 -- (图文代码罗列很清晰,比官方文档好) -- (基础)
http://www.webgl3d.cn/pages/aac9ab/
  1. 国内博主的threejs相关
  • threejs基础图文教程 -- (优先看)
  • 免费素材、threejs基础图文、其他课程
  • 公开的视频列表
https://www.three3d.cn/
http://www.cpengx.cn/
https://www.ixigua.com/home/10552573903/?list_entrance=search
  1. 博主文章 - (涉及很多知识点 - 逐章 - 讲述代码齐全)
https://juejin.cn/user/4485631602599495/posts

文档以及示例学习

  • 为了真正能够让你的场景借助three.js来进行显示,我们需要以下几个对象,这样我们就能透过摄像机渲染出场景:
    场景、相机、渲染器

  • 场景、相机、渲染器 均为threejs内置库

import * as THREE from 'https://unpkg.com/three/build/three.module.js';

※ 渲染器、场景、模型、灯光

threejs结构初步了解
场景 - 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();

实例:

  1. 场景内的模型看向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 );
}
  1. 删除场景内的网格模型
    示例: 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 --;
    }
}
光源
  1. 半球光 - 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 );
  1. 平行光 - 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 );

※ 模型在(球形光、平行光)中的不同效果下的差异


球形光+平行光

平行光

球形光

没有任何光
  1. 点光源 - 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 );
  1. 环境光 - 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 ) - 旋转 参数弧度
创建网格模型的方式
  1. 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 );
  1. 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

通过摄像机,我们可以看到场景内的模型,摄像机可以调整视野角度、宽高比等

  1. 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 );


camera.position.set( 900, 0, 0 )

camera.position.set( 0, 900, 0 )

camera.position.set( 0, 0, 900 )

PerspectiveCamera( fov : Number, aspect : Number, near : Number, far : Number )
参数说明:

参数1 - 视野角度(FOV): 
视野角度就是无论在什么时候,你所能在显示器上看到的场景的范围,它的单位是角度(与弧度区分开)
参数2 - 长宽比(aspect ratio): 
一个物体的宽除以它的高的值,相当于电影幕布的比例
参数3 - 长宽比(near):
摄像机视锥体近端面
参数4 - 长宽比(far):
摄像机视锥体远端面
  1. CameraHelper - 摄像机椎体
    用于模拟相机视锥体的辅助对象.它使用LineSegments来模拟相机视锥体.
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const helper = new THREE.CameraHelper( camera );
scene.add( helper );
透视相机-摄像机椎体
  1. 正交相机 - 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 );
正交相机-摄像机椎体
  1. 摄像机阵列 - 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 );
}
轨道控制器 - 操控屏幕移动、旋转等
  1. 轨道控制器 - 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;
限制轨道控制器范围
  1. 第一人称视角轨道控制器 - 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();
}
  1. 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 );
变换控制器操控角色手臂位置
  1. 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

类似cs第一人称视角的移动

三维向量 - 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();)

代码:

  1. 基本运作
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 );
}
  1. 线性动画
    完成一些指定的动画 (位置、大小、颜色、透明度)
    示例: 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 - 音频的使用
  1. 全局音频
  • 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();
});
  1. 位置音频 - 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 - 源码结构图


OimoPhysics结构图
  • 存储网格数据信息的实例 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 ) {
    //
} );

//-- 待完成....

练习笔记

  1. 调用模型的某一个动画
    在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
  1. 添加一个临时测试地面
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 );
  1. 添加雾
    这个类中的参数定义了线性雾。

Fog( color : Integer, near : Float, far : Float )
参数:

  • near - 开始应用雾的最小距离,默认1
  • far - 结束计算、应用雾的最大距离,默认1000

代码:

const scene = new THREE.Scene();
scene.fog = new THREE.Fog( 0xa0a0a0, 200, 1000 );
  1. 海浪的制作
    官方的海浪参考: 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 );
}
  1. 显示隐藏某个模型,通过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;
  1. 镜头光晕
    代码:
//-- 导入工具
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 );
镜头光晕
  1. 操纵摄像机围绕中心点移动,并聚焦中心点
    代码:
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 );
}
  1. 设置鼠标触碰模型的位置与朝向
// -- 看看从相机到世界的光线是否击中了我们的一个网格
const intersects = raycaster.intersectObject( mesh );
//-- 设置位置与朝向
xxx.position.set( 0, 0, 0 );
xxx.lookAt( intersects[ 0 ].face.normal );
  1. 获取模型的不同部位,以及控制这些部位移动变化
    官方示例 - 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 );
}
获取模型部位数据,并变化它
  1. 平行光照射物体,物体添加阴影
  • 平行光 - 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 );
}
  1. 通过集合(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 );
}
  1. 给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)
}
  1. 给一个场景内的模型添加热区用于跳转链接
    实现思路:
  • 添加一个平面覆盖再模型的某个位置,如: 手机模型的屏幕
  • 给屏幕添加一个自定义属性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类型来增加判断条件种类

  1. 键盘(操控干)控制移动
    ....待完成

  2. 视差动画与模型动画结合
    包含: 粒子创建、摄像机移动、移动端适配
    文章地址:
    https://juejin.cn/post/7118746934000484365/
    demo地址:
    https://gaohaoyang.github.io/threeJourney/21-scrollBasedAnimation/

  3. 微信小程序中使用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-
  1. 二维图片实现伪三维动画
    说明:
    将图片,通过网站计算出前景后景,分离出深度图,然后将深度图按照深色移动慢,浅色移动快,来完成伪3D动画
    资源网址
https://convert.leiapix.com/
https://www.bilibili.com/video/BV1Gg411X7FY?p=61&vd_source=948598a2d43866b02bf79d40339f8de5
  1. 粒子效果
    参考帖子: 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/ - 第三人称视角,度假小岛(模型、灯光、交互)

未完待续...

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,454评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,553评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,921评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,648评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,770评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,950评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,090评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,817评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,275评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,592评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,724评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,409评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,052评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,815评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,043评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,503评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,627评论 2 350

推荐阅读更多精彩内容