diff --git a/src/utils/plane_grass.js b/src/utils/plane_grass.js new file mode 100644 index 0000000..0fe686e --- /dev/null +++ b/src/utils/plane_grass.js @@ -0,0 +1,161 @@ +import * as THREE from "three"; +const BLADE_WIDTH = 0.1 +const BLADE_HEIGHT = 0.8 +const BLADE_HEIGHT_VARIATION = 0.6 +const BLADE_VERTEX_COUNT = 5 +const BLADE_TIP_OFFSET = 0.1 + +function interpolate(val, oldMin, oldMax, newMin, newMax) { + return ((val - oldMin) * (newMax - newMin)) / (oldMax - oldMin) + newMin +} + +class GrassGeometry extends THREE.BufferGeometry { + constructor(size, count) { + super() + + const positions = [] + const uvs = [] + const indices = [] + + for (let i = 0; i < count; i++) { + const surfaceMin = (size / 2) * -1 + const surfaceMax = size / 2 + + // ✅ 直接在正方形范围内取随机点 + const x = surfaceMin + Math.random() * size + const y = surfaceMin + Math.random() * size + + uvs.push( + ...Array.from({ length: BLADE_VERTEX_COUNT }).flatMap(() => [ + interpolate(x, surfaceMin, surfaceMax, 0, 1), + interpolate(y, surfaceMin, surfaceMax, 0, 1) + ]) + ) + + const blade = this.computeBlade([x, 0, y], i) + positions.push(...blade.positions) + indices.push(...blade.indices) + } + + + this.setAttribute( + 'position', + new THREE.BufferAttribute(new Float32Array(positions), 3) + ) + this.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(uvs), 2)) + this.setIndex(indices) + this.computeVertexNormals() + } + computeBlade(center, index = 0) { + const height = BLADE_HEIGHT + Math.random() * BLADE_HEIGHT_VARIATION + const vIndex = index * BLADE_VERTEX_COUNT + + // Randomize blade orientation and tip angle + const yaw = Math.random() * Math.PI * 2 + const yawVec = [Math.sin(yaw), 0, -Math.cos(yaw)] + const bend = Math.random() * Math.PI * 2 + const bendVec = [Math.sin(bend), 0, -Math.cos(bend)] + + // Calc bottom, middle, and tip vertices + const bl = yawVec.map((n, i) => n * (BLADE_WIDTH / 2) * 1 + center[i]) + const br = yawVec.map((n, i) => n * (BLADE_WIDTH / 2) * -1 + center[i]) + const tl = yawVec.map((n, i) => n * (BLADE_WIDTH / 4) * 1 + center[i]) + const tr = yawVec.map((n, i) => n * (BLADE_WIDTH / 4) * -1 + center[i]) + const tc = bendVec.map((n, i) => n * BLADE_TIP_OFFSET + center[i]) + + // Attenuate height + tl[1] += height / 2 + tr[1] += height / 2 + tc[1] += height + + return { + positions: [...bl, ...br, ...tr, ...tl, ...tc], + indices: [ + vIndex, + vIndex + 1, + vIndex + 2, + vIndex + 2, + vIndex + 4, + vIndex + 3, + vIndex + 3, + vIndex, + vIndex + 2 + ] + } + } +} + +class Grass extends THREE.Mesh { + constructor(size, count) { + const geometry = new GrassGeometry(size, count) + const material = new THREE.ShaderMaterial({ + uniforms: { + uCloud: { value: "" }, + offsetX: { value: 0.5 }, + offsetY: { value: 0.3 }, + uTime: { value: 0 }, + }, + side: THREE.DoubleSide, + vertexShader: ` uniform float uTime; + uniform float offsetX; + uniform float offsetY; + + varying vec3 vPosition; + varying vec2 vUv; + varying vec3 vNormal; + + float wave(float waveSize, float tipDistance, float centerDistance) { + // Tip is the fifth vertex drawn per blade + bool isTip = (gl_VertexID + 1) % 5 == 0; + + float waveDistance = isTip ? tipDistance : centerDistance; + return sin((uTime / 500.0) + waveSize) * waveDistance; + } + + void main() { + vPosition = position; + vUv = uv; + + // Cloud shadow move + vUv.x += uTime * 0.0001 * offsetX; + vUv.y += uTime * 0.0001 * offsetY; + + vNormal = normalize(normalMatrix * normal); + if (vPosition.y < 0.0) { + vPosition.y = 0.0; + } else { + vPosition.x += wave(uv.x * 10.0, 0.3, 0.1); + } + gl_Position = projectionMatrix * modelViewMatrix * vec4(vPosition, 1.0); + }`, + fragmentShader: ` uniform sampler2D uCloud; + uniform float uTime; + varying vec3 vPosition; + varying vec2 vUv; + varying vec3 vNormal; + + vec3 green = vec3(0.2, 0.6, 0.3); + + void main() { + vec3 color = mix(green * 0.7, green, vPosition.y); + color = mix(color, texture2D(uCloud, vUv).rgb, 0.4); + float lighting = normalize(dot(vNormal, vec3(10))); + gl_FragColor = vec4(color + lighting * 0.03, 1.0); + }`, + }) + super(geometry, material) + const floor = new THREE.Mesh( + new THREE.PlaneGeometry(50, 50).rotateX(Math.PI / 2), + material + ) + floor.position.y = -Number.EPSILON + this.add(floor) + + } + update(time) { + this.material.uniforms.uTime.value = time + } +} + +const planeGrass = new Grass(50, 100000) +export { planeGrass } diff --git a/src/views/bigScreen/home/index copy.vue b/src/views/bigScreen/home/index copy.vue deleted file mode 100644 index 760ddac..0000000 --- a/src/views/bigScreen/home/index copy.vue +++ /dev/null @@ -1,638 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/views/bigScreen/home/index.vue b/src/views/bigScreen/home/index.vue index 68c35d9..c5a9d02 100644 --- a/src/views/bigScreen/home/index.vue +++ b/src/views/bigScreen/home/index.vue @@ -55,6 +55,7 @@ import { lastData } from "@/api/bigScreen/api.js"; import { drawAxes } from "@/utils/three.js"; import { connectWebsocket, closeWebsocket } from "@/utils/websocket.js"; +import { planeGrass } from "@/utils/plane_grass.js"; export default { name: 'Home', props: { @@ -259,6 +260,13 @@ export default { this.onBallClick(falg, val) }) this.legendList = this.demo1.getGroups() + planeGrass.position.set(25, 1, 25) + this.demo1.scene.add(planeGrass) + function animate(time) { + requestAnimationFrame(animate) + planeGrass.update(time) + } + animate()