实用指南:Threejs案例实践笔记
2025-09-21 21:41 tlnshuju 阅读(18) 评论(0) 收藏 举报目录
1. 圣诞贺卡
1.1. 项目搭建
安装库
安装threejs
npm install three 或 yarn add three 或 pnpm install three安装gsap
npm install gsap 或 yarn add gsap 或 pnpm install gsap
项目初始化
准备:
准备场景模型scene.glb,并放置相应目录
准备解压器文件夹draco,并放置相应目录
初始代码
导入依赖,定义变量关联元素
import { ref, onMounted } from 'vue' import * as THREE from "three" import gsap from 'gsap' // 导入动画gsap import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js" // 轨道控制器 import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js' // 导入Draco加载器 import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' // 导入gltf加载器 const demo = ref(null)静态页面配置
<template> <div ref="demo" /> < /template> <style scoped> * { margin: 0; padding: 0; } canvas { width: 100%; height: 100%; position: fixed; left: 0; top: 0; } < /style>threejs初始化配置
// 1. --- 创建场景 --- const scene = new THREE.Scene() // 2. --- 创建相机 --- const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) camera.position.set(-3.23, 2.98, 4) // 相机位置 camera.updateProjectionMatrix() // 3. --- 创建渲染器 --- const renderer = new THREE.WebGLRenderer({ antialias: true, // 开启抗锯齿 }) renderer.setSize(window.innerWidth, window.innerHeight) demo.value.appendChild(renderer.domElement) // 4. --- 轨道控制器 --- const controls = new OrbitControls(camera, renderer.domElement) controls.enableDamping = true // 设置阻尼 controls.dampingFactor = 0.05 // 5. --- 模型加载 --- const dracoLoader = new DRACOLoader() // 实例化draco加载器 dracoLoader.setDecoderPath("./draco/") // 设置draco解压器路径 const gltfLoader = new GLTFLoader() // 实例化gltf加载器 gltfLoader.setDRACOLoader(dracoLoader) // gltf加载器关联draco解压器 gltfLoader.load( "../public/model/scene.glb", (gltf) => scene.add(gltf.scene) ) // 6. --- 添加光源 --- const light = new THREE.DirectionalLight(0xffffff, 1) light.position.set(0, 50, 0) scene.add(light) // 7. --- 渲染 --- const render = () => { requestAnimationFrame(render) // 请求动画帧 renderer.render(scene, camera) controls.update() } render() // 窗口大小调整 window.addEventListener("resize", () => { renderer.setSize(window.innerWidth, window.innerHeight) camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() }) onMounted(() => { demo.value.appendChild(renderer.domElement) })效果图:

1.2. 水面与天空
准备环境纹理贴图sky.hdr,并放置相应目录
加载环境纹理
import { RGBELoader } from 'three/examples/jsm/Addons.js' // 导入加载器 ... // 加载环境纹理 const rgbeLoader = new RGBELoader() rgbeLoader.load( '/texture/hdr/sky.hdr', (texture) => { texture.mapping = THREE.EquirectangularReflectionMapping // 设置纹理映射 scene.background = texture scene.environment = texture }, ) // 设置色调映射 renderer.toneMapping = THREE.ACESFilmicToneMapping controls.enableDamping = true效果图:

设置水面效果
import { Water } from 'three/examples/jsm/objects/Water2.js' // 导入自带水面库 ... // 设置水面效果 const waterGeometry = new THREE.CircleGeometry(300, 32) const water = new Water(waterGeometry, { textureWidth: 1024, textureHeight: 1024, color: 0xeeeeff, flowDirection: new THREE.Vector2(1, 1), // waves direction scale: 100, // wavesize }) water.rotation.x = -Math.PI / 2 water.position.y = -0.4 scene.add(water)效果图:

1.3. 场景光源
给小木屋添加点光源
注意开启阴影要对渲染器、物体、光源分别进行设置
// 设置色调映射 ... renderer.shadowMap.enabled = true; // 1.渲染器开启阴影 renderer.physicallyCorrectLights = true; ... gltfLoader.load("/model/scene.glb", (gltf) => { const model = gltf.scene model.traverse((child) => { ... // 2.物体允许接收后投射阴影 if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; } }) ... } ) // 点光源 const pointLight = new THREE.PointLight(0xffffff, 10) pointLight.position.set(0.1, 2.4, 0) pointLight.castShadow = true // 3.点光源开启阴影 scene.add(pointLight)效果图:

循环创建发光小球并绑定点光源,组成点光源组
// 创建点光源 const pointLight = new THREE.PointLight(0xffffff, 50) pointLight.position.set(0.1, 2.4, 0) pointLight.castShadow = true // 创建点光源组 const pointLightGroup = new THREE.Group() pointLightGroup.position.set(-8, 2.5, -1.5) const radius = 3 const pointLightList = [] for (let i = 0; i < 3; i++) { // 创建球体用于绑定光源 const sphereGeometry = new THREE.SphereGeometry(0.2, 32, 32) const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xffffff, emissiveIntensity: 10 // 亮度 }) const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial) pointLightList.push(sphere); sphere.position.set( radius * Math.cos(i * 2 * Math.PI / 3), Math.cos(i * 2 * Math.PI / 3), radius * Math.sin(i * 2 * Math.PI / 3), ) const pointLight = new THREE.PointLight(0xffffff, 50) sphere.add(pointLight) // 绑定点光源 pointLightGroup.add(sphere) } scene.add(pointLightGroup)这里来解释这段代码:
代码:radius * Math.cos(i * 2 * Math.PI / 3)
圆为2π( 即2 * Math.PI),我们有三个小球,那么每个小球就应该相隔 (2 / 3)π, 那么每个小球的位置应该是i * (2 /3)π,即分别为0,(2 / 3)π ,(4 /3)* π
pointLightList是干啥用的?:这里只是往pointLightList添加小球,小球绑定了点光源,那么后面的gsap动画就直接操作小球就可以了(这里threejs应该在底层做了优化, 不然 pointLightList.push(sphere),应该写在最后一行的)

使点光源组运动
// 使用补间函数,从0到2π,使小球旋转运动 const options = { angle: 0, }; gsap.to(options, { angle: Math.PI * 2, duration: 10, repeat: -1, ease: "linear", // 这里要用双引号,不然要报错 onUpdate: () => { pointLightGroup.rotation.y = options.angle; // 让球上下摆动 pointLightList.forEach((item, index) => { item.position.set( radius * Math.cos(index * 2 * Math.PI / 3), Math.cos(index * 2 * Math.PI / 3 + options.angle * 5), radius * Math.sin(index * 2 * Math.PI / 3) ) }); }, })效果图:
光源组运动
1.4. 相机与文字
监听鼠标滚轮事件
const index = ref(0) window.addEventListener('wheel', (e) => { if (e.deltaY > 0) { index.value++ if (index.value >= scenes.length) { index.value = 0 } } scenes[index.value].callback() }, false )利用数组存储数据即交互逻辑
const scenes = [ { text: '《认真的白学》', callback: () => { // 切换对应位置 translateCamera( new THREE.Vector3(-3.23, 3, 4.06), new THREE.Vector3(-8, 2, 0) ) console.log(111); } }, { text: '学下得那么深', callback: () => { // 切换对应位置 translateCamera( new THREE.Vector3(7, 0, 23), new THREE.Vector3(0, 0, 0) ) console.log(222); } }, { text: '学得那么认真', callback: () => { // 切换对应位置 translateCamera( new THREE.Vector3(10, 3, 0), new THREE.Vector3(5, 2, 0) ) console.log(333); } }, { text: '倒映出我躺在学中的伤痕', callback: () => { // 切换对应位置 translateCamera( new THREE.Vector3(7, 0, 23), new THREE.Vector3(0, 0, 0) ) console.log(444); } }, { text: '完', callback: () => { // 切换对应位置 translateCamera( new THREE.Vector3(-20, 1.3, 6.6), new THREE.Vector3(5, 2, 0) ) console.log(555); } } ]微调一下结构样式,添加相应文字
<template> <div ref="demo"> <h1 style="padding: 100px 50px; font-size: 50px; color: #fff" class= "text"> { { scenes[index].text } } < /h1> < /div> < /template> <style scoped> * { margin: 0; padding: 0; } canvas { width: 100%; height: 100%; position: fixed; left: 0; top: 0; z-index: 1; } .text { position: fixed; z-index: 2; } < /style>使用补间动画移动相机
let timeLine1 = gsap.timeline() let timeLine2 = gsap.timeline() // 相机的移动(position:移动的目标位置, target:看向的位置) const translateCamera = (position, target) => { timeLine1.to(camera.position, { x: position.x, y: position.y, z: position.z, duration: 1, ease: "power2.inOut", }) timeLine2.to(controls.target, { x: target.x, y: target.y, z: target.z, duration: 1, ease: "power2.inOut", }) }
1.5. 星与心
创建星实例并随即分布到天上
// 实例化满天星 const starsInstance = new THREE.InstancedMesh( new THREE.SphereGeometry(0.1, 32, 32), sphereMaterial, 100 ) // 随机分布到天上 const starsArr = [] const endArr = [] for (let i = 0; i < 100; i++) { const x = (Math.random() - 0.5) * 100 const y = (Math.random() - 0.5) * 100 const z = (Math.random() - 0.5) * 100 starsArr.push(new THREE.Vector3(x, y, z)) const matrix = new THREE.Matrix4() matrix.setPosition(x, y, z) starsInstance.setMatrixAt(i, matrix) } scene.add(starsInstance)创建爱心路径并获取点
// 创建爱心路径 const heartPath = new THREE.Shape() heartPath.moveTo(25, 25) heartPath.bezierCurveTo(25, 25, 20, 0, 0, 0) heartPath.bezierCurveTo(-30, 0, -30, 35, -30, 35) heartPath.bezierCurveTo(-30, 55, -10, 77, 25, 95) heartPath.bezierCurveTo(60, 77, 80, 55, 80, 35) heartPath.bezierCurveTo(80, 35, 80, 0, 50, 0) heartPath.bezierCurveTo(35, 0, 25, 25, 25, 25) // 根据爱心路径获取点 const center = new THREE.Vector3(0, 2, 10) for (let i = 0; i < 100; i++) { const point = heartPath.getPoint(i / 100) endArr.push(new THREE.Vector3( point.x * 0.1 + center.x, point.y * 0.1 + center.y, center.z)) }创建满天星和爱心的切换动画
// 创建爱心动画 const makeHeart = () => { const params = { time: 0 } gsap.to(params, { time: 1, duration: 1, onUpdate: () => { for (let i = 0; i < 100; i++) { const x = starsArr[i].x + (endArr[i].x - starsArr[i].x) * params.time const y = starsArr[i].y + (endArr[i].y - starsArr[i].y) * params.time const z = starsArr[i].z + (endArr[i].z - starsArr[i].z) * params.time const matrix = new THREE.Matrix4() matrix.setPosition(x, y, z) starsInstance.setMatrixAt(i, matrix) } starsInstance.instanceMatrix.needsUpdate = true } }) } // 爱心复原 const restoreHeart = () => { const params = { time: 0, } gsap.to(params, { time: 1, duration: 1, onUpdate: () => { for (let i = 0; i < 100; i++) { const x = endArr[i].x + (starsArr[i].x - endArr[i].x) * params.time const y = endArr[i].y + (starsArr[i].y - endArr[i].y) * params.time const z = endArr[i].z + (starsArr[i].z - endArr[i].z) * params.time const matrix = new THREE.Matrix4() matrix.setPosition(x, y, z) starsInstance.setMatrixAt(i, matrix) } starsInstance.instanceMatrix.needsUpdate = true } }) }
1.6. 代码奉上
- 代码差异
- 资源路径可能不同,按照实际路径修改几款
- 部分静态页面实现不同,我的文字使用固定定位和层级处理,切换相机没有动画
- 我的水面代码好像有问题,没有波光粼粼的感觉
1.6.1. 最终效果
1.6.2. 我的代码
<script setup>
import { ref, onMounted
} from 'vue'
import * as THREE from "three"
import gsap from 'gsap' // 导入动画gsap
import { OrbitControls
} from "three/examples/jsm/controls/OrbitControls.js" // 轨道控制器
import { DRACOLoader
} from 'three/examples/jsm/loaders/DRACOLoader.js' // 导入Draco加载器
import { GLTFLoader
} from 'three/examples/jsm/loaders/GLTFLoader.js' // 导入gltf加载器
import { RGBELoader
} from 'three/examples/jsm/loaders/RGBELoader'
import { Water
} from 'three/examples/jsm/objects/Water2.js'
const demo = ref(null)
// 1. --- 创建场景 ---
const scene = new THREE.Scene()
// 2. --- 创建相机 ---
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
)
camera.position.set(-3.23, 2.98, 4) // 相机位置
camera.updateProjectionMatrix()
// 3. --- 创建渲染器 ---
const renderer = new THREE.WebGLRenderer({
antialias: true, // 开启抗锯齿
})
renderer.setSize(window.innerWidth, window.innerHeight)
// 设置色调映射
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.5;
renderer.shadowMap.enabled = true;
// 渲染器开启阴影
renderer.physicallyCorrectLights = true;
// 4. --- 轨道控制器 ---
const controls = new OrbitControls(camera, renderer.domElement)
// controls.enableDamping = true // 设置阻尼
// 5. --- 模型加载 ---
const dracoLoader = new DRACOLoader() // 实例化draco加载器
dracoLoader.setDecoderPath("./draco/") // 设置draco解压器路径
const gltfLoader = new GLTFLoader() // 实例化gltf加载器
gltfLoader.setDRACOLoader(dracoLoader) // gltf加载器关联draco解压器
gltfLoader.load("/model/scene.glb",
(gltf) =>
{
const model = gltf.scene
model.traverse((child) =>
{
if (child.name === "Plane") {
child.visible = false
}
// 物体允许接收后投射阴影
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
})
scene.add(model)
}
)
// 加载环境纹理
const rgbeLoader = new RGBELoader()
rgbeLoader.load(
'/texture/hdr/sky.hdr',
(texture) =>
{
texture.mapping = THREE.EquirectangularReflectionMapping // 设置纹理映射
scene.background = texture
scene.environment = texture
},
)
// 设置水面效果
const waterGeometry = new THREE.CircleGeometry(300, 32)
const water = new Water(waterGeometry, {
textureWidth: 1024,
textureHeight: 1024,
color: 0xeeeeff,
flowDirection: new THREE.Vector2(1, 1), // waves direction
scale: 100, // wavesize
})
water.rotation.x = -Math.PI / 2
water.position.y = -0.4
scene.add(water)
// 6. --- 添加光源 ---
const light = new THREE.DirectionalLight(0xffffff, 1)
light.position.set(0, 50, 0)
scene.add(light)
// 创建点光源
const pointLight = new THREE.PointLight(0xffffff, 50)
pointLight.position.set(0.1, 2.4, 0)
pointLight.castShadow = true
// 创建点光源组
const pointLightGroup = new THREE.Group()
pointLightGroup.position.set(-8, 2.5, -1.5)
const radius = 3
const pointLightList = []
// 创建球体用于绑定光源
const sphereGeometry = new THREE.SphereGeometry(0.2, 32, 32)
const sphereMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
emissive: 0xffffff,
emissiveIntensity: 10 // 亮度
})
for (let i = 0; i <
3; i++) {
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial)
pointLightList.push(sphere);
sphere.position.set(
radius * Math.cos(i * 2 * Math.PI / 3),
Math.cos(i * 2 * Math.PI / 3),
radius * Math.sin(i * 2 * Math.PI / 3),
)
const pointLight = new THREE.PointLight(0xffffff, 50)
sphere.add(pointLight) // 绑定点光源
pointLightGroup.add(sphere)
}
scene.add(pointLightGroup)
// 使用补间函数,从0到2π,使小球旋转运动
const options = {
angle: 0,
};
gsap.to(options, {
angle: Math.PI * 2,
duration: 10,
repeat: -1,
ease: "linear", // 这里要用双引号,不然要报错
onUpdate: () =>
{
pointLightGroup.rotation.y = options.angle;
// 让球上下摆动
pointLightList.forEach((item, index) =>
{
item.position.set(
radius * Math.cos(index * 2 * Math.PI / 3),
Math.cos(index * 2 * Math.PI / 3 + options.angle * 5),
radius * Math.sin(index * 2 * Math.PI / 3)
)
});
},
})
// 7. --- 渲染 ---
const render = () =>
{
requestAnimationFrame(render) // 请求动画帧
renderer.render(scene, camera)
controls.update()
}
render()
// 数组存储数据即交互逻辑
const scenes = [
{
text: '《认真的白学》',
callback: () =>
{
// 切换对应位置
translateCamera(
new THREE.Vector3(-3.23, 3, 4.06),
new THREE.Vector3(-8, 2, 0)
)
}
}, {
text: '学下得那么深',
callback: () =>
{
// 切换对应位置
translateCamera(
new THREE.Vector3(7, 0, 23),
new THREE.Vector3(0, 0, 0)
)
}
}, {
text: '学得那么认真',
callback: () =>
{
// 切换对应位置
translateCamera(
new THREE.Vector3(10, 3, 0),
new THREE.Vector3(5, 2, 0)
)
}
}, {
text: '倒映出我躺在学中的伤痕',
callback: () =>
{
// 切换对应位置
translateCamera(
new THREE.Vector3(7, 0, 23),
new THREE.Vector3(0, 0, 0)
)
// 爱心
makeHeart()
}
}, {
text: '完',
callback: () =>
{
// 切换对应位置
translateCamera(
new THREE.Vector3(-20, 1.3, 6.6),
new THREE.Vector3(5, 2, 0)
)
}
}
]
const index = ref(0)
// 监听鼠标滚轮事件
window.addEventListener('wheel', (e) =>
{
if (e.deltaY >
0) {
index.value++
if (index.value >= scenes.length) {
index.value = 0
restoreHeart() // 复原
}
}
scenes[index.value].callback()
},
false
)
// 使用补间动画移动相机
let timeLine1 = gsap.timeline()
let timeLine2 = gsap.timeline()
// 相机的移动(position:移动的目标位置, target:看向的位置)
const translateCamera = (position, target) =>
{
timeLine1.to(camera.position, {
x: position.x,
y: position.y,
z: position.z,
duration: 1,
ease: "power2.inOut",
})
timeLine2.to(controls.target, {
x: target.x,
y: target.y,
z: target.z,
duration: 1,
ease: "power2.inOut",
})
}
console.log(scenes);
// 实例化满天星
const starsInstance = new THREE.InstancedMesh(
new THREE.SphereGeometry(0.1, 32, 32),
sphereMaterial,
100
)
// 随机分布到天上
const starsArr = []
const endArr = []
for (let i = 0; i <
100; i++) {
const x = (Math.random() - 0.5) * 100
const y = (Math.random() - 0.5) * 100
const z = (Math.random() - 0.5) * 100
starsArr.push(new THREE.Vector3(x, y, z))
const matrix = new THREE.Matrix4()
matrix.setPosition(x, y, z)
starsInstance.setMatrixAt(i, matrix)
}
scene.add(starsInstance)
// 创建爱心路径
const heartPath = new THREE.Shape()
heartPath.moveTo(25, 25)
heartPath.bezierCurveTo(25, 25, 20, 0, 0, 0)
heartPath.bezierCurveTo(-30, 0, -30, 35, -30, 35)
heartPath.bezierCurveTo(-30, 55, -10, 77, 25, 95)
heartPath.bezierCurveTo(60, 77, 80, 55, 80, 35)
heartPath.bezierCurveTo(80, 35, 80, 0, 50, 0)
heartPath.bezierCurveTo(35, 0, 25, 25, 25, 25)
// 根据爱心路径获取点
const center = new THREE.Vector3(0, 2, 10)
for (let i = 0; i <
100; i++) {
const point = heartPath.getPoint(i / 100)
endArr.push(new THREE.Vector3(
point.x * 0.1 + center.x,
point.y * 0.1 + center.y,
center.z))
}
// 创建爱心动画
const makeHeart = () =>
{
const params = {
time: 0
}
gsap.to(params, {
time: 1,
duration: 1,
onUpdate: () =>
{
for (let i = 0; i <
100; i++) {
const x = starsArr[i].x + (endArr[i].x - starsArr[i].x) * params.time
const y = starsArr[i].y + (endArr[i].y - starsArr[i].y) * params.time
const z = starsArr[i].z + (endArr[i].z - starsArr[i].z) * params.time
const matrix = new THREE.Matrix4()
matrix.setPosition(x, y, z)
starsInstance.setMatrixAt(i, matrix)
}
starsInstance.instanceMatrix.needsUpdate = true
}
}
)
}
// 爱心复原
const restoreHeart = () =>
{
const params = {
time: 0,
};
gsap.to(params,{
time: 1,
duration: 1,
onUpdate: () =>
{
for (let i = 0; i <
100; i++) {
const x = starsArr[i].x + (starsArr[i].x - endArr[i].x) * params.time
const y = starsArr[i].y + (starsArr[i].y - endArr[i].y) * params.time
const z = starsArr[i].z + (starsArr[i].z - endArr[i].z) * params.time
const matrix = new THREE.Matrix4()
matrix.setPosition(x, y, z)
starsInstance.setMatrixAt(i, matrix)
}
starsInstance.instanceMatrix.needsUpdate = true
}
})
}
onMounted(() =>
{
demo.value.appendChild(renderer.domElement)
})
<
/script>
<template>
<div ref="demo">
<h1 style="padding: 100px 50px; font-size: 50px; color: #fff" class=
"text">
{
{ scenes[index].text
}
}
<
/h1>
<
/div>
<
/template>
<style scoped>
* {
margin: 0;
padding: 0;
}
canvas {
width: 100%;
height: 100%;
position: fixed;
left: 0;
top: 0;
z-index: 1;
}
.text {
position: fixed;
z-index: 2;
}
<
/style>
1.6.3. 老陈打码
<template>
<div class=
"scenes" style="
position: fixed;
left: 0;
top: 0;
z-index: 10;
pointer-events: none;
transition: all 1s;
" :style="{
transform: `translate3d(0, ${-index * 100
}vh, 0)`,
}">
<div v-for="item in scenes" style="width: 100vw; height: 100vh">
<h1 style="padding: 100px 50px; font-size: 50px; color: #fff">
{
{ item.text
}
}
<
/h1>
<
/div>
<
/div>
<
/template>
<script setup>
import * as THREE from "three";
import { OrbitControls
} from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader
} from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader
} from "three/examples/jsm/loaders/DRACOLoader";
import { RGBELoader
} from "three/examples/jsm/loaders/RGBELoader";
import { Water
} from "three/examples/jsm/objects/Water2";
import gsap from "gsap";
import { ref
} from "vue";
// 初始化场景
const scene = new THREE.Scene();
// 初始化相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(-3.23, 2.98, 4.06);
camera.updateProjectionMatrix();
// 初始化渲染器
const renderer = new THREE.WebGLRenderer({
// 设置抗锯齿
antialias: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 设置色调映射
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 0.5;
renderer.shadowMap.enabled = true;
renderer.physicallyCorrectLights = true;
// 设置水面效果
// 初始化控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.target.set(-8, 2, 0);
controls.enableDamping = true;
// 初始化loader
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("./draco/");
const gltfLoader = new GLTFLoader();
gltfLoader.setDRACOLoader(dracoLoader);
// 加载环境纹理
let rgbeLoader = new RGBELoader();
rgbeLoader.load("./textures/sky.hdr", (texture) =>
{
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.background = texture;
scene.environment = texture;
});
// 加载模型
gltfLoader.load("./model/scene.glb", (gltf) =>
{
const model = gltf.scene;
model.traverse((child) =>
{
if (child.name === "Plane") {
child.visible = false;
}
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
scene.add(model);
});
// 创建水面
const waterGeometry = new THREE.CircleGeometry(300, 32);
const water = new Water(waterGeometry, {
textureWidth: 1024,
textureHeight: 1024,
color: 0xeeeeff,
flowDirection: new THREE.Vector2(1, 1),
scale: 100,
});
water.rotation.x = -Math.PI / 2;
water.position.y = -0.4;
scene.add(water);
// 添加平行光
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0, 50, 0);
scene.add(light);
// 添加点光源
const pointLight = new THREE.PointLight(0xffffff, 50);
pointLight.position.set(0.1, 2.4, 0);
pointLight.castShadow = true;
scene.add(pointLight);
// 创建点光源组
const pointLightGroup = new THREE.Group();
pointLightGroup.position.set(-8, 2.5, -1.5);
let radius = 3;
let pointLightArr = [];
for (let i = 0; i <
3; i++) {
// 创建球体当灯泡
const sphereGeometry = new THREE.SphereGeometry(0.2, 32, 32);
const sphereMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff,
emissive: 0xffffff,
emissiveIntensity: 10,
});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
pointLightArr.push(sphere);
sphere.position.set(
radius * Math.cos((i * 2 * Math.PI) / 3),
Math.cos((i * 2 * Math.PI) / 3),
radius * Math.sin((i * 2 * Math.PI) / 3)
);
let pointLight = new THREE.PointLight(0xffffff, 50);
sphere.add(pointLight);
pointLightGroup.add(sphere);
}
scene.add(pointLightGroup);
// 使用补间函数,从0到2π,使灯泡旋转
let options = {
angle: 0,
};
gsap.to(options, {
angle: Math.PI * 2,
duration: 10,
repeat: -1,
ease: "linear",
onUpdate: () =>
{
pointLightGroup.rotation.y = options.angle;
pointLightArr.forEach((item, index) =>
{
item.position.set(
radius * Math.cos((index * 2 * Math.PI) / 3),
Math.cos((index * 2 * Math.PI) / 3 + options.angle * 5),
radius * Math.sin((index * 2 * Math.PI) / 3)
);
});
},
});
function render() {
requestAnimationFrame(render);
renderer.render(scene, camera);
controls.update();
}
render();
// 使用补间动画移动相机
let timeLine1 = gsap.timeline();
let timeline2 = gsap.timeline();
// 定义相机移动函数
function translateCamera(position, target) {
timeLine1.to(camera.position, {
x: position.x,
y: position.y,
z: position.z,
duration: 1,
ease: "power2.inOut",
});
timeline2.to(controls.target, {
x: target.x,
y: target.y,
z: target.z,
duration: 1,
ease: "power2.inOut",
});
}
let scenes = [
{
text: "圣诞快乐",
callback: () =>
{
// 执行函数切换位置
translateCamera(
new THREE.Vector3(-3.23, 3, 4.06),
new THREE.Vector3(-8, 2, 0)
);
},
},
{
text: "感谢在这么大的世界里遇见了你",
callback: () =>
{
// 执行函数切
translateCamera(new THREE.Vector3(7, 0, 23), new THREE.Vector3(0, 0, 0));
},
},
{
text: "愿与你探寻世界的每一个角落",
callback: () =>
{
// 执行函数切
translateCamera(new THREE.Vector3(10, 3, 0), new THREE.Vector3(5, 2, 0));
},
},
{
text: "愿将天上的星星送给你",
callback: () =>
{
// 执行函数切
translateCamera(new THREE.Vector3(7, 0, 23), new THREE.Vector3(0, 0, 0));
makeHeart();
},
},
{
text: "愿疫情结束,大家健康快乐!",
callback: () =>
{
// 执行函数切
translateCamera(
new THREE.Vector3(-20, 1.3, 6.6),
new THREE.Vector3(5, 2, 0)
);
},
},
];
let index = ref(0);
let isAnimate = false;
// 监听鼠标滚轮事件
window.addEventListener(
"wheel",
(e) =>
{
if (isAnimate) return;
isAnimate = true;
if (e.deltaY >
0) {
index.value++;
if (index.value > scenes.length - 1) {
index.value = 0;
restoreHeart();
}
}
scenes[index.value].callback();
setTimeout(() =>
{
isAnimate = false;
}, 1000);
},
false
);
// 实例化创建漫天星星
let starsInstance = new THREE.InstancedMesh(
new THREE.SphereGeometry(0.1, 32, 32),
new THREE.MeshStandardMaterial({
color: 0xffffff,
emissive: 0xffffff,
emissiveIntensity: 10,
}),
100
);
// 星星随机到天上
let starsArr = [];
let endArr = [];
for (let i = 0; i <
100; i++) {
let x = Math.random() * 100 - 50;
let y = Math.random() * 100 - 50;
let z = Math.random() * 100 - 50;
starsArr.push(new THREE.Vector3(x, y, z));
let matrix = new THREE.Matrix4();
matrix.setPosition(x, y, z);
starsInstance.setMatrixAt(i, matrix);
}
scene.add(starsInstance);
// 创建爱心路径
let heartShape = new THREE.Shape();
heartShape.moveTo(25, 25);
heartShape.bezierCurveTo(25, 25, 20, 0, 0, 0);
heartShape.bezierCurveTo(-30, 0, -30, 35, -30, 35);
heartShape.bezierCurveTo(-30, 55, -10, 77, 25, 95);
heartShape.bezierCurveTo(60, 77, 80, 55, 80, 35);
heartShape.bezierCurveTo(80, 35, 80, 0, 50, 0);
heartShape.bezierCurveTo(35, 0, 25, 25, 25, 25);
// 根据爱心路径获取点
let center = new THREE.Vector3(0, 2, 10);
for (let i = 0; i <
100; i++) {
let point = heartShape.getPoint(i / 100);
endArr.push(
new THREE.Vector3(
point.x * 0.1 + center.x,
point.y * 0.1 + center.y,
center.z
)
);
}
// 创建爱心动画
function makeHeart() {
let params = {
time: 0,
};
gsap.to(params, {
time: 1,
duration: 1,
onUpdate: () =>
{
for (let i = 0; i <
100; i++) {
let x = starsArr[i].x + (endArr[i].x - starsArr[i].x) * params.time;
let y = starsArr[i].y + (endArr[i].y - starsArr[i].y) * params.time;
let z = starsArr[i].z + (endArr[i].z - starsArr[i].z) * params.time;
let matrix = new THREE.Matrix4();
matrix.setPosition(x, y, z);
starsInstance.setMatrixAt(i, matrix);
}
starsInstance.instanceMatrix.needsUpdate = true;
},
});
}
function restoreHeart() {
let params = {
time: 0,
};
gsap.to(params, {
time: 1,
duration: 1,
onUpdate: () =>
{
for (let i = 0; i <
100; i++) {
let x = endArr[i].x + (starsArr[i].x - endArr[i].x) * params.time;
let y = endArr[i].y + (starsArr[i].y - endArr[i].y) * params.time;
let z = endArr[i].z + (starsArr[i].z - endArr[i].z) * params.time;
let matrix = new THREE.Matrix4();
matrix.setPosition(x, y, z);
starsInstance.setMatrixAt(i, matrix);
}
starsInstance.instanceMatrix.needsUpdate = true;
},
});
}
<
/script>
<style>
* {
margin: 0;
padding: 0;
}
canvas {
width: 100vw;
height: 100vh;
position: fixed;
left: 0;
top: 0;
}
<
/style>
2. 汽车展示
2.1. 项目搭建
基础代码
<script setup> import { ref, onMounted } from 'vue' import * as THREE from "three" import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js" // 轨道控制器 const demo = ref(null) // 1. --- 创建场景 --- const scene = new THREE.Scene() // 2. --- 创建相机 --- const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ) camera.position.set(0, 2, 6) // 相机位置 camera.updateProjectionMatrix() // 更新投影矩阵 // 3. --- 创建渲染器 --- const renderer = new THREE.WebGLRenderer({ antialias: true, // 开启抗锯齿 }) renderer.setSize(window.innerWidth, window.innerHeight) // 渲染函数 const render = () => { renderer.render(scene, camera) controls && controls.update() requestAnimationFrame(render) // 渲染下一帧 } // 挂载到dom中 onMounted(() => { demo.value.appendChild(renderer.domElement) // 将渲染器添加到场景中 renderer.setClearColor("#000") scene.background = new THREE.Color("#ccc") scene.environment = new THREE.Color("#ccc") const grid = new THREE.GridHelper(10, 10)// 添加网格地面 grid.material.transparent = true grid.material.opacity = 0.2 scene.add(grid) render() // 调用渲染函数 }) // 4. --- 添加控制器 --- const controls = new OrbitControls(camera, renderer.domElement) controls.update() // 5. ---窗口大小调整 --- window.addEventListener("resize", () => { renderer.setSize(window.innerWidth, window.innerHeight) camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() }) < /script> <template> <div ref="demo"> < /div> < /template> <style scoped> * { margin: 0; padding: 0; } canvas { width: 100%; height: 100%; position: fixed; left: 0; top: 0; z-index: 1; } < /style>模型准备
- 准备draco文件夹,将其复制到public文件夹下
- 准备汽车模型car.glb,放到相应文件夹下
效果图:

2.2. 模型与灯光
模型加载
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js' // 导入Draco加载器 import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' // 导入gltf加载器 ... // 模型载入 const loader = new GLTFLoader() const dracoLoader = new DRACOLoader() dracoLoader.setDecoderPath("/draco/") // 设置draco解码器路径 loader.setDRACOLoader(dracoLoader) // 绑定draco解码器 loader.load("/model/car.glb", (gltf) => { scene.add(gltf.scene) })效果图:

因为模型为物理材质,需要光才能看见模型颜色
添加灯光
// 添加灯光 const light1 = new THREE.DirectionalLight(0xffffff, 1) light1.position.set(0, 0, 10) const light2 = new THREE.DirectionalLight(0xffffff, 1) light2.position.set(0, 0, -10) const light3 = new THREE.DirectionalLight(0xffffff, 1) light3.position.set(10, 0, 0) const light4 = new THREE.DirectionalLight(0xffffff, 1) light4.position.set(-10, 0, 0) const light5 = new THREE.DirectionalLight(0xffffff, 1) light5.position.set(0, 10, 0) const light6 = new THREE.DirectionalLight(0xffffff, 1) light6.position.set(5, 10, 0) const light7 = new THREE.DirectionalLight(0xffffff, 1) light7.position.set(0, 10, 5) const light8 = new THREE.DirectionalLight(0xffffff, 1) light8.position.set(0, 10,-5) const light9 = new THREE.DirectionalLight(0xffffff, 1) light9.position.set(-5, 10, 0) scene.add(light1, light2, light3, light4, light5, light6, light7, light8, light9)效果图:

2.3. 车身材质
模型组成
打印gltf.scene,发现汽车是由66个模型组成,接下来我们对这些物体进行设置,来完成对模型的操作
loader.load("/model/car.glb", (gltf) => { scene.add(gltf.scene) console.log(gltf); })
获取模型名字(仅作为参考,最好在3D建模软件导出时就取好名字)
loader.load("/model/car.glb", (gltf) => { const car = gltf.scene car.traverse((child) => { if (child.isMesh) { console.log(child.name); } }) scene.add(car) })
创建变量存储材质
// - 创建材质 - // 车身材质 const bodyMaterial = new THREE.MeshPhysicalMaterial({ color: 0xff0000, metalness: 1, roughness: 0.5, clearcoat: 1, clearcoatRoughness: 0 }) // 前部车身材质 const frontMaterial = new THREE.MeshPhysicalMaterial({ color: 0xff0000, metalness: 1, roughness: 0.5, clearcoat: 1, clearcoatRoughness: 0 }) // 车的轮毂 const wheelMaterial = new THREE.MeshPhysicalMaterial({ color: 0xff0000, metalness: 1, roughness: 0.1, }) // 车玻璃 const glassMaterial = new THREE.MeshPhysicalMaterial({ color: 0xffffff, metalness: 0, roughness: 0, transmission: 1, transparent: true }) // 引擎盖 const hoodMaterial = new THREE.MeshPhysicalMaterial({ color: 0xff0000, metalness: 1, roughness: 0.5, clearcoat: 1, clearcoatRoughness: 0 })对车的各个部位模型进行赋值
loader.load("/model/car.glb", (gltf) => { const car = gltf.scene car.traverse((child) => { if (child.isMesh) { // 判断是否是轮毂 if (child.name.includes("轮毂")) { child.material = wheelMaterial wheels.push(child) } // 判断是否是车身主体 else if (child.name.includes("Mesh002")) { carBody = child carBody.material = bodyMaterial } // 判断是否是前部车身 else if (child.name.includes("前脸")) { frontCar = child frontCar.material = frontMaterial } // 是否是引擎盖 else if (child.name.includes("引擎盖_1")) { hoodCar = child hoodCar.material = hoodMaterial } // 是否是挡风玻璃 else if (child.name.includes("挡风玻璃")) { glassCar = child glassCar.material = glassMaterial } } }) scene.add(car) })效果图:

2.4. 材质切换
静态页面改变(我用了scss,如果要像我这样使用,请安装相应依赖)
<template> <div ref="demo"> <div class= "menu"> <div class= "title"> <h1>汽车展示与选配< /h1> < /div> < !-- 车身颜色 --> <h2>请选择车身颜色< /h2> <div class= "select"> <div class= "option" v-for="(item, index) in colors" :key="index" @click="selectColor(index)"> <div class= "option-color" :style="{ background: item }" /> < /div> < /div> < !-- 贴膜材质 --> <h2>请选择贴膜材质< /h2> <div class= "select"> <div class= "option" v-for="(item, index) in materials" :key="index" @click="selectMaterial(index)"> <button class= "option-material" :style="{ background: item }"> { { item.name } }< /button> < /div> < /div> < /div> < /div> < /template> <style scoped lang="scss"> * { margin: 0; padding: 0; } canvas { width: 100%; height: 100%; position: fixed; left: 0; top: 0; z-index: 1; } .menu { position: fixed; top: 20px; right: 20px; z-index: 2; .select { display: inline-block; display: flex; .option { display: flex; .option-color { width: 30px; height: 30px; border: 1px solid #ccc; margin: 10px; display: inline-block; cursor: pointer; border-radius: 10px; } } } } < /style>颜色切换
// 设置颜色数组 const colors = ["red", "yellow", "blue", "green", "orange", "pink", "purple", "white", "black", "gray"] ... // 颜色选择函数 const selectColor = (index) => { bodyMaterial.color.set(colors[index]) frontMaterial.color.set(colors[index]) glassMaterial.color.set(colors[index]) hoodMaterial.color.set(colors[index]) wheelMaterial.color.set(colors[index]) }贴膜材质切换
// 设置贴膜数组 const materials = [ { name: '磨砂', value: 1 }, { name: '冰晶', value: 0 } ] ... // 材质选择函数 const selectMaterial = (index) => { bodyMaterial.clearcoatRoughness = materials[index].value frontMaterial.clearcoatRoughness = materials[index].value hoodMaterial.clearcoatRoughness = materials[index].value wheelMaterial.clearcoatRoughness = materials[index].value }效果图:
黄色冰晶

绿色磨砂

2.5. 源码奉上
2.5.1.我的代码
<script setup>
import { ref, onMounted
} from 'vue'
import * as THREE from "three"
import { OrbitControls
} from "three/examples/jsm/controls/OrbitControls.js" // 轨道控制器
import { DRACOLoader
} from 'three/examples/jsm/loaders/DRACOLoader.js' // 导入Draco加载器
import { GLTFLoader
} from 'three/examples/jsm/loaders/GLTFLoader.js' // 导入gltf加载器
const demo = ref(null)
// 1. --- 创建场景 ---
const scene = new THREE.Scene()
// - 模型 -
const wheels = [] // 轮子
let carBody, // 主体车身
frontCar, // 前部车身
hoodCar, // 引擎盖
glassCar // 挡风玻璃
// - 创建材质 -
// 车身材质
const bodyMaterial = new THREE.MeshPhysicalMaterial({
color: 0xff0000,
metalness: 1,
roughness: 0.5,
clearcoat: 1,
clearcoatRoughness: 0
})
// 设置颜色数组
const colors = ["red", "yellow", "blue", "green",
"orange", "pink", "purple", "white", "black", "gray"]
// 设置贴膜数组
const materials = [
{ name: '磨砂', value: 1
},
{ name: '冰晶', value: 0
}
]
// 前部车身材质
const frontMaterial = new THREE.MeshPhysicalMaterial({
color: 0xff0000,
metalness: 1,
roughness: 0.5,
clearcoat: 1,
clearcoatRoughness: 0
})
// 车的轮毂
const wheelMaterial = new THREE.MeshPhysicalMaterial({
color: 0xff0000,
metalness: 1,
roughness: 0.1,
})
// 车玻璃
const glassMaterial = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
metalness: 0,
roughness: 0,
transmission: 1,
transparent: true
})
// 引擎盖
const hoodMaterial = new THREE.MeshPhysicalMaterial({
color: 0xff0000,
metalness: 1,
roughness: 0.5,
clearcoat: 1,
clearcoatRoughness: 0
})
// 2. --- 创建相机 ---
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
)
camera.position.set(0, 2, 6) // 相机位置
camera.updateProjectionMatrix() // 更新投影矩阵
// 3. --- 创建渲染器 ---
const renderer = new THREE.WebGLRenderer({
antialias: true, // 开启抗锯齿
})
renderer.setSize(window.innerWidth, window.innerHeight)
// 渲染函数
const render = () =>
{
renderer.render(scene, camera)
controls && controls.update()
requestAnimationFrame(render) // 渲染下一帧
}
// 挂载到dom中
onMounted(() =>
{
demo.value.appendChild(renderer.domElement) // 将渲染器添加到场景中
renderer.setClearColor("#000")
scene.background = new THREE.Color("#ccc")
scene.environment = new THREE.Color("#ccc")
const grid = new THREE.GridHelper(10, 10)// 添加网格地面
grid.material.transparent = true
grid.material.opacity = 0.2
scene.add(grid)
render() // 调用渲染函数
})
// 4. --- 添加控制器 ---
const controls = new OrbitControls(camera, renderer.domElement)
controls.update()
// 模型载入
const loader = new GLTFLoader()
const dracoLoader = new DRACOLoader()
dracoLoader.setDecoderPath("/draco/") // 设置draco解码器路径
loader.setDRACOLoader(dracoLoader) // 绑定draco解码器
loader.load("/model/car.glb", (gltf) =>
{
const car = gltf.scene
car.traverse((child) =>
{
if (child.isMesh) {
// 判断是否是轮毂
if (child.name.includes("轮毂")) {
child.material = wheelMaterial
wheels.push(child)
}
// 判断是否是车身主体
else if (child.name.includes("Mesh002")) {
carBody = child
carBody.material = bodyMaterial
}
// 判断是否是前部车身
else if (child.name.includes("前脸")) {
frontCar = child
frontCar.material = frontMaterial
}
// 是否是引擎盖
else if (child.name.includes("引擎盖_1")) {
hoodCar = child
hoodCar.material = hoodMaterial
}
// 是否是挡风玻璃
else if (child.name.includes("挡风玻璃")) {
glassCar = child
glassCar.material = glassMaterial
}
}
})
scene.add(car)
})
// 添加灯光
const light1 = new THREE.DirectionalLight(0xffffff, 1)
light1.position.set(0, 0, 10)
const light2 = new THREE.DirectionalLight(0xffffff, 1)
light2.position.set(0, 0, -10)
const light3 = new THREE.DirectionalLight(0xffffff, 1)
light3.position.set(10, 0, 0)
const light4 = new THREE.DirectionalLight(0xffffff, 1)
light4.position.set(-10, 0, 0)
const light5 = new THREE.DirectionalLight(0xffffff, 1)
light5.position.set(0, 10, 0)
const light6 = new THREE.DirectionalLight(0xffffff, 1)
light6.position.set(5, 10, 0)
const light7 = new THREE.DirectionalLight(0xffffff, 1)
light7.position.set(0, 10, 5)
const light8 = new THREE.DirectionalLight(0xffffff, 1)
light8.position.set(0, 10, -5)
const light9 = new THREE.DirectionalLight(0xffffff, 1)
light9.position.set(-5, 10, 0)
scene.add(light1, light2, light3, light4, light5, light6, light7, light8, light9)
// 5. ---窗口大小调整
window.addEventListener("resize", () =>
{
renderer.setSize(window.innerWidth, window.innerHeight)
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
})
// 颜色选择函数
const selectColor = (index) =>
{
bodyMaterial.color.set(colors[index])
frontMaterial.color.set(colors[index])
glassMaterial.color.set(colors[index])
hoodMaterial.color.set(colors[index])
wheelMaterial.color.set(colors[index])
}
// 材质选择函数
const selectMaterial = (index) =>
{
bodyMaterial.clearcoatRoughness = materials[index].value
frontMaterial.clearcoatRoughness = materials[index].value
hoodMaterial.clearcoatRoughness = materials[index].value
wheelMaterial.clearcoatRoughness = materials[index].value
}
<
/script>
<template>
<div ref="demo">
<div class=
"menu">
<div class=
"title">
<h1>汽车展示与选配<
/h1>
<
/div>
<
!-- 车身颜色 -->
<h2>请选择车身颜色<
/h2>
<div class=
"select">
<div class=
"option" v-for="(item, index) in colors" :key="index" @click="selectColor(index)">
<div class=
"option-color" :style="{ background: item }" />
<
/div>
<
/div>
<
!-- 贴膜材质 -->
<h2>请选择贴膜材质<
/h2>
<div class=
"select">
<div class=
"option" v-for="(item, index) in materials" :key="index" @click="selectMaterial(index)">
<button class=
"option-material" :style="{ background: item }">
{
{ item.name
}
}<
/button>
<
/div>
<
/div>
<
/div>
<
/div>
<
/template>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
canvas {
width: 100%;
height: 100%;
position: fixed;
left: 0;
top: 0;
z-index: 1;
}
.menu {
position: fixed;
top: 20px;
right: 20px;
z-index: 2;
.title {
}
.select {
display: inline-block;
display: flex;
.option {
display: flex;
.option-color {
width: 30px;
height: 30px;
border: 1px solid #ccc;
margin: 10px;
display: inline-block;
cursor: pointer;
border-radius: 10px;
}
}
}
}
<
/style>
2.5.2.老陈打码
<template>
<div class=
"home">
<div class=
"canvas-container" ref="canvasDom">
<
/div>
<div class=
"home-content">
<div class=
"home-content-title">
<h1>汽车展示与选配<
/h1>
<
/div>
<h2>选择车身颜色<
/h2>
<div class=
"select">
<div
class=
"select-item"
v-for="(item, index) in colors"
:key="index"
@click="selectColor(index)"
>
<div
class=
"select-item-color"
:style="{ backgroundColor: item }"
>
<
/div>
<
/div>
<
/div>
<h2>选择贴膜材质<
/h2>
<div class=
"select">
<div
class=
"select-item"
v-for="(item, index) in materials"
:key="index"
@click="selectMaterial(index)"
>
<div class=
"select-item-text">
{
{ item.name
}
}<
/div>
<
/div>
<
/div>
<
/div>
<
/div>
<
/template>
<script setup>
import * as THREE from "three";
import { onMounted, reactive, ref
} from "vue";
import { OrbitControls
} from "three/examples/jsm/controls/OrbitControls";
import { GLTFLoader
} from "three/examples/jsm/loaders/GLTFLoader";
import { DRACOLoader
} from "three/examples/jsm/loaders/DRACOLoader";
let controls;
let canvasDom = ref(null);
// 创建场景
const scene = new THREE.Scene();
// 创建相机
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.set(0, 2, 6);
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
// 抗锯齿
antialias: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
const render = () =>
{
renderer.render(scene, camera);
controls && controls.update();
requestAnimationFrame(render);
};
let wheels = [];
let carBody, frontCar, hoodCar, glassCar;
// 创建材质
const bodyMaterial = new THREE.MeshPhysicalMaterial({
color: 0xff0000,
metalness: 1,
roughness: 0.5,
clearcoat: 1,
clearcoatRoughness: 0,
});
const frontMaterial = new THREE.MeshPhysicalMaterial({
color: 0xff0000,
metalness: 1,
roughness: 0.5,
clearcoat: 1,
clearcoatRoughness: 0,
});
const hoodMaterial = new THREE.MeshPhysicalMaterial({
color: 0xff0000,
metalness: 1,
roughness: 0.5,
clearcoat: 1,
clearcoatRoughness: 0,
});
const wheelsMaterial = new THREE.MeshPhysicalMaterial({
color: 0xff0000,
metalness: 1,
roughness: 0.1,
});
const glassMaterial = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
metalness: 0,
roughness: 0,
transmission: 1,
transparent: true,
});
let colors = ["red", "blue", "green", "gray", "orange", "purple"];
let selectColor = (index) =>
{
bodyMaterial.color.set(colors[index]);
frontMaterial.color.set(colors[index]);
hoodMaterial.color.set(colors[index]);
wheelsMaterial.color.set(colors[index]);
// glassMaterial.color.set(colors[index]);
};
let materials = [
{ name: "磨砂", value: 1
},
{ name: "冰晶", value: 0
},
];
let selectMaterial = (index) =>
{
bodyMaterial.clearcoatRoughness = materials[index].value;
frontMaterial.clearcoatRoughness = materials[index].value;
hoodMaterial.clearcoatRoughness = materials[index].value;
};
onMounted(() =>
{
// 把渲染器插入到dom中
// console.log(canvasDom.value);
canvasDom.value.appendChild(renderer.domElement);
// 初始化渲染器,渲染背景
renderer.setClearColor("#000");
scene.background = new THREE.Color("#ccc");
scene.environment = new THREE.Color("#ccc");
render();
// 添加网格地面
const gridHelper = new THREE.GridHelper(10, 10);
gridHelper.material.opacity = 0.2;
gridHelper.material.transparent = true;
scene.add(gridHelper);
// 添加控制器
controls = new OrbitControls(camera, renderer.domElement);
controls.update();
// 添加gltf汽车模型
const loader = new GLTFLoader();
const dracoLoader = new DRACOLoader();
dracoLoader.setDecoderPath("./draco/gltf/");
loader.setDRACOLoader(dracoLoader);
loader.load("./model/bmw01.glb", (gltf) =>
{
const bmw = gltf.scene;
// console.log(gltf);
bmw.traverse((child) =>
{
if (child.isMesh) {
console.log(child.name);
}
// 判断是否是轮毂
if (child.isMesh && child.name.includes("轮毂")) {
child.material = wheelsMaterial;
wheels.push(child);
}
// 判断是否是车身
if (child.isMesh && child.name.includes("Mesh002")) {
carBody = child;
carBody.material = bodyMaterial;
}
// 判断是否是前脸
if (child.isMesh && child.name.includes("前脸")) {
child.material = frontMaterial;
frontCar = child;
}
// 判断是否是引擎盖
if (child.isMesh && child.name.includes("引擎盖_1")) {
child.material = hoodMaterial;
hoodCar = child;
}
// 判断是否是挡风玻璃
if (child.isMesh && child.name.includes("挡风玻璃")) {
child.material = glassMaterial;
glassCar = child;
}
});
scene.add(bmw);
});
// 添加灯光
const light1 = new THREE.DirectionalLight(0xffffff, 1);
light1.position.set(0, 0, 10);
scene.add(light1);
const light2 = new THREE.DirectionalLight(0xffffff, 1);
light2.position.set(0, 0, -10);
scene.add(light2);
const light3 = new THREE.DirectionalLight(0xffffff, 1);
light3.position.set(10, 0, 0);
scene.add(light3);
const light4 = new THREE.DirectionalLight(0xffffff, 1);
light4.position.set(-10, 0, 0);
scene.add(light4);
const light5 = new THREE.DirectionalLight(0xffffff, 1);
light5.position.set(0, 10, 0);
scene.add(light5);
const light6 = new THREE.DirectionalLight(0xffffff, 0.3);
light6.position.set(5, 10, 0);
scene.add(light6);
const light7 = new THREE.DirectionalLight(0xffffff, 0.3);
light7.position.set(0, 10, 5);
scene.add(light7);
const light8 = new THREE.DirectionalLight(0xffffff, 0.3);
light8.position.set(0, 10, -5);
scene.add(light8);
const light9 = new THREE.DirectionalLight(0xffffff, 0.3);
light9.position.set(-5, 10, 0);
scene.add(light9);
});
<
/script>
<style>
* {
margin: 0;
padding: 0;
}
.home-content {
position: fixed;
top: 0;
right: 20px;
}
.select-item-color {
width: 50px;
height: 50px;
border: 1px solid #ccc;
margin: 10px;
display: inline-block;
cursor: pointer;
border-radius: 10px;
}
.select {
display: flex;
}
<
/style>
浙公网安备 33010602011771号