一.搭建最简易的基础场景
关于threejs基本知识:
场景scene
摄像机camera
渲染器 renderer
几何体geometry
//引入three.js
import * as THREE from 'three';
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
//1.创建场景
const scene = new THREE.Scene()
//2.创建相机
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.01, 10)
//设置相机的位置,相机需放到物体的外面才能看到物体的展示
camera.position.set(0.3, 0.3, 1.5)
//3.创建渲染器
const renderer = new THREE.WebGLRenderer({antialias: true})
renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(renderer.domElement)
//4.创建物体
const cube = new THREE.Mesh(
new THREE.BoxGeometry(1, 1, 1),
new THREE.MeshBasicMaterial({color: 0x00ff00})
)
scene.add(cube)
const controls = new OrbitControls(camera, renderer.domElement)
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
controls.update()
}
animate()
效果:
![](https://i-blog.csdnimg.cn/blog_migrate/7d78aebfdd8e8fde307803d635227ba3.gif)
二.加载展厅空间
需要用到的资源有:
展馆的模型:
![](https://i-blog.csdnimg.cn/blog_migrate/b6e2d1396e3867d346131a00d63369b1.gif)
贴图:
![](https://i-blog.csdnimg.cn/blog_migrate/8c75942b0e3d846c0bf9afb7a7e48c26.png)
大屏上用到的视频资源:
![](https://i-blog.csdnimg.cn/blog_migrate/eb5c82490220ea071d759971eac54580.png)
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
new GLTFLoader().load('../resources/models/zhanguan.glb', (gltf) => {
scene.add(gltf.scene);
gltf.scene.traverse((child)=>{
if (child.name === '2023') {
const video = document.createElement('video');
video.src = "./resources/yanhua.mp4";
video.muted = true;
video.autoplay = "autoplay";
video.loop = true;
video.play();
const videoTexture = new THREE.VideoTexture(video);
const videoMaterial = new THREE.MeshBasicMaterial({map: videoTexture});
child.material = videoMaterial;
}
if (child.name === '大屏幕01') {
const video = document.createElement('video');
video.src = "./resources/video01.mp4";
video.muted = true;
video.autoplay = "autoplay";
video.loop = true;
video.play();
const videoTexture = new THREE.VideoTexture(video);
const videoMaterial = new THREE.MeshBasicMaterial({map: videoTexture});
child.material = videoMaterial;
}
if (child.name === '大屏幕02' || child.name === '操作台屏幕') {
const video = document.createElement('video');
video.src = "./resources/video01.mp4";
video.muted = true;
video.autoplay = "autoplay";
video.loop = true;
video.play();
const videoTexture = new THREE.VideoTexture(video);
const videoMaterial = new THREE.MeshBasicMaterial({map: videoTexture});
child.material = videoMaterial;
}
if (child.name === '环形屏幕') {
const video = document.createElement('video');
video.src = "./resources/video02.mp4";
video.muted = true;
video.autoplay = "autoplay";
video.loop = true;
video.play();
const videoTexture = new THREE.VideoTexture(video);
const videoMaterial = new THREE.MeshBasicMaterial({map: videoTexture});
child.material = videoMaterial;
}
if (child.name === '柱子屏幕') {
const video = document.createElement('video');
video.src = "./resources/yanhua.mp4";
video.muted = true;
video.autoplay = "autoplay";
video.loop = true;
video.play();
const videoTexture = new THREE.VideoTexture(video);
const videoMaterial = new THREE.MeshBasicMaterial({map: videoTexture});
child.material = videoMaterial;
}
})
mixer = new THREE.AnimationMixer(gltf.scene);
const clips = gltf.animations; // 播放所有动画
clips.forEach(function (clip) {
const action = mixer.clipAction(clip);
action.loop = THREE.LoopOnce;
// 停在最后一帧
action.clampWhenFinished = true;
action.play();
});
})
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
controls.update();
if (mixer) {
mixer.update(0.02);
}
}
animate();
效果:
![](https://i-blog.csdnimg.cn/blog_migrate/39b87c9821ad87da41ec97daedd9f40f.gif)
三.加载角色和控制角色
要实现角色的走动需要禁用掉:
// const controls = new OrbitControls(camera, renderer.domElement);
// controls.update();
增加相机对象指向坐标原点:
camera.lookAt(new THREE.Vector3(0, 0, 0));
let playerMesh;
let actionWalk, actionIdle;
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);
//控制人物朝向朝里,旋转180弧度
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);
// THREE.AnimationUtils.subclip动作面片剪辑工具
//剪辑走路动作
const clipWalk = THREE.AnimationUtils.subclip(gltf.animations[0], 'walk', 0, 30);
actionWalk = playerMixer.clipAction(clipWalk);
// actionWalk.play();
//剪辑暂停动作
const clipIdle = THREE.AnimationUtils.subclip(gltf.animations[0], 'idle', 31, 281);
actionIdle = playerMixer.clipAction(clipIdle);
actionIdle.play();
});
实现角色根据鼠标偏移方向左右偏移
let preClientX;
window.addEventListener('mousemove', (e) => {
if (preClientX && playerMesh) {
playerMesh.rotateY(-(e.clientX - preClientX) * 0.01);
}
preClientX = e.clientX;
});
如果直接一个动画切换到另外一个动画,看起来比较生硬,我们使用渐变切换动画工作函数过渡动画:
function crossPlay(curAction, newAction) {
curAction.fadeOut(0.3);
newAction.reset();
newAction.setEffectiveWeight(1);
newAction.play();
newAction.fadeIn(0.3);
}
实现人物走路动画:
//isWalk是否走路的标识,用来标记渐变动作切换函数crossPlay只调用一次
let isWalk = false;
const playerHalfHeight = new THREE.Vector3(0, 0.8, 0);
window.addEventListener('keydown', (e) => {
if (e.key === 'w') {
if (!isWalk) {
crossPlay(actionIdle, actionWalk);
isWalk = true;
}
}
})
window.addEventListener('keyup', (e) => {
if (e.key === 'w') {
crossPlay(actionWalk, actionIdle);
isWalk = false;
}
});
效果:
![](https://i-blog.csdnimg.cn/blog_migrate/185fabcdae5e11d2e5760e2cf45de4ca.gif)
四.射线碰撞
const playerHalfHeight = new THREE.Vector3(0, 0.8, 0);
//curPos:记录角色当前的位置
const curPos = playerMesh.position.clone();
//frontPos:角色向前一步的位置,并把位置记录下来
playerMesh.translateZ(1);
const frontPos = playerMesh.position.clone();
//角色退回原来的位置
playerMesh.translateZ(-1);
//frontPos向量减去curPos的向量,normalize把向量缩放成1
const frontVector3 = frontPos.sub(curPos).normalize()
//playerMesh.position.clone().add(playerHalfHeight) 角色的位置增加半个高度
const raycasterFront = new THREE.Raycaster(playerMesh.position.clone().add(playerHalfHeight), frontVector3);
//检测哪些物体
const collisionResultsFrontObjs = raycasterFront.intersectObjects(scene.children);
//如果角色与场景的距离大于1,就可以向前移动
if (collisionResultsFrontObjs && collisionResultsFrontObjs[0] && collisionResultsFrontObjs[0].distance > 1) {
playerMesh.translateZ(0.1);
}
五.阴影
实现阴影四步:
打开renderer阴影
const renderer = new THREE.WebGLRenderer();
renderer.shadowMap.enabled = true;
1打开灯光阴影,设置灯光阴影大小
const light = new THREE.DirectionalLight( 0xffffff, 1 );
light.position.set( 0, 1, 0 );
light.castShadow = true; // 默认值false
scene.add( light );
2设置灯光阴影贴图大小
light.shadow.mapSize.width = 512; //默认值
light.shadow.mapSize.height = 512; //默认值
设置阴影体远近大小,不在这个之内的显示不出来
light.shadow.camera.near = 0.5; //默认值
light.shadow.camera.far = 500; //默认值
light.shadow.camera.left = -shadowDistance;
light.shadow.camera.right = shadowDistance;
light.shadow.camera.top = shadowDistance;
light.shadow.camera.bottom = -shadowDistance;
设置mesh投射,接收阴影
gltf.scene.traverse((child) => {
child.castShadow = true;
child.receiveShadow = true;
})
阴影调试辅助工具
const helper = new THREE.CameraHelper( light.shadow.camera );
scene.add( helper );
效果:
![](https://i-blog.csdnimg.cn/blog_migrate/0a6f97c67ad3ad1eaf6b31b51c038ff0.png)
六.结尾
完整代码展示:
import * as THREE from 'three';
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader';
let mixer;
let playerMixer;
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(5, 10, 25);
// const controls = new OrbitControls(camera, renderer.domElement);
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.position.set (10, 10, 10);
directionLight.lookAt(new THREE.Vector3(0, 0, 0));
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;
// const boxGeometry = new THREE.BoxGeometry(1,1,1);
// const boxMaterial = new THREE.MeshBasicMaterial({color: 0x00ff00});
// const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);
// scene.add(boxMesh);
// const axesHelper = new THREE.AxesHelper(10);
// scene.add(axesHelper)
let playerMesh;
let actionWalk, actionIdle;
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);
// actionWalk.play();
const clipIdle = THREE.AnimationUtils.subclip(gltf.animations[0], 'idle', 31, 281);
actionIdle = playerMixer.clipAction(clipIdle);
actionIdle.play();
// const clips = gltf.animations; // 播放所有动画
// clips.forEach(function (clip) {
// const action = mixer.clipAction(clip);
// action.loop = THREE.LoopOnce;
// // 停在最后一帧
// action.clampWhenFinished = true;
// action.play();
// });
});
let isWalk = false;
const playerHalfHeight = new THREE.Vector3(0, 0.8, 0);
window.addEventListener('keydown', (e) => {
if (e.key === 'w') {
// playerMesh.translateZ(0.1);
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(playerHalfHeight), frontVector3);
const collisionResultsFrontObjs = raycasterFront.intersectObjects(scene.children);
console.log(collisionResultsFrontObjs);
if (collisionResultsFrontObjs && collisionResultsFrontObjs[0] && collisionResultsFrontObjs[0].distance > 1) {
playerMesh.translateZ(0.1);
}
if (!isWalk) {
crossPlay(actionIdle, actionWalk);
isWalk = true;
}
}
if (e.key === 's') {
playerMesh.translateZ(-0.1);
}
})
window.addEventListener('keyup', (e) => {
if (e.key === 'w') {
crossPlay(actionWalk, actionIdle);
isWalk = false;
}
});
let preClientX;
window.addEventListener('mousemove', (e) => {
if (preClientX && playerMesh) {
playerMesh.rotateY(-(e.clientX - preClientX) * 0.01);
}
preClientX = e.clientX;
});
new GLTFLoader().load('../resources/models/zhanguan.glb', (gltf) => {
// console.log(gltf);
scene.add(gltf.scene);
gltf.scene.traverse((child) => {
// console.log(child.name);
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 = "autoplay";
video.loop = true;
video.play();
const videoTexture = new THREE.VideoTexture(video);
const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
child.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 = "autoplay";
video.loop = true;
video.play();
const videoTexture = new THREE.VideoTexture(video);
const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
child.material = videoMaterial;
}
if (child.name === '环形屏幕') {
const video = document.createElement('video');
video.src = "./resources/video02.mp4";
video.muted = true;
video.autoplay = "autoplay";
video.loop = true;
video.play();
const videoTexture = new THREE.VideoTexture(video);
const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
child.material = videoMaterial;
}
if (child.name === '柱子屏幕') {
const video = document.createElement('video');
video.src = "./resources/yanhua.mp4";
video.muted = true;
video.autoplay = "autoplay";
video.loop = true;
video.play();
const videoTexture = new THREE.VideoTexture(video);
const videoMaterial = new THREE.MeshBasicMaterial({ map: videoTexture });
child.material = videoMaterial;
}
})
mixer = new THREE.AnimationMixer(gltf.scene);
const clips = gltf.animations; // 播放所有动画
clips.forEach(function (clip) {
const action = mixer.clipAction(clip);
action.loop = THREE.LoopOnce;
// 停在最后一帧
action.clampWhenFinished = true;
action.play();
});
})
new RGBELoader()
.load('../resources/sky.hdr', function (texture) {
// scene.background = texture;
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.render(scene, camera);
});
function crossPlay(curAction, newAction) {
curAction.fadeOut(0.3);
newAction.reset();
newAction.setEffectiveWeight(1);
newAction.play();
newAction.fadeIn(0.3);
}
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
// controls.update();
// if (donuts){
// donuts.rotation.y += 0.01;
// }
if (mixer) {
mixer.update(0.02);
}
if (playerMixer) {
playerMixer.update(0.015);
}
}
animate();
以上就在大帅老师特训营中学到的,加入猿创营 (v:dashuailaoyuan),一起交流学习吧~