<template>
<div id="webgl-box" ref="webglBox" style="width: 100vw; height: 100vh;"></div>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import * as THREE from 'three'
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
const webglBox = ref(null)
let renderer, scene, camera, animationId, controls
function createSolarPanel(x, y, z) {
// 太阳能板:用多个倾斜的矩形模拟
const group = new THREE.Group()
const panelGeometry = new THREE.BoxGeometry(4, 0.2, 2)
const panelMaterial = new THREE.MeshPhongMaterial({ color: 0x1e88e5 })
for (let i = 0; i < 3; i++) {
const panel = new THREE.Mesh(panelGeometry, panelMaterial)
panel.position.set(x + i * 4.5, y + 0.2, z)
panel.rotation.x = -Math.PI / 6
group.add(panel)
}
return group
}
function createWindTurbine(x, y, z) {
// 风机:底座+杆+三片叶片
const group = new THREE.Group()
// 杆
const poleGeometry = new THREE.CylinderGeometry(0.2, 0.4, 8, 16)
const poleMaterial = new THREE.MeshPhongMaterial({ color: 0xffffff })
const pole = new THREE.Mesh(poleGeometry, poleMaterial)
pole.position.set(x, y + 4, z)
group.add(pole)
// 机舱
const nacelleGeometry = new THREE.SphereGeometry(0.5, 16, 16)
const nacelle = new THREE.Mesh(nacelleGeometry, poleMaterial)
nacelle.position.set(x, y + 8, z)
group.add(nacelle)
// 叶片
for (let i = 0; i < 3; i++) {
const bladeGeometry = new THREE.BoxGeometry(0.1, 3, 0.3)
const blade = new THREE.Mesh(bladeGeometry, new THREE.MeshPhongMaterial({ color: 0x90caf9 }))
blade.position.set(x, y + 9.5, z)
blade.rotation.z = (i * 2 * Math.PI) / 3
group.add(blade)
}
return group
}
function createBuilding(x, z, w, h, d, color = 0x8d6e63) {
// 厂房/办公楼
const geometry = new THREE.BoxGeometry(w, h, d)
const material = new THREE.MeshPhongMaterial({ color })
const mesh = new THREE.Mesh(geometry, material)
mesh.position.set(x, h / 2, z)
return mesh
}
function createGround() {
// 地面
const geometry = new THREE.PlaneGeometry(120, 80)
const material = new THREE.MeshPhongMaterial({ color: 0xa5d6a7 })
const mesh = new THREE.Mesh(geometry, material)
mesh.rotation.x = -Math.PI / 2
mesh.position.y = 0
return mesh
}
function createSolarPanelArray(rows, cols, startX, startZ, gapX, gapZ) {
// 批量生成光伏板阵列
const group = new THREE.Group()
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
const geometry = new THREE.BoxGeometry(4, 0.2, 2)
const material = new THREE.MeshPhongMaterial({ color: 0x1e88e5 })
const mesh = new THREE.Mesh(geometry, material)
mesh.position.set(startX + j * gapX, 0.2, startZ + i * gapZ)
mesh.rotation.x = -Math.PI / 6
group.add(mesh)
}
}
return group
}
function createInverter(x, z) {
// 逆变器/配电箱
const geometry = new THREE.BoxGeometry(2, 1, 1)
const material = new THREE.MeshPhongMaterial({ color: 0xffc107 })
const mesh = new THREE.Mesh(geometry, material)
mesh.position.set(x, 0.5, z)
return mesh
}
function createRoad(x, z, w, d) {
// 道路
const geometry = new THREE.BoxGeometry(w, 0.05, d)
const material = new THREE.MeshPhongMaterial({ color: 0x757575 })
const mesh = new THREE.Mesh(geometry, material)
mesh.position.set(x, 0.025, z)
return mesh
}
function loadModel(url, onLoad) {
const loader = new GLTFLoader()
loader.load(url, gltf => {
const model = gltf.scene
onLoad && onLoad(model)
})
}
function initThree() {
scene = new THREE.Scene()
scene.background = new THREE.Color(0xe3f2fd)
// 摄像机
const width = webglBox.value.clientWidth
const height = webglBox.value.clientHeight
camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000)
camera.position.set(-100, 80, 100)
camera.lookAt(0, 0, 0)
// 渲染器
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setClearColor(0x000000, 0)
renderer.setSize(width, height)
webglBox.value.appendChild(renderer.domElement)
// 灯光
scene.add(new THREE.AmbientLight(0xffffff, 0.7))
const dirLight = new THREE.DirectionalLight(0xffffff, 0.7)
dirLight.position.set(30, 50, 30)
scene.add(dirLight)
// 地面
scene.add(createGround())
// 光伏板阵列(假设3组,每组5x10块)
scene.add(createSolarPanelArray(5, 10, -40, -20, 6, 4))
scene.add(createSolarPanelArray(5, 10, -40, 10, 6, 4))
scene.add(createSolarPanelArray(5, 10, 20, -5, 6, 4))
// 逆变器
scene.add(createInverter(-10, -30))
scene.add(createInverter(30, 30))
// 厂房/办公楼
scene.add(createBuilding(50, 30, 12, 6, 8, 0x607d8b))
scene.add(createBuilding(-50, 30, 8, 4, 6, 0x8d6e63))
// 道路
scene.add(createRoad(0, 0, 100, 4))
scene.add(createRoad(30, 20, 4, 40))
// 坐标轴
scene.add(new THREE.AxesHelper(40))
// === 新增:初始化 OrbitControls ===
controls = new OrbitControls(camera, renderer.domElement)
controls.enableDamping = true
controls.dampingFactor = 0.05
controls.minDistance = 10
controls.maxDistance = 200
controls.maxPolarAngle = Math.PI // 允许看到场景下方
controls.minPolarAngle = 0 // 允许看到场景上方
controls.enablePan = true
controls.enableZoom = true
// 动画
function animate() {
animationId = requestAnimationFrame(animate)
controls && controls.update() // === 新增:每帧更新控制器 ===
// 可加风机叶片旋转等动画
scene.traverse(obj => {
if (obj.geometry && obj.geometry.type === 'BoxGeometry' && obj.material.color.getHex() === 0x90caf9) {
obj.rotation.y += 0.05 // 让风机叶片转动
}
})
renderer.render(scene, camera)
}
animate()
}
function onWindowResize() {
if (!renderer || !camera || !webglBox.value) return
const width = webglBox.value.clientWidth
const height = webglBox.value.clientHeight
camera.aspect = width / height
camera.updateProjectionMatrix()
renderer.setSize(width, height)
}
onMounted(() => {
initThree()
window.addEventListener('resize', onWindowResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', onWindowResize)
if (animationId) cancelAnimationFrame(animationId)
if (controls) controls.dispose()
if (renderer) {
renderer.dispose()
renderer.forceContextLoss && renderer.forceContextLoss()
renderer.domElement && renderer.domElement.remove()
}
})
</script>
<style scoped>
#webgl-box {
width: 100vw;
height: 100vh;
overflow: hidden;
/* 科技感渐变+网格 */
background:
repeating-linear-gradient(0deg, rgba(76,192,141,0.08) 0, rgba(76,192,141,0.08) 1px, transparent 1px, transparent 40px),
repeating-linear-gradient(90deg, rgba(76,192,141,0.08) 0, rgba(76,192,141,0.08) 1px, transparent 1px, transparent 40px),
linear-gradient(135deg, #0f2027 0%, #2c5364 100%);
}
</style>