# 基础知识

学习官网 (opens new window)

# 什么是WEBGL?

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

# 什么是Three.js?

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

# WEBGL和Three.js的关系

WebGL原生的api是一种非常低级的接口,而且还需要一些数学和图形学的相关技术。对于没有相关基础的人来说,入门真的很难,Three.js将入门的门槛降低了整整的一大截,对WebGL进行封装,简化我们创建三维动画场景的过程。只要你有一定的JavaScript的基础,有一定的前端经验,用不了多长时间,三维制作会变得很简单。
用最简单的一句话概括:WebGL和Three.js的关系,相当于JavaScript和Jquery的关系。

# Cesium和Three.js的关系

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

# ThingJS和Three.js的关系

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

# Three.js相关的开源库

# 案例

# 目录结构

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

# jsm文件夹

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

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

# 注意事项

  • 在移动端网页里流畅运行,最多不能超过10万个面
  • 利用threejs提供的editor,我们可以将模型的格式进行转换并导出

# 右手坐标系

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

右手坐标系

# 基础示例

<!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();
    /**
     * 创建网格模型
     */
    // var geometry = new THREE.SphereGeometry(60, 40, 40); //创建一个球体几何对象
    var geometry = new THREE.BoxGeometry(100, 100, 100); //创建一个立方体几何对象Geometry
    var material = new THREE.MeshLambertMaterial({
      color: 0x0000ff
    }); //材质对象Material
    var mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh
    scene.add(mesh); //网格模型添加到场景中
    /**
     * 光源设置
     */
    //点光源
    var point = new THREE.PointLight(0xffffff);
    point.position.set(400, 200, 300); //点光源位置
    scene.add(point); //点光源添加到场景中
    //环境光
    var ambient = new THREE.AmbientLight(0x444444);
    scene.add(ambient);
    // console.log(scene)
    // console.log(scene.children)
    /**
     * 相机设置
     */
    var width = window.innerWidth; //窗口宽度
    var height = window.innerHeight; //窗口高度
    var k = width / height; //窗口宽高比
    var s = 200; //三维场景显示范围控制系数,系数越大,显示的范围越大
    //创建相机对象
    var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
    camera.position.set(200, 300, 200); //设置相机位置
    camera.lookAt(scene.position); //设置相机方向(指向的场景对象)
    /**
     * 创建渲染器对象
     */
    var renderer = new THREE.WebGLRenderer();
    renderer.setSize(width, height);//设置渲染区域尺寸
    renderer.setClearColor(0xb9d3ff, 1); //设置背景颜色
    document.body.appendChild(renderer.domElement); //body元素中插入canvas对象
    //执行渲染操作   指定场景、相机作为参数
    renderer.render(scene, camera);
  </script>
</body>
</html>

# 整个程序的结构

整体结构

# 场景对象Scene

//创建场景对象Scene
var scene = new THREE.Scene();

# 几何体Geometry

//创建一个立方体几何对象Geometry
var geometry = new THREE.BoxGeometry(100, 100, 100);
//创建一个球体几何对象
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); 
scene.add(mesh); //网格模型添加到场景中

# 光源Light

//创建了一个点光源对象,参数0xffffff定义的是光照强度
var point=new THREE.PointLight(0xffffff);
point.position.set(400, 200, 300); //点光源位置
scene.add(point); //点光源添加到场景中
//环境光
var ambient = new THREE.AmbientLight(0x444444);
scene.add(ambient);

# 相机Camera

var width = window.innerWidth; //窗口宽度
var height = window.innerHeight; //窗口高度
var k = width / height; //窗口宽高比
var s = 200; //三维场景显示范围控制系数,系数越大,显示的范围越大
//创建了一个正射投影相机对象,前四个参数定义的是拍照窗口大小
var camera = new THREE.OrthographicCamera(-s * k, s * k, s, -s, 1, 1000);
//定义相机的位置x、y、z
camera.position.set(200, 300, 200);
//定义相机的拍照方向
camera.lookAt(scene.position);

# 渲染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);

# 周期性渲染requestAnimationFrame

渲染频率控制在每秒30~60次,人的视觉效果都很正常,也可以兼顾渲染性能

requestAnimationFrame() //参数是将要被调用函数的函数名,一般默认保持60FPS的频率(每秒渲染60次)

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

# 贴图

  • 普通贴图
    • material.map,替代颜色
  • 法线贴图
    • material.normalMap,让细节程度较低的表面生成高细节程度的精确光照方向和反射效果
  • 环境光遮蔽贴图
    • material.aoMap,用来描绘物体和物体相交或靠近的时候遮挡周围漫反射光线的效果
  • 环境反射贴图
    • material.envMap,用于模拟材质反射周围环境的效果

# 常见3D模型文件格式FBX、GLFT、GLB

  • gltf:JSON格式,已成为Web端标准。描述存储模型和材质,动画数据,骨骼,蒙皮,场景层次及灯光
  • glb:是gltf的二进制文件,用于传输
  • FBX:Autodesk FBX,闭源格式。支持3D模型、场景层次、材质照明、动画、骨骼、蒙皮、及混合形状

# 常用方法

# 修改中心点

controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.target.set( x, y, z );

# 旋转 rotation \ rotateOnAxis \ rotateX

  • rotation:物体的局部旋转,以弧度来表示
object3D.rotation.x = MathUtils.degToRad(90)

//角度转弧度 
THREE.MathUtils.degToRad(deg)

//弧度转角度 
THREE.MathUtils.radToDeg (rad)
  • rotateOnAxis 在局部空间中绕着该物体的轴来旋转一个物体
//参数:(axis : Vector3, angle : Float)
object3D.rotateOnAxis(new Vector3(1,0,0),3.14)
  • rotateX / rotateY / rotateZ:绕局部空间的轴旋转这个物体
object3D.rotateX (MathUtils.degToRad(90))
object3D.rotateY (MathUtils.degToRad(90))
object3D.rotateZ (MathUtils.degToRad(90))

# 角度弧度互转

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

# 加载glb、gltf模型

loadGlbModel() {
  const loader = new GLTFLoader()
  // const dracoLoader = new DRACOLoader()
  // dracoLoader.setDecoderPath('/draco/')
  // dracoLoader.preload()
  // loader.setDRACOLoader(dracoLoader)
  loader.load(`${this.publicPath}model/12OJJ6MOWT722N61Z5N92KA9C.glb`, (gltf) => {
    console.log(gltf, '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);
    // gltf.rotateY(Math.PI / 2);
    // this.groupBox.add(gltf);
    this.scene.add(gltf.scene)
  }, (xhr) => {
      console.log((xhr.loaded / xhr.total) * 100 + '% loaded')
  }, (error) => {
      console.error(error)
  })
}

# 加载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)
})

# 加载FBX模型

//  加载 FBX 模型
loadFbxModel() {
  const loader = new FBXLoader();
  loader.load(`${this.publicPath}model/glbxz.com6031.FBX`, object => {//加载路径fbx文件
    console.log(object, 'object----->>>')
    object.traverse( child => {
      if ( child.isMesh ){
        child.castShadow = true;
        child.receiveShadow = true;
      }
    });
    this.scene.add(object);//模型
  })
}

# 加载json模型

//加载 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);
  })
}

# Canvas画布

Three.js提供了几种可以在场景中使用的字体。这些字体基于由TypeFace.js库提供的字体。TypeFace.js库可以将TrueType和OpenType字体转换为JavaScript文件或者JSON文件,以便在网页中的JavaScript程序中直接使用。在旧版本的Three.js使用字体时,需要用转换得到的JavaScript文件,而新版Three.js改为使用JSON文件了。

转换得到JSON文件后,你可以使用THREE.FontLoader加载字体,并将字体对象赋给THREE.TextGeometry的font属性。使用Facetype.js生成json字体入口 (opens new window)

function add3DFont() {
	new THREE.FontLoader().load('font/FZYaoTi_Regular.json', function(font) {
		//加入立体文字
		var text = new THREE.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);
	});
}

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

// 创建LED电子屏幕
function addLEDScreen() {
	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 = 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);
}

# 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>

# 展示三维模型的问题

在展示模型的时候遇到了一些问题,模型的尺寸、位置和旋转角度每次都靠手工调整,非常的不方便,就想着写一个方法来随心所欲的控制模型的尺寸、位置、角度。

//先看看官方加载外部模型的标准代码
var mtlLoader = new THREE.MTLLoader();
mtlLoader.load(mtlPath, function (materials) {
	materials.preload();

	var loader = new THREE.OBJLoader();
	loader.setMaterials(materials);
	loader.load(modelPath, function (obj) {
		
		obj.castShadow = true;
		obj.receiveShadow = true;

		//场景加载
		scene.add(obj);
		//渲染
		renderer.render(scene, camera);
	}
});
  • 问题1:模型尺寸

外部模型往往是3d max导出,模型的大小尺寸和实际中往往不一样。three.js 官方有scale 属性可以更改模型的尺寸比例,代码如下。

obj.scale.set(x,y,z);

现在问题来了,我知道的参数其实是模型的长、宽、高,并不是比例。解决的方法如下,先计算出模型的实际长宽高,转换成比例。

//计算模型尺寸
var box = new THREE.Box3();
box.expandByObject(obj);

var length = box.max.x - box.min.x;
var width = box.max.z - box.min.z;
var height = box.max.y - box.min.y;

//l w h对应模型的长宽高
obj.scale.set(l / length, h / height, w / width);
  • 问题2:模型角度

和模型尺寸一样,外部模型的朝向和实际中有时候不同,有时候一种模型会在场景中有多个朝向的分布,例如办公室的椅子。还是先看看官方的方法position属性可以设置模型x、y、z的转向。代码如下:

obj.position.set(angleX, angleY, angleZ);

现在看起来是没有问题了,不过当你遇到一个模型中心点坐标和场景中心点坐标相差几百万单位的时候,估计旋转后连模型影子都看不见了。解决方法是增加一个容器,将模型放入容器中,然后指定容器的中心点,然后旋转容器代替旋转模型即可。

let wrapper = new THREE.Object3D();
//模型在场景中的为准
wrapper.position.set(x,y,z);
wrapper.add(obj);

wrapper.rotation.set(angleX, angleY, angleZ);
  • 问题三:模型位置

模型的位置是最头痛的,three.js默认的以模型的中心偏移来定位的,同样看看官方的方法,有个position属性可以更改模型位置。如果遇到问题二里面那种偏移超级远的模型就尴尬了。

我的解决思路是先计算出模型的实际尺寸,然后再找到模型的中心点,根据x=0,y=0,z=0将模型移动到正常位置,然后通过问题二的容器解决方法来重新设置容器位置解决。代码如下:

//计算模型尺寸
var box = new THREE.Box3();
box.expandByObject(obj);

var x = (box.max.x + box.min.x) / 2;
var y = (box.max.y + box.min.y) / 2;
var z = (box.max.z + box.min.z) / 2;

obj.position.set(0 - x, 0 - y, 0 - z);

obj.castShadow = true;
obj.receiveShadow = true;

let wrapper = new THREE.Object3D();
//模型在场景中实际位置
var pt={x:0,y:0,z:0};
wrapper.position.set(pt.x, pt.y, pt.z);
wrapper.add(obj);

# 案例使用

# 全景工具

使用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)