Skip to content

道路流光

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <div v-if="isRunning" id="roadFlowingLight" class="stage"></div>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
  Scene,
  WebGLRenderer,
  PerspectiveCamera,
  Color,
  AmbientLight,
  DirectionalLight,
  PlaneGeometry,
  MeshLambertMaterial,
  Mesh,
  Vector2,
  Vector3,
  LineCurve3,
  BufferGeometry,
  Float32BufferAttribute,
  ShaderMaterial,
  Points,
  Shape,
  Path,
  ShapeGeometry,
  MeshPhongMaterial,
  DoubleSide,
  Texture,
} from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

const ratio = ref<any>({ value: 0 })
const requestID = ref<any>()
let next = 0
const isRunning = ref(false)
let sceneResources

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    sceneResources = await initScene()
  } else {
    isRunning.value = false
    destroy()
  }
}

const vertexShader = `
  // 接收js传入的attribute值,会经过线性插值
  attribute float current;

  // 接收js传入的uniform值
  uniform float uSize;
  uniform float uTime;
  uniform float uRange;
  uniform float uTotal;
  uniform float uSpeed;

  // 向片元着色器传值颜色和透明度
  varying float vOpacity;

  void main () {
    float size = uSize;
    // 根据时间确定当前飞线的位置, 以结束点为准
    float currentEnd = mod(uTime * uSpeed, uTotal);
    // 判断当前像素点是否在飞线范围内,如果在范围内设置尺寸和透明度
    if (current < currentEnd && current > currentEnd - uRange) {
      // 设置渐变的尺寸,头大尾小
      float sizePct = (uRange - (currentEnd - current)) / uRange;
      // size *= sizePct;
      vOpacity = clamp(1.0 * sizePct, 0.2, 1.0);
    } else if (current < currentEnd - uRange){
      vOpacity = 0.05;
    } else {
      vOpacity = 0.05;
    }
    // 将颜色传递给片元着色器
    // 设置点的大小
    gl_PointSize = size * 0.4;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`
const fragmentShader = `
  precision mediump float;
  // 接收顶点着色器传入的值
  varying float vOpacity;
  uniform vec3 uColor;

  void main () {
    // 设置颜色
    gl_FragColor = vec4(uColor, vOpacity);
  }
`

 // 道路的点数据
 const pointArr = [
  // 外圈
  300, -300, 0, 300, 300, 0,
  300, 300, 0, -300, 300, 0,
  -300, 300, 0, -300, -300, 0,
  -300, -300, 0, 300, -300, 0,
  // 内圈
  200, -200, 0, 200, 200, 0,
  200, 200, 0, -200, 200, 0,
  -200, 200, 0, -200, -200, 0,
  -200, -200, 0, 200, -200, 0
]

// 流光配置数据
const flyConf = {
  range: 100, // 飞线长度
  color: '#fe7', // 颜色
  speed: 80, // 速度
  size: 14 // 飞线点点的大小
}

const pointsArr1: any = []
const pointsArr2: any = []

const initScene = () => {
  const ele = document.getElementById('roadFlowingLight') as HTMLElement
  const width = Number(window.getComputedStyle(ele).width.split('px')[0])
  const height = Number(window.getComputedStyle(ele).height.split('px')[0])

  const scene = new Scene()

  const camera: any = new PerspectiveCamera(75, width / height, 0.1, 3000)
  camera.position.set(0, 0, 1000)
  scene.add(camera)

  const renderer: any = new WebGLRenderer({
    antialias: true,
    alpha: true
  })
  renderer.setSize(width, height)
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  renderer.setClearColor(new Color('#32373E'), 1)
  ele.appendChild(renderer.domElement)

  // 添加 OrbitControls
  const createOrbitControls = () => {
    const controls = new OrbitControls(camera, renderer.domElement)
    controls.enableDamping = true
    controls.dampingFactor = 0.25
    controls.enableZoom = true
    return controls
  }

  const createLight = () => {
    // 环境光
    const light = new AmbientLight(0xadadad) // soft white light
    scene.add(light)

    // 平行光源
    const directionalLight: any = new DirectionalLight(0xffffff, 1)
    directionalLight.position.set(1000, 1000, 0)
    scene.add(directionalLight)
  }
  
  // 创建地板
  const createGround = () => {
    const planeGeo = new PlaneGeometry(800, 800) // width = 1, height = 1, widthSegments(宽度分段) = 1, heightSegments(高度分段) = 1
    const planeMaterial: any = new MeshLambertMaterial({
      color: new Color('#efe')
    })
    const planeMesh: any = new Mesh(planeGeo, planeMaterial)
    planeMesh.rotation.x = -Math.PI / 2
    scene.add(planeMesh)
  }
 
  // 创建流光
  const createLightLine = () => {
    for (let i = 0; i < pointArr.length; i += 6) {
      if (i < 24) {
        pointsArr1.push(new Vector2(pointArr[i], pointArr[i + 1]))
      } else {
        pointsArr2.push(new Vector2(pointArr[i], pointArr[i + 1]))
      }

      let start = new Vector3(
        pointArr[i],
        pointArr[i + 1],
        pointArr[i + 2]
      )

      let end = new Vector3(
        pointArr[i + 3],
        pointArr[i + 4],
        pointArr[i + 5]
      )

      const curve = new LineCurve3(start, end)
      const number = start.distanceTo(end)

      const points = curve?.getPoints(number)
      const positions: any = []
      const current: any = []
      points.forEach((item: any, index) => {
        current.push(index)
        positions.push(item.x, item.y, item.z)
      })

      const flyGeo = new BufferGeometry()
      flyGeo.setAttribute(
        'position',
        new Float32BufferAttribute(positions, 3)
      )
      flyGeo.setAttribute(
        'current',
        new Float32BufferAttribute(current, 1)
      )

      const flyMaterial: any = new ShaderMaterial({
        transparent: true,
        depthWrite: false,
        depthTest: false,
        // blending: THREE.AdditiveBlending,
        uniforms: {
          uSize: {
            // 点的大小
            value: flyConf.size
          },
          uTime: ratio.value, // 时间
          uColor: {
            // 颜色
            value: new Color(flyConf.color)
          },
          uRange: {
            // 飞线长度
            value: flyConf.range
          },
          uTotal: {
            // 轨迹总长度,(点的总个数)
            value: number
          },
          uSpeed: {
            // 飞行速度
            value: flyConf.speed
          }
        },
        vertexShader,
        fragmentShader
      })

      // 创建并添加到场景中
      const flyPoints = new Points(flyGeo, flyMaterial)
      scene.add(flyPoints)
    }
  }

  // 内圈外圈之间的颜色
  const createBetweenBackground = () => {
    const shape = new Shape(pointsArr1)
    const holePath = new Path(pointsArr2)
    shape.holes.push(holePath)
    const geometry1 = new ShapeGeometry(shape)
    const material1: any = new MeshPhongMaterial({
      color: new Color('#5fc2ef'),
      side: DoubleSide
    })
    const mesh1 = new Mesh(geometry1, material1)
    scene.add(mesh1)
  }
 

  const runAnimate = () => {
    next += 0.12
    ratio.value.value = next

    requestID.value = requestAnimationFrame(runAnimate)
    renderer.render(scene, camera)
  }

  
 
  createLight()
  createGround()
  createLightLine()
  createBetweenBackground()
  runAnimate()
  const controls = createOrbitControls()

  return {
    renderer,
    scene,
    controls
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.scene.clear()
    sceneResources.scene.traverse((child) => {
      if (child.geometry) child.geometry?.dispose()
      if (child.material) {
        if (child.material.map) child.material.map?.dispose()
        child.material?.dispose()
      }
    })
    if (sceneResources.scene.background) {
      if (sceneResources.scene.background instanceof Texture) {
        sceneResources.scene.background?.dispose()
      }
    }
    sceneResources.renderer?.dispose()
    sceneResources.renderer.forceContextLoss()
    sceneResources.controls?.dispose()

    cancelAnimationFrame(requestID.value)

    ratio.value.value = 0
    next = 0

    sceneResources = null
  }
}

onMounted(async() => {
  await nextTick()
})

onUnmounted(() => {
  destroy()
})
</script>

<style scoped>
</style>

雷达 -1

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <div v-if="isRunning" id="radar1" class="stage"></div>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import {
  Scene,
  WebGLRenderer,
  PerspectiveCamera,
  Color,
  AmbientLight,
  Mesh,
  DoubleSide,
  Texture,
  PlaneGeometry,
  ShaderMaterial,
  Clock
} from 'three'

const requestID = ref<any>()
const addTime = ref<any>({ value: 0 })
const isRunning = ref(false)
let clock: any = new Clock()
let sceneResources

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    sceneResources = await initScene()
  } else {
    isRunning.value = false
    destroy()
  }
}

const vertexShader = `
  varying vec2 vUv;
  varying vec3 v_position;

  void main() {
    vUv = uv;
    v_position =  position;

    vec4 modelPosition = modelMatrix * vec4( position, 1.0 );
    gl_Position = projectionMatrix * viewMatrix * modelPosition;
  }
`

const fragmentShader = `
  varying vec2 vUv;
  uniform vec3 uColor;
  uniform float uTime;
  varying vec3 v_position;

  float sdCircle(vec2 p, float r) {
    return length(p) - r;
  }

  float sdBox(in vec2 p, in vec2 b) {
    vec2 d = abs(p) - b;
    return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
  }

  float opSubtraction(float d1, float d2) {
    return max(-d1, d2);
  }

  // 旋转函数
  vec2 rotate(vec2 uv, float rotation, vec2 mid) {
    return vec2(
      cos(rotation) * (uv.x - mid.x) + sin(rotation) * (uv.y - mid.y) + mid.x , 
      cos(rotation) * (uv.y - mid.y) - sin(rotation) * (uv.x - mid.x) + mid.y
    );
  }

  // 等边三角形
  float sdEquilateralTriangle(in vec2 p, in float r) {
    const float k = sqrt(3.0);
    p.x = abs(p.x) - r;
    p.y = p.y + r / k;
    if (p.x + k * p.y > 0.0) p = vec2(p.x - k * p.y, -k * p.x - p.y) / 2.0;
    p.x -= clamp(p.x, -2.0 * r, 0.0);
    return -length(p) * sign(p.y);
  }

  // 设置三角形
  float mixSjx(float progress, vec2 uv, float rotateNumber) {
    float sjxd = sdEquilateralTriangle(rotate(uv, rotateNumber, vec2(0.5)) - vec2(0.5, (progress * 0.05) + 0.03), 0.006);
    float sjxc = smoothstep(0.0007, 0.0007, sjxd);
    return sjxc;
  }

  void main() {
    vec2 uv = vUv;
    float txUtime = 2.2;
    float dis = length(v_position - vec3(0));

    // 画缺角的圆
    if (dis < (txUtime + 0.02) && dis > txUtime) {
      gl_FragColor = vec4(vec3(0.74, 0.95, 1.00), 1.0); 
    }
    if (uv.x > 0.48 && uv.x < 0.52) {
      gl_FragColor = vec4(0.0); 
    }
    if (uv.y > 0.48 && uv.y < 0.52) {
      gl_FragColor = vec4(0.0); 
    }
    if (uv.x + uv.y > 1.0 && uv.x + uv.y < 1.03) {
      gl_FragColor = vec4(0.0); 
    }
    if (uv.x - uv.y > 0.05 && uv.x - uv.y < 0.08) {
      gl_FragColor = vec4(0.0); 
    }

    // 画可缩小的圆
    float progress = abs(sin(uTime)) ;
    float d1 = sdCircle(uv - vec2(0.5), 0.36);
    float d2 = sdBox(uv - vec2(0.5), vec2(0.4, ((progress + 1.4) * 0.5) * 0.2));
    float d3 = sdCircle(uv - vec2(0.5), 0.35);

    float d4 = opSubtraction(d1, d3);
    float d5 = opSubtraction(d2, d4);

    float c = smoothstep(0.007, 0.007, d5);
    gl_FragColor = mix(vec4(vec3(0.87, 0.98, 1.00), 1.0) ,gl_FragColor, c);


    // 画中心的 雷达
    vec2 rotateVuv = rotate(vUv , uTime ,vec2(0.5));
    float opacity = 1.0 - step(0.276, distance(uv, vec2(0.5)));
    float st = 1.0 - step(0.01, mod(dis, 0.456));
    float angle = atan(rotateVuv.x - 0.5, rotateVuv.y - 0.5);
    float strength = (angle + 3.14) / 6.28 * 5.0; 

    if (st == 1.0) {
      gl_FragColor = mix(vec4(0.0), vec4(vec3(0.87, 0.98, 1.00), 1.0), opacity);
    } else {
      vec4 atanColor = mix(vec4(vec3(0.35, 0.76, 0.83), 1.0), vec4(1.0, 1.0, 1.0, 0.0), opacity * strength);
      vec4 color = mix(gl_FragColor, atanColor, opacity);
      gl_FragColor = color;
    }

    // 画十字架
    if (uv.x + uv.y >0.998 && uv.x + uv.y < 0.99999) {
      gl_FragColor = mix(gl_FragColor, vec4(vec3(0.953, 0.969, 0.89), 1.0), opacity); 
    }
    if (uv.x - uv.y > 0.0 && uv.x - uv.y < 0.002) {
      gl_FragColor = mix(gl_FragColor, vec4(vec3(0.953, 0.969, 0.89), 1.0), opacity); 
    }


    // 画三角形
    float colors[4];
    colors[0] = 0.0;
    colors[1] = 1.57;
    colors[2] = -1.57;
    colors[3] = 3.14;

    for (int i = 0; i < 4; ++i) {
      gl_FragColor = mix(vec4(vec3(1.0, 1.0, 0.0), 1.0), gl_FragColor, mixSjx(progress, uv, colors[i]));
    }
  }
`

const initScene = () => {
  const ele = document.getElementById('radar1') as HTMLElement
  const width = Number(window.getComputedStyle(ele).width.split('px')[0])
  const height = Number(window.getComputedStyle(ele).height.split('px')[0])

  const scene = new Scene()

  const camera: any = new PerspectiveCamera(75, width / height, 0.1, 3000)
  camera.position.set(0, 0, 50)
  scene.add(camera)

  const renderer: any = new WebGLRenderer({
    antialias: true,
    alpha: true
  })
  renderer.setSize(width, height)
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  renderer.setClearColor(new Color('#32373E'), 1)
  ele.appendChild(renderer.domElement)

  // 添加 OrbitControls
  const createOrbitControls = () => {
    const controls = new OrbitControls(camera, renderer.domElement)
    controls.enableDamping = true
    controls.dampingFactor = 0.25
    controls.enableZoom = true
    return controls
  }

   // 光
  const createLight = () => {
    const ambient = new AmbientLight(0x444444)
    scene.add(ambient)
  }

  const createPlane = () => {
    const geometry = new PlaneGeometry(200, 200) // width = 1, height = 1, widthSegments(宽度分段) = 1, heightSegments(高度分段) = 1
    const material = new ShaderMaterial({
      uniforms: {
        uTime: addTime.value,
      },
      side: DoubleSide,
      transparent: true,
      vertexShader: vertexShader,
      fragmentShader: fragmentShader
    })

    const mesh = new Mesh(geometry, material)
    scene.add(mesh)
    mesh.rotation.z = Math.PI / 2

    return mesh
  }

  createPlane()
  createLight()

  const runAnimate = () => {
    requestID.value = requestAnimationFrame(runAnimate)
    renderer.render(scene, camera)

    addTime.value.value = clock.getElapsedTime()
  }


  runAnimate()
  
  const controls = createOrbitControls()

  return {
    renderer,
    scene,
    controls,
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.scene.clear()
    sceneResources.scene.traverse((child) => {
      if (child.geometry) child.geometry?.dispose()
      if (child.material) {
        if (child.material.map) child.material.map?.dispose()
        child.material?.dispose()
      }
    })
    if (sceneResources.scene.background) {
      if (sceneResources.scene.background instanceof Texture) {
        sceneResources.scene.background?.dispose()
      }
    }
    sceneResources.renderer?.dispose()
    sceneResources.renderer.forceContextLoss()
    sceneResources.controls?.dispose()

    cancelAnimationFrame(requestID.value)

    addTime.value.value = 0
    sceneResources = null
  }
}

onMounted(async() => {
  await nextTick()
})

onUnmounted(() => {
  destroy()
  clock = null
})
</script>

雷达 -2

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <div v-if="isRunning" id="radar2" class="stage"></div>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import {
  Scene,
  WebGLRenderer,
  PerspectiveCamera,
  Color,
  AmbientLight,
  Mesh,
  DoubleSide,
  Texture,
  PlaneGeometry,
  ShaderMaterial,
} from 'three'

const requestID = ref<any>()
const isRunning = ref(false)
let sceneResources

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    sceneResources = await initScene()
  } else {
    isRunning.value = false
    destroy()
  }
}

const vertexShader = `
  varying vec3 vp;
  void main() {
    vp = position;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`

const fragmentShader = `
  varying vec3 vp;
					
  uniform vec3 u_color;
  uniform float u_pi;
  uniform float u_radius;
  uniform float u_rotation_step;
  
  float getLength(float x, float y) {
    return sqrt((x - 0.0) * (x - 0.0) + (y - 0.0) * (y - 0.0));
  }
  
  void main() {
    // 旋转
    float angOffset = u_rotation_step * 0.05;
    float cosAng = cos(angOffset);
    float sinAng = sin(angOffset);
    mat2 modelMatrix = mat2(
      cosAng,sinAng,
      -sinAng,cosAng
    );
    vec2 point = modelMatrix * vp.xy;
    
    
    // ang=[-π,π]
    // atan(y,x)用于将XY坐标,返回弧度
    float ang = atan(point.y, point.x);
    // (u_pi - π)  或者 (u_pi - - π) 取值 0 ~ 2π
    float radians = u_pi - ang;
    float opacity = radians / (u_pi * 8.0);
    // float opacity = 1.0;
    
    // 隐藏某些部分
    if (abs(radians) > 1.0) {
      opacity = 0.0;
    }
    
    // 距离
    float uLength = getLength(point.x, point.y);
    if (uLength > u_radius) {
      opacity = 0.0;
    }
    
    gl_FragColor = vec4(u_color, opacity);
  }
`

const initScene = () => {
  const ele = document.getElementById('radar2') as HTMLElement
  const width = Number(window.getComputedStyle(ele).width.split('px')[0])
  const height = Number(window.getComputedStyle(ele).height.split('px')[0])

  const scene = new Scene()

  const camera: any = new PerspectiveCamera(75, width / height, 0.1, 3000)
  camera.position.set(0, 0, 50)
  scene.add(camera)

  const renderer: any = new WebGLRenderer({
    antialias: true,
    alpha: true
  })
  renderer.setSize(width, height)
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  renderer.setClearColor(new Color('#32373E'), 1)
  ele.appendChild(renderer.domElement)

  // 添加 OrbitControls
  const createOrbitControls = () => {
    const controls = new OrbitControls(camera, renderer.domElement)
    controls.enableDamping = true
    controls.dampingFactor = 0.25
    controls.enableZoom = true
    return controls
  }

   // 光
  const createLight = () => {
    const ambient = new AmbientLight(0x444444)
    scene.add(ambient)
  }

  const createPlane = () => {
    const radarGeom = new PlaneGeometry(100, 100, 100, 100) // width = 1, height = 1, widthSegments(宽度分段) = 1, heightSegments(高度分段) = 1
    const radarMat = new ShaderMaterial({
      vertexShader: vertexShader,
      fragmentShader: fragmentShader,
      side: DoubleSide,
      uniforms: {
        u_color: { value: new Color('#f00') },
        u_radius: { value: 50.0 },
        u_rotation_step: { value: 0.0 },
        u_pi: {  value: 3.14  }
      },
      transparent: true,
      depthWrite: false,
    })

    const radar = new Mesh(radarGeom, radarMat)
    scene.add(radar)
    return radar
  }

  createLight()
  const radar = createPlane()

  const runAnimate = () => {
    radar.material.uniforms.u_rotation_step.value += 0.5

    requestID.value = requestAnimationFrame(runAnimate)
    renderer.render(scene, camera)
  }


  runAnimate()
  
  const controls = createOrbitControls()

  return {
    renderer,
    scene,
    controls,
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.scene.clear()
    sceneResources.scene.traverse((child) => {
      if (child.geometry) child.geometry?.dispose()
      if (child.material) {
        if (child.material.map) child.material.map?.dispose()
        child.material?.dispose()
      }
    })
    if (sceneResources.scene.background) {
      if (sceneResources.scene.background instanceof Texture) {
        sceneResources.scene.background?.dispose()
      }
    }
    sceneResources.renderer?.dispose()
    sceneResources.renderer.forceContextLoss()
    sceneResources.controls?.dispose()

    cancelAnimationFrame(requestID.value)

    sceneResources = null
  }
}

onMounted(async() => {
  await nextTick()
})

onUnmounted(() => {
  destroy()
})
</script>

雷达 -3

把多个geo合并成一个并添加shader
点击运行
<template>
  <div>
    <div>把多个geo合并成一个并添加shader</div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <div v-if="isRunning" id="radar3" class="stage"></div>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import {
  Scene,
  WebGLRenderer,
  PerspectiveCamera,
  Color,
  AmbientLight,
  Mesh,
  DoubleSide,
  Texture,
  PlaneGeometry,
  ShaderMaterial,
  BufferGeometry,
  BufferAttribute,
  BoxGeometry,
  MeshStandardMaterial,
} from 'three'

const requestID = ref<any>()
const isRunning = ref(false)
let sceneResources

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    sceneResources = await initScene()
  } else {
    isRunning.value = false
    destroy()
  }
}

function mergeAttributes(attributes) {
  let TypedArray
  let itemSize
  let normalized
  let gpuType = -1
  let arrayLength = 0
  for (let i = 0; i < attributes.length; ++i) {
    const attribute = attributes[i]
    if (TypedArray === undefined) TypedArray = attribute.array.constructor
    if (TypedArray !== attribute.array.constructor) {
      console.error(
        'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.array must be of consistent array types across matching attributes.'
      )
      return null
    }
    if (itemSize === undefined) itemSize = attribute.itemSize
    if (itemSize !== attribute.itemSize) {
      console.error(
        'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.itemSize must be consistent across matching attributes.'
      )
      return null
    }
    if (normalized === undefined) normalized = attribute.normalized
    if (normalized !== attribute.normalized) {
      console.error(
        'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.normalized must be consistent across matching attributes.'
      )
      return null
    }
    if (gpuType === -1) gpuType = attribute.gpuType
    if (gpuType !== attribute.gpuType) {
      console.error(
        'THREE.BufferGeometryUtils: .mergeAttributes() failed. BufferAttribute.gpuType must be consistent across matching attributes.'
      )
      return null
    }
    arrayLength += attribute.count * itemSize
  }
  const array = new TypedArray(arrayLength)
  const result: any = new BufferAttribute(array, itemSize, normalized)
  let offset = 0
  for (let i = 0; i < attributes.length; ++i) {
    const attribute = attributes[i]
    if (attribute.isInterleavedBufferAttribute) {
      const tupleOffset = offset / itemSize
      for (let j = 0, l = attribute.count; j < l; j++) {
        for (let c = 0; c < itemSize; c++) {
          const value = attribute.getComponent(j, c)
          result.setComponent(j + tupleOffset, c, value)
        }
      }
    } else {
      array.set(attribute.array, offset)
    }
    offset += attribute.count * itemSize
  }
  if (gpuType !== undefined) {
    result.gpuType = gpuType
  }
  return result
}

function mergeGeometries(geometries, useGroups = false) {
  const isIndexed = geometries[0].index !== null
  const attributesUsed = new Set(Object.keys(geometries[0].attributes))
  const morphAttributesUsed = new Set(Object.keys(geometries[0].morphAttributes))
  const attributes = {}
  const morphAttributes: any = {}
  const morphTargetsRelative = geometries[0].morphTargetsRelative
  const mergedGeometry = new BufferGeometry()
  let offset = 0
  for (let i = 0; i < geometries.length; ++i) {
    const geometry = geometries[i]
    let attributesCount = 0
    // ensure that all geometries are indexed, or none
    if (isIndexed !== (geometry.index !== null)) {
      console.error('THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i +
        '. All geometries must have compatible attributes; make sure index attribute exists among all geometries, or in none of them.'
      )
      return null
    }
    // gather attributes, exit early if they're different
    for (const name in geometry.attributes) {
      if (!attributesUsed.has(name)) {
        console.error('THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i +
          '. All geometries must have compatible attributes; make sure "' + name +
          '" attribute exists among all geometries, or in none of them.')
        return null
      }
      if (attributes[name] === undefined) attributes[name] = []
      attributes[name].push(geometry.attributes[name])
      attributesCount++
    }
    // ensure geometries have the same number of attributes
    if (attributesCount !== attributesUsed.size) {
      console.error('THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i +
        '. Make sure all geometries have the same number of attributes.')
      return null
    }
    // gather morph attributes, exit early if they're different
    if (morphTargetsRelative !== geometry.morphTargetsRelative) {
      console.error('THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i +
        '. .morphTargetsRelative must be consistent throughout all geometries.')
      return null
    }
    for (const name in geometry.morphAttributes) {
      if (!morphAttributesUsed.has(name)) {
        console.error('THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i +
          '.  .morphAttributes must be consistent throughout all geometries.')
        return null
      }
      if (morphAttributes[name] === undefined) morphAttributes[name] = []
      morphAttributes[name].push(geometry.morphAttributes[name])
    }
    if (useGroups) {
      let count
      if (isIndexed) {
        count = geometry.index.count
      } else if (geometry.attributes.position !== undefined) {
        count = geometry.attributes.position.count
      } else {
        console.error('THREE.BufferGeometryUtils: .mergeGeometries() failed with geometry at index ' + i +
          '. The geometry must have either an index or a position attribute')
        return null
      }
      mergedGeometry.addGroup(offset, count, i)
      offset += count
    }
  }
  // merge indices
  if (isIndexed) {
    let indexOffset = 0
    const mergedIndex: any = []
    for (let i = 0; i < geometries.length; ++i) {
      const index = geometries[i].index
      for (let j = 0; j < index.count; ++j) {
        mergedIndex.push(index.getX(j) + indexOffset)
      }
      indexOffset += geometries[i].attributes.position.count
    }
    mergedGeometry.setIndex(mergedIndex)
  }
  // merge attributes
  for (const name in attributes) {
    const mergedAttribute = mergeAttributes(attributes[name])
    if (!mergedAttribute) {
      console.error('THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' + name +
        ' attribute.')
      return null
    }
    mergedGeometry.setAttribute(name, mergedAttribute)
  }
  // merge morph attributes
  for (const name in morphAttributes) {
    const numMorphTargets = morphAttributes[name][0].length
    if (numMorphTargets === 0) break
    mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {}
    mergedGeometry.morphAttributes[name] = []
    for (let i = 0; i < numMorphTargets; ++i) {
      const morphAttributesToMerge: any = []
      for (let j = 0; j < morphAttributes[name].length; ++j) {
        morphAttributesToMerge.push(morphAttributes[name][j][i])
      }
      const mergedMorphAttribute = mergeAttributes(morphAttributesToMerge)
      if (!mergedMorphAttribute) {
        console.error('THREE.BufferGeometryUtils: .mergeGeometries() failed while trying to merge the ' +
          name + ' morphAttribute.')
        return null
      }
      mergedGeometry.morphAttributes[name].push(mergedMorphAttribute)
    }
  }
  return mergedGeometry
}

const vertexShader = `
  varying vec3 vp;
  void main() {
    vp = position;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`

const fragmentShader = `
  varying vec3 vp;
					
  uniform vec3 u_color;
  uniform float u_pi;
  uniform float u_radius;
  uniform float u_rotation_step;
  
  float getLength(float x, float y) {
    return sqrt((x - 0.0) * (x - 0.0) + (y - 0.0) * (y - 0.0));
  }
  
  void main() {
    // 旋转
    float angOffset = u_rotation_step * 0.05;
    float cosAng = cos(angOffset);
    float sinAng = sin(angOffset);
    mat2 modelMatrix = mat2(
      cosAng,sinAng,
      -sinAng,cosAng
    );
    vec2 point = modelMatrix * vp.xy;
    
    
    // ang=[-π,π]
    // atan(y,x)用于将XY坐标,返回弧度
    float ang = atan(point.y, point.x);
    // (u_pi - π)  或者 (u_pi - - π) 取值 0 ~ 2π
    float radians = u_pi - ang;
    float opacity = radians / (u_pi * 1.0);
    // float opacity = 1.0;
    
    // 隐藏某些部分
    if (abs(radians) > 1.0) {
      opacity = 0.0;
    }
    
    // 距离
    float uLength = getLength(point.x, point.y);
    if (uLength > u_radius) {
      opacity = 0.0;
    }
    
    gl_FragColor = vec4(u_color, opacity);
  }
`

const initScene = () => {
  const ele = document.getElementById('radar3') as HTMLElement
  const width = Number(window.getComputedStyle(ele).width.split('px')[0])
  const height = Number(window.getComputedStyle(ele).height.split('px')[0])

  const scene = new Scene()

  const camera: any = new PerspectiveCamera(75, width / height, 0.1, 3000)
  camera.position.set(0, 0, 50)
  scene.add(camera)

  const renderer: any = new WebGLRenderer({
    antialias: true,
    alpha: true
  })
  renderer.setSize(width, height)
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  renderer.setClearColor(new Color('#32373E'), 1)
  ele.appendChild(renderer.domElement)

  // 添加 OrbitControls
  const createOrbitControls = () => {
    const controls = new OrbitControls(camera, renderer.domElement)
    controls.enableDamping = true
    controls.dampingFactor = 0.25
    controls.enableZoom = true
    return controls
  }

   // 光
  const createLight = () => {
    const ambient = new AmbientLight(0x444444)
    scene.add(ambient)
  }

  function getRandomInt(min, max) {
    // 包括 min,不包括 max
    min = Math.ceil(min);  // 如果 min 不是整数,向上取整
    max = Math.floor(max); // 如果 max 不是整数,向下取整
    return Math.floor(Math.random() * (max - min + 1)) + min; // 含 min,不含 max
  }

  const createPlane = () => {
    const radarGeom = new PlaneGeometry(100, 100, 1, 1)
    const mat = new MeshStandardMaterial({
      color: '#f00',
      side: DoubleSide
    })
    const plane = new Mesh(radarGeom, mat)
    scene.add(plane)
  }

  const createMergeGeo = () => {
    const radarMat = new ShaderMaterial({
      vertexShader: vertexShader,
      fragmentShader: fragmentShader,
      side: DoubleSide,
      uniforms: {
        u_color: { value: new Color('#0f0') },
        u_radius: { value: 50.0 },
        u_rotation_step: { value: 0.0 },
        u_pi: {  value: 3.14  }
      },
      transparent: true,
      depthWrite: false,
    })

    const mergeGeoArr: any = []
    for (let i = 0; i < 3; i++) {
      const boxGeometry = new BoxGeometry(10, 10, 10)
      const x = getRandomInt(0, 40)
      const y = getRandomInt(0, 40)
      boxGeometry.translate(x, y, 0)
      mergeGeoArr.push(boxGeometry)
    }
    const radarGeom = new PlaneGeometry(100, 100, 1, 1)
    const mergeGeo: any = mergeGeometries([...mergeGeoArr, radarGeom])
    const radar = new Mesh(mergeGeo, radarMat)
    scene.add(radar)
    return radar
  }

  createLight()
  createPlane()
  const radar = createMergeGeo()

  const runAnimate = () => {
    radar.material.uniforms.u_rotation_step.value += 0.5

    requestID.value = requestAnimationFrame(runAnimate)
    renderer.render(scene, camera)
  }


  runAnimate()
  
  const controls = createOrbitControls()

  return {
    renderer,
    scene,
    controls,
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.scene.clear()
    sceneResources.scene.traverse((child) => {
      if (child.geometry) child.geometry?.dispose()
      if (child.material) {
        if (child.material.map) child.material.map?.dispose()
        child.material?.dispose()
      }
    })
    if (sceneResources.scene.background) {
      if (sceneResources.scene.background instanceof Texture) {
        sceneResources.scene.background?.dispose()
      }
    }
    sceneResources.renderer?.dispose()
    sceneResources.renderer.forceContextLoss()
    sceneResources.controls?.dispose()

    cancelAnimationFrame(requestID.value)

    sceneResources = null
  }
}

onMounted(async() => {
  await nextTick()
})

onUnmounted(() => {
  destroy()
})
</script>

高度渐变色

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <div v-if="isRunning" id="highGradientColor" class="stage"></div>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
  Scene,
  PerspectiveCamera,
  WebGLRenderer,
  Color,
  AmbientLight,
  DirectionalLight,
  PlaneGeometry,
  Mesh,
  ShaderMaterial,
  DoubleSide,
  Clock,
  Texture
} from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

const addTime = ref<any>({ value: 0 })
const requestID = ref<any>()
const isRunning = ref(false)
let clock: any = new Clock()
let sceneResources

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    sceneResources = await initScene()
  } else {
    isRunning.value = false
    destroy()
  }
}

const vertexShaderReplacements = `
  precision highp float;

  uniform float uTime;
  uniform float uInterval;

  varying vec3 fPosition;
  varying float vInterval;
  varying float vOpacity;
  
  void main() {
    vec4 pos = modelViewMatrix * vec4(position, 1.0);
    
    fPosition = (modelMatrix * vec4(position, 1.0)).xyz;

    vInterval = uInterval;
    vOpacity = sin(uTime);

    gl_Position = projectionMatrix * pos;
  }
  `
  const fragmentShaderReplacements = `
  precision highp float;

  varying vec3 fPosition;
  varying float vInterval;
  varying float vOpacity;

  void d_color() {
    float dataY = fPosition.y;
    float dataI = vInterval;
    vec3 color = vec3(0.0, 0.0, 0.0);

    if (dataY <= -dataI) {
      // 蓝色-蓝绿
      // 0,0,1 -> 0,1,1
      color = vec3(0.0, 0.0, 1.0);
    } else if (dataY > -dataI && dataY <= 0.0) {
      float g = 1.0 - (-dataY / dataI);
      color = vec3(0.0, g, 1.0);
    } else if (dataY > 0.0 && dataY <= dataI) {
      // 蓝绿-绿
      // 0,1,1 -> 0,1,0
      float g = 1.0 - dataY / dataI;
      color = vec3(0.0, 1.0, g);
    } else if (dataY > dataI && dataY <= 2.0 * dataI) {
      // 绿-浅绿
      // 0,1,0 -> 0.5,1,0
      float r = 0.5 * ((dataY - dataI) / dataI);
      color = vec3(r, 1.0, 0.0);
    } else if (dataY > 2.0 * dataI && dataY <= 3.0 * dataI) {
      // 浅绿-黄
      // 0.5,1,0 -> 1,1,0
      float r = 0.5 + ((dataY - 2.0 * dataI) / dataI) * 0.5;
      color = vec3(r, 1.0, 0.0);
    } else if (dataY > 3.0 * dataI && dataY <= 4.0 * dataI) {
      // 黄-土黄
      // 1,1,0 -> 1,0.76,0
      float g = 1.0 - ((dataY - 3.0 * dataI) / dataI) * (1.0 - 0.76);
      color = vec3(1.0, g, 0.0);
    } else if (dataY > 4.0 * dataI && dataY <= 5.0 * dataI) {
      // 土黄-橙
      // 1,0.76,0 -> 1,0.58,0
      float g = 0.76 - ((dataY -  4.0 * dataI) / dataI) * (0.76 - 0.58);
      color = vec3(1.0, g, 0.0);
    } else if (dataY > 5.0 * dataI && dataY <= 6.0 * dataI) {
      // 橙-红
      // 1,0.58,0 -> 1,0,0
      float g = 0.58 - ((dataY - 5.0 * dataI) / dataI) * 0.58;
      color = vec3(1.0, g, 0.0);
    } else {
      // 红
      // 1.0,0.0,0.0
      color = vec3(1.0, 0.0, 0.0);
    }

    gl_FragColor = vec4(color, vOpacity);
  }

  void main() {
    d_color();
  }
  `

const initScene = () => {
  const ele = document.getElementById('highGradientColor') as HTMLElement
  const width = Number(window.getComputedStyle(ele).width.split('px')[0])
  const height = Number(window.getComputedStyle(ele).height.split('px')[0])

  const scene = new Scene()

  const camera: any = new PerspectiveCamera(75, width / height, 0.1, 3000)
  camera.position.set(0, 0, 1000)
  scene.add(camera)

  const renderer: any = new WebGLRenderer({
    antialias: true,
    alpha: true
  })
  renderer.setSize(width, height)
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  renderer.setClearColor(new Color('#32373E'), 1)
  ele.appendChild(renderer.domElement)

  // 添加 OrbitControls
  const createOrbitControls = () => {
    const controls = new OrbitControls(camera, renderer.domElement)
    controls.enableDamping = true
    controls.dampingFactor = 0.25
    controls.enableZoom = true
  }

  // 光
  const createLight = () => {
     //环境光
    const ambient = new AmbientLight(0x444444)
    scene.add(ambient)

    // 平行光
    const directionalLight = new DirectionalLight(0xffffff)
    // 平行光配置
    directionalLight.position.set(-40, 60, -10)
    directionalLight.castShadow = true
    directionalLight.shadow.camera.near = 20
    directionalLight.shadow.camera.far = 200
    directionalLight.shadow.camera.left = -50
    directionalLight.shadow.camera.right = 50
    directionalLight.shadow.camera.top = 50
    directionalLight.shadow.camera.bottom = -50
    // 距离和强度
    directionalLight.intensity = 0.5
    // 设置阴影的分辨率
    directionalLight.shadow.mapSize.width = 1024
    directionalLight.shadow.mapSize.height = 1024
    scene.add(directionalLight)
  }

  // 创建平面
  const createPlane = () => {
    const geometry = new PlaneGeometry(30, 200) // width = 1, height = 1, widthSegments(宽度分段) = 1, heightSegments(高度分段) = 1
    const material = new ShaderMaterial({
      // wireframe: true,
      side: DoubleSide,
      uniforms: {
        uInterval: {
          value: 25.0
        },
        uTime: addTime.value,
      },
      vertexShader: vertexShaderReplacements,
      fragmentShader: fragmentShaderReplacements
    })

    const plane = new Mesh(geometry, material)
    // plane.position.set(0, 100, 0)
    scene.add(plane)
  }
 
  const runAnimate = () => {
    addTime.value.value = clock.getElapsedTime()
    requestID.value = requestAnimationFrame(runAnimate)
    renderer.render(scene, camera)
  }

  
 
  createLight()
  createPlane()
  runAnimate()
  const controls = createOrbitControls()

  return {
    renderer,
    scene,
    controls
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.scene.clear()
    sceneResources.scene.traverse((child) => {
      if (child.geometry) child.geometry?.dispose()
      if (child.material) {
        if (child.material.map) child.material.map?.dispose()
        child.material?.dispose()
      }
    })
    if (sceneResources.scene.background) {
      if (sceneResources.scene.background instanceof Texture) {
        sceneResources.scene.background?.dispose()
      }
    }
    sceneResources.renderer?.dispose()
    sceneResources.renderer.forceContextLoss()
    sceneResources.controls?.dispose()

    cancelAnimationFrame(requestID.value)
    
    addTime.value.value = 0
    sceneResources = null
  }
}

onMounted(async() => {
  await nextTick()
})

onUnmounted(() => {
  destroy()
  clock = null
})
</script>

<style scoped>
</style>

扩散波纹

点击运行
<template>
  <div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <div v-if="isRunning" id="diffusionRipple" class="stage"></div>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
  Scene,
  WebGLRenderer,
  PerspectiveCamera,
  Color,
  AmbientLight,
  BoxGeometry,
  Mesh,
  ShaderMaterial,
  DoubleSide,
  Texture,
} from 'three'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'

const requestID = ref<any>()
const isRunning = ref(false)
let sceneResources

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    sceneResources = await initScene()
  } else {
    isRunning.value = false
    destroy()
  }
}

const vertexShader = `
  varying vec3 vp;
  void main() {
    vp = position;
    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
  }
`
const fragmentShader = `
  varying vec3 vp;
  uniform vec3 u_color;
  uniform vec3 u_tcolor;
  uniform float u_r;
  uniform float u_length;
  uniform float u_max;
  
  float getLength(float x, float y) {
    return sqrt((x - 0.0) * (x - 0.0) + (y - 0.0) * (y - 0.0));
  }
  
  void main() {
    float uOpacity = 0.3;
    vec3 vColor = u_color;
    float uLength = getLength(vp.x, vp.z);
    if (uLength <= u_r && uLength > u_r - u_length) {
      float opacity = sin((u_r - uLength) / uLength);
      uOpacity = opacity;
      if (vp.y < 0.0) {
        vColor = u_color * opacity;
      } else {
        vColor = u_tcolor;
      }
    }
    gl_FragColor = vec4(vColor, uOpacity);
  }
`

const initScene = () => {
  const ele = document.getElementById('diffusionRipple') as HTMLElement
  const width = Number(window.getComputedStyle(ele).width.split('px')[0])
  const height = Number(window.getComputedStyle(ele).height.split('px')[0])

  const scene = new Scene()

  const camera: any = new PerspectiveCamera(75, width / height, 0.1, 3000)
  camera.position.set(0, 0, 1000)
  scene.add(camera)

  const renderer: any = new WebGLRenderer({
    antialias: true,
    alpha: true
  })
  renderer.setSize(width, height)
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  renderer.setClearColor(new Color('#32373E'), 1)
  ele.appendChild(renderer.domElement)

  // 添加 OrbitControls
  const createOrbitControls = () => {
    const controls = new OrbitControls(camera, renderer.domElement)
    controls.enableDamping = true
    controls.dampingFactor = 0.25
    controls.enableZoom = true
    return controls
  }

  const createLight = () => {
    const light = new AmbientLight(0xadadad)
    scene.add(light)
  }
  
  const createBox = () => {
    const geometry = new BoxGeometry(300, 300, 300)
    const material = new ShaderMaterial({
      uniforms: {
        u_color: { value: new Color('#5588aa') },
        u_tcolor: { value: new Color('#f55c1a') },
        u_r: { value: 0.25 },
        u_length: { value: 20 },
      },
      side: DoubleSide,
      transparent: true,
      vertexShader: vertexShader,
      fragmentShader: fragmentShader
    })
    const mesh = new Mesh(geometry, material)
    scene.add(mesh)

    return mesh
  }

  createLight()
  const box = createBox()
  const material = box.material

  const runAnimate = () => {
    material.uniforms.u_r.value += 1
    if (material.uniforms.u_r.value >= 300) {
      material.uniforms.u_r.value = 20
    }

    requestID.value = requestAnimationFrame(runAnimate)
    renderer.render(scene, camera)
  }
  runAnimate()
  const controls = createOrbitControls()

  return {
    renderer,
    scene,
    controls
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.scene.clear()
    sceneResources.scene.traverse((child) => {
      if (child.geometry) child.geometry?.dispose()
      if (child.material) {
        if (child.material.map) child.material.map?.dispose()
        child.material?.dispose()
      }
    })
    if (sceneResources.scene.background) {
      if (sceneResources.scene.background instanceof Texture) {
        sceneResources.scene.background?.dispose()
      }
    }
    sceneResources.renderer?.dispose()
    sceneResources.renderer.forceContextLoss()
    sceneResources.controls?.dispose()

    cancelAnimationFrame(requestID.value)

    sceneResources = null
  }
}

onMounted(async() => {
  await nextTick()
})

onUnmounted(() => {
  destroy()
})
</script>

模拟建筑

把多个geo合并成一个并添加shader
点击运行
<template>
  <div>
    <div>把多个geo合并成一个并添加shader</div>
    <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    <div v-if="isRunning" id="radar2" class="stage"></div>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'
import {
  Scene,
  WebGLRenderer,
  PerspectiveCamera,
  Color,
  AmbientLight,
  Mesh,
  Texture,
  ShaderMaterial,
  BoxGeometry,
  BufferAttribute,
  EdgesGeometry,
  LineBasicMaterial,
  LineSegments
} from 'three'

const requestID = ref<any>()
const isRunning = ref(false)
let sceneResources

const onTrigger = async () => {
  if (!isRunning.value) {
    isRunning.value = true
    await nextTick()
    sceneResources = await initScene()
  } else {
    isRunning.value = false
    destroy()
  }
}

const initScene = () => {
  const ele = document.getElementById('radar2') as HTMLElement
  const width = Number(window.getComputedStyle(ele).width.split('px')[0])
  const height = Number(window.getComputedStyle(ele).height.split('px')[0])

  const scene = new Scene()

  const camera: any = new PerspectiveCamera(75, width / height, 0.1, 3000)
  camera.position.set(80, 80, 80)
  scene.add(camera)

  const renderer: any = new WebGLRenderer({
    antialias: true,
    alpha: true
  })
  renderer.setSize(width, height)
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
  renderer.setClearColor(new Color('#32373E'), 1)
  ele.appendChild(renderer.domElement)

  // 添加 OrbitControls
  const createOrbitControls = () => {
    const controls = new OrbitControls(camera, renderer.domElement)
    controls.enableDamping = true
    controls.dampingFactor = 0.25
    controls.enableZoom = true
    return controls
  }

   // 光
  const createLight = () => {
    const ambient = new AmbientLight(0x444444)
    scene.add(ambient)
  }

  const buildingSweepingLightShader = {
    uniforms: {
      boxH: {
        type: 'f',
        value: -10.0
      }
    },
    vertexShader: `
      attribute vec3 color;

      varying vec3 vColor;
      varying float v_pz;
      
      void main() {
        v_pz = position.y;
        vColor = color;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      uniform float boxH;
      varying vec3 vColor;
      varying float v_pz;
      
      float plot(float pct) {
        return smoothstep(pct - 8.0, pct, v_pz) - smoothstep(pct, pct + 0.02, v_pz);
      }
      
      void main() {
        float f1 = plot(boxH);
        vec4 b1 = mix(vec4(0.8, 0.1, 0.1, 0.1), vec4(f1, f1, f1, 1.0), 0.1);
        
        gl_FragColor = mix(vec4(vColor, 1.0), b1, b1);
        gl_FragColor = vec4(vec3(gl_FragColor), 0.9);
      }
    `
  }

  const createBuildings = () => {
    const buildMat = new ShaderMaterial({
      uniforms: buildingSweepingLightShader.uniforms,
      vertexShader: buildingSweepingLightShader.vertexShader,
      fragmentShader: buildingSweepingLightShader.fragmentShader,
    })
    buildMat.needsUpdate = true
    
    
    for (let i = 0; i < 10; i++) {
      const height = Math.random() * 10 + 2
      const width = 3
      const cubeGeom = new BoxGeometry(width, height, width)
      cubeGeom.setAttribute('color', new BufferAttribute(new Float32Array(24 * 3), 3))

      const colors = cubeGeom.attributes.color
      let r = Math.random() * 0.2
      let g = Math.random() * 0.2
      let b = Math.random() * 0.2
      // 设置立方体的6个面的24个顶点的颜色
      for (let i = 0; i < 24; i++) {
        colors.setXYZ(r, g, b, 0.8)
      }
      // 重置立方体顶部的四边形的4个顶点的颜色
      const k = 2
      colors.setXYZ(k * 4 + 0, .0, g, 1.0)
      colors.setXYZ(k * 4 + 1, .0, g, 1.0)
      colors.setXYZ(k * 4 + 2, .0, g, 1.0)
      colors.setXYZ(k * 4 + 3, .0, g, 1.0)
      const cube = new Mesh(cubeGeom, buildMat)
      cube.position.set(Math.random() * 100 - 50, height / 2, Math.random() * 100 - 50)
      scene.add(cube)

      // 绘制边框线
      const lineGeom = new EdgesGeometry(cubeGeom)
      const lineMaterial = new LineBasicMaterial({
        color: 0x0F8BF5,
        linewidth: 10,
        linecap: 'round',
        linejoin: 'round'
      })
      const line = new LineSegments(lineGeom, lineMaterial)
      line.scale.copy(cube.scale)
      line.rotation.copy(cube.rotation)
      line.position.copy(cube.position)
      scene.add(line)
    }
  }

  createLight()
  createBuildings()

  const runAnimate = () => {
    buildingSweepingLightShader.uniforms.boxH.value += 0.1
    if (buildingSweepingLightShader.uniforms.boxH.value > 10) {
      buildingSweepingLightShader.uniforms.boxH.value = -10.0
    }

    requestID.value = requestAnimationFrame(runAnimate)
    renderer.render(scene, camera)
  }


  runAnimate()
  
  const controls = createOrbitControls()

  return {
    renderer,
    scene,
    controls,
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.scene.clear()
    sceneResources.scene.traverse((child) => {
      if (child.geometry) child.geometry?.dispose()
      if (child.material) {
        if (child.material.map) child.material.map?.dispose()
        child.material?.dispose()
      }
    })
    if (sceneResources.scene.background) {
      if (sceneResources.scene.background instanceof Texture) {
        sceneResources.scene.background?.dispose()
      }
    }
    sceneResources.renderer?.dispose()
    sceneResources.renderer.forceContextLoss()
    sceneResources.controls?.dispose()

    cancelAnimationFrame(requestID.value)

    sceneResources = null
  }
}

onMounted(async() => {
  await nextTick()
})

onUnmounted(() => {
  destroy()
})
</script>