# 基础知识

学习官网 (opens new window) 源码地址 (opens new window)

# 什么是WEBGL?

  • WebGL (Web图形库) 是一种JavaScript API,用于在浏览器中呈现交互式3D和2D图形,而无需任何插件
  • WebGL通过引入与OpenGL紧密相符合的API,可以在HTML5 元素中使用
  • WebGL给我们提供了一系列的图形接口,能够让我们通过js去使用GPU来进行浏览器图形渲染的工具

# 什么是ThreeJS?

Three.js是一款webGL框架,在WebGL的api接口基础上,又进行的一层封装,由于其易用性、开源性被广泛应用

# 什么是Cesium?

Cesium是国外一个基于WebGL编写的地图引擎,支持3D,2D,2.5D形式的地图展示,专注于Gis

# 什么是ThingJS?

ThingJS是2018年新兴的3D框架,对Three.js的进一步封装,无需关心渲染、网格、光线等复杂概念,旨在简化3D应用开发,主要针对物联网领域

# Three.js相关的开源库

# 目录结构

build  # 构建完成的Three.js,可以直接引入使用,并有压缩版
docs  # Three.js 的官方文档
editor  # Three.js的一个网页版的模型编辑器
examples  # Three.js的官方案例,如果全都学会,必将成为大神
src  # 这里面放置的全是编译Three.js的源文件,每一个.js文件对应帮助文档doc中的一个构造函数
files # 资源文件
manual # 使用手册
playground  # 包含基于 Three.js 的示例的存储库,用于学习目的
test  # 一些官方测试代码,我们一般用不到
utils  # 一些相关插件
其他  # 开发环境搭建、开发所需要的文件,如果不对Three.js进行二次开发,用不到

# jsm文件夹

js和jsm文件夹是对映关系,jsm主要用在es6 import中

import {OrbitControls} from "three/examples/jsm/controls/OrbitControls"

# 右手坐标系

# 右手背对着屏幕放置,拇指即指向X轴的正方向。伸出食指和中指
# 如右图所示,食指指向Y轴的正方向,中指所指示的方向即是Z轴的正方向

右手坐标系

# 角度弧度互转

弧度 = 角度 / 180 * Math.PI
角度 = 弧度 * 180 / Math.PI
角度 转 弧度 THREE.MathUtils.degToRad(deg)
弧度 转 角度 THREE.MathUtils.radToDeg (rad)

# 注意事项

  • 在移动端网页里流畅运行,最多不能超过10万个面

# 如何使用

# Vue中使用ThreeJS

  • npm安装特定版本three.js(注意使用哪个版本,查文档就查对应版本)
npm install three@0.148.0 --save
# typescript中使用需要加上下面:
npm install @types/three@0.148.0 --save
  • ES6语法引入three.js核心
import * as THREE from 'three'
  • 引入three.js其他扩展库
# 在threejs文件包中examples/jsm目录下,你还可以看到各种不同功能的扩展库
# 一般来说,你项目用到那个扩展库,就引入那个,用不到就不需要引入

# 引入扩展库OrbitControls.js
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
# 引入扩展库GLTFLoader.js
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
# 扩展库引入——旧版本,比如122, 新版本路径addons替换了examples/jsm
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';

# HTML中使用ThreeJS

给script标签设置type="module",也可以在.html文件中使用import方式引入three.js

<script type="module">
// 现在浏览器支持ES6语法,自然包括import方式引入js文件
import * as THREE from './build/three.module.js';
</script>

.html文件引入three.js,最好的方式就是通过配置 type="importmap" 。这样方便直接复制源码

// 具体路径配置,你根据自己文件目录设置
<script type="importmap">
    {
		"imports": {
			"three": "../../../three.js/build/three.module.js"
			// 引入threejs扩展库
			"three/addons/": "../../../three.js/examples/jsm/"
		}
	}
</script>

// 配置type="importmap",.html文件也能和项目开发环境一样方式引入threejs
<script type="module">
    import * as THREE from 'three';
	// 扩展库OrbitControls.js
	import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
	// 扩展库GLTFLoader.js
	import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
	
    // 浏览器控制台测试,是否引入成功
    console.log(THREE.Scene);
	console.log(OrbitControls);
	console.log(GLTFLoader);
</script>

# 整体实例

整体实例






















 


 
 



 





 





 



 









 

 






 










<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>第一个three.js文件_WebGL三维场景</title>
  <style>
    body {
      margin: 0;
      overflow: hidden;
      /* 隐藏body窗口区域滚动条 */
    }
  </style>
  <!--引入three.js三维引擎-->
  <script src="http://www.yanhuangxueyuan.com/versions/threejsR92/build/three.js"></script>
  <!-- <script src="./three.js"></script> -->
  <!-- <script src="http://www.yanhuangxueyuan.com/threejs/build/three.js"></script> -->
</head>

<body>
  <script>
	//创建场景对象Scene
    var scene = new THREE.Scene();
    
	//创建网格模型Geometry
    var geometry = new THREE.BoxGeometry(100, 100, 100); //创建一个立方体几何对象Geometry
    // var geometry = new THREE.SphereGeometry(60, 40, 40); //创建一个球体几何对象
	
	//创建材质Material
	//构造函数的参数是一个对象,对象包含了颜色、透明度等属性
    var material = new THREE.MeshLambertMaterial({
      color: 0x0000ff
    });
	
	//网格模型对象Mesh(物体)
	//网格模型对象的参数几何体Geometry、材质Material
    var mesh = new THREE.Mesh(geometry, material);
	//默认在原点位置
	mesh.positon.set(100,100,100);
    scene.add(mesh); //网格模型添加到场景中
	
    //创建光源Light
    var point = new THREE.PointLight(0xffffff);//点光源
    point.position.set(400, 200, 300); //点光源位置
    scene.add(point); //点光源添加到场景中
	
    var ambient = new THREE.AmbientLight(0x444444);////环境光
    scene.add(ambient);
	
	//创建画布Canvas,定义相机渲染输出的画布尺寸
    var width = window.innerWidth; //窗口宽度
    var height = window.innerHeight; //窗口高度
    var k = width / height; //窗口宽高比
    var s = 200; //三维场景显示范围控制系数,系数越大,显示的范围越大
	
    //创建相机Camera
	// const camera = new THREE.PerspectiveCamera();//透视投影相机
	//创建了一个正射投影相机对象,前四个参数定义的是拍照窗口大小
    var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
    camera.position.set(200, 300, 200); //设置相机位置x、y、z
    camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
	//camera.lookAt(0, 0, 0); //指向坐标原点
	//camera.lookAt(mesh.position);//指向mesh对应的位置
	
    //创建渲染器Renderer
    var renderer = new THREE.WebGLRenderer();
	//this.renderer = new THREE.WebGLRenderer({ antialias: true }) // 是否执行抗锯齿
    renderer.setSize(width, height);//设置渲染区域尺寸
    renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色及透明度
    document.body.appendChild(renderer.domElement); //body元素中插入canvas对象
    //执行渲染操作   指定场景、相机作为参数
    renderer.render(scene, camera);
  </script>
</body>
</html>

# 相机介绍

# 透视投影相机 PerspectiveCamera:四锥体

  • 用来模拟人眼所看到的景象,它是3D场景的中使用得最普遍的投影模式,类似于手电光投影到墙面
  • 一般用于人在场景用漫游,或者高俯视整个场景,大部分3D项目

透视投影相机

// fov:相机视锥体竖直方向视野角度,从视图的底部到顶部,以角度来表示,默认50
// aspect:相机视锥体水平方向和竖直方向长度比,一般设置为Canvas画布宽高比width / height,默认1
// near:相机视锥体近裁截面相对相机距离,默认0.1
// far:相机视锥体远裁截面相对相机距离,far-near构成了视锥体高度方向,默认2000
PerspectiveCamera(fov:Number, aspect:Number, near:Number, far:Number)

// width和height用来设置Three.js输出的Canvas画布尺寸(像素px)
const width = 800; //宽度
const height = 500; //高度
// 30:视场角度, width / height:Canvas画布宽高比, 1:近裁截面, 3000:远裁截面
const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);

canvas画布宽高度动态变化

window.onresize = function () {
    // 重置渲染器输出画布canvas尺寸
    renderer.setSize(window.innerWidth, window.innerHeight);
    // 重新调整相机视锥体水平方向和竖直方向长度比
    camera.aspect = window.innerWidth / window.innerHeight;
    // 渲染器执行render方法的时候会读取相机对象的投影矩阵属性projectionMatrix
    // 但是不会每渲染一帧,就通过相机的属性计算投影矩阵(节约计算资源)
    // 如果相机的一些属性发生了变化,需要执行updateProjectionMatrix ()方法更新相机的投影矩阵
    camera.updateProjectionMatrix();
};

# 正交相机 OrthographicCamera:长方体

  • 在这种投影模式下,无论物体距离相机距离远或者近,在最终渲染的图片中物体的大小都保持不变
  • 这对于渲染2D场景、地图或者UI元素是非常有用的,类似于平行光投影到墙面

透视投影相机

// left:摄像机长方体左侧面
// right:摄像机长方体右侧面
// top:摄像机长方体上侧面
// bottom:摄像机长方体下侧面
// near:摄像机长方体近端面
// far:摄像机长方体远端面
OrthographicCamera(left:Number,right:Number,top:Number,bottom:Number,near:Number,far:Number)

canvas画布宽高度动态变化

window.onresize = function () {
    const width = window.innerWidth; //canvas画布宽度
    const height = window.innerHeight; //canvas画布高度
    // 1. WebGL渲染器渲染的Cnavas画布尺寸更新
    renderer.setSize(width, height);
    // 2.1.更新相机参数
    const k = width / height; //canvas画布宽高比
    camera.left = -s*k;
    camera.right = s*k;
    // 2.2.相机的left, right, top, bottom属性变化了,通知threejs系统
    camera.updateProjectionMatrix();
};

# 辅助观察坐标系 AxesHelper

const axesHelper = new THREE.AxesHelper(150);
scene.add(axesHelper);

# 网格地面辅助观察 GridHelper

// size:坐标格尺寸。默认为 10
// divisions:坐标格细分次数。默认为 10
// colorCenterLine:中线颜色。值可以为 Color 类型, 16进制 和 CSS 颜色名。默认为 0x444444
// colorGrid:坐标格网格线颜色。值可以为 Color 类型, 16进制 和 CSS 颜色名。默认为 0x888888
const gridHelper = new THREE.GridHelper(300, 25, 0x004444, 0x888888);
scene.add(gridHelper);

# 相机可视化辅助观察 CameraHelper

const cameraHelper = new THREE.CameraHelper(camera);
scene.add(cameraHelper);

# 相机控件 OrbitControls

展示模型的时候,可以通过相机控件OrbitControls实现旋转缩放预览效果,本质上就是改变了相机的参数

const controls = new OrbitControls(camera, renderer.domElement);
// 监听鼠标、键盘事件,如果OrbitControls改变了相机参数,重新调用渲染器渲染三维场景
controls.addEventListener('change', function () {
    renderer.render(scene, camera);
});

OrbitControls旋转缩放限制

controls.enablePan = false; // 禁止右键拖拽
controls.enableZoom = false;// 禁止缩放
controls.enableRotate = false; // 禁止旋转

// 透视投影相机缩放范围
controls.minDistance = 200; // 相机位置与观察目标点最小值
controls.maxDistance = 500; // 相机位置与观察目标点最大值

// 正投影相机缩放范围
controls.minZoom = 0.5;
controls.maxZoom = 2;

controls.getDistance(); // 相机位置与目标观察点距离

// 上下旋转范围
controls.minPolarAngle = 0;// 默认值0
controls.maxPolarAngle = Math.PI;// 默认值Math.PI,设置为90度,看不到底部

// 左右旋转范围
controls.minAzimuthAngle = -Math.PI/2;
controls.maxAzimuthAngle = Math.PI/2;

# 改变相机观察目标点

相机控件OrbitControls会影响lookAt设置,注意手动设置OrbitControls的目标参数






 

 

// 改变相机观察目标点
camera.lookAt(1000, 0, 1000);

const controls = new OrbitControls(camera, renderer.domElement);
// 相机控件.target属性在OrbitControls.js内部表示相机目标观察点,默认0,0,0
controls.target.set(1000, 0, 1000);
// update()函数内会执行camera.lookAt(controls.targe)
controls.update();

# 渲染器介绍

# Canvas画布插入到任意HTML元素中

<div id="webgl" style="margin-top: 200px;margin-left: 100px;"></div>
<script>
   document.getElementById('webgl').appendChild(renderer.domElement);
</script>

# 周期性渲染 requestAnimationFrame

默认每秒钟执行60次,每次执行16.7毫秒










 



const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height);
// renderer.render(scene, camera); //执行渲染操作
document.body.appendChild(renderer.domElement);

// 渲染函数
function render() {
    renderer.render(scene, camera); //执行渲染操作
    mesh.rotateY(0.01);//每次绕y轴旋转0.01弧度
    requestAnimationFrame(render);//请求再次执行渲染函数render,渲染下一帧
}
render();

注意

设置了周期性渲染,OrbitControls就不用再通过change执行renderer.render(scene, camera)了
毕竟渲染循环一直在执行renderer.render(scene, camera);

# 锯齿模糊处理


 




 

// 设置渲染器锯齿属性
renderer.antialias = true

// 获取你屏幕对应的设备像素比
const devicePixelRatio = window.devicePixelRatio;
// 告诉threejs,以免渲染模糊问题
renderer.setPixelRatio(devicePixelRatio);

# 渲染器的输出编码

默认为THREE.SRGBColorSpace,需要和纹理对象Texture颜色空间属性一直

// THREE.NoColorSpace = ""
// THREE.SRGBColorSpace = "srgb"
// THREE.LinearSRGBColorSpace = "srgb-linear"

renderer.outputEncoding = THREE.SRGBColorSpace

# 设置背景透明度

renderer.setClearAlpha(0.8); // 设置透明度
renderer.setClearColor(0xb9d3ff, 0.4); // 设置背景颜色和透明度

# 渲染结果保存为图片




 










// WebGL渲染器设置
const renderer = new THREE.WebGLRenderer({
    //想把canvas画布上内容下载到本地,需要设置为true
    preserveDrawingBuffer:true,
});

const link = document.createElement('a');
// 通过超链接herf属性,设置要保存到文件中的数据
const canvas = renderer.domElement; //获取canvas对象
link.href = canvas.toDataURL("image/png");
canvas.toDataURL("image/png");
canvas.toDataURL("image/jpeg");
canvas.toDataURL("image/bmp");

# 查看渲染帧率stats.js

通过stats.js库可以查看three.js当前的渲染性能

//引入性能监视器stats.js
import Stats from 'three/addons/libs/stats.module.js';

//创建stats对象
const stats = new Stats();
//stats.domElement:web页面上输出计算结果,一个div元素,
document.body.appendChild(stats.domElement);
// 渲染函数
function render() {
	//requestAnimationFrame循环调用的函数中调用方法update(),来刷新时间
	stats.update();
	renderer.render(scene, camera); //执行渲染操作
	requestAnimationFrame(render); //请求再次执行渲染函数render,渲染下一帧
}
render();

stats方法setMode(mode)可以更换不同的显示模式

// 显示:渲染帧率  刷新频率,一秒渲染次数 
stats.setMode(0);//默认模式
//显示:渲染周期 渲染一帧多长时间(单位:毫秒ms)
stats.setMode(1);

注意

受电脑性能以及需要渲染的物体数量的影响,渲染帧率会自动变化

# 几何体介绍

# 曲线 Curve

threejs提供了很多常用的曲线或直线API,可以直接使用。这些API曲线都有一个共同的父类Curve

# 2D曲线                                # 3D曲线
  # 直线  LineCurve                       # 直线  LineCurve3
  # 圆弧  ArcCurve                        
  # 椭圆  EllipseCurve                    
  # 二维样条曲线  SplineCurve              # 三维样条曲线  CatmullRomCurve3
  # 二维贝塞尔曲线                         # 三维贝塞尔曲线   
    # 二次  QuadraticBezierCurve            # 二次  QuadraticBezierCurve3
	# 三次  CubicBezierCurve                # 三次  CubicBezierCurve3

# 直线 LineCurve

2D直线线段LineCurve,参数是表示x、y坐标的二维向量Vector2对象

new THREE.LineCurve(new THREE.Vector2(), new THREE.Vector2());

3D直线线段LineCurve3,参数是表示x、y、z坐标的三维向量Vector3对象

new THREE.LineCurve3(new THREE.Vector3(), new THREE.Vector3());

# 椭圆弧线 EllipseCurve

椭圆曲线x和y方向半径相同,就是一个圆的效果

// aX, aY	椭圆中心坐标
// xRadius	椭圆x轴半径
// yRadius	椭圆y轴半径
// aStartAngle	弧线开始角度,从x轴正半轴开始,默认0,弧度单位
// aEndAngle	弧线结束角度,从x轴正半轴算起,默认2 x Math.PI,弧度单位
// aClockwise	是否顺时针绘制,默认值为false
EllipseCurve( aX, aY, xRadius,yRadius, aStartAngle, aEndAngle, aClockwise )

绘制一个椭圆曲线的流程

// 1、定义椭圆
const ellipseCurve = new THREE.EllipseCurve(0, 0, 100, 50);
// 2、获取顶点数据 getPoints
const points = ellipseCurve.getPoints(50);
// 3、提取曲线坐标数据
const geometry1 = new THREE.BufferGeometry().setFromPoints( points );
// 4、线模型调用线材质绘制曲线
const material = new THREE.LineBasicMaterial({ color: 0x00fffff });
// 5、线模型
const line = new THREE.Line(geometry1, material);
scene.add(line)

通过.getSpacedPoints()和.getPoints()一样也可以从曲线Curve上返回一系列曲线上的顶点坐标

getSpacedPoints()  # 是按照曲线长度等间距返回顶点数据
getPoints()  # 会考虑曲线斜率变化,斜率变化快的位置返回的顶点更密集

# 圆弧线 ArcCurve

圆弧线 ArcCurve 的父类是椭圆弧线 EllipseCurve ,语法和椭圆弧线 EllipseCurve 相似

// aX, aY	圆心坐标
// aRadius	圆弧半径
// aStartAngle	弧线开始角度,从x轴正半轴开始,默认0,弧度单位
// aEndAngle	弧线结束角度,从x轴正半轴算起,默认2 x Math.PI,弧度单位
// aClockwise	是否顺时针绘制,默认值为false
ArcCurve( aX, aY, aRadius, aStartAngle, aEndAngle, aClockwise )

# 三维样条曲线 CatmullRomCurve3

在三维空间中随意设置几个顶点坐标,就可以生成一条穿过这几个点的光滑曲线
















 


 
 
 
 







// points:Vector3点数组
// closed:该曲线是否闭合,默认值为false
// curveType:曲线的类型,默认值为centripetal,可能的值为centripetal、chordal和catmullrom
// tension:曲线的张力,默认为0.5
CatmullRomCurve3( points:Array, closed:Boolean, curveType:String, tension:Float )

// 三维向量Vector3创建一组顶点坐标
const arr = [
    new THREE.Vector3(-50, 20, 90),
    new THREE.Vector3(-10, 40, 40),
    new THREE.Vector3(0, 0, 0),
    new THREE.Vector3(60, -60, 0),
    new THREE.Vector3(70, 0, 80)
]
// 三维样条曲线
const curve = new THREE.CatmullRomCurve3(arr);

//曲线上获取点
const pointsArr = curve.getPoints(100); 
const geometry = new THREE.BufferGeometry();
//读取坐标数据赋值给几何体顶点
geometry.setFromPoints(pointsArr); 
// 线材质
const material = new THREE.LineBasicMaterial({
    color: 0x00fffff
});
// 线模型
const line = new THREE.Line(geometry, material);

# 二维样条曲线 SplineCurve

二维样条曲线默认情况下就是在XOY平面生成一个平面的样条曲线

// 二维向量Vector2创建一组顶点坐标
const arr = [
    new THREE.Vector2(-100, 0),
    new THREE.Vector2(0, 30),
    new THREE.Vector2(100, 0),
];
// 二维样条曲线
const curve = new THREE.SplineCurve(arr);

# 二维贝塞尔曲线

  • 创建一条平滑的二维二次贝塞尔曲线, 由起点、终点和一个控制点所定义










 
 



// v0:起点
// v1:中间的控制点
// v2:终点
QuadraticBezierCurve( v0:Vector2, v1:Vector2, v2:Vector2 )

const curve = new THREE.QuadraticBezierCurve(
	new THREE.Vector2( -10, 0 ),
	new THREE.Vector2( 20, 15 ),
	new THREE.Vector2( 10, 0 )
);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
const curveObject = new THREE.Line( geometry, material );

可以发现贝塞尔曲线经过p0、p2两个点,但是不经过p1点,贝塞尔曲线与直线p01和p12相切

二维二次贝塞尔曲线

  • 二维三次贝塞尔曲线与二维二次贝赛尔曲线区别就是多了一个控制点












 
 



// v0:起点
// v1:第一个控制点
// v2:第二个控制点
// v3:终点
CubicBezierCurve ( v0:Vector2, v1:Vector2, v2:Vector2, v3:Vector2 )

const curve = new THREE.CubicBezierCurve(
	new THREE.Vector2( -10, 0 ),
	new THREE.Vector2( -5, 15 ),
	new THREE.Vector2( 20, 15 ),
	new THREE.Vector2( 10, 0 )
);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
const curveObject = new THREE.Line( geometry, material );

二维三次贝塞尔曲线

# 三维贝塞尔曲线

  • 三维二次贝赛尔曲线与二维二次贝赛尔曲线区别就是多了一个维度,参数是三维向量对象Vector3










 
 



// v0:起点
// v1:中间的控制点
// v2:终点
QuadraticBezierCurve3( v0:Vector3, v1:Vector3, v2:Vector3 )

const curve = new THREE.QuadraticBezierCurve3(
	new THREE.Vector3( -10, 0, 0 ),
	new THREE.Vector3( 20, 15, 0 ),
	new THREE.Vector3( 10, 0, 0 )
);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
const curveObject = new THREE.Line( geometry, material );
  • 三维三次贝赛尔曲线与二维三次贝塞尔曲线区别就是多了一个维度,参数是三维向量对象Vector3












 
 



// v0:起点
// v1:第一个控制点
// v2:第二个控制点
// v3:终点
CubicBezierCurve3( v0:Vector3, v1:Vector3, v2:Vector3, v3:Vector3 )

const curve = new THREE.CubicBezierCurve3(
	new THREE.Vector3( -10, 0, 0 ),
	new THREE.Vector3( -5, 15, 0 ),
	new THREE.Vector3( 20, 15, 0 ),
	new THREE.Vector3( 10, 0, 0 )
);
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial( { color: 0xff0000 } );
const curveObject = new THREE.Line( geometry, material );

# 组合曲线 CurvePath

通过threejs组合曲线CurvePath对象,你可以把直线、圆弧、贝塞尔等线条拼接为一条曲线











 
 
 
 





// 一个圆弧和直线组合拼接的的U形效果
const R = 80;//圆弧半径
const H = 200;//直线部分高度
// 直线1
const line1 = new THREE.LineCurve(new THREE.Vector2(R, H), new THREE.Vector2(R, 0));
// 圆弧
const arc = new THREE.ArcCurve(0, 0, R, 0, Math.PI, true);
// 直线2
const line2 = new THREE.LineCurve(new THREE.Vector2(-R, 0), new THREE.Vector2(-R, H));

// CurvePath创建一个组合曲线对象
const CurvePath = new THREE.CurvePath();
//line1, arc, line2拼接出来一个U型轮廓曲线,注意顺序
CurvePath.curves.push(line1, arc, line2);
//组合曲线上获取点
const pointsArr = CurvePath.getPoints(16); 
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints(pointsArr); //读取坐标数据赋值给几何体顶点

# 多边形轮廓 Shape

是一组二维向量Vector2创建的,父类是Path,Path的父类是CurvePath
Path提供了直线、圆弧、贝塞尔、样条等绘制方法

# 属性
currentPoint  # 当前点,默认值Vector2(0,0)
holes: Array  # 孔洞

# 方法
moveTo(x, y)  # 将当前点移动到x, y
lineTo(x, y)  # 将当前点连接一条直线到x,y
arc(x, y, radius, startAngle, endAngle, clockwise)  # 相对圆弧,圆心是相对当前点而言
absarc(x, y, radius, startAngle, endAngle, clockwise)  # 绝对圆弧,圆心是相对原点而言
ellipse(x, y, xRadius, yRadius, startAngle, endAngle, clockwise, rotation)  # 相对圆
absellipse(x, y, xRadius, yRadius, startAngle, endAngle, clockwise, rotation)  # 绝对圆
splineThru (points:Array) # 添加二维样条曲线
quadraticCurveTo(cpX:Float, cpY:Float, x:Float, y:Float)  # 从当前点创建一条二维二次曲线
bezierCurveTo(cp1X:Float, cp1Y:Float, cp2X:Float, cp2Y:Float, x:Float, y:Float) #二维三次曲线

只有二维的 ShapeGeometry(形状)和三维的 ExtrudeGeometry(拉伸体)可以使用 Shape 类型










 

 




// 绘制一个矩形轮廓Shape
const shape = new THREE.Shape();
shape.moveTo(10, 0); //.currentPoint变为(10,0)
// 绘制直线线段,起点(10,0),结束点(100,0)
shape.lineTo(100, 0);//.currentPoint变为(100, 0)
shape.lineTo(100, 100);//.currentPoint变为(100, 100)
shape.lineTo(10, 100);//.currentPoint变为(10, 100)

// ShapeGeometry填充Shape获得一个平面几何体
const geometry = new THREE.ShapeGeometry(shape);
// ExtrudeGeometry拉伸Shape获得一个长方体几何体
const geometry = new THREE.ExtrudeGeometry(shape, {
    depth:20,//拉伸长度
    bevelEnabled:false,//禁止倒角
});

设置内孔的轮廓,形状上的孔洞











 
 

const path1 = new THREE.Path();// 圆孔1
path1.absarc(20, 20, 10);
const path2 = new THREE.Path();// 圆孔2
path2.absarc(80, 20, 10);
const path3 = new THREE.Path();// 方形孔
path3.moveTo(50, 50);
path3.lineTo(80, 50);
path3.lineTo(80, 80);
path3.lineTo(50, 80);

//三个内孔轮廓分别插入到holes属性中
shape.holes.push(path1, path2,path3);

# 平面几何

# 圆形 CircleGeometry

// radius:圆形的半径,默认值为1
// segments:分段(三角面)的数量,最小值为3,默认值为32
// thetaStart:第一个分段的起始角度,默认为0
// thetaLength:圆形扇区的中心角,通常被称为“θ”(西塔)。默认值是2*Pi
CircleGeometry(radius:Float, segments:Integer, thetaStart:Float, thetaLength:Float)

# 平面 PlaneGeometry

// width:平面沿着X轴的宽度。默认值是1
// height:平面沿着Y轴的高度。默认值是1
// widthSegments:(可选)平面的宽度分段数,默认值是1
// heightSegments:(可选)平面的高度分段数,默认值是1
PlaneGeometry(width:Float, height:Float, widthSegments:Integer, heightSegments:Integer)

# 圆环 RingGeometry

// innerRadius:内部半径,默认值为0.5
// outerRadius:外部半径,默认值为1
// thetaSegments:圆环的分段数。这个值越大,圆环就越圆。最小值为3,默认值为32
// phiSegments:最小值为1,默认值为8
// thetaStart:起始角度,默认值为0
// thetaLength:圆心角,默认值为Math.PI * 2
RingGeometry(innerRadius:Float, outerRadius:Float, thetaSegments:Integer, 
             phiSegments:Integer, thetaStart:Float, thetaLength:Float)

# 形状 ShapeGeometry

已知一个多边形的外轮廓坐标,想通过这些外轮廓坐标生成一个多边形几何体平面

// shapes:一个单独的shape,或者一个包含形状的Array
// curveSegments - Integer - 每一个形状的分段数,默认值为12
ShapeGeometry(shapes:Array, curveSegments:Integer)

// 一组二维向量表示一个多边形轮廓坐标
const pointsArr = [
    new THREE.Vector2(-50, -50),
    new THREE.Vector2(-60, 0),
    new THREE.Vector2(0, 50),
    new THREE.Vector2(60, 0),
    new THREE.Vector2(50, -50),
]
// Shape表示一个平面多边形轮廓,参数是二维向量构成的数组pointsArr
const shape = new THREE.Shape(pointsArr);
// 把Shape作为ShapeGeometry的参数,形成一个多边形平面几何体
const geometry = new THREE.ShapeGeometry(shape);
const material = new THREE.MeshLambertMaterial({
    wireframe:true,
});

# 立体几何

# 立方体 BoxGeometry

// width:X轴上面的宽度,默认值为1
// height:Y轴上面的高度,默认值为1
// depth:Z轴上面的深度,默认值为1
// widthSegments:(可选)宽度的分段数,默认值是1
// heightSegments:(可选)高度的分段数,默认值是1
// depthSegments:(可选)深度的分段数,默认值是1
BoxGeometry(width:Float, height:Float, depth:Float, 
            widthSegments:Integer, heightSegments:Integer, depthSegments:Integer)

# 球体 SphereGeometry

// radius:球体半径,默认为1
// widthSegments:水平分段数(沿着经线分段),最小值为3,默认值为32
// heightSegments:垂直分段数(沿着纬线分段),最小值为2,默认值为16
// phiStart:指定水平(经线)起始角度,默认值为0
// phiLength:指定水平(经线)扫描角度的大小,默认值为 Math.PI * 2
// thetaStart:指定垂直(纬线)起始角度,默认值为0
// thetaLength:指定垂直(纬线)扫描角度大小,默认值为 Math.PI
SphereGeometry(radius:Float, widthSegments:Integer, heightSegments:Integer, 
               phiStart:Float, phiLength:Float, thetaStart:Float, thetaLength:Float)

# 圆柱体 CylinderGeometry

// radiusTop:圆柱的顶部半径,默认值是1
// radiusBottom:圆柱的底部半径,默认值是1
// height:圆柱的高度,默认值是1
// radialSegments:圆柱侧面周围的分段数,默认为32
// heightSegments:圆柱侧面沿着其高度的分段数,默认值为1
// openEnded:指明该圆柱的底面是开放的还是封顶的。默认值为false,即其底面默认是封顶的
// thetaStart:第一个分段的起始角度,默认为0
// thetaLength:圆柱底面圆扇区的中心角,通常被称为θ(西塔)。默认值是2*Pi
CylinderGeometry(radiusTop:Float, radiusBottom:Float, height:Float, 
                 radialSegments:Integer, heightSegments:Integer, openEnded:Boolean, 
				 thetaStart:Float, thetaLength:Float)

圆锥 ConeGeometry

如果 radiusTop 或者 radiusBottom 设置成0则成为圆锥

# 胶囊体 CapsuleGeometry

// radius:胶囊半径。可选的; 默认值为1
// length:中间区域的长度。可选的; 默认值为1
// capSegments:构造盖子的曲线部分的个数。可选的; 默认值为4
// radialSegments:覆盖胶囊圆周的分离的面的个数。可选的; 默认值为8
CapsuleGeometry(radius:Float, length:Float, capSubdivisions:Integer, radialSegments:Integer)

# 管道体 TubeGeometry

// path:Curve - 一个由基类Curve继承而来的3D路径
// tubularSegments:Integer - 组成这一管道的分段数,默认值为64
// radius:Float - 管道的半径,默认值为1
// radialSegments:Integer - 管道横截面的分段数目,默认值为8
// closed:Boolean 管道的两端是否闭合,默认值为false
TubeGeometry(path:Curve, tubularSegments:Integer, radius:Float, radialSegments:Integer, 
             closed:Boolean)

// 三维样条曲线
const path = new THREE.CatmullRomCurve3([
    new THREE.Vector3(-50, 20, 90),
    new THREE.Vector3(-10, 40, 40),
    new THREE.Vector3(0, 0, 0),
    new THREE.Vector3(60, -60, 0),
    new THREE.Vector3(70, 0, 80)
]);

// path:路径   40:沿着轨迹细分数  2:管道半径   25:管道截面圆细分数
const geometry = new THREE.TubeGeometry(path, 40, 2, 25);

# 圆环体 TorusGeometry

// radius - 环面的半径,从环面的中心到管道横截面的中心。默认值是1
// tube:管道的半径,默认值为0.4
// radialSegments:管道横截面的分段数,默认值为12
// tubularSegments:管道的分段数,默认值为48
// arc:圆环的圆心角(单位是弧度),默认值为Math.PI * 2
TorusGeometry(radius:Float, tube:Float, radialSegments:Integer, 
              tubularSegments:Integer, arc:Float)

# 圆环扭结体 TorusKnotGeometry

// radius - 圆环的半径,默认值为1
// tube:管道的半径,默认值为0.4
// tubularSegments:管道的分段数量,默认值为64
// radialSegments:横截面分段数量,默认值为8
// p:这个值决定了几何体将绕着其旋转对称轴旋转多少次,默认值是2
// q:这个值决定了几何体将绕着其内部圆环旋转多少次,默认值是3
TorusKnotGeometry(radius:Float, tube:Float, tubularSegments:Integer, 
                  radialSegments:Integer, p:Integer, q:Integer)

# 多面体 PolyhedronGeometry

多面体在三维空间中具有一些平面的立体图形。这个类将一个顶点数组投射到一个球面上,之后将它们细分为所需的细节级别。 这个类由DodecahedronGeometry、IcosahedronGeometry、OctahedronGeometry和TetrahedronGeometry 所使用,以生成它们各自的几何结构。

// vertices:一个顶点Array(数组):[1,1,1, -1,-1,-1, ... ]
// indices:一个构成面的索引Array(数组), [0,1,2, 2,3,0, ... ]
// radius:Float - 最终形状的半径
// detail:Integer - 将对这个几何体细分多少个级别。细节越多,形状就越平滑
PolyhedronGeometry(vertices:Array, indices:Array, radius:Float, detail:Integer)
// 四面缓冲几何体 TetrahedronGeometry
TetrahedronGeometry(radius:Float, detail:Integer)

// 八面体 OctahedronGeometry
OctahedronGeometry(radius:Float, detail:Integer)

// 十二面体 DodecahedronGeometry
DodecahedronGeometry(radius:Float, detail:Integer)

// 二十面体 IcosahedronGeometry
IcosahedronGeometry(radius:Float, detail:Integer)

# 旋转成型 LatheGeometry

利用一个2D轮廓,绕着Y轴进行旋转变换生成一个3D的几何体曲面,创建具有轴对称的网格,比如花瓶


















 

// points:一个Vector2对象数组。每个点的X坐标必须大于0
// segments:要生成的车削几何体圆周分段的数量,默认值是12
// phiStart:以弧度表示的起始角度,默认值为0。
// phiLength:车削部分的弧度范围,2PI将是一个完全闭合的车削几何体,小于2PI是部分的。默认值是2PI
LatheGeometry(points:Array, segments:Integer, phiStart:Float, phiLength:Float)


// 通过二维样条曲线SplineCurve生成一个光滑的曲线旋转轮廓
const curve = new THREE.SplineCurve([
    new THREE.Vector2(50, 60),
    new THREE.Vector2(25, 0),
    new THREE.Vector2(50, -60)
]);
//曲线上获取点,作为旋转几何体的旋转轮廓
const pointsArr = curve.getPoints(50); 
console.log('旋转轮廓数据',pointsArr);
// LatheGeometry:pointsArr轮廓绕y轴旋转生成几何体曲面
const geometry = new THREE.LatheGeometry(pointsArr, 30);

# 拉伸体 ExtrudeGeometry

是基于一个基础的平面轮廓Shape进行变换,生成一个几何体
















 
 
 
 
 
 
 



// shapes:形状或者一个包含形状的数组
// options:一个包含多个参数的对象
ExtrudeGeometry(shapes:Array, options:Object)

// Shape表示一个平面多边形轮廓
const shape = new THREE.Shape([
    // 按照特定顺序,依次书写多边形顶点坐标
    new THREE.Vector2(-50, -50), //多边形起点
    new THREE.Vector2(-50, 50),
    new THREE.Vector2(50, 50),
    new THREE.Vector2(50, -50),
]);
//拉伸造型
const geometry = new THREE.ExtrudeGeometry(shape,
    {
        depth: 20, //拉伸长度
		// 圆角配置
		bevelThickness: 5, // 角尺寸:拉伸方向
		bevelSize: 5, // 角尺寸:垂直拉伸方向
		bevelSegments: 20, // 圆角:倒角细分精度,默认3
		// 直角配置
		bevelSegments: 1, // 直角
    }
);

也可以让一个平面轮廓Shape沿着曲线扫描成型




















 
 



// 扫描轮廓:Shape表示一个平面多边形轮廓
const shape = new THREE.Shape([
    // 按照特定顺序,依次书写多边形顶点坐标
    new THREE.Vector2(0,0), //多边形起点
    new THREE.Vector2(0,10),
    new THREE.Vector2(10,10),
    new THREE.Vector2(10,0),
]);
// 扫描轨迹:创建轮廓的扫描轨迹(3D样条曲线)
const curve = new THREE.CatmullRomCurve3([
    new THREE.Vector3( -10, -50, -50 ),
    new THREE.Vector3( 10, 0, 0 ),
    new THREE.Vector3( 8, 50, 50 ),
    new THREE.Vector3( -5, 0, 100)
]);
// 扫描造型:扫描默认没有倒角
const geometry = new THREE.ExtrudeGeometry(
    shape, // 扫描轮廓
    {
        extrudePath:curve,// 扫描轨迹
        steps:100// 沿着路径细分精度,越大越光滑
    }
);

# 边缘几何体 EdgesGeometry

作为一个辅助对象来查看geometry的边界线,参数是 geometry













 










// 外部gltf模型设置材质和边线
loader.load("../建筑模型.gltf", function (gltf) {
    // 递归遍历设置每个模型的材质,同时设置每个模型的边线
    gltf.scene.traverse(function (obj) {
        if (obj.isMesh) {
            // 模型材质重新设置
            obj.material = new THREE.MeshLambertMaterial({
                color: 0x004444,
                transparent: true,
                opacity: 0.5,
            });
            // 模型边线设置
            const edges = new THREE.EdgesGeometry(obj.geometry);
            const edgesMaterial = new THREE.LineBasicMaterial({
                color: 0x00ffff,
            })
            const line = new THREE.LineSegments(edges, edgesMaterial);
            obj.add(line);
        }
    });
    model.add(gltf.scene);
})

# 网格几何体 WireframeGeometry

作为一个辅助对象来对一个geometry以线框的形式进行查看

const geometry = new THREE.SphereGeometry( 100, 100, 100 );

const wireframe = new THREE.WireframeGeometry( geometry );

const line = new THREE.LineSegments( wireframe );
line.material.depthTest = false;
line.material.opacity = 0.25;
line.material.transparent = true;

scene.add( line );

# 缓冲几何体

BufferGeometry是没有任何形状的空几何体,可通过顶点数据自定义任何几何形状,是所有几何体的父类

const geometry = new THREE.BufferGeometry();

# 属性缓冲区对象 BufferAttribute

这个类用于存储与缓冲几何体相关联的属性
例如:顶点位置向量、面片索引、法向量、颜色值、UV坐标以及任何自定义 attribute

顶点位置数据  # geometry.attributes.position
顶点法向量数据  # geometry.attributes.normal
顶点UV数据  # geometry.attributes.uv
顶点颜色数据  # geometry.attributes.color

# 顶点位置 geometry.attributes.position










 
 
 
 
 
 
 
 
 
 
 
 









//通过javascript类型化数组Float32Array创建一组xyz坐标数据用来表示几何体的顶点坐标
const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    50, 0, 0, //顶点2坐标
    0, 100, 0, //顶点3坐标
    0, 0, 10, //顶点4坐标
    0, 0, 100, //顶点5坐标
    50, 0, 10, //顶点6坐标
]);

// 创建缓冲区对象
// 三个为一组,表示一个顶点的xyz坐标
const attribute = new THREE.BufferAttribute(vertices,3);

// 设置几何体attributes属性的位置属性
geometry.attributes.position = attribue;

// 也可以使用如下方法
// 把数组vertices里面的坐标数据提取出来,赋值给 geometry.attributes.position 属性
geometry.setFromPoints(vertices);

// 点渲染模式
const material = new THREE.PointsMaterial({
    color: 0xffff00,
    size: 10.0 //点对象像素尺寸
}); 

//把几何体渲染为点
const points = new THREE.Points(geometry, material); //点模型对象

# 顶点索引 geometry.index

























 
 

// 原始顶点位置坐标数据
const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    80, 0, 0, //顶点2坐标
    80, 80, 0, //顶点3坐标
    0, 0, 0, //顶点4坐标   和顶点1位置相同
    80, 80, 0, //顶点5坐标  和顶点3位置相同
    0, 80, 0, //顶点6坐标
]);

// 把三角形重复的顶点位置坐标删除
const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    80, 0, 0, //顶点2坐标
    80, 80, 0, //顶点3坐标
    0, 80, 0, //顶点4坐标
]);

// 通过javascript类型化数组Uint16Array创建顶点索引数据
const indexes = new Uint16Array([
    // 下面索引值对应顶点位置数据中的顶点坐标
    0, 1, 2, 0, 2, 3,
])

// 索引数据赋值给几何体的index属性
geometry.index = new THREE.BufferAttribute(indexes, 1); //1个为一组

# 顶点法线 geometry.attributes.normal

使用受光照影响的材质,几何体BufferGeometry需要定义顶点法线数据





 


// MeshBasicMaterial不受光照影响
// 使用受光照影响的材质,几何体Geometry需要定义顶点法线数据
const material = new THREE.MeshLambertMaterial({
    color: 0x0000ff, 
    side: THREE.DoubleSide, //两面可见
});

Three.js中法线是通过顶点定义,默认情况下,每个顶点都有一个法线数据










 
 








 
 

// 无顶点索引
const normals = new Float32Array([
    0, 0, 1, //顶点1法线( 法向量 )
    0, 0, 1, //顶点2法线
    0, 0, 1, //顶点3法线
    0, 0, 1, //顶点4法线
    0, 0, 1, //顶点5法线
    0, 0, 1, //顶点6法线
]);
// 设置几何体的顶点法线属性.attributes.normal
geometry.attributes.normal = new THREE.BufferAttribute(normals, 3); 

// 有顶点索引
const normals = new Float32Array([
    0, 0, 1, //顶点1法线( 法向量 )
    0, 0, 1, //顶点2法线
    0, 0, 1, //顶点3法线
    0, 0, 1, //顶点4法线
]);
// 设置几何体的顶点法线属性.attributes.normal
geometry.attributes.normal = new THREE.BufferAttribute(normals, 3);

# 顶点UV geometry.attributes.uv

顶点UV坐标的作用是从纹理贴图上提取像素映射到网格模型Mesh的几何体表面上

// 浏览器控制台查看常用几何体默认的UV坐标数据
const geometry = new THREE.PlaneGeometry(200, 100); //矩形平面
const geometry = new THREE.BoxGeometry(100, 100, 100); //长方体
const geometry = new THREE.SphereGeometry(100, 30, 30);//球体

console.log('uv',geometry.attributes.uv);

注意

顶点UV坐标 geometry.attributes.uv 和顶点位置坐标 geometry.attributes.position 是一一对应的

顶点UV坐标可以在0~1.0之间任意取值,纹理贴图左下角对应的UV坐标是(0,0),右上角对应的坐标(1,1)

纹理贴图UV坐标范围

const uvs = new Float32Array([
    0, 0, //图片左下角
    1, 0, //图片右下角
    1, 1, //图片右上角
    0, 1, //图片左上角
]);

// 设置几何体attributes属性的位置uv属性
geometry.attributes.uv = new THREE.BufferAttribute(uvs, 2); //2个为一组,表示一个顶点的纹理坐标

UV顶点坐标可以根据需要在0~1之间任意设置,主要看你想把图片的哪部分映射到几何体表面上

// 获取纹理贴图四分之一
const uvs = new Float32Array([
    0, 0, 
    0.5, 0, 
    0.5, 0.5, 
    0, 0.5, 
]);

通过纹理对象的偏移属性.offset可以实现UV动画效果


 

 



 





// 设置U方向阵列模式
texture.wrapS = THREE.RepeatWrapping;
// uv两个方向纹理重复数量
texture.repeat.x=50;//注意选择合适的阵列数量

// 渲染循环
function render() {
    texture.offset.x +=0.1;//设置纹理动画:偏移量根据纹理和动画需要,设置合适的值
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}
render();

# 顶点颜色 geometry.attributes.color

与几何体BufferGeometry顶点位置数据一一对应,每个点对应一个位置数据,同时对应一个颜色数据
















 
 

const geometry = new THREE.BufferGeometry(); //创建一个几何体对象
const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    50, 0, 0, //顶点2坐标
    0, 25, 0, //顶点3坐标
]);
// 顶点位置
geometry.attributes.position = new THREE.BufferAttribute(vertices, 3);

// 设置几何体attributes属性的颜色color属性
const colors = new Float32Array([
    1, 0, 0, //顶点1颜色
    0, 0, 1, //顶点2颜色
    0, 1, 0, //顶点3颜色
]);
//3个为一组,表示一个顶点的颜色数据RGB
geometry.attributes.color = new THREE.BufferAttribute(colors, 3); 

如果希望顶点颜色geometry.attributes.color起作用,需要设置材质属性vertexColors:true


 
 




const material = new THREE.MeshBasicMaterial({
    //使用顶点颜色数据,color属性可以不用设置
    vertexColors:true,//默认false,设置为true表示使用顶点颜色渲染
    size: 20.0, //点对象像素尺寸
});
const points = new THREE.Mesh(geometry, material); //点模型对象

# 旋转、缩放、平移几何体

这些方法本质上都是改变几何体的顶点数据



 






// 几何体xyz三个方向都放大2倍
geometry.scale(2, 2, 2);
// 几何体沿着x轴平移50,没有translateX,那是模型的方法
geometry.translate(50, 0, 0);
// 几何体绕着x轴旋转45度,默认绕几何体中心旋转
geometry.rotateX(Math.PI / 4);
//居中:已经偏移的几何体居中,执行.center(),可以看到几何体重新与坐标原点重合
geometry.center();

# 模型介绍

模型即物体,父类为Object3D

# 基本属性

# 三维向量 Vector3

所有Vector3类型的属性都有下列方法



 



 

 


 
 
 



 

 

const v3 = new THREE.Vector3(0,0,0);
// 修改某个分量的值
v3.set(10,0,0);
v3.x = 100;

// 设置模型在场景Scene中的位置
mesh.position.set(80,2,10);
// 设置模型xyz方向分别缩放0.5,1.5,2倍
mesh.scale.set(0.5, 1.5, 2)

// 平移方法
mesh.translateX(100);// 沿着x轴正方向平移距离100
mesh.translateY(100);// 沿着y轴正方向平移距离100
mesh.translateZ(100);// 沿着z轴正方向平移距离100

// 沿着自定义的方向移动
const axis = new THREE.Vector3(1, 1, 1);// 向量Vector3对象表示方向
axis.normalize(); // 向量归一化,x、y、z三个方向的和是1
// 沿着axis轴表示方向平移100
mesh.translateOnAxis(axis, 100);

# 欧拉角 Euler

通过指定轴顺序和其各个轴向上的旋转角度来旋转一个物体





 


 


 


 
 
 



 

// x:(可选) 用弧度表示x轴旋转量。 默认值是 0
// y:(可选) 用弧度表示y轴旋转量。 默认值是 0
// z:(可选) 用弧度表示z轴旋转量。 默认值是 0
// order:(可选) 表示旋转顺序的字符串,默认为'XYZ'(必须是大写)
Euler( x:Float, y:Float, z:Float, order:String )

// 创建一个欧拉对象,表示绕着xyz轴分别旋转45度,0度,90度
const Euler = new THREE.Euler( Math.PI/4,0, Math.PI/2);

// 绕y轴的角度设置为60度
mesh.rotation.y = Math.PI/3;

// 旋转方法
mesh.rotateX(Math.PI/4);// 绕x轴旋转π/4,默认绕几何体中心旋转
mesh.rotateY(Math.PI/4);// 绕y轴旋转π/4,默认绕几何体中心旋转
mesh.rotateZ(Math.PI/4);// 绕z轴旋转π/4,默认绕几何体中心旋转

// 沿着自定义的方向旋转
const axis = new THREE.Vector3(0,1,0);// 向量axis
mesh.rotateOnAxis(axis,Math.PI/8);// 绕axis轴旋转π/8

# 颜色对象 Color


 




 


 
 
 
 
 
 
 






// 创建一个颜色对象
const color = new THREE.Color();// 默认是纯白色0xffffff
const color = new THREE.Color(0x00ff00);
console.log('查看颜色对象结构', color);// 可以查看rgb的值

// 改变颜色的方法
color.r = 0.0;
color.b = 0.0;

color.setRGB(0,1,0);// RGB方式设置颜色
color.setHex(0x00ff00);// 十六进制方式设置颜色
color.setStyle('#00ff00');// 前端CSS颜色值设置颜色
// 都可以作为.set()的参数
color.set('rgb(0,255,0)');
color.set(0x00ff00);
color.set('#00ff00');

// 重置模型材质的颜色
material.color.set(0x00ffff);
material.color.set('#00ff00');
material.color.set('rgb(0,255,0)');

颜色渐变插值







 






 

// 通过一个百分比参数可以控制Color1和Color2两种颜色混合的百分比
lerpColors(Color1, Color2, percent)  

const c1 = new THREE.Color(0xff0000); //红色
const c2 = new THREE.Color(0x0000ff); //蓝色
const c = new THREE.Color();
c.lerpColors(c1,c2, 0.3);

// c1与c2颜色混合,混合后的rgb值,赋值给c1的.r、.g、.b属性
Color1.lerp(Color2, percent)

const c1 = new THREE.Color(0xff0000); //红色
const c2 = new THREE.Color(0x0000ff); //蓝色
c1.lerp(c2, percent);

# 常用模型

# 网格模型 Mesh

表示基于以三角形为多边形网格的物体的类。同时也作为其他类的基类,例如SkinnedMesh

// geometry(可选):BufferGeometry的实例
// material(可选):一个Material,或是一个包含有Material的数组,默认值是一个MeshBasicMaterial
Mesh( geometry:BufferGeometry, material:Material )

网格模型三角形:正反面,默认反面看不见

# 眼睛(相机)对着三角形的一个面
# 如果三个顶点的顺序是逆时针方向,该面视为正面
# 如果三个顶点的顺序是顺时针方向,该面视为反面

正反面

// 一个矩形平面,通过两个三角形拼接而成,并且保证矩形平面两个三角形的正面是一样的
const vertices = new Float32Array([
    0, 0, 0, //顶点1坐标
    80, 0, 0, //顶点2坐标
    80, 80, 0, //顶点3坐标

    0, 0, 0, //顶点4坐标   和顶点1位置相同
    80, 80, 0, //顶点5坐标  和顶点3位置相同
    0, 80, 0, //顶点6坐标
]);

网格模型:双面可见

new THREE.MeshBasicMaterial({
    side: THREE.FrontSide, //默认只有正面可见
	side: THREE.DoubleSide, //两面可见
});

# 点模型 Points

一个用于显示点的类

// geometry(可选):BufferGeometry的实例
// material(可选):一个Material,默认值是一个PointsMaterial
Points( geometry:BufferGeometry, material:Material )

# 线模型 Line

  • 环线(LineLoop):闭合线条
  • 线段(LineSegments):非连续的线条
// geometry(可选):BufferGeometry的实例
// material(可选):一个Material,默认值是一个具有随机颜色的LineBasicMaterial
Line( geometry:BufferGeometry, material:Material )

# 材质介绍

所有材质都会从父类Material继承一些属性和方法


 
 


 

 
 




 


// 透明设置
material.transparent = true;// 开启透明
material.opacity = 0.5;// 设置透明度

// 克隆 clone()
const mesh2 = mesh.clone();
// 克隆几何体和材质,重新设置mesh2的材质和几何体属性
mesh2.geometry = mesh.geometry.clone();
mesh2.material = mesh.material.clone();
// 改变mesh2颜色,不会改变mesh的颜色
mesh2.material.color.set(0xff0000);

// 复制 copy()
mesh.position.copy(mesh2.position);// 第1步位置重合
mesh.position.y += 100;// 第2步mesh在原来y的基础上增加100

# 网格材质 MeshMaterial

# 基础网格材质 MeshBasicMaterial

不会受到光照影响

// 从Material继承的任何属性都可以从此处传入
MeshBasicMaterial( parameters:Object )

# 漫反射网格材质 MeshLambertMaterial

会受到光照影响,该材质也可以称为Lambert网格材质






 


// 从Material继承的任何属性都可以从此处传入
MeshLambertMaterial( parameters:Object )

const material = new THREE.MeshLambertMaterial({
    color: 0x00ffff, 
    wireframe:true,//线条模式渲染mesh对应的三角形数据
});

# 高光网格材质 MeshPhongMaterial

一种用于具有镜面高光的光泽表面的材质






 
 


// 从Material继承的任何属性都可以从此处传入
MeshPhongMaterial( parameters:Object )

const material = new THREE.MeshPhongMaterial({
    color: 0xff0000,
    shininess: 20, //高光部分的亮度,默认30
    specular: 0x444444, //高光部分的颜色
});

# 点材质 PointsMaterial

// 从Material继承的任何属性都可以从此处传入
PointsMaterial( parameters:Object )

# 线材质 LineMaterial

# 基础线条材质 LineBasicMaterial

一种用于绘制线框样式几何体的材质

// 从Material继承的任何属性都可以从此处传入
LineBasicMaterial( parameters:Object )

// 线材质对象
const material = new THREE.LineBasicMaterial({
    color: 0xff0000 //线条颜色
}); 
// 创建线模型对象
const line = new THREE.Line(geometry, material);

# 虚线材质 LineDashedMaterial

一种用于绘制虚线样式几何体的材质

// 从Material继承的任何属性都可以从此处传入
LineDashedMaterial( parameters:Object )

# PBR材质 physically-based rendering

基于物理的光照技术,可以提供更逼真的、更接近生活中的材质效果,当然也会占用更多的电脑硬件资源

# 标准网格材质 MeshStandardMaterial

一种基于物理的标准材质





 
 
 
 


// 从Material继承的任何属性都可以从此处传入
MeshStandardMaterial( parameters:Object )

new THREE.MeshStandardMaterial({
	// 0.0到1.0之间的值可用于生锈的金属外观,默认是0.5
    metalness: 1.0,// 金属度属性
	// 0.0表示平滑的镜面反射,1.0表示完全漫反射,默认0.5
	roughness: 0.5,// 表面粗糙度
})

# 物理网格材质 MeshPhysicalMaterial

MeshStandardMaterial的扩展,提供了更高级的基于物理的渲染属性





 
 
 
 
 
 
 
 
 


// 从Material继承的任何属性都可以从此处传入
MeshPhysicalMaterial( parameters:Object )

new THREE.MeshPhysicalMaterial( {
	// 清漆层属性:可以用来模拟物体表面一层透明图层,就好比你在物体表面刷了一层透明清漆
	clearcoat: 1.0, // 范围0到1。默认0
	// 清漆层粗糙度属性:表示物体表面透明涂层.clearcoat对应的的粗糙度
	clearcoatRoughness: 0.1,// 范围是为0.0至1.0。默认值为0.0
	
	// 玻璃材质透光率
	transmission: 1.0, // 范围是从0.0到1.0。默认值为0.0
	// 折射率,不同材质的折射率,可以百度搜索
	ior:1.5,// 非金属材料的折射率从1.0到2.333。默认值为1.5
});

# 贴图介绍

# 纹理贴图 material.map

纹理贴图map和color属性值会混合。如果设置了纹理贴图map不用设置color的值,color默认白色0xffffff




 




 








const geometry = new THREE.PlaneGeometry(200, 100); 
//纹理贴图加载器TextureLoader
const texLoader = new THREE.TextureLoader();
// .load()方法异步加载图像,返回一个纹理对象Texture
const texture = texLoader.load('./earth.jpg', function () {
    renderer.render(scene, camera);
})
// 设置纹理对象的颜色空间
texture.colorSpace  = THREE.SRGBColorSpace
const material = new THREE.MeshLambertMaterial({
	// 不用设置color的值
	// color: 0x00ffff,
	
    // 设置纹理贴图:Texture对象作为材质map属性的属性值
    map: texture,// map表示材质的颜色贴图属性
});

# 颜色空间 colorSpace

纹理对象Texture颜色空间(colorSpace)属性默认值是THREE.NoColorSpace

THREE.NoColorSpace = ""
THREE.SRGBColorSpace = "srgb" # SRGB颜色空间
THREE.LinearSRGBColorSpace = "srgb-linear" # 线性颜色空间
texture.colorSpace  = THREE.SRGBColorSpace

# 阵列设置 wrapS/wrapT

纹理对象Texture的阵列设置

wrapS # 定义了纹理贴图在水平方向上排列方式,在UV映射中对应于U
wrapT # 定义了纹理贴图在垂直方向上排列方式,在UV映射中对应于V

THREE.ClampToEdgeWrapping # 默认,纹理边缘将被推到外部边缘的纹素
THREE.RepeatWrapping  # 纹理将简单地重复到无穷大
THREE.MirroredRepeatWrapping  # 纹理将重复到无穷大,在每次重复时将进行镜像
// 设置阵列模式
texture.wrapS = THREE.RepeatWrapping;
texture.wrapT = THREE.RepeatWrapping;
// uv两个方向纹理重复数量
texture.repeat.set(12,12);//注意选择合适的阵列数量

# 背景透明 transparent

把一个背景透明的.png图像作为平面矩形网格模型Mesh的颜色贴图

const material = new THREE.MeshBasicMaterial({
    map: textureLoader.load('./compass.png'),   
    // 使用背景透明的png贴图,注意允许透明   
    transparent: true, 
});

# Canvas贴图 canvasTexture

设置填充文字,作为纹理 CanvasTexture 引入到材质 MeshPhongMaterial 中



















 
 
 
 
 





function addCanvas() {
	var canvas = document.createElement("canvas");
	canvas.width = 512;
	canvas.height = 64;
	var c = canvas.getContext('2d');
	c.fillStyle = "#aaaaff";
	c.fillRect(0, 0, 512, 64);
	// 文字
	c.beginPath();
	c.translate(256, 32);
	c.fillStyle = "#FF0000"; //文本填充颜色
	c.font = "bold 28px 宋体"; //字体样式设置
	c.textBaseline = "middle"; //文本与fillText定义的纵坐标
	c.textAlign = "center"; //文本居中(以fillText定义的横坐标)
	c.fillText("左本的博客,Three.js3D文字", 0, 0);
 
	var cubeGeometry = new THREE.BoxGeometry(512, 64, 5);
	// CanvasTexture纹理
	canvasTexture = new THREE.CanvasTexture(canvas);
	canvasTexture.wrapS = THREE.RepeatWrapping;
	var material = new THREE.MeshPhongMaterial({
		map: canvasTexture, // 设置纹理贴图
	});
	var cube = new THREE.Mesh(cubeGeometry, material);
	cube.rotation.y += Math.PI; //-逆时针旋转,+顺时针
	scene.add(cube);
}

# 环境贴图 material.envMap

环境贴图就是一个模型周围环境的图像,对PBR材质渲染效果影响较大,一般用于渲染PBR材质的模型

# 对于PBR材质,即使不添加任何光源,只使用环境贴图,物体表面的颜色也能看到
# 这说明环境贴图其实相当于提供了物体周围环境发射或反射的光线

# 立方体纹理加载器 CubeTextureLoader

使用 load() 方法加载6张图片,返回一个立方体纹理对象 CubeTexture (父类是纹理对象Texture)










 
 
 
 


// 上下左右前后6张贴图构成一个立方体空间
// 'px.jpg', 'nx.jpg':x轴正方向、负方向贴图  p:正positive  n:负negative
// 'py.jpg', 'ny.jpg':y轴贴图
// 'pz.jpg', 'nz.jpg':z轴贴图
const textureCube = new THREE.CubeTextureLoader().setPath('./环境贴图/')
                    .load(['px.jpg', 'nx.jpg', 'py.jpg', 'ny.jpg', 'pz.jpg', 'nz.jpg']);

// 通过PBR材质的贴图属性可以实现模型表面反射周围景物
new THREE.MeshStandardMaterial({
    metalness: 1.0, // 金属度属性
    roughness: 0.5, // 表面粗糙度
	envMapIntensity: 1.0, // 环境贴图对模型表面的影响能力
    envMap: textureCube, // 设置pbr材质环境贴图
})

如果希望环境贴图能影响场景中所有的Mesh,可以通过Scene的场景环境属性设置

scene.environment = textureCube;

纹理和渲染器颜色空间一致

//如果renderer.outputEncoding=THREE.sRGBEncoding;环境贴图需要保持一致
cubeTexture.encoding = THREE.SRGBColorSpace;

# hdr格式的环境贴图 RGBELoader





 
 
 


import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js';

const rgbeLoader = new RGBELoader();
rgbeLoader.load('./envMap.hdr', function (envMap) {
    scene.environment = envMap;
    // hdr作为环境贴图生效,设置.mapping为EquirectangularReflectionMapping
    envMap.mapping = THREE.EquirectangularReflectionMapping;
})

# GUI可视化调试PBR材质属性

import { GUI } from 'three/examples/jsm/libs/lil-gui.module.min.js';
const gui = new GUI();

const matFolder = gui.addFolder('材质属性');
matFolder.add(mesh.material,'metalness',0,1);
matFolder.add(mesh.material,'roughness',0,1);
matFolder.add(mesh.material,'clearcoat',0,1);
matFolder.add(mesh.material,'clearcoatRoughness',0,1);
matFolder.add(mesh.material,'envMapIntensity',0,10);
matFolder.add(mesh.material,'transmission',0,1);
matFolder.add(mesh.material,'ior',0,3);

# 车外壳材质设置

const mesh = gltf.scene.getObjectByName('外壳01');
// 创建一个MeshPhysicalMaterial材质
mesh.material = new THREE.MeshPhysicalMaterial({
        color: mesh.material.color, //默认颜色
		// PBR材质设置
        metalness: 0.9,// 车外壳金属度
        roughness: 0.5,// 车外壳粗糙度
		// 车外壳油漆效果
		clearcoat: 1.0,// 物体表面清漆层或者说透明涂层的厚度
		clearcoatRoughness: 0.1,// 透明涂层表面的粗糙度
			
        envMap: textureCube, // 环境贴图
        envMapIntensity: 2.5, // 环境贴图对Mesh表面影响程度
})

# 玻璃材质设置

const mesh = gltf.scene.getObjectByName('玻璃01')
mesh.material = new THREE.MeshPhysicalMaterial({
    metalness: 0.0,//玻璃非金属 
    roughness: 0.0,//玻璃表面光滑
	
	transmission: 1.0, //玻璃材质透光率
	ior:1.5,//折射率
		
    envMap:textureCube,//环境贴图
    envMapIntensity: 1.0, //环境贴图对Mesh表面影响程度
})
  • 普通贴图
    • material.map,替代颜色
  • 法线贴图
    • material.normalMap,让细节程度较低的表面生成高细节程度的精确光照方向和反射效果
  • 环境光遮蔽贴图
    • material.aoMap,用来描绘物体和物体相交或靠近的时候遮挡周围漫反射光线的效果
  • 环境反射贴图
    • material.envMap,用于模拟材质反射周围环境的效果

# 光源介绍

# 环境光 AmbientLight

环境光没有特定方向,不会产生阴影,只是整体改变场景的光照明暗

// 参数1:一个表示颜色的 Color 的实例、字符串或数字,默认为一个白色
// 参数2:光照强度。默认值为 1
const ambient = new THREE.AmbientLight(0xffffff, 5);
scene.add(ambient);

# 点光源 PointLight

可以类比为一个发光点,就像生活中一个灯泡以灯泡为中心向四周发射光线

// color(可选)一个表示颜色的 Color 的实例、字符串或数字,默认值为 0xffffff
// intensity(可选)光照强度。默认值为 1
// distance(可选)光源照射的最大距离。默认值为 0(无限远)
// decay(可选)沿着光照距离的衰退量。默认值为 2
PointLight( color:Color, intensity:Float, distance:Number, decay:Float )

// 也可以通过光照属性设置
pointLight.intensity = 1.0;

//点光源位置
pointLight.position.set(400, 0, 0);//点光源放在x轴上
scene.add(pointLight); //点光源添加到场景中

点光源辅助观察 PointLightHelper

// 参数1:要模拟的光源
// 参数2:点光源球形辅助对象的尺寸。默认为 1
const pointLightHelper = new THREE.PointLightHelper(pointLight, 10);
scene.add(pointLightHelper);

# 平行光 DirectionalLight

这种光的表现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光的效果

// 参数1:一个表示颜色的 Color 的实例、字符串或数字,默认为一个白色
// 参数2:光照强度。默认值为 1
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
directionalLight.position.set(80, 100, 50);
// 平行光指向对象网格模型mesh,可以不设置,默认的位置是0,0,0
directionalLight.target = mesh;
scene.add(directionalLight);

平行光辅助观察 DirectionalLightHelper

// 参数1:要模拟的光源
// 参数2:平面的尺寸。默认为 1
// 参数3:如果没有设置颜色将使用光源的颜色
const dirLightHelper = new THREE.DirectionalLightHelper(directionalLight, 5, 0xff0000);
scene.add(dirLightHelper);

平行光阴影 DirectionalLightShadow

// 1、模型阴影投射.castShadow
mesh.castShadow = true;

// 2、光源阴影投射属性.castShadow
directionalLight.castShadow = true;

// 3、模型阴影接收属性.receiveShadow
planeMesh.receiveShadow = true;

// 4、允许渲染器渲染阴影 .shadowMap
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;

// 5、可视化.shadow.camera
const cameraHelper = new THREE.CameraHelper(directionalLight.shadow.camera);
scene.add(cameraHelper);

// 6、设置阴影渲染范围.shadow.camera
// 平行光阴影相机属性.shadow.camera的属性值是一个正投影相机对象OrthographicCamera,属性相同
directionalLight.shadow.camera.left = -50;
directionalLight.shadow.camera.right = 50;
directionalLight.shadow.camera.top = 200;
directionalLight.shadow.camera.bottom = -100;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 600;

// .shadow.mapSize:阴影贴图尺寸(提升边缘渲染效果),默认512x512
directionalLight.shadow.mapSize.set(128,128)
// .shadow.radius:弱化模糊阴影边缘
directionalLight.shadow.radius = 3;

# 聚光灯光源 SpotLight

光线从一个点沿一个方向射出,随着光线照射的变远,光线圆锥体的尺寸也逐渐增大

// color(可选)一个表示颜色的 Color 的实例、字符串或数字,默认为白色(0xffffff)
// intensity(可选)光照强度。默认值为 1
// distance 光源照射的最大距离。默认值为 0(无限远)
// angle 光线照射范围的角度。默认值为 Math.PI/3
// penumbra 聚光锥的半影衰减百分比。默认值为 0
// decay 沿着光照距离的衰减量。默认值为 2
SpotLight(color:Color,intensity:Float,distance:Float,angle:Radians,penumbra:Float,decay:Float)

聚光源目标对象.target和光源的位置.position共同确定聚光源照射方向

// 设置聚光光源位置
spotLight.position.set(0, 50, 0);

// 目标对象是一个模型对象Object3D,默认在坐标原点
spotLight.target.position.set(50,0,0);
// 目标对象添加到场景中.target.position才会起作用
scene.add(spotLight.target);

聚光灯辅助观察 SpotLightHelper

const spotLight = new THREE.SpotLight( 0xffffff )
spotLight.position.set( 10, 10, 10 )
scene.add( spotLight )

// light:被模拟的光源
// color:(可选) 如果没有赋值辅助对象将使用光源的颜色
const spotLightHelper = new THREE.SpotLightHelper( spotLight )
scene.add( spotLightHelper )

# 半球光 HemisphereLight

光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色,半球光不能投射阴影

// skyColor(可选)一个表示颜色的 Color 的实例、字符串或数字,默认为白色(0xffffff)
// groundColor(可选)一个表示颜色的 Color 的实例、字符串或数字,默认为白色(0xffffff)
// intensity(可选)光照强度。默认值为 1
const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 )
scene.add(light)

半球光辅助观察 HemisphereLightHelper

const light = new THREE.HemisphereLight( 0xffffbb, 0x080820, 1 )

// light:被模拟的光源
// size:用于模拟光源的网格尺寸
// color:(可选的) 如果没有赋值辅助对象将使用光源的颜色
const helper = new THREE.HemisphereLightHelper( light, 5 )
scene.add( helper )

# 平面光光源 RectAreaLight

平面光光源从一个矩形平面上均匀地发射光线。这种光源可以用来模拟像明亮的窗户或者条状灯光光源

  • 不支持阴影,其它光源都有阴影
  • 只支持 MeshStandardMaterial 和 MeshPhysicalMaterial 两种材质
  • 你必须在你的场景中加入 RectAreaLightUniformsLib,并调用 init()
// color(可选)一个表示颜色的 Color 的实例、字符串或数字,默认为白色(0xffffff)
// intensity(可选)光源强度/亮度 。默认值为 1
// width(可选)光源宽度。默认值为 10
// height(可选)光源高度。默认值为 10
RectAreaLight( color:Color, intensity:Float, width:Float, height:Float )

平面光辅助观察 RectAreaLightHelper

import { RectAreaLightHelper } from 'three/addons/helpers/RectAreaLightHelper.js'
const light = new THREE.RectAreaLight( 0xffffbb, 1.0, 5, 5 )

// light:被模拟的光源
// color:(可选) 如果没有赋值辅助对象将使用光源的颜色
const helper = new RectAreaLightHelper( light )
scene.add( helper )

# 层级模型

场景 scene 是 group 的父对象,group 是 mesh1、mesh2 的父对象。这样就构成了一个三层的层级结构









 

 







 


//创建两个网格模型mesh1、mesh2
const geometry = new THREE.BoxGeometry(20, 20, 20);
const material = new THREE.MeshLambertMaterial({color: 0x00ffff});
const mesh1 = new THREE.Mesh(geometry, material);
const mesh2 = new THREE.Mesh(geometry, material);
mesh2.translateX(25);

// 创建层级模型
const group = new THREE.Group();
// 把mesh1型插入到组group中,mesh1作为group的子对象
group.add(mesh1);
// 把mesh2型插入到组group中,mesh2作为group的子对象
group.add(mesh2);

// 把group插入到场景中作为场景子对象
scene.add(group);

// 查看子对象.children
console.log('查看Scene的子对象',scene.children);
console.log('查看group的子对象',group.children);

如果父对象group进行旋转、缩放、平移变换,子对象同样跟着变换

// 沿着Y轴平移mesh1和mesh2的父对象,mesh1和mesh2跟着平移
group.translateY(100);

// 父对象缩放,子对象跟着缩放
group.scale.set(4,4,4);

// 父对象旋转,子对象跟着旋转
group.rotateY(Math.PI/6);

场景对象Scene、组对象Group的.add()方法都是继承自它们共同的基类(父类)Object3D


 





 


 

// 可以单独插入一个对象,也可以同时插入多个子对象
group.add(mesh1,mesh2);

// Object3D作为Group来使用
const mesh1 = new THREE.Mesh(geometry, material);
const mesh2 = new THREE.Mesh(geometry, material);
const obj = new THREE.Object3D();// 作为mesh1和mesh2的父对象
obj.add(mesh1,mesh2);

// mesh也可以添加子对象,mesh基类也是Object3D
mesh1.add(mesh2);

# 移除对象 .remove()

// 移除父对象group的子对象网格模型mesh1
group.remove(mesh1);
// 一次移除多个子对象
group.remove(mesh1,mesh2);

# 模型隐藏或显示 .visible




 


group.visible =false;// 隐藏一个包含多个模型的组对象group

// 隐藏网格模型mesh,visible的默认值是true
// 注意如果mesh2和mesh的.material属性指向同一个材质,mesh2也会跟着mesh隐藏
mesh.material.visible =false;

# 模型命名 .name

const group1 = new THREE.Group();
group1.name='东区房子';
const mesh1 = new THREE.Mesh(geometry, material);
mesh1.name='一号楼';
group1.add(mesh1);

const group2 = new THREE.Group();
group2.name='西区房子';
const mesh2 = new THREE.Mesh(geometry, material);
mesh2.name='二号楼';
group2.add(mesh2);

const model = new THREE.Group();
model.name='小区房子';
model.add(group1, group2);

# 递归遍历方法 .traverse()

// 递归遍历model包含所有的模型节点
model.traverse(function(obj) {
    console.log('所有模型节点的名称',obj.name);
    // obj.isMesh:if判断模型对象obj是不是网格模型'Mesh'
    if (obj.isMesh) {//判断条件也可以是obj.type === 'Mesh'
        obj.material.color.set(0xffff00);
    }
});

# 查找某个具体的模型 .getObjectByName()

// 返回名.name为"4号楼"对应的对象
const nameNode = scene.getObjectByName ("4号楼");
nameNode.material.color.set(0xff0000);

# 本地坐标和世界坐标

  • 任何一个模型的本地坐标(局部坐标)就是模型的.position属性
  • 任何一个模型的世界坐标,就是模型自身.position和所有父对象.position累加的坐标

获取世界坐标 .getWorldPosition()

// 声明一个三维向量用来表示某个坐标
const worldPosition = new THREE.Vector3();
// 获取mesh的世界坐标
mesh.getWorldPosition(worldPosition);
console.log('世界坐标',worldPosition);
console.log('本地坐标',mesh.position);

给子对象添加一个局部坐标系

//可视化mesh的局部坐标系
const meshAxesHelper = new THREE.AxesHelper(50);
mesh.add(meshAxesHelper);

# GUI的使用

轻量级的图形用户界面库,可以很容易地创建出能够改变代码变量的界面组件

// 引入dat.gui.js的一个类GUI
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
//创建GUI对象
const gui = new GUI();

# add方法

// 通过GUI改变环境光强度属性.intensity
gui.add(ambient, 'intensity', 0, 2.0);

// 通过GUI改变改变物体位置
gui.add(mesh.position, 'x', 0, 180);
gui.add(mesh.position, 'y', 0, 180);
gui.add(mesh.position, 'z', 0, 180);


// 下拉菜单
const obj = {
    scale: 0,
};
// 数组(下拉菜单)
gui.add(obj, 'scale', [-100, 0, 100]).name('y坐标').onChange(function (value) {
    mesh.position.y = value;
});
// 对象(下拉菜单)
gui.add(obj, 'scale', {
    left: -100,
    center: 0,
    right: 100
    // 左: -100,//可以用中文
    // 中: 0,
    // 右: 100
}).name('位置选择').onChange(function (value) {
    mesh.position.x = value;
});


// 单选框
const obj = {
    bool: false,
};
// 改变的obj属性数据类型是布尔值,交互界面是单选框
gui.add(obj, 'bool').name('是否旋转');

# addColor方法

生成颜色值改变的交互界面

const obj = {
    color:0x00ffff,
};
// .addColor()生成颜色值改变的交互界面
gui.addColor(obj, 'color').onChange(function(value){
    mesh.material.color.set(value);
});

# name方法

改变gui生成交互界面显示的内容

gui.add(ambient, 'intensity', 0, 2.0).name('环境光强度');
gui.add(directionalLight, 'intensity', 0, 2.0).name('平行光强度');

# step方法

可以设置交互界面每次改变属性值间隔是多少

gui.add(ambient, 'intensity', 0, 2.0).name('环境光强度').step(0.1);

# onChange方法

const obj = {
    x: 30,
};
// 当obj的x属性变化的时候,就把此时obj.x的值value赋值给mesh的x坐标
gui.add(obj, 'x', 0, 180).onChange(function(value){
    mesh.position.x = value;
	// 你可以写任何你想跟着obj.x同步变化的代码
	// 比如mesh.position.y = value;
});

# addFolder方法

可以创建一个子菜单,进行分组

// 环境光子菜单
const ambientFolder = gui.addFolder('环境光');
dirFolder.open();//打开菜单
ambientFolder.add(ambient, 'intensity',0,2);

// 平行光子菜单
const dirFolder = gui.addFolder('平行光');
dirFolder.close();//关闭菜单
dirFolder.add(directionalLight, 'intensity',0,2);

// 子菜单嵌套子菜单
const dirFolder2 = dirFolder.addFolder('位置');//子菜单的子菜单
dirFolder2.close();//关闭菜单
dirFolder2.add(directionalLight.position, 'x',-400,400);
dirFolder2.add(directionalLight.position, 'y',-400,400);
dirFolder2.add(directionalLight.position, 'z',-400,400);

# 三维包围盒

包围盒Box3表示三维长方体所包围的区域,参数min和max属性值都是三维向量Vector3

# 包围盒需要通过xyz坐标来表示,X范围[Xmin,Xmax],Y范围[Ymin,Ymax],Z范围[Zmin,Zmax]
min属性值是Vector3(Xmin, Ymin, Zmin)
max属性值是Vector3(Xmax, Ymax, Zmax)
const box = new THREE.Box3()
box.min = new THREE.Vector3(-10, -10, 0);
box.max = new THREE.Vector3(10, 10, 10);

# 常用方法

expandByObject():计算模型最小包围盒

// 计算模型最小包围盒 expandByObject()
const box3 = new THREE.Box3();
// 模型对象,比如mesh或group
box3.expandByObject(mesh); 
console.log('查看包围盒',box3);
// 浏览器控制台你可以通过.min和.max属性查看模型的包围盒信息

getSize():返回包围盒具体的长宽高尺寸

const scale = new THREE.Vector3()
// 获得包围盒长宽高尺寸,结果保存在参数三维向量对象scale中
box3.getSize(scale)
console.log('模型包围盒尺寸', scale);

getCenter():返回包围盒几何中心

const center = new THREE.Vector3()
box3.getCenter(center)
console.log('模型中心坐标', center);

# 辅助查看对象 Box3Helper

三维包围盒 Box3 的辅助查看对象

// 参数1:包围盒,参数2:(可选的) 线框盒子的颜色,默认为 0xffff00
const helper = new THREE.Box3Helper( box, 0xffff00 );
scene.add( helper );

# 加载文件

常见3D模型文件格式:

gltf  # JSON格式,已成为Web端标准。可以包含所有的三维模型相关信息的数据
      # 比如模型层级关系、PBR材质、纹理贴图、骨骼动画、变形动画
	  # 有些glTF文件会关联一个或多个.bin文件,.bin文件以二进制形式存储了模型的顶点数据等信息
	  
glb  # 是gltf的二进制文件,可以把.gltf模型和贴图信息全部合成得到一个glb文件中
     # glb文件相对gltf文件体积更小,网络传输自然更快
	 
fbx  # Autodesk FBX,闭源格式。支持3D模型、场景层次、材质照明、动画、骨骼、蒙皮、及混合形状
obj  # 适用于简单的静态模型。支持3D模型、场景层次、材质照明、动画、骨骼、蒙皮、及混合形状

# 加载glb、gltf模型

  • 单独.gltf文件,单独.glb文件,.gltf + .bin + 贴图文件,这三种不同形式的,都使用GLTFLoader加载
  • .gltf + .bin + 贴图文件,这种以单独文件形式存在的,注意不要随意改变子文件相对父文件gltf的目录















 
 
 
 
 
 
 
 
 
 
 













import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';

loadGlbModel() {
  const loader = new GLTFLoader()
  loader.load(`${this.publicPath}model/12OJJ6MOWT722N61Z5N92KA9C.glb`, gltf => {
    gltf.scene.scale.set(100,100,100)
    gltf.scene.position.set(0,0,0)
	
    let axis = new THREE.Vector3(0,1,0);//向量axis
    gltf.scene.rotateOnAxis(axis,Math.PI/2);
    //绕axis轴逆旋转π/16
    gltf.scene.rotateOnAxis(axis,Math.PI/-20);
    gltf.scene.rotateOnAxis(axis,Math.PI/50);
	
	
	// .load()方法加载图像,返回一个纹理对象Texture
	const texLoader = new THREE.TextureLoader()
	const texture = texLoader.load('earth.jpg', function () {
	    renderer.render(scene, camera)
	})
	// 纹理贴图的颜色空间
	texture.colorSpace = THREE.SRGBColorSpace
	// 纹理对象Texture翻转属性.flipY默认值是true
	texture.flipY = false;
	// 网格模型Mesh更换颜色贴图
	gltf.scene.getObjectByName('Mesh077').material.map = texture
	
	
	// 返回的场景对象gltf.scene插入到threejs场景中
    scene.add(gltf.scene)
	// 需要渲染一下场景
	renderer.render(scene, camera);
  }, (xhr) => {
      console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
  }, (error) => {
      console.error(error)
  })
}

注意

给gltf模型更换贴图时,需要给更换的Mesh模型添加UV贴图,即使是空的

模型更换贴图

# 加载FBX模型















 
 



import { FBXLoader } from 'three/examples/jsm/loaders/FBXLoader.js';

loadFbxModel() {
  const loader = new FBXLoader();
  loader.load(`${this.publicPath}model/glbxz.com6031.FBX`, object => {
    // 递归遍历批量修改FBX所有Mesh的信息
    object.traverse( child => {
      if ( child.isMesh ){
        child.castShadow = true;
        child.receiveShadow = true;
      }
    });
	// 插入到threejs场景中
    this.scene.add(object);
	// 需要渲染一下场景
	renderer.render(scene, camera);
  })
}

# 加载draco压缩后的模型

  • 通过Draco进行压缩
#全局安装
npm install -g gltf-pipeline

#压缩glb文件 -b表示输出glb格式 -f表示输出gltf格式  -d表示压缩
gltf-pipeline -i model.glb -b -d

#压缩glb文件并将纹理图片分离出来
gltf-pipeline -i model.glb -b -d -t

#更多参数查阅
gltf-pipeline -h
  • 在顶部引入'DRACOLoader'
import * as THREE from '../build/three.module.js'; 
import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
import { DRACOLoader } from './jsm/loaders/DRACOLoader.js';
  • 在threejs中进行加载,在draco文件中找到draco_decoder.js
// 创建加载器
const gltfLoader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
// 在draco文件中找到draco_decoder.js这个文件
//如果是vue直接放在项目的public下的'./gltfdraco/'目录即可
//这个路径主要是放draco的一些js文件的
dracoLoader.setDecoderPath('./gltfdraco/'); //这个路径是放draco_decoder.js这个文件的
dracoLoader.setDecoderConfig({ type: 'js' });
dracoLoader.preload();
gltfLoader.setDRACOLoader(dracoLoader);
// 然后直接加载模型即可
gltfLoader.load('./model/zhuji08-draco.glb',function(gltf){
	scene.add(group)
})

# 加载json格式的模型

loadJsonModel() {
  //设置相机位置
  this.camera.position.z = 130
  this.camera.position.y = 80
  const loader = new THREE.ObjectLoader()
  loader.load(`${this.publicPath}model/xxxx.json`, json => {
    //处理加载模型为黑色问题
    json.traverse(child => {
      if (child.isMesh) {
        child.material.emissive = child.material.color
        child.material.emissiveMap = child.material.map
      }
    })
    this.scene.add(group)
  }, xhr => {
    // called while loading is progressing
    console.log(`${( xhr.loaded / xhr.total * 100 )}% loaded`);
  }, error => {
    // called when loading has errors
    console.error('An error happened', error);
  })
}

# 加载字体

字体转JSON文件 (opens new window) 可以使用 FontLoader 加载字体,并将字体对象赋给 TextGeometry 的font属性

import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js';
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js';

function add3DFont() {
	new FontLoader().load('font/FZYaoTi_Regular.json', function(font) {
		//加入立体文字
		var text = new TextGeometry("左本的博客,Three.js3D文字", {
			// 设定文字字体
			font: font,
			//尺寸
			size: 24,
			//厚度
			height: 5
		});
		text.computeBoundingBox();
		// 设置偏移
		text.translate(-220, 0, 0);
		//3D文字材质
		var m = new THREE.MeshStandardMaterial({
			color: "#FF0000"
		});
		fontMesh = new THREE.Mesh(text, m)
		fontMesh.position.y = 100;
		scene.add(fontMesh);
	});
}

# OrbitControls辅助设置相机参数

实际开发的时候,可以通过OrbitControls旋转缩放预览3D模型,辅助你选择合适的相机参数

function render() {
  requestAnimationFrame(render);
  // 浏览器控制台查看相机位置变化
  console.log('camera.position',camera.position);
  // 浏览器控制台查看controls.target变化,辅助设置lookAt参数
  console.log('controls.target',controls.target);
}
render();

# 外部模型材质是否共享的问题

由于楼房的Mesh共享了1号楼Mesh的材质,当你通过mesh1.material改变mesh1材质,本质上是改变所有楼Mesh的材质

const mesh1 = gltf.scene.getObjectByName("1号楼");
//1. 改变1号楼Mesh材质颜色
mesh1.material.color.set(0xff0000);

如果单独改变一个模型的材质,比如颜色,下面两个方案,可以任选其一:

  • 三维建模软件中设置,需要代码改变材质的Mesh不要共享材质,要独享材质
  • 代码批量更改:克隆材质对象,重新赋值给mesh的材质属性
//用代码方式解决mesh共享材质问题
gltf.scene.getObjectByName("小区房子").traverse(function (obj) {
    if (obj.isMesh) {
        // .material.clone()返回一个新材质对象,和原来一样,重新赋值给.material属性
        obj.material = obj.material.clone();
    }
});
mesh1.material.color.set(0xffff00);
mesh2.material.color.set(0x00ff00);

# BSP挖洞

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title></title>
    <style>
        body { margin: 0; }
        canvas { width: 100%; height: 100%; }
    </style>
</head>
<body>
    <script src="js/three.js"></script>
    <script src="js/controls/OrbitControls.js"></script>
    <script src="js/threeBSP.js"></script>
    <script>
        var scene;
		var renderer;
		var camera;
		var w = window.innerWidth;
		var h = window.innerHeight;
		var h = 
        function initScene(){
            scene = new THREE.Scene();
        }
 
        
        function initCamera(){
            camera = new THREE.PerspectiveCamera( 60, w /h, 0.1, 1000);
            camera.position.z = 40;
            camera.position.y = 40;
            camera.position.x = 20;
            camera.lookAt({x:0,y:0,z:1});
            controls = new THREE.OrbitControls( camera );
        }
 
        
        function initRender(){
            renderer = new THREE.WebGLRenderer({antialias: true});
            renderer.setSize( w, h);
            document.body.appendChild( renderer.domElement );
            renderer.setClearColor(0xFFFFFF, 1.0);
        }
 
 
        function initObject(){
 
            // 墙面3
            var cubeGeometry = new THREE.BoxGeometry(1, 10, 30);
            var cube = new THREE.Mesh( cubeGeometry ); // 设置墙面位置
 
 
            // 窗户
            var door = new THREE.BoxGeometry(1, 8, 15);
            var doorMesh = new THREE.Mesh( door);
            doorMesh.position.z = 5
 
            var cubeBSP = new ThreeBSP(cube);
            var doorBSP = new ThreeBSP(doorMesh);
 
            resultBSP = cubeBSP.subtract(doorBSP); // 墙体挖窗户
            result = resultBSP.toMesh();
            
			
            var cubeGeometry = result.geometry
			
            var cubeMaterial = new THREE.MeshBasicMaterial({
            	map:THREE.ImageUtils.loadTexture('module/1.jpg')
            })
			
            qiangTiMesh = new THREE.Mesh(cubeGeometry,cubeMaterial);
            scene.add(qiangTiMesh);
        }
 
        function render() {
            requestAnimationFrame( render );
            renderer.render( scene, camera );
        }
        init();
        render();
 
        function init(){
            initRender();
            initScene();
            initCamera();
            initObject();
        }
    </script>
</body>
</html>

# 案例使用

# 山脉地形高度可视化

// 1、山脉几何体y坐标范围
loader.load("../地形.glb", function (gltf) { 
    model.add(gltf.scene);
    const mesh = gltf.scene.children[0];
    const pos = mesh.geometry.attributes.position;
    const count = pos.count;

    // 1. 计算模型y坐标高度差
    const yArr = [];//顶点所有y坐标,也就是地形高度
    for (let i = 0; i < count; i++) {
        yArr.push(pos.getY(i));//获取顶点y坐标,也就是地形高度
    }
    yArr.sort();//数组元素排序,从小到大
    const miny = yArr[0];//y最小值
    const maxy = yArr[yArr.length - 1];//y最大值
    const height = maxy - miny; //山脉整体高度 
})

// 2、计算每个顶点的颜色值
const colorsArr = [];
const c1 = new THREE.Color(0x0000ff);//山谷颜色
const c2 = new THREE.Color(0xff0000);//山顶颜色
for (let i = 0; i < count; i++) {
    //当前高度和整体高度比值
    const percent = (pos.getY(i) - miny) / height;
    const c = c1.clone().lerp(c2, percent);//颜色插值计算
    colorsArr.push(c.r, c.g, c.b); 
}
const colors = new Float32Array(colorsArr);
// 设置几何体attributes属性的颜色color属性
mesh.geometry.attributes.color = new THREE.BufferAttribute(colors, 3);

// 3. 设置材质,使用顶点颜色渲染
mesh.material = new THREE.MeshLambertMaterial({
    vertexColors:true,
});

# 全景工具

使用3D引擎先搭一个基本的3D场景

var scene, camera, renderer;

function initThree(){
	var w = document.body.clientWidth;
	var h = document.body.clientHeight;
    //场景
    scene = new THREE.Scene();
    //镜头
    camera = new THREE.PerspectiveCamera(90, w / h, 0.1, 100);
    camera.position.set(0, 0, 0.01);
    //渲染器
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(w, h);
    document.getElementById("container").appendChild(renderer.domElement);
    //镜头控制器
    var controls = new THREE.OrbitControls(camera, renderer.domElement);
    
    //一会儿在这里添加3D物体

    loop();
}

//帧同步重绘
function loop() {
    requestAnimationFrame(loop);
    renderer.render(scene, camera);
}

window.onload = initThree;

现在我们能看到一个黑乎乎的世界,因为现在scene里什么都没有,接着我们要把三维物体放进去了,使用3D引擎的实现方式无非都是以下几种

# 使用立方体(box)实现

这种方式最容易理解,我们在一个房间里,看向天花板,地面,正面,左右两面,背面共计六面。我们把所有六个视角拍成照片就得到六张图

使用立方体(box)实现

现在我们直接使用立方体(box)搭出这样一个房间

var materials = [];
//根据左右上下前后的顺序构建六个面的材质集
var texture_left = new THREE.TextureLoader().load( './images/scene_left.jpeg' );
materials.push( new THREE.MeshBasicMaterial( { map: texture_left} ) );

var texture_right = new THREE.TextureLoader().load( './images/scene_right.jpeg' );
materials.push( new THREE.MeshBasicMaterial( { map: texture_right} ) );

var texture_top = new THREE.TextureLoader().load( './images/scene_top.jpeg' );
materials.push( new THREE.MeshBasicMaterial( { map: texture_top} ) );

var texture_bottom = new THREE.TextureLoader().load( './images/scene_bottom.jpeg' );
materials.push( new THREE.MeshBasicMaterial( { map: texture_bottom} ) );

var texture_front = new THREE.TextureLoader().load( './images/scene_front.jpeg' );
materials.push( new THREE.MeshBasicMaterial( { map: texture_front} ) );

var texture_back = new THREE.TextureLoader().load( './images/scene_back.jpeg' );
materials.push( new THREE.MeshBasicMaterial( { map: texture_back} ) );

var box = new THREE.Mesh( new THREE.BoxGeometry( 1, 1, 1 ), materials );
scene.add(box);

现在我们把镜头camera(也就是人的视角),放到box内,并且让所有贴图向内翻转后,VR全景就实现了

box.geometry.scale( 1, 1, -1 );

threejs官方立方体全景示例 (opens new window)

# 使用球体(sphere)实现

我们将房间360度球形范围内所有的光捕捉到一个图片上,再将这张图片展开为矩形,就能得到这样一张全景图片

使用球体(sphere)实现

//节点数量越大,需要计算的三角形就越多,影响性能
var sphereGeometry=new THREE.SphereGeometry(/*半径*/1,/*垂直节点数量*/50,/*水平节点数量*/50);

var sphere = new THREE.Mesh(sphereGeometry);
//用线框模式大家可以看得清楚是个球体而不是圆形
sphere.material.wireframe  = true;
scene.add(sphere);

现在我们把这个全景图片贴到这个球体上

var texture = new THREE.TextureLoader().load('./images/scene.jpeg');
var sphereMaterial = new THREE.MeshBasicMaterial({map: texture});

var sphere = new THREE.Mesh(sphereGeometry,sphereMaterial);
// sphere.material.wireframe  = true;

把镜头camera(也就是人的视角),放到球体内,并且让所有贴图向内翻转后,VR全景就实现了

var sphereGeometry = new THREE.SphereGeometry(/*半径*/1, 50, 50);
sphereGeometry.scale(1, 1, -1);

threejs官方球体全景示例 (opens new window)

# 添加信息点

在VR全景中,我们需要放置一些信息点,用户点击之后做一些动作

//建立点的数组
var hotPoints=[
    {
        position:{
            x:0,
            y:0,
            z:-0.2
        },
        detail:{
            "title":"信息点1"
        }
    },
    {
        position:{
            x:-0.2,
            y:-0.05,
            z:0.2
        },
        detail:{
            "title":"信息点2"
        }
    }
];


var pointTexture = new THREE.TextureLoader().load('images/hot.png');
var material = new THREE.SpriteMaterial( { map: pointTexture} );

//遍历这个数组,并将信息点的指示图添加到3D场景中
for(var i=0;i<hotPoints.length;i++){
    var sprite = new THREE.Sprite( material );
    sprite.scale.set( 0.1, 0.1, 0.1 );
    sprite.position.set( hotPoints[i].position.x
	                   , hotPoints[i].position.y
					   , hotPoints[i].position.z );

   scene.add( sprite );
}

添加点击事件,首先将全部的sprite放到一个数组里

sprite.detail = hotPoints[i].detail;
poiObjects.push(sprite);

然后我们通过射线检测(raycast),就像是镜头中心向鼠标所点击的方向发射出一颗子弹,去检查这个子弹最终会打中哪些物体

document.querySelector("#container").addEventListener("click",function(event){
    event.preventDefault();

    var raycaster = new THREE.Raycaster();
    var mouse = new THREE.Vector2();

    mouse.x = ( event.clientX / document.body.clientWidth ) * 2 - 1;
    mouse.y = - ( event.clientY / document.body.clientHeight ) * 2 + 1;

    raycaster.setFromCamera( mouse, camera );

    var intersects = raycaster.intersectObjects( poiObjects );
    if(intersects.length>0){
        alert("点击了热点"+intersects[0].object.detail.title);
    }
});

合成后的全景图工具 (opens new window)

# 元宇宙交互

效果预览 (opens new window)

初始化项目

//首先我们使用 vite 创建 vanilla-ts 项目,并且安装 Three.js。
pnpm create vite three-demo-4 --template vanilla-ts
cd three-demo-4
pnpm i
pnpm install three
pnpm i --save-dev @types/three

//使用 pnpm run dev 启动项目,打开 http://localhost:5173/,可以看到 vite 初始化的页面
我们直接把 main.ts 和 style.css 里面原来的代码删掉,在里面写我们的代码

创建场景、相机和渲染器

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.01, 50);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.shadowMap.enabled = true;
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
camera.position.set(0, 3, 25);

添加背景色及灯光

scene.background = new THREE.Color(0.2, 0.2, 0.2);

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

const directionLight = new THREE.DirectionalLight(0xffffff, 0.2);
scene.add(directionLight);

directionLight.lookAt(new THREE.Vector3(0, 0, 0));

添加展馆

let mixer: AnimationMixer;
new GLTFLoader().load('../resources/models/zhanguan.glb', (gltf) => {
	scene.add(gltf.scene);
	mixer = new THREE.AnimationMixer(gltf.scene);
})

渲染场景

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
  if (mixer) {
    mixer.update(0.02);
  }
}

animate();

当浏览器窗口变化时,实时调整

window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight
  camera.updateProjectionMatrix()
  renderer.setSize(window.innerWidth, window.innerHeight)
})

给这个展馆添加各个屏幕及视频

new GLTFLoader().load('../resources/models/zhanguan.glb', (gltf) => {

  scene.add(gltf.scene);

  gltf.scene.traverse((child) => {

    child.castShadow = true;
    child.receiveShadow = true;

    if (child.name === '2023') {
      const video = document.createElement('video');
      video.src = "./resources/yanhua.mp4";
      video.muted = true;
      video.autoplay = true;
      video.loop = true;
      video.play();
      const videoTexture = new THREE.VideoTexture(video);
      const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
      (child as THREE.Mesh).material = videoMaterial;
    }

    if (child.name === '大屏幕01' || child.name === '大屏幕02' || 
	                   child.name === '操作台屏幕' || child.name === '环形屏幕2') {
      const video = document.createElement('video');
      video.src = "./resources/video01.mp4";
      video.muted = true;
      video.autoplay = true;
      video.loop = true;
      video.play();
      const videoTexture = new THREE.VideoTexture(video);
      const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
      (child as THREE.Mesh).material = videoMaterial;
    }

    if (child.name === '环形屏幕') {
      const video = document.createElement('video');
      video.src = "./resources/video02.mp4";
      video.muted = true;
      video.autoplay = true;
      video.loop = true;
      video.play();
      const videoTexture = new THREE.VideoTexture(video);
      const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
      (child as THREE.Mesh).material = videoMaterial;
    }

    if (child.name === '柱子屏幕') {
      const video = document.createElement('video');
      video.src = "./resources/yanhua.mp4";
      video.muted = true;
      video.autoplay = true;
      video.loop = true;
      video.play();
      const videoTexture = new THREE.VideoTexture(video);
      const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
      (child as THREE.Mesh).material = videoMaterial;
    }
  })

  mixer = new THREE.AnimationMixer(gltf.scene);
})

然后把人物加到展馆里面,并且更新 animate 函数

// 添加人物
let playerMixer: AnimationMixer;
let playerMesh: THREE.Group
let actionWalk: AnimationAction
let actionIdle: AnimationAction
const lookTarget = new THREE.Vector3(0, 2, 0);
new GLTFLoader().load('../resources/models/player.glb', (gltf) => {
  playerMesh = gltf.scene;
  scene.add(gltf.scene);

  playerMesh.traverse((child) => {
    child.receiveShadow = true;
    child.castShadow = true;
  })

  playerMesh.position.set(0, 0, 11.5);
  playerMesh.rotateY(Math.PI);

  playerMesh.add(camera);
  camera.position.set(0, 2, -5);
  camera.lookAt(lookTarget);

  const pointLight = new THREE.PointLight(0xffffff, 1.5);
  playerMesh.add(pointLight);
  pointLight.position.set(0, 1.8, -1);

  playerMixer = new THREE.AnimationMixer(gltf.scene);

  // 人物行走时候的状态
  const clipWalk = THREE.AnimationUtils.subclip(gltf.animations[0], 'walk', 0, 30);
  actionWalk = playerMixer.clipAction(clipWalk);

  // 人物停止时候的状态
  const clipIdle = THREE.AnimationUtils.subclip(gltf.animations[0], 'idle', 31, 281);
  actionIdle = playerMixer.clipAction(clipIdle);
  actionIdle.play();
});

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
  
  if (mixer) {
    mixer.update(0.02);
  }
  
  if (playerMixer) {
    playerMixer.update(0.015);
  }
}

下面让鼠标控制转镜头,按键盘的 W 让人物可以在展馆里行走

let isWalk = false;
const playerHalfH = new THREE.Vector3(0, 0.8, 0);
window.addEventListener('keydown', (e) => {
  if (e.key === 'w') {
    const curPos = playerMesh.position.clone();
    playerMesh.translateZ(1);
    const frontPos = playerMesh.position.clone();
    playerMesh.translateZ(-1);

    const frontVector3 = frontPos.sub(curPos).normalize()

    const raycasterFront = 
	      new THREE.Raycaster(playerMesh.position.clone().add(playerHalfH), frontVector3);
		  
    const collisionResultsFrontObjs = raycasterFront.intersectObjects(scene.children);

    if (collisionResultsFrontObjs && collisionResultsFrontObjs[0] 
	                              && collisionResultsFrontObjs[0].distance > 1) {
      playerMesh.translateZ(0.1);
    }

    if (!isWalk) {
      crossPlay(actionIdle, actionWalk);
      isWalk = true;
    }
  }
})

window.addEventListener('keyup', (e) => {
  if (e.key === 'w') {
    crossPlay(actionWalk, actionIdle);
    isWalk = false;
  }
});

let preClientX: number;
window.addEventListener('mousemove', (e) => {
  if (preClientX && playerMesh) {
    playerMesh.rotateY(-(e.clientX - preClientX) * 0.01);
  }
  preClientX = e.clientX;
});

function crossPlay(curAction: AnimationAction, newAction: AnimationAction) {
  curAction.fadeOut(0.3);
  newAction.reset();
  newAction.setEffectiveWeight(1);
  newAction.play();
  newAction.fadeIn(0.3);
}

最后给展馆设置阴影

// 设置阴影
directionLight.castShadow = true;

directionLight.shadow.mapSize.width = 2048;
directionLight.shadow.mapSize.height = 2048;

const shadowDistance = 20;
directionLight.shadow.camera.near = 0.1;
directionLight.shadow.camera.far = 40;
directionLight.shadow.camera.left = -shadowDistance;
directionLight.shadow.camera.right = shadowDistance;
directionLight.shadow.camera.top = shadowDistance;
directionLight.shadow.camera.bottom = -shadowDistance;
directionLight.shadow.bias = -0.001;

仓库地址 (opens new window)

# 其他案例

汽车展厅文字 (opens new window) 汽车展厅视频 (opens new window) 库房、档案室 (opens new window)