This commit is contained in:
2025-11-20 11:35:20 +08:00
parent c925021920
commit f4bec6f03e
23 changed files with 1046 additions and 12 deletions

View File

@@ -2,24 +2,51 @@
## three.js 是什么
https://threejs.org/manual/#zh/fundamentals
## 为什么选择 three.js
- WebGL
- https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API
- three.js
- https://threejs.org/
- http://www.webgl3d.cn/
- Babylon.js
- https://www.babylonjs.com/
- https://cnbabylon.com/
## three.js 核心概念
https://threejs.org/manual/#zh/fundamentals
### Scene 场景
### Camera 相机
### Renderer 渲染器
## three.js 常用类/对象
### Object3D
### Material
https://threejs.org/manual/#zh/materials
### Texture
### OrbitControls
## Demos
### 基础几何体
### 材质与纹理
### 光照与阴影
### 动画与交互
### 粒子系统
### 加载GLB模型
### 添加灯
### 添加控制器、AxesHelper
### 设置地面
### 地面上设置光效
### 模型漫游(巡检业务)
### [x] 模型动画(模型本身存在动画)
### 修改材质(设备告警业务)
### 粒子效果(蒸汽)
### 模型与Web的交互
### 后期处理效果
### 漫游 视角切换
### 标签
### 公共函数抽象
## 如何与建模沟通
## blender使用
## blender使用
## 如何与建模沟通

0
src/composes/index.ts Normal file
View File

View File

View File

@@ -0,0 +1,2 @@
export * from './loader'
export * from './texture'

View File

@@ -0,0 +1,5 @@
import type * as THREE from 'three'
export interface ScenceOptions {
background?: THREE.Texture<unknown> | THREE.CubeTexture | THREE.Color | null
}

View File

@@ -0,0 +1,13 @@
import * as THREE from 'three'
export function useThreeLoader() {
const textureLoader = new THREE.TextureLoader()
return {
textureLoader,
}
}
const threeLoaders = useThreeLoader()
export { threeLoaders }

View File

@@ -0,0 +1,5 @@
import * as THREE from 'three'
export function useThreeMesh() {
}

View File

@@ -0,0 +1,9 @@
import * as THREE from 'three'
export function useThreeScene() {
function createScene(options) {
const scene = new THREE.Scene()
}
return { createScene }
}

View File

@@ -0,0 +1,22 @@
import type * as THREE from 'three'
import { threeLoaders } from './loader'
export function useThreeTexture() {
// 加载纹理贴图
function loadTexture(src: string) {
return new Promise<THREE.Texture>((resolve, reject) => {
threeLoaders.textureLoader.load(
src,
(texture) => {
resolve(texture)
},
undefined,
(err) => {
reject(err)
},
)
})
}
return { loadTexture }
}

View File

@@ -9,6 +9,61 @@ const router = createRouter({
name: 'home',
component: HomeView,
},
{
path: '/three/1',
name: 'three-1',
component: () => import('../views/three/1/index.vue'),
},
{
path: '/three/2',
name: 'three-2',
component: () => import('../views/three/2/index.vue'),
},
{
path: '/three/3',
name: 'three-3',
component: () => import('../views/three/3/index.vue'),
},
{
path: '/three/4',
name: 'three-4',
component: () => import('../views/three/4/index.vue'),
},
{
path: '/three/5',
name: 'three-5',
component: () => import('../views/three/5/index.vue'),
},
{
path: '/three/6',
name: 'three-6',
component: () => import('../views/three/6/index.vue'),
},
{
path: '/three/7',
name: 'three-7',
component: () => import('../views/three/7/index.vue'),
},
{
path: '/three/8',
name: 'three-8',
component: () => import('../views/three/8/index.vue'),
},
{
path: '/three/9',
name: 'three-9',
component: () => import('../views/three/9/index.vue'),
},
{
path: '/three/10',
name: 'three-10',
component: () => import('../views/three/10/index.vue'),
},
{
path: '/three/11',
name: 'three-11',
component: () => import('../views/three/11/index.vue'),
},
{
path: '/3d-point',
name: '3d-point',

View File

@@ -1,5 +1,18 @@
<script lang="ts" setup>
const demos = [{ name: '3D Point', path: '/3d-point' }]
const demos = [
{ name: '加载GLB模型', path: '/three/1' },
{ name: '添加灯光', path: '/three/2' },
{ name: '添加控制器、AxesHelper', path: '/three/3' },
{ name: '设置地面', path: '/three/3' },
{ name: '地面上设置光效', path: '/three/4' },
{ name: '模型漫游(巡检业务)', path: '/three/5' },
{ name: '模型动画(模型本身存在动画)', path: '/three/6' },
{ name: '漫游 修改材质(设备告警业务)', path: '/three/7' },
{ name: '粒子效果(蒸汽)', path: '/three/8' },
{ name: '模型与Web的交互', path: '/three/9' },
{ name: '后期处理效果', path: '/three/10' },
{ name: '公共函数抽象', path: '/three/11' },
]
</script>
<template>

View File

@@ -0,0 +1,75 @@
<script lang="ts" setup>
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { onMounted, shallowRef } from 'vue'
const threeRef = shallowRef<HTMLCanvasElement>()
function initModel() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
const scene = new THREE.Scene()
scene.background = new THREE.Color(0xFFFFFF)
// 创建透视相机 PerspectiveCamera(视野角度, 宽高比, 近截面, 远截面)
// 透视相机更符合人眼的视觉效果 近大远小
const camera = new THREE.PerspectiveCamera(
75,
width / height,
0.1,
1000,
)
camera.position.set(0, 2, 12)
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
// 开启阴影贴图
renderer.shadowMap.enabled = true
// 设置阴影贴图类型 THREE.PCFSoftShadowMap=柔和阴影 THREE.PCFShadowMap=硬阴影
// 柔和阴影计算量更大,性能更低
// 硬阴影计算量更小,性能更高
renderer.shadowMap.type = THREE.PCFSoftShadowMap
threeRef.value!.appendChild(renderer.domElement)
const loader = new GLTFLoader()
loader.load('/mzjc_bansw.glb', (gltf) => {
scene.add(gltf.scene)
})
function animate() {
// 60Hz 1000/60=16.6ms
requestAnimationFrame(animate)
// 执行渲染
renderer.render(scene, camera)
// 适配高分辨率屏幕
renderer.setPixelRatio(width / height)
// 设置渲染器尺寸
renderer.setSize(width, height)
// 更新相机宽高比
camera.aspect = width / height
// 更新相机投影矩阵
camera.updateProjectionMatrix()
}
animate()
}
onMounted(initModel)
</script>
<template>
<div ref="threeRef" class="model" />
<router-link
to="/three/2"
class="position-fixed left-20px top-20px"
>
添加灯光
</router-link>
</template>
<style lang="scss" scoped>
.model {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,6 @@
<script lang="ts" setup>
</script>
<template>
<div>1</div>
</template>

View File

@@ -0,0 +1,6 @@
<script lang="ts" setup>
</script>
<template>
<div>1</div>
</template>

183
src/views/three/2/index.vue Normal file
View File

@@ -0,0 +1,183 @@
<script lang="ts" setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { onMounted, shallowRef } from 'vue'
const threeRef = shallowRef<HTMLCanvasElement>()
let scene: THREE.Scene | undefined
let camera: THREE.PerspectiveCamera | undefined
let renderer: THREE.WebGLRenderer | undefined
function getElementSize() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
return { width, height }
}
function initModel() {
const { width, height } = getElementSize()
scene = new THREE.Scene()
scene.background = new THREE.Color(0x000000)
camera = new THREE.PerspectiveCamera(
75,
width / height,
0.1,
1000,
)
camera.position.set(0, 2, 12)
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
threeRef.value!.appendChild(renderer.domElement)
const loader = new GLTFLoader()
loader.load('/mzjc_bansw.glb', (gltf) => {
scene!.add(gltf.scene)
})
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
const axesHelper = new THREE.AxesHelper(5)
scene!.add(axesHelper)
function animate() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
requestAnimationFrame(animate)
renderer!.render(scene!, camera!)
renderer!.setPixelRatio(width / height)
renderer!.setSize(width, height)
camera!.aspect = width / height
camera!.updateProjectionMatrix()
}
animate()
}
function addSphereGeometry(x: number, y: number, z: number) {
const geometry = new THREE.SphereGeometry(15, 32, 16)
const material = new THREE.MeshBasicMaterial({ color: 0xFFFF00 })
const sphere = new THREE.Mesh(geometry, material)
sphere.scale.set(0.005, 0.005, 0.005)
sphere.position.set(x, y, z)
scene!.add(sphere)
}
// 添加环境光
// 均匀照亮整个场景,无方向、无阴影
// 提供基础亮度,防止未被其他光照到的区域全黑
function addAmbientLight() {
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.5) // 颜色, 强度
scene!.add(ambientLight)
}
// 平行光
// 模拟太阳光,所有光线平行照射,可产生阴影
// 主光源,适合户外或强定向照明
// 光源位置决定阴影位置
function addDirectionalLight() {
const directionalLight = new THREE.DirectionalLight(0x00FFFF, 0.5) // 颜色, 强度
directionalLight.position.set(1, 2, 8) // 光源位置(方向由位置决定)
// 启用阴影(需开启渲染器和物体的阴影支持)
directionalLight.castShadow = true
scene!.add(directionalLight)
const directionalLightHelper = new THREE.DirectionalLightHelper(directionalLight)
scene!.add(directionalLightHelper)
addSphereGeometry(1, 2, 8)
}
// 点光源
// 向各个方向均匀发射光线,可产生阴影
// 模拟灯泡等局部光源效果
// 光源位置决定阴影位置
// 光照强度随距离衰减 模拟现实世界中光强随传播距离自然衰减的物理行为
function addPointLight() {
const pointLight = new THREE.PointLight(0x00FFFF, 10, 1000) // 颜色, 强度, 距离衰减
pointLight.position.set(1, 2, 8) // 光源位置
// 启用阴影(需开启渲染器和物体的阴影支持)
pointLight.castShadow = true
scene!.add(pointLight)
const pointLightHelper = new THREE.PointLightHelper(pointLight)
scene!.add(pointLightHelper)
addSphereGeometry(1, 2, 8)
}
// 聚光灯
// 定向锥形光束,可产生阴影
// 模拟手电筒或舞台聚光灯效果
function addSpotLight() {
const spotLight = new THREE.SpotLight(0x00FFFF, 100)// 颜色, 强度
spotLight.position.set(1, 2, 8)
spotLight.angle = Math.PI / 6 // 光锥角度(弧度)
spotLight.penumbra = 0.2 // 半影模糊程度 (0~1)
spotLight.decay = 2 // 衰减指数
spotLight.distance = 100 // 最大距离
scene!.add(spotLight)
// 可选:显示光锥辅助线
const spotLightHelper = new THREE.SpotLightHelper(spotLight)
scene!.add(spotLightHelper)
addSphereGeometry(1, 2, 8)
}
// 半球光
// 模拟天空和地面的漫反射光照效果
// 天空颜色从上方照射,地面颜色从下方反射
// 柔和的户外环境光,比 AmbientLight 更真实
function addHemisphereLight() {
const hemisphereLight = new THREE.HemisphereLight(0x00FFFF, 0xFFFFBB, 1) // 天空颜色, 地面颜色, 强度
scene!.add(hemisphereLight)
const hemisphereLightHelper = new THREE.HemisphereLightHelper(hemisphereLight, 5)
scene!.add(hemisphereLightHelper)
}
onMounted(initModel)
</script>
<template>
<div ref="threeRef" class="model" />
<router-link
to="/three/3"
class="position-fixed left-20px top-20px"
>
添加控制器AxesHelper
</router-link>
<div class="position-fixed right-20px top-20px flex gap-12px">
<button @click="addAmbientLight">
添加环境光
</button>
<button @click="addDirectionalLight">
添加平行光
</button>
<button @click="addPointLight">
添加点光源
</button>
<button @click="addSpotLight">
添加聚光灯
</button>
<button @click="addHemisphereLight">
添加半球光
</button>
</div>
</template>
<style lang="scss" scoped>
.model {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,86 @@
<script lang="ts" setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { onMounted, shallowRef } from 'vue'
const threeRef = shallowRef<HTMLCanvasElement>()
let scene: THREE.Scene | undefined
let camera: THREE.PerspectiveCamera | undefined
let renderer: THREE.WebGLRenderer | undefined
function getElementSize() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
return { width, height }
}
function initModel() {
const { width, height } = getElementSize()
scene = new THREE.Scene()
scene.background = new THREE.Color(0x000000)
camera = new THREE.PerspectiveCamera(
75,
width / height,
0.1,
1000,
)
camera.position.set(0, 2, 12)
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
threeRef.value!.appendChild(renderer.domElement)
const loader = new GLTFLoader()
loader.load('/mzjc_bansw.glb', (gltf) => {
scene!.add(gltf.scene)
})
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
const axesHelper = new THREE.AxesHelper(5)
scene!.add(axesHelper)
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1.2) // 颜色, 强度
scene!.add(ambientLight)
function animate() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
requestAnimationFrame(animate)
renderer!.render(scene!, camera!)
renderer!.setPixelRatio(width / height)
renderer!.setSize(width, height)
camera!.aspect = width / height
camera!.updateProjectionMatrix()
}
animate()
}
onMounted(initModel)
</script>
<template>
<div ref="threeRef" class="model" />
<router-link
to="/three/4"
class="position-fixed left-20px top-20px"
>
设置地面
</router-link>
</template>
<style lang="scss" scoped>
.model {
width: 100%;
height: 100%;
}
</style>

135
src/views/three/4/index.vue Normal file
View File

@@ -0,0 +1,135 @@
<script lang="ts" setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { onMounted, shallowRef } from 'vue'
const threeRef = shallowRef<HTMLCanvasElement>()
let scene: THREE.Scene | undefined
let camera: THREE.PerspectiveCamera | undefined
let renderer: THREE.WebGLRenderer | undefined
function getElementSize() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
return { width, height }
}
function initModel() {
const { width, height } = getElementSize()
scene = new THREE.Scene()
scene.background = new THREE.Color(0x000000)
camera = new THREE.PerspectiveCamera(
75,
width / height,
0.1,
1000,
)
camera.position.set(0, 2, 12)
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
threeRef.value!.appendChild(renderer.domElement)
const loader = new GLTFLoader()
loader.load('/mzjc_bansw.glb', (gltf) => {
scene!.add(gltf.scene)
})
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
const axesHelper = new THREE.AxesHelper(5)
scene!.add(axesHelper)
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1.2) // 颜色, 强度
scene!.add(ambientLight)
function animate() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
requestAnimationFrame(animate)
renderer!.render(scene!, camera!)
renderer!.setPixelRatio(width / height)
renderer!.setSize(width, height)
camera!.aspect = width / height
camera!.updateProjectionMatrix()
}
animate()
}
function addPlaneGeometry() {
// 创建地面几何体:宽度 20高度 20细分 1x1足够平坦
const groundGeometry = new THREE.PlaneGeometry(20, 20, 1, 1)
// 使用标准材质(支持光照和阴影)
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0xAAAAAA, // 灰色
roughness: 0.8,
metalness: 0.2,
})
// 创建网格
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
// 旋转 90 度使其平放在 XZ 平面上(默认 PlaneGeometry 在 XY 平面)
ground.rotation.x = -Math.PI / 2
// 可选:启用接收阴影(如果场景中有灯光和阴影)
ground.receiveShadow = true
// 添加到场景
scene!.add(ground)
const gridHelper = new THREE.GridHelper(100, 20, 0x444444, 0x222222)
scene!.add(gridHelper)
}
function addBoxGeometry() {
// 创建一个 20×0.2×20 的长方体(宽×高×深)
const groundGeometry = new THREE.BoxGeometry(20, 0.2, 20)
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x556B2F })
const ground = new THREE.Mesh(groundGeometry, groundMaterial)
ground.position.y = -0.1 // 让顶部对齐 Y=0 平面 几何体的原点pivot point默认在它的中心
ground.receiveShadow = true
scene!.add(ground)
const gridHelper = new THREE.GridHelper(100, 20, 0x444444, 0x222222)
scene!.add(gridHelper)
}
onMounted(initModel)
</script>
<template>
<div ref="threeRef" class="model" />
<router-link
to="/three/5"
class="position-fixed left-20px top-20px"
>
地面上设置光效
</router-link>
<div class="position-fixed right-20px top-20px flex gap-12px">
<button @click="addPlaneGeometry">
添加2D地面
</button>
<button @click="addBoxGeometry">
添加有厚度的地面
</button>
</div>
</template>
<style lang="scss" scoped>
.model {
width: 100%;
height: 100%;
}
</style>

273
src/views/three/5/index.vue Normal file
View File

@@ -0,0 +1,273 @@
<script lang="ts" setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { onBeforeUnmount, onMounted, shallowRef } from 'vue'
// ========== 常量定义 ==========
const GROUND_SIZE = 30
const GROUND_THICKNESS = 0.2
const LINE_COLORS = [0xFF0000, 0x00FF00, 0x0000FF, 0xFFFF00, 0xFF00FF, 0x00FFFF]
const LINE_SPEED = 0.1
const LINE_DELAY = 300
// ========== 类型定义 ==========
interface LineState {
mesh: THREE.Mesh
direction: number
startDelay: number
elapsed: number
axis: 'x' | 'z'
}
// ========== 状态变量 ==========
const threeRef = shallowRef<HTMLCanvasElement>()
let scene: THREE.Scene
let camera: THREE.PerspectiveCamera
let renderer: THREE.WebGLRenderer
let controls: OrbitControls
let animationId: number
const lineStates: LineState[] = []
function getElementSize() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
return { width, height }
}
// ========== 初始化场景 ==========
function initModel() {
const { width, height } = getElementSize()
// 创建场景
scene = new THREE.Scene()
scene.background = new THREE.Color(0x000000)
// 创建相机
camera = new THREE.PerspectiveCamera(75, width / height, 0.1, 1000)
camera.position.set(3, 8, 12)
// 创建渲染器
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.setPixelRatio(window.devicePixelRatio)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
renderer.localClippingEnabled = true
threeRef.value!.appendChild(renderer.domElement)
// 加载模型
const loader = new GLTFLoader()
loader.load('/mzjc_bansw.glb', (gltf) => {
scene.add(gltf.scene)
})
// 创建控制器
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
// 添加辅助工具
const axesHelper = new THREE.AxesHelper(5)
scene.add(axesHelper)
// 添加环境光
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 1.2)
scene.add(ambientLight)
// 添加地面
addGround()
// 启动动画循环
animate()
}
// ========== 添加地面 ==========
function addGround() {
const geometry = new THREE.BoxGeometry(GROUND_SIZE, GROUND_THICKNESS, GROUND_SIZE)
const material = new THREE.MeshStandardMaterial({ color: 0x222222 })
const ground = new THREE.Mesh(geometry, material)
ground.position.y = -GROUND_THICKNESS / 2
ground.receiveShadow = true
scene.add(ground)
}
// ========== 添加地面光效 ==========
function addGroundLightEffect() {
// 创建裁剪平面
const clipPlanes = [
new THREE.Plane(new THREE.Vector3(1, 0, 0), GROUND_SIZE / 2),
new THREE.Plane(new THREE.Vector3(-1, 0, 0), GROUND_SIZE / 2),
new THREE.Plane(new THREE.Vector3(0, 0, 1), GROUND_SIZE / 2),
new THREE.Plane(new THREE.Vector3(0, 0, -1), GROUND_SIZE / 2),
]
const geometry = new THREE.BoxGeometry(GROUND_SIZE, 0.1, 0.5)
LINE_COLORS.forEach((color, index) => {
const material = new THREE.MeshBasicMaterial({
color: new THREE.Color(color),
transparent: true,
opacity: 0.15 * (index + 1) * Math.random(),
clippingPlanes: clipPlanes,
clipShadows: true,
depthWrite: false,
})
const offset = -GROUND_SIZE / 2 + (GROUND_SIZE / LINE_COLORS.length) * (index + 0.5)
const isEven = index % 2 === 0
// 创建X轴线条
const xMesh = new THREE.Mesh(geometry, material.clone())
xMesh.position.set(isEven ? GROUND_SIZE : -GROUND_SIZE, 0, offset)
xMesh.renderOrder = 1
scene.add(xMesh)
lineStates.push({
mesh: xMesh,
direction: isEven ? -1 : 1,
startDelay: index * LINE_DELAY,
elapsed: 0,
axis: 'x',
})
// 创建Z轴线条
const zMesh = new THREE.Mesh(geometry, material.clone())
zMesh.position.set(offset, 0.01, isEven ? GROUND_SIZE : -GROUND_SIZE)
zMesh.rotation.y = Math.PI / 2
zMesh.renderOrder = 2
scene.add(zMesh)
lineStates.push({
mesh: zMesh,
direction: isEven ? -1 : 1,
startDelay: index * LINE_DELAY,
elapsed: 0,
axis: 'z',
})
})
}
// ========== 更新线条动画 ==========
function updateLines(deltaTime: number) {
lineStates.forEach((state) => {
state.elapsed += deltaTime
if (state.elapsed < state.startDelay)
return
const pos = state.axis === 'x' ? state.mesh.position.x : state.mesh.position.z
// 边界检测并反转方向
if (pos >= GROUND_SIZE)
state.direction = -1
else if (pos <= -GROUND_SIZE)
state.direction = 1
// 更新位置
const delta = LINE_SPEED * state.direction
if (state.axis === 'x') {
state.mesh.position.x += delta
}
else {
state.mesh.position.z += delta
}
})
}
// ========== 主动画循环 ==========
let lastTime = 0
function animate(time = 0) {
animationId = requestAnimationFrame(animate)
const deltaTime = time - lastTime
lastTime = time
// 更新控制器
controls.update()
// 更新线条动画
updateLines(deltaTime)
// 响应式调整渲染器大小
const { width, height } = getElementSize()
if (camera.aspect !== width / height) {
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height)
}
renderer.render(scene, camera)
}
// ========== 清理资源 ==========
function cleanup() {
if (animationId)
cancelAnimationFrame(animationId)
if (controls)
controls.dispose()
if (renderer) {
renderer.dispose()
threeRef.value?.removeChild(renderer.domElement)
}
scene?.traverse((object) => {
if (object instanceof THREE.Mesh) {
object.geometry?.dispose()
if (Array.isArray(object.material)) {
object.material.forEach(m => m.dispose())
}
else {
object.material?.dispose()
}
}
})
lineStates.length = 0
}
function pauseGroundLightEffect() {
cancelAnimationFrame(animationId)
}
function resumeGroundLightEffect() {
animate()
}
function destoryGroundLightEffect() {
lineStates.forEach((state) => {
state.mesh.parent?.remove(state.mesh)
})
}
onMounted(initModel)
onBeforeUnmount(cleanup)
</script>
<template>
<div ref="threeRef" class="model" />
<router-link to="/three/5" class="position-fixed left-20px top-20px">
地面上设置光效
</router-link>
<div class="position-fixed right-20px top-20px flex gap-12px">
<button @click="addGroundLightEffect">
添加地面光效
</button>
<button @click="pauseGroundLightEffect">
暂停地面光效
</button>
<button @click="resumeGroundLightEffect">
继续地面光效
</button>
<button @click="destoryGroundLightEffect">
销毁地面光效
</button>
</div>
</template>
<style lang="scss" scoped>
.model {
width: 100%;
height: 100%;
}
</style>

103
src/views/three/6/index.vue Normal file
View File

@@ -0,0 +1,103 @@
<script lang="ts" setup>
import * as THREE from 'three'
import { OrbitControls } from 'three/addons/controls/OrbitControls.js'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { onMounted, shallowRef } from 'vue'
const threeRef = shallowRef<HTMLCanvasElement>()
function initModel() {
const width = threeRef.value!.clientWidth
const height = threeRef.value!.clientHeight
const scene = new THREE.Scene()
scene.background = new THREE.Color(0x000000)
const camera = new THREE.PerspectiveCamera(
75,
width / height,
0.1,
1000,
)
camera.position.set(0.1, 0.1, 1)
const renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(width, height)
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
threeRef.value!.appendChild(renderer.domElement)
const controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
const axesHelper = new THREE.AxesHelper(5)
scene.add(axesHelper)
// 环境光
const ambientLight = new THREE.AmbientLight(0xFFFFFF, 0.5)
scene.add(ambientLight)
// 方向光(主光)
const directionalLight = new THREE.DirectionalLight(0xFFFFFF, 0.8)
directionalLight.position.set(20, 30, 20)
directionalLight.castShadow = true
directionalLight.shadow.mapSize.set(2048, 2048)
directionalLight.shadow.camera.near = 0.5
directionalLight.shadow.camera.far = 50
directionalLight.shadow.camera.left = -20
directionalLight.shadow.camera.right = 20
directionalLight.shadow.camera.top = 20
directionalLight.shadow.camera.bottom = -20
scene.add(directionalLight)
// 点光源
const pointLight = new THREE.PointLight(0x409EFF, 0.5, 50)
pointLight.position.set(0, 10, 0)
scene.add(pointLight)
const loader = new GLTFLoader()
loader.load('/lxb_grp.glb', (gltf) => {
scene.add(gltf.scene)
const { animations = [] } = gltf
animations.forEach((clip) => {
const mixer = new THREE.AnimationMixer(gltf.scene)
const action = mixer.clipAction(clip)
action.play()
const clock = new THREE.Clock()
// 在动画循环中更新混合器
function updateMixer() {
requestAnimationFrame(updateMixer)
const delta = clock.getDelta()
mixer.update(delta)
}
updateMixer()
})
})
function animate() {
requestAnimationFrame(animate)
renderer.render(scene, camera)
renderer.setSize(width, height)
camera.aspect = width / height
camera.updateProjectionMatrix()
}
animate()
}
onMounted(initModel)
</script>
<template>
<div ref="threeRef" class="model" />
</template>
<style lang="scss" scoped>
.model {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,6 @@
<script lang="ts" setup>
</script>
<template>
<div>1</div>
</template>

View File

@@ -0,0 +1,6 @@
<script lang="ts" setup>
</script>
<template>
<div>1</div>
</template>

View File

@@ -0,0 +1,6 @@
<script lang="ts" setup>
</script>
<template>
<div>1</div>
</template>

View File

@@ -3,13 +3,11 @@ import { fileURLToPath, URL } from 'node:url'
import vue from '@vitejs/plugin-vue'
import Unocss from 'unocss/vite'
import { defineConfig } from 'vite'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
Unocss(),
],
resolve: {