# 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>
# 模型沿着轨迹进行运动
<!DOCTYPE html>
<html>
<head>
<title>Threejs加载城市obj模型,加载人物gltf模型,Tweenjs实现人物根据规划的路线运动</title>
<script type="text/javascript" src="libs/three.js"></script>
<script type="text/javascript" src="libs/OrbitControls.js"></script>
<script type="text/javascript" charset="UTF-8" src="libs/Tween.min.js"></script>
<script type="text/javascript" charset="UTF-8" src="libs/GLTFLoader.js"></script>
<script type="text/javascript" src="libs/OBJLoader.js"></script>
<script type="text/javascript" src="libs/MTLLoader.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>
<div id="dom"></div>
<script type="text/javascript">
var camera;
var renderer;
var clock = new THREE.Clock();
var mixer = new THREE.AnimationMixer();
var clipAction
var animationClip
var pobj
function init() {
// 创建一个场景,它将包含我们所有的元素,如物体,相机和灯光。
var scene = new THREE.Scene();
var urls = [
'assets/textures/posx.jpg',
'assets/textures/negx.jpg',
'assets/textures/posy.jpg',
'assets/textures/negy.jpg',
'assets/textures/posz.jpg',
'assets/textures/negz.jpg'
];
var cubeLoader = new THREE.CubeTextureLoader();
scene.background = cubeLoader.load(urls);
// 创建一个摄像机,它定义了我们正在看的地方
camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.1, 1000);
// 将摄像机对准场景的中心
camera.position.x = 20;
camera.position.y = 15;
camera.position.z = 35;
camera.lookAt(scene.position);
var orbit = new THREE.OrbitControls(camera);
// 创建一个渲染器并设置大小,WebGLRenderer将会使用电脑显卡来渲染场景
// initialize basic renderer
renderer = new THREE.WebGLRenderer({
antialias: true,
logarithmicDepthBuffer: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
// 将平面添加到场景中
var plane = createPlaneGeometryBasicMaterial();
scene.add(plane);
// 在屏幕上显示坐标轴
var axes = new THREE.AxesHelper(100);
scene.add(axes);
// var trackballControls = initTrackballControls(camera, renderer);
// 添加环境光
scene.add(new THREE.AmbientLight(0x666666));
scene.add(new THREE.AmbientLight("#ffffff", 1));
// 将呈现器的输出添加到HTML元素
document.getElementById("dom").appendChild(renderer.domElement);
var points = initLine();
// 将球体添加到场景中
initModel();
initPeople();
// 启动动画
renderScene();
var i = 0;
function tweenComplete() {
if (i < points.length) {
switch (i) {
case 0:
pobj.rotateY(Math.PI);
break;
case 1:
case 5:
case 8:
case 9:
pobj.rotateY(-0.5 * Math.PI);
break;
case 2:
case 3:
case 4:
case 6:
case 7:
pobj.rotateY(0.5 * Math.PI);
break;
case 10:
mixer.stopAllAction();
break;
}
tween = new TWEEN.Tween(points[i])
.to(points[i + 1], 3000)
.easing(TWEEN.Easing.Linear.None)
.onUpdate(function() {
pobj.position.set(this.x, this.y, this.z);
})
.onComplete(tweenComplete)
.start();
i++;
}
}
// 添加模型
function initModel() {
var mtlLoader = new THREE.MTLLoader();
mtlLoader.setPath("assets/models/obj_mtl/")
mtlLoader.load('city.mtl', function(materials) {
materials.preload();
var objLoader = new THREE.OBJLoader();
objLoader.setMaterials(materials);
objLoader.load('assets/models/obj_mtl/city.obj', function(object) {
mesh = object;
mesh.scale.set(3, 3, 3);
mesh.position.y = -5;
scene.add(mesh);
});
});
}
// 添加人物模型
function initPeople() {
var loader = new THREE.GLTFLoader();
loader.load('assets/models/man/man.gltf', function(result) {
result.scene.scale.set(1, 1, 1);
result.scene.translateY(0);
pobj = result.scene;
scene.add(result.scene);
tweenComplete();
mixer = new THREE.AnimationMixer(result.scene);
animationClip = result.animations[0];
clipAction = mixer.clipAction(animationClip).play();
animationClip = clipAction.getClip();
});
}
// 创建一个平面
function createPlaneGeometryBasicMaterial() {
var textureLoader = new THREE.TextureLoader();
var cubeMaterial = new THREE.MeshStandardMaterial({
map: textureLoader.load("assets/textures/cd.jpg"),
});
cubeMaterial.map.wrapS = THREE.RepeatWrapping;
cubeMaterial.map.wrapT = THREE.RepeatWrapping;
cubeMaterial.map.repeat.set(18, 18)
// 创建地平面并设置大小
var planeGeometry = new THREE.PlaneGeometry(500, 500);
var plane = new THREE.Mesh(planeGeometry, cubeMaterial);
// 设置平面位置并旋转
plane.rotation.x = -0.5 * Math.PI;
plane.position.x = 0;
plane.position.y = -5;
plane.position.z = 0;
return plane;
}
// 初始化线路
function initLine() {
var pArr = [{
x: 5 * 3,
y: -3.8,
z: -0.7 * 3
}, {
x: -0.6 * 3,
y: -3.8,
z: -0.7 * 3
}, {
x: -0.6 * 3,
y: -3.8,
z: -1.8 * 3
}, {
x: -4 * 3,
y: -3.8,
z: -1.8 * 3
}, {
x: -4 * 3,
y: -3.8,
z: 2.8 * 3
}, {
x: -1.2 * 3,
y: -3.8,
z: 2.8 * 3
}, {
x: -1.2 * 3,
y: -3.8,
z: 4.3 * 3
}, {
x: 1.7 * 3,
y: -3.8,
z: 4.3 * 3
}, {
x: 1.7 * 3,
y: -3.8,
z: -0.4 * 3
}, {
x: 4.4 * 3,
y: -3.8,
z: -0.4 * 3
}, {
x: 4.4 * 3,
y: -3.8,
z: 5 * 3
}];
var points = [];
var geometry = new THREE.Geometry();
for (var i = 0; i < pArr.length; i++) {
var randomX = pArr[i].x;
var randomY = pArr[i].y;
var randomZ = pArr[i].z;
var vector = new THREE.Vector3(randomX, randomY, randomZ);
geometry.vertices.push(vector);
points.push(vector);
}
var material = new THREE.LineBasicMaterial({
color: 0xff0000
});
var line = new THREE.Line(geometry, material);
scene.add(line);
return points;
}
// 动画渲染
var step = 5;
function renderScene() {
TWEEN.update();
orbit.update();
var delta = clock.getDelta();
mixer.update(delta);
// 使用requestAnimationFrame函数进行渲染
requestAnimationFrame(renderScene);
renderer.render(scene, camera);
}
// 渲染的场景
renderer.render(scene, camera);
document.addEventListener('mousedown', onDocumentMouseDown, false);
function onDocumentMouseDown(event) {
// 点击屏幕创建一个向量
var vector = new THREE.Vector3((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window
.innerHeight) * 2 + 1, 0.5);
vector = vector.unproject(camera); // 将屏幕的坐标转换成三维场景中的坐标
var raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
var intersects = raycaster.intersectObjects(mesh.children, true);
console.log(intersects)
if (intersects.length > 0) {
// intersects[0].object.material.color.set("#ffffff");
}
}
// 创建一个球形几何体
function createSphereGeometryLambertMaterial(point) {
// 创建一个球体
var sphereGeometry = new THREE.SphereGeometry(0.2, 20, 20);
var sphereMaterial = new THREE.MeshBasicMaterial({
color: 0x7777ff,
wireframe: true
});
var sphereMaterial = new THREE.MeshLambertMaterial({
color: 0xff0000
});
var sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
// 设置该物体投射阴影
sphere.castShadow = true;
// 位置范围
sphere.position.x = point.x;
sphere.position.y = point.y;
sphere.position.z = point.z;
return sphere;
}
}
window.onload = init;
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// 监听调整大小事件
window.addEventListener('resize', onResize, false);
</script>
</body>
</html>
# 曲线轨迹动画
let curve = null;
function makeCurve() {
curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(5, 0, 0),
new THREE.Vector3(0, 0, 5)
]);
curve.curveType = "catmullrom";
curve.closed = true;// 设置是否闭环
curve.tension = 0.5; // 设置线的张力,0为无弧度折线
// 为曲线添加材质在场景中显示出来,不显示也不会影响运动轨迹
const points = curve.getPoints(50);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.LineBasicMaterial({ color: 0x000000 });
const curveObject = new THREE.Line(geometry, material);
scene.add(curveObject)
}
let progress = 0; // 物体运动时在运动路径的初始位置,范围0~1
const velocity = 0.001; // 影响运动速率的一个值,范围0~1,和渲染频率结合计算才能得到真正的速率
// 物体沿线移动方法
function moveOnCurve() {
if (curve == null || model == null) {
console.log("Loading")
} else {
if (progress <= 1 - velocity) {
const point = curve.getPointAt(progress); // 获取样条曲线指定点坐标
const pointBox = curve.getPointAt(progress + velocity); // 获取样条曲线指定点坐标
if (point && pointBox) {
model.position.set(point.x, point.y, point.z);
var targetPos = pointBox; //目标位置点
var offsetAngle = 0; //目标移动时的朝向偏移
// 以下代码在多段路径时可重复执行
var mtx = new THREE.Matrix4() // 创建一个4维矩阵
// 构造一个旋转矩阵,从eye 指向 target,由向量 up 定向
// lookAt(eye:Vector3, target:Vector3, up:Vector3 ):this
mtx.lookAt(model.position, targetPos, model.up) // 设置朝向
var euler = new THREE.Euler(0, offsetAngle, 0)
mtx.multiply(new THREE.Matrix4().makeRotationFromEuler(euler))
// 计算出需要进行旋转的四元数值
var toRot = new THREE.Quaternion().setFromRotationMatrix(mtx)
model.quaternion.slerp(toRot, 0.2)
}
progress += velocity;
} else {
progress = 0;
}
}
};
// moveOnCurve()需要在渲染中一直调用更新,以达到物体移动效果
function animate() {
requestAnimationFrame(animate);
moveOnCurve();
renderer.render(scene, camera);
};
# 山地高度可视化
// 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,
});
# Sprite模拟下雨
// 提供一个背景透明的png雨滴贴图,然后作为Sprite的颜色贴图
const texture = new THREE.TextureLoader().load("./雨滴.png");
const spriteMaterial = new THREE.SpriteMaterial({
map: texture,
});
// 雨滴在3D空间随机分布
const group = new THREE.Group();
for (let i = 0; i < 16000; i++) {
// 精灵模型共享材质
const sprite = new THREE.Sprite(spriteMaterial);
group.add(sprite);
sprite.scale.set(1, 1, 1);
// 设置精灵模型位置,在长方体空间上上随机分布
const x = 1000 * (Math.random() - 0.5);
const y = 600 * Math.random();
const z = 1000 * (Math.random() - 0.5);
sprite.position.set(x, y, z)
}
// 周期性改变雨滴Sprite位置,根据时间计算Sprite位置
const clock = new THREE.Clock();
function loop() {
// loop()两次执行时间间隔
const t = clock.getDelta();
group.children.forEach(sprite => {
// 雨滴的y坐标每次减t*60
sprite.position.y -= t*60;
if (sprite.position.y < 0) {
sprite.position.y = 600;
}
});
requestAnimationFrame(loop);
}
loop();
// 相机镜头 near 参数调整大一些,避免相机镜头附近的雨滴偏大
// const camera = new THREE.PerspectiveCamera(30, width / height, 1, 3000);
const camera = new THREE.PerspectiveCamera(30, width / height, 50, 3000);
# 第一、三人称漫游
- 记录键盘按键WASD状态
// 声明一个对象keyStates用来记录键盘事件状态
const keyStates = {
// // false表示没有按下,true表示按下状态
// keyW:false,
// keyA:false,
// keyS:false,
// keyD:false,
};
// 当某个键盘按下设置对应属性设置为true
document.addEventListener('keydown', (event) => {
keyStates[event.code] = true;
});
// 当某个键盘抬起设置对应属性设置为false
document.addEventListener('keyup', (event) => {
keyStates[event.code] = false;
});
// 循环执行的函数中测试W键盘状态值
function render() {
if(keyStates.W){
console.log('W键按下');
}else{
console.log('W键松开');
}
requestAnimationFrame(render);
}
render();
- W键控制人物模型运动
// 用三维向量表示玩家角色(人)运动漫游速度
const v = new THREE.Vector3(0, 0, 3);
// 渲染循环
const clock = new THREE.Clock();
function render() {
const deltaTime = clock.getDelta();
if (keyStates.W) {
// 在间隔deltaTime时间内,玩家角色位移变化计算(速度*时间)
const deltaPos = v.clone().multiplyScalar(deltaTime);
player.position.add(deltaPos);// 更新玩家角色的位置
}
mixer.update(deltaTime);// 更新播放器相关的时间
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
- 设置加速度、限制最高速度、设置阻尼减速
// 用三维向量表示玩家角色(人)运动漫游速度
const v = new THREE.Vector3(0, 0, 0);// 初始速度设置为0
const a = 12;// 加速度:调节按键加速快慢
const vMax = 5;// 限制玩家角色最大速度
const damping = -0.04;// 阻尼 当没有WASD加速的时候,角色慢慢减速停下来
// 渲染循环
const clock = new THREE.Clock();
function render() {
const deltaTime = clock.getDelta();
// 限制最高速度
if (v.length() < vMax) {
if (keyStates.W) {
// 先假设W键对应运动方向为z
const front = new THREE.Vector3(0,0,1);
// W键按下时候,速度随着时间增加
v.add(front.multiplyScalar(a * deltaTime));
}
// 后退
if (keyStates.S) {
// 与W按键相反方向
const front = new THREE.Vector3(0, 0, -1);
v.add(front.multiplyScalar(a * deltaTime));
}
}
// 阻尼减速,乘以一个小于1的数值,这样重复多次执行以后,速度就会逼近0
v.addScaledVector(v, damping);
//更新玩家角色的位置 当v是0的时候,位置更新也不会变化
const deltaPos = v.clone().multiplyScalar(deltaTime);
player.position.add(deltaPos);
mixer.update(deltaTime);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
- 相机跟着玩家走(第三人称漫游)
const camera = new THREE.PerspectiveCamera(30,...);
// 把相机作为玩家角色的子对象,这样相机的位置和姿态就会跟着玩家角色改变
player.add(camera);// 相机作为人的子对象
// 玩家角色后面一点 对应fov 30度
camera.position.set(0, 1.6, -5.5);
camera.lookAt(0, 1.6, 0);// 对着人身上某个点
// 注释相机空间OrbitControls代码,避免影响相机W、A、S、D对相机的控制
const controls = new OrbitControls(camera, renderer.domElement);
- 鼠标左键拖动时候,旋转玩家角色
let leftButtonBool = false;// 记录鼠标左键状态
document.addEventListener('mousedown', () => {
leftButtonBool = true;
});
document.addEventListener('mouseup', () => {
leftButtonBool = false;
});
document.addEventListener('mousemove', (event) => {
// 鼠标左键按下时候,才旋转玩家角色
if(leftButtonBool){
// event.movementX表示鼠标左右方向滑动的距离,单位是像素,往右滑动是正,往左滑动是负
// 注意 rotation.y += 与 -= 区别,左右旋转时候方向相反
player.rotation.y -= event.movementX / 600;
}
});
- 鼠标上下移动只改变相机视角
// 这样虽然可以通过player控制子对象相机视角上下俯仰
// 但是玩家角色模型也必须跟着旋转,这样会改变人与地面位置关系
document.addEventListener('mousemove', (event) => {
if(leftButtonBool){
// 左右旋转
player.rotation.y -= event.movementX / 600;
// 玩家角色绕x轴旋转 视角上下俯仰
player.rotation.x -= event.movementY / 600;
}
});
// 可以在相机camera和玩家角色模型player之间,嵌入一个子节点cameraGroup
// 作为相机的父对象,作为玩家角色模型player的子对象
// 层级关系:player <—— cameraGroup <—— camera
const cameraGroup = new THREE.Group();
cameraGroup.add(camera);
player.add(cameraGroup);
// 上下俯仰角度范围
const angleMin = THREE.MathUtils.degToRad(-15);// 角度转弧度
const angleMax = THREE.MathUtils.degToRad(15);
document.addEventListener('mousemove', (event) => {
if(leftButtonBool){
// 左右旋转
player.rotation.y -= event.movementX / 600;
// 鼠标上下滑动,让相机视线上下转动
// 相机父对象cameraGroup绕着x轴旋转,camera跟着转动
cameraGroup.rotation.x -= event.movementY / 600;
// 一旦判断.rotation.x小于-15,就设置为-15,大于15,就设置为15
if (cameraGroup.rotation.x < angleMin) {
cameraGroup.rotation.x = angleMin;
}
if (cameraGroup.rotation.x > angleMax) {
cameraGroup.rotation.x = angleMax
};
}
});
- 获取玩家(相机)正前方方向
function render() {
if (v.length() < vMax) {
if (keyStates.W) {
const front = new THREE.Vector3();
// obj.getWorldDirection()表示的获取obj对象自身z轴正方向在世界坐标空间中的方向
// 模型没有任何旋转情况,.getWorldDirection()获取的结果(0,0,1)
player.getWorldDirection(front);// 获取玩家角色(相机)正前方
v.add(front.multiplyScalar(a * deltaTime));
}
if (keyStates.S) {
const front = new THREE.Vector3();
player.getWorldDirection(front);
// - a:与W按键反向相反
v.add(front.multiplyScalar(- a * deltaTime));
}
}
}
- 玩家角色左右运动(叉乘)
// 两个向量a、b叉乘有一个特点,叉乘结果是一个同时垂直于a和b的向量
// 这就是说只要知道玩家角色模型人正前方方向和高度方向向量,就可以计算出来人的左右方向
// 玩家角色的正前方
const front = new THREE.Vector3();
player.getWorldDirection(front);
// 玩家角色的高度方向(竖直方向)
const up = new THREE.Vector3(0, 1, 0);// y方向
// A和D按键对应的方向计算代码
if (keyStates.A) {// 向左运动
const front = new THREE.Vector3();
player.getWorldDirection(front);
const up = new THREE.Vector3(0, 1, 0);// y方向
const left = up.clone().cross(front);
v.add(left.multiplyScalar(a * deltaTime));
}
if (keyStates.D) {// 向右运动
const front = new THREE.Vector3();
player.getWorldDirection(front);
const up = new THREE.Vector3(0, 1, 0);//y方向
// 叉乘获得垂直于向量up和front的向量 左右与叉乘顺序有关
// 可以用右手螺旋定则判断,也可以代码测试结合3D场景观察验证
const right = front.clone().cross(up);
v.add(right.multiplyScalar(a * deltaTime));
}
- 鼠标点击页面随便一个位置,就会进入指针锁定模式
// 鼠标滑动时候,受到浏览器窗口范围限制,不能无限制移动,这时可以执行requestPointerLock()锁定指针
// 此时鼠标箭头不见了,鼠标可以上下左右无限滑动,按下键盘左上角Esc按键,鼠标指针箭头恢复到原来状态
// 当鼠标左键按下后进入指针锁定模式(鼠标无限滑动)
addEventListener( 'mousedown', () => {
document.body.requestPointerLock();//body页面指针锁定
});
- 在指针锁定模式下,改变玩家人姿态角度
// 鼠标左右移动,人绕y轴旋转
addEventListener('mousemove', (event) => {
// 进入指针模式后,才能根据鼠标位置控制人旋转
if (document.pointerLockElement == document.body) {
// 鼠标左右滑动,让人左右转向(绕y轴旋转),相机会父对象人绕左右转向
// 加减法根据左右方向对应关系设置,缩放倍数根据,相应敏感度设置
person.rotation.y -= event.movementX / 500;
}
});
// 执行document.exitPointerLock();可以退出指针锁定,或者键盘键盘Esc退出指针锁定模式
- 第一人称视角,简单点说,就是看不到玩家角色的模型,相当于把相机放在人的前面
// camera.position.set(0, 1.6, -2.3);// 第三人称
// camera.lookAt(0, 1.6, 0);
camera.position.set(0, 1.6, 1);// 第一人称
camera.lookAt(0, 1.6, 2);// 目标观察点注意在相机位置前面一点
- 第一、第三人称,快捷键v切换
let viewBool = true;// true表示第三人称,false表示第一人称
document.addEventListener('keydown', (event) => {
if (event.code === 'KeyV') {
if (viewBool) {
// 切换到第一人称
camera.position.z = 1;// 相机在人前面一点 看不到人模型即可
} else {
// 切换到第三人称
camera.position.z = -2.3;// 相机在人后面一点
}
viewBool = !viewBool;
}
});
- 骨骼动画与运动状态关联
const clipArr = gltf.animations;//所有骨骼动画
const actionObj = {};//包含所有动作action
for (let i = 0; i < clipArr.length; i++) {
const clip = aniArr[i];//休息、步行、跑步等动画的clip数据
const action = mixer.clipAction(clip);//clip生成action
action.name = clip.name;//action命名name
// 批量设置所有动画动作的权重
if (action.name === 'Idle') {
action.weight = 1.0;//这样默认播放Idle对应的休息动画
} else {
action.weight = 0.0;
}
action.play();
// action动画动作名字作为actionObj的属性
actionObj[action.name] = action;
}
// 动作切换函数
let currentAction = actionObj['Idle'];//记录当前播放的动作
// 切换不同动作
function changeAction(actionName) {
currentAction.weight = 0.0;//原来动作权重为0,不播放
const action = actionObj[actionName];//新的需要播放的动作
action.weight = 1.0;//将要播放的动作权重为1
currentAction = action;//替换记录的动作
}
// 根据玩家角色速度v设置休息和步行动作
function playerUpdate(deltaTime) {
const vL = v.length();
if (vL < 0.2) {//速度小于0.2切换到站着休息状态
// 如果当前就是Idle状态,不需要再次执行changeAction
if (currentAction.name != 'Idle') changeAction('Idle');
} else {//步行状态
if (currentAction.name != 'Walk') changeAction('Walk');
}
}
# 全景工具
使用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)搭出这样一个房间
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度球形范围内所有的光捕捉到一个图片上,再将这张图片展开为矩形,就能得到这样一张全景图片
//节点数量越大,需要计算的三角形就越多,影响性能
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);
}
});
# 元宇宙交互
初始化项目
//首先我们使用 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)
← 基础信息