开发助手
素材
帮助理解
物体交互
模型检查
工具
Vite 处理 public 资源
- threejs 里的各种加载器:设置 setPath
textureLoader = new THREE.TextureLoader().setPath('images/')
- vite.config.js 配置增加
assetsInclude: ["**/*.m4a", "**/*.mp4", "**/*.gltf"]
认识模型类型资源 - public 里的资源在 js 里引用时不要加
/
、public/
,如在 public 下有 sounds 资源,只需要写:sounds/game/Ambient.m4a
即可;如在 public 下有 videos 资源,只需要写:videos/video1.mp4
THREE.JS 使用记录
解决旋转不同轴之后带来的问题(万向轴)
- 当旋转了 x 轴,此时 Y 轴已经不再是原来的方向(默认朝上),而是始终垂直与旋转后的 X 轴。
解决方案 1:
reorder('YXZ')
,重新设定 XYZ 轴的设定顺序
mesh.rotation.reorder("YXZ");
mesh.rotation.x = Math.PI / 4;
mesh.rotation.y = Math.PI / 4;
// 以上代码表示:
// Y轴先旋转45°,然后X轴再旋转45°。目的是合理的安排我们达到某个旋转效果凡人顺序。
解决方案 2:用 Group 包裹目标元素。(自定义箭头指示器时遇到的问题)
先调整目标元素的旋转、位置,然后加入到 Group 中,以 Group 作为最终生成的物体,并添加到 scene 中。
让移动物体始终保持在屏幕中央位置:角色移动控制,视角跟踪
camera.lookAt(objMesh.position);
淡入淡出效果:改变物体材质的 opacity 属性
// 初始状态
let material = new THREE.MeshBasicMaterial({
transparent: true,
opacity: 0.1,
map: texture,
});// x6个
let materials = [material x6个]
let geometry = new THREE.BoxGeometry(100, 100, 100);
let cube = new THREE.Mesh(geometry, materials);
// 淡入
cube.material.map((item) => {
gsap.to(item, {
opacity: 1,
duration: 1,
});
});
// 淡出
cube.material.map((item) => {
gsap.to(item, {
opacity: 0,
duration: 1,
});
});
场景加入了灯光没效果?
场景需要开启阴影,灯光需要开启投影,物体需要允许投影,地板需要接受投影。
- 渲染器开启:
renderer.shadowMap.enabled = true;
激活 - 渲染器开启:
renderer.physicallyCorrectLights = true;
进行优化 - 检查灯光的
castShadow
是否开启:light.castShadow = true; // default false
- 物体材质不能是
MeshBasicMaterial
,需要是MeshStandardMaterial
(需要深入验证) - 开启物体的
castShadow
选项:plane.castShadow = true;
- 需要一个接收阴影的物体(一般是地面):
floor.receiveShadow = true
/** Open Shadows */
// step1.渲染器开启阴影
renderer.shadowMap.enabled = true;
// step2.物体开启投下阴影
moonLight.castShadow = true
doorLight.castShadow = true
ghost1.castShadow = true
ghost2.castShadow = true
ghost3.castShadow = true
walls.castShadow = true
roof.castShadow = true
// step3.地板开启接收阴影
floor.receiveShadow = true
// stre4.优化阴影效果
renderer.shadowMap.type = THREE.PCFSoftShadowMap; // default THREE.PCFShadowMap
doorLight.shadow.mapSize.width = 256
doorLight.shadow.mapSize.height = 256
doorLight.shadow.camera.far = 5
ghost1.shadow.mapSize.width = 256
ghost1.shadow.mapSize.height = 256
ghost1.shadow.camera.far = 5
迷雾效果:解决场景边缘割裂问题
const FLOOR_SIZE = 40;
const FOG_COLOR = "#262837";
const scene = new THREE.Scene();
const renderer = new THREE.WebGL1Renderer({ antialias: true });
// 像场景中添加「迷雾」
scene.fog = new THREE.Fog(FOG_COLOR, 1, FLOOR_SIZE - 15);
// 解决场景边缘割裂问题(无缝融合)
renderer.setClearColor(FOG_COLOR);
渲染器优化(场景效果)
- 解决多个模型组合后,模型闪烁问题
const renderer = new THREE.WebLRenderer(
// 设置抗锯齿
antialias: true,
// 对数深度缓冲区
logarithmicDepthBuffer: true,
)
- 优化场景中显示效果
renderer.shadowMap.enabled = true; //开启阴影
renderer.shadowMap.type = THREE.PCFSoftShadowMap; //调整 render 的阴影算法为 PCFSoftShadowMap
renderer.physicallyCorrectLights = true; //以符合物理直觉的方式显示光
renderer.outputEncoding = THREE.sRGBEncoding; //色彩饱和度高一点
renderer.toneMapping = THREE.ACESFilmicToneMapping; //电影渲染效果
renderer.toneMappingExposure = 1.5; //电影效果曝光度
给物体添加渐变色材质
// 三个像素,每个像素一种颜色的图片[黑,暗,亮]
const gradientTexture = textureLoader.load("/images/texture/3_colors.png");
// 就近选色,不要自动进行颜色过渡
gradientTexture.magFilter = THREE.NearestFilter;
const material = new THREE.MeshToonMaterial({
color: "#ffffff",
gradientMap: gradientTexture, //设置渐变色(明,暗,更暗)
});
const mesh = new THREE.Mesh(
new THREE.TorusKnotGeometry(0.8, 0.35, 100, 16),
material
);
mesh.castShadow = true;
CubeTextureLoader 立体场景
scene.background = new THREE.CubeTextureLoader()
.setPath("/images/skybox/race.parts/")
.load([
"px.png", //left(+x)
"nx.png", //right(-x)
"py.png", //top(+y)
"ny.png", //bottom(-y)
"pz.png", //front(+z)
"nz.png", //back(-z)
]);
视差效果
粒子效果
/**
* 创建粒子
*/
const particlesCount = 200; //粒子数量
const particlesPostions = new Float32Array(particlesCount * 3); //每三个长度单位代表一个(粒子)坐标
for (let i = 0; i < particlesCount; i++) {
particlesPostions[i * 3 + 0] = (Math.random() - 0.5) * 12; //i.x
particlesPostions[i * 3 + 1] =
objectDistance / 2 - Math.random() * (objectDistance * objects.length); //i.y [-height, 2*height+height/2]
particlesPostions[i * 3 + 2] = (Math.random() - 0.5) * 12; //i.z
}
const particlesGeometry = new THREE.BufferGeometry(); //创建Buffer物体
// 设置Buffer物体的位置信息
particlesGeometry.setAttribute(
"position",
new THREE.BufferAttribute(particlesPostions, 3)
);
// 粒子材质
const particlesMaterial = new THREE.PointsMaterial({
color: palette.material_color,
sizeAttenuation: true, //开启粒子衰减,越远越小
size: 0.03,
});
// 粒子物体
const particlesMesh = new THREE.Points(particlesGeometry, particlesMaterial);
scene.add(particlesMesh);
物理世界 CANNON.js,设置材料的几种方式
- 组合两种材质
// Materials-给重力世界增加材质,模拟真实世界物体接触的效果(摩擦力、反弹力)
const concreteMaterial = new CANNON.Material("concrete"); //混凝土材质
const plasticMaterial = new CANNON.Material("plastic"); //塑料材质
const concretePlasticContactMaterial = new CANNON.ContactMaterial(
concreteMaterial,
plasticMaterial,
{
friction: 0.1, //摩擦力
restitution: 0.7, //反弹力
}
);
world.addContactMaterial(concretePlasticContactMaterial);
// 并且,把concreteMaterial和plasticMaterial分别加到地板和物体上去
// Sphere
const sphereShape = new CANNON.Sphere(1); //和ball半径一致
const sphereBody = new CANNON.Body({
mass: 5, //球体质量
position: new CANNON.Vec3(0, 8, 0), //用真空三位向量定位
shape: sphereShape,
material: plasticMaterial, //++
}); //类似Three.Group
world.addBody(sphereBody);
// Floor
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
floorBody.mass = 0;
floorBody.material = concreteMaterial; //++
floorBody.addShape(floorShape);
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); //沿X轴旋转90度,放平
world.addBody(floorBody);
- 只用一种默认的材料
/**方式2.只用一种默认的材料 */
const defaultMaterial = new CANNON.Material("default");
const defaultContactMaterial = new CANNON.ContactMaterial(
defaultMaterial,
defaultMaterial,
{
friction: 0.1, //摩擦力
restitution: 0.7, //反弹力
}
);
world.addContactMaterial(defaultContactMaterial);
// Sphere
const sphereShape = new CANNON.Sphere(1); //和ball半径一致
const sphereBody = new CANNON.Body({
mass: 5, //球体质量
position: new CANNON.Vec3(0, 8, 0), //用真空三位向量定位
shape: sphereShape,
material: defaultMaterial,
}); //类似Three.Group
world.addBody(sphereBody);
// Floor
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
floorBody.mass = 0;
floorBody.material = defaultMaterial;
floorBody.addShape(floorShape);
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); //沿X轴旋转90度,放平
world.addBody(floorBody);
- 最精简方式:只给 world 设置默认材质,不再给物体和地板设置材料
/**方式3.只给world设置默认材质 */
const defaultMaterial = new CANNON.Material("default");
const defaultContactMaterial = new CANNON.ContactMaterial(
defaultMaterial,
defaultMaterial,
{
friction: 0.1, //摩擦力
restitution: 0.7, //反弹力
}
);
world.addContactMaterial(defaultContactMaterial);
world.defaultContactMaterial = defaultContactMaterial; //++
// Sphere
const sphereShape = new CANNON.Sphere(1); //和ball半径一致
const sphereBody = new CANNON.Body({
mass: 5, //球体质量
position: new CANNON.Vec3(0, 8, 0), //用真空三位向量定位
shape: sphereShape,
}); //类似Three.Group
world.addBody(sphereBody);
// Floor
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
floorBody.mass = 0;
floorBody.addShape(floorShape);
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); //沿X轴旋转90度,放平
world.addBody(floorBody);
物理世界,增加可接触地板
// Canno-Floor
const floorShape = new CANNON.Plane();
const floorBody = new CANNON.Body();
floorBody.mass = 0;
floorBody.addShape(floorShape);
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2); //沿X轴旋转90度,放平
world.addBody(floorBody);
物理世界 CANNON.js,作用力
1.applyLocalForce 给物体局部的作用力
sphereBody.applyLocalForce(
new CANNON.Vec3(300, 0, 0),
new CANNON.Vec3(0, 0, 0)
); //朝X轴正方向,给球中心一个300的力
2.applyForce 给全局的作用力
sphereBody.applyForce(new CANNON.Vec3(-1, 0, 0), sphereBody.position); //朝X轴反方向,持续给一个模拟风吹的力,可以放在animate里持续调用
物理世界,监听碰撞接触,并优化碰撞发声技巧
let controlObjects = [];
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
const boxMaterial = new THREE.MeshStandardMaterial({
color: "silver",
roughness: 0.3,
metalness: 0.3,
});
function createBoxObject(width, height, depth, position = { x, y, z }) {
// 立方体-Mesh
const mesh = new THREE.Mesh(boxGeometry, boxMaterial);
mesh.scale.set(width, height, depth);
mesh.position.copy(position);
mesh.castShadow = true;
scene.add(mesh);
// 立方体-Body
const boxShape = new CANNON.Box(
new CANNON.Vec3(width / 2, height / 2, depth / 2)
); //canno.js的立方体不同之处:以盒子中心点为起点向对立方向画w,h,d
const body = new CANNON.Body({
mass: 1, //立方体质量
position: new CANNON.Vec3(0, 3, 0), //用真空三位向量定位
shape: boxShape,
material: defaultMaterial,
}); //类似Three.Group
body.position.copy(position);
// 监听碰撞接触
body.addEventListener("collide", playHitSound);
world.addBody(body);
controlObjects.push({
mesh,
body,
});
}
// Sounds
const hitSound = new Audio("/sounds/collide.mp3");
/**
* 优化发生技巧!
* collision:碰撞强度
* 1.可以根据collision.contact.getImpactVelocityAlongNormal()获取到碰撞强度
* 2.根据碰撞强度,选择性的播放声音
* 3.根据碰撞强度,播放不同音量大小的声音
* 4.根据碰撞强度,播放不同声音
* 5.可以通过随机音量,让效果更真实
* 6.开始播放前,先暂停上一次播放,效果更真实
*/
const playHitSound = (collision) => {
hitSound.currentTime = 0;
if (!collision) {
hitSound.play();
return;
}
let strong = collision.contact.getImpactVelocityAlongNormal(); //获取碰撞强度
if (strong > 1.5) {
hitSound.volume = Math.random(); //随机音量,更真实
hitSound.currentTime = 0;
hitSound.play();
}
};
const clock = new THREE.Clock();
let oldElapsedTime = 0;
function animate() {
// update physic world
const elapsedTime = clock.getElapsedTime();
const deltaTime = elapsedTime - oldElapsedTime;
oldElapsedTime = elapsedTime;
world.step(1 / 60, deltaTime, 3);
// 批量更新重力物体
controlObjects.map(({ mesh, body }) => {
mesh.position.copy(body.position); //用物理世界中的物体位置更新threejs中的物体位置
mesh.quaternion.copy(body.quaternion); //处理旋转,如立方体下落或者碰撞应该会倾倒再静止
});
renderer.render(scene, camera);
window.requestAnimationFrame(animate);
}
animate();
物理世界,碰撞 1.性能优化
// 创建重力系统
let world = new CANNON.World();
world.boradphase = new CANNON.SAPBroadphase(world); //一种更优质高效的碰撞检测(检测自身cell和相邻的cell里的物体)
world.allowSleep = true; //允许物体沉睡,这样就不会去测试它,提高性能!
world.gravity.set(0, -9.82, 0);
gui 的使用
新建一个操作对象,并把键注册到 gui 操作面板。
gui.add(debugObj, "生成立方体");
let controlObjects = [];
const gui = new dat.GUI({ name: "My GUI" });
const folder1 = gui.addFolder("Test Field");
const debugObj = {
生成立方体: () => {
console.log("生成立方体");
createBoxObject(Math.random() * 3, Math.random() * 3, Math.random() * 3, {
x: (Math.random() - 0.5) * 10,
y: 4,
z: (Math.random() - 0.5) * 10,
});
},
移除物体: () => {
console.log("移除物体");
controlObjects.map(({ mesh, body }) => {
body.removeEventListener("collide", playHitSound);
world.removeBody(body);
scene.remove(mesh);
});
controlObjects = [];
},
};
gui.add(debugObj, "生成立方体");
gui.add(debugObj, "移除物体");
⏱️ 播放模型动画
1.多个独立动画:直接按索引取对应动画播放
playerMixer = new THREE.AnimationMixer(gltf.scene);
actionIdel = gltf.animations[0]; //动画1
actionWalk = gltf.animations[1]; //动画2
// 分别取出播放
playerMixer.clipAction(actionIdel).play();
playerMixer.clipAction(actionWalk).play();
// 一个提取动画的方法
function createModelActions(gltf) {
let mixer = new THREE.AnimationMixer(gltf.scene);
let actions = { lastAction: "" };
gltf.animations.map((item) => {
actions[item.name] = mixer.clipAction(item); //存储动作播放函数
});
return { mixer, actions };
}
2.多个动画合并为一个动画:需借助 threejs 动画工具进行裁剪。
比如 idel、walk、jump 这些动作都在一个时间线上,需要借助 threejs 的动画工具函数对动画帧按照指定时间进行裁切,分别得到需要的动画。
playerMixer = new THREE.AnimationMixer(gltf.scene);
const clipIdel = THREE.AnimationUtils.subclip(
gltf.animations[0],
"idel",
0,
30
);
actionIdel = playerMixer.clipAction(clipIdel);
actionIdel.play();
3.多个动画流畅切换
function switchAction(curAction, newAction) {
curAction.fadeOut(0.3); //淡出
newAction.reset(); //重置播放位置
newAction.setEffectiveWeight(1); //设置动画权重
newAction.play();
newAction.fadeIn(0.3); //淡入
}
设置视频纹理报错
【THREE.WebGLState: DOMException: Failed to execute 'texImage2D' on 'WebGLRenderingContext': The video element contains cross-origin data, and may not be loaded.】
给 video 增加 crossorigin 属性,video.setAttribute("crossorigin", "anonymous");
。
iOS 设备播放失败
ios 设备禁止自动播放,所以默认情况下,创建好 video 后是不能调用 video.play()的。需要在合适的时机再去触发!
在一些移动浏览器(chrome)里自动个全屏播放,
给 video 增加两个属性,video.setAttribute("webkit-playsinline", true);video.setAttribute("playsinline", true);
以下是一个完善的创建 vidoe 的方法:
function createVideo({ src = "", loop = true, muted = true }) {
const video = document.createElement("video");
video.src = src;
video.loop = loop;
video.setAttribute("webkit-playsinline", true);
video.setAttribute("playsinline", true);
video.setAttribute("crossorigin", "anonymous"); //解决视频资源跨域问题,否则在设置VideoTexture时会报错
video.muted = muted;
// video.play(); //如果直接播放 ios上会出错!
return video;
}