Skip to content

线条

fps: 0
点击运行
<template>
  <div>
    <div class="flex space-between">
      <div>fps: {{ fps }}</div>
      <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    </div>
    <canvas v-if="isRunning" id="shaderLine" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
  Engine,
  Scene,
  ArcRotateCamera,
  Vector3,
  Color4,
  Color3,
  HemisphericLight,
  MeshBuilder,
  Effect,
  ShaderMaterial
} from 'babylonjs'
import {
  AdvancedDynamicTexture,
  StackPanel,
  Control,
  TextBlock,
} from 'babylonjs-gui'

let sceneResources

const fps = ref(0)
const isRunning = ref(false)

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


// const vertexShader = `
//   attribute vec3 position;
//   attribute vec2 uv;

//   uniform mat4 worldViewProjection;

//   varying vec3 vPosition;
//   varying vec2 vUv;

//   void main() { 
//     vUv = uv; 
//     gl_Position = worldViewProjection * vec4(position, 1.0);
//   }
//   `
// const fragmentShader = `
//   uniform float iTime;
//   uniform vec2 iResolution; 
//   varying vec2 vUv;

//   vec3 hsb2rgb(in vec3 c) {
//     vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0), 6.0)-3.0)-1.0, 0.0,1.0 );
//     rgb = rgb*rgb*(3.0-2.0*rgb);
//     return c.z * mix( vec3(1.0), rgb, c.y);
//   }
//   void main(void) {
//     vec2 p = (2.0*vUv.xy-iResolution.xy)/iResolution.y;
//     float r = length(p) * 0.9;
//     vec3 color = hsb2rgb(vec3(0.24, 0.7, 0.4));

//     float a = pow(r, 2.0);
//     float b = sin(r * 0.8 - 1.6);
//     float c = sin(r - 0.010);
//     float s = sin(a - iTime * 3.0 + b) * c;

//     color *= abs(1.0 / (s * 10.8)) - 0.01;
//     gl_FragColor = vec4(color, 1.);
//     }
//   `

const sinCosPosition = (i, dense) => {
  let r = 20
  let deg = (i / 180) * Math.PI * dense
  let x = Math.cos(deg) * r
  let y = Math.sin(deg) * r
  return {
    x,
    y
  }
}

const initScene = async () => {
  const ele = document.getElementById("shaderLine") as any

  ele.addEventListener('wheel', function(event) {
    // 根据需要处理滚动
    // 例如,可以修改相机的半径或角度
    event.preventDefault() // 阻止默认滚动行为
  })

  const engine: any = new Engine(ele, true, {
    preserveDrawingBuffer: true,
    stencil: true,
    disableWebGL2Support: false
  })

  const scene = new Scene(engine)
  scene.useRightHandedSystem = true

  const camera = new ArcRotateCamera('camera', -Math.PI / 1.5, Math.PI / 2.2, 15, new Vector3(0, 0, 0), scene)
  camera.upperBetaLimit = Math.PI / 2.2
  camera.wheelPrecision = 30
  camera.panningSensibility = 200
  camera.attachControl(ele, true)
  camera.setPosition(new Vector3(200, 200, 200))

  const createLight = () => {
    const light = new HemisphericLight('light',new Vector3(1, 1, 0), scene)
    return light
  }

  const createAxis = () => {
    const axisX = MeshBuilder.CreateLines(
      'axisX', {
        colors: [new Color4(1, 0, 0, 1), new Color4(1, 0, 0, 1)],
        points: [new Vector3(0, 0, 0), new Vector3(80, 0, 0)]
      },
      scene
    )

    const axisY = MeshBuilder.CreateLines(
      'axisY', {
        colors: [new Color4(0, 1, 0, 1),  new Color4(0, 1, 0, 1)  ],
        points: [new Vector3(0, 0, 0), new Vector3(0, 80, 0) ]
      },
      scene
    )

    const axisZ = MeshBuilder.CreateLines(
      'axisZ', {
        colors: [new Color4(0, 0, 1, 1), new Color4(0, 0, 1, 1)],
        points: [new Vector3(0, 0, 0), new Vector3(0, 0, 80)]
      },
      scene
    )

    return [axisX, axisY, axisZ]
  }

  const createGui = async () => {
    const adt = AdvancedDynamicTexture.CreateFullscreenUI('UI')

    const xBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    xBox.position = new Vector3(80, 0, 0)
    const xPanel = new StackPanel()
    xPanel.width = '20px'
    xPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    xPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const x = new TextBlock()
    x.text = 'X'
    x.height = '30px'
    x.color = 'red'
    adt.addControl(xPanel)
    xPanel.addControl(x)
    xPanel.linkWithMesh(xBox)

    const yBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    yBox.position = new Vector3(0, 80, 0)
    const yPanel = new StackPanel()
    yPanel.width = '20px'
    yPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    yPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const y = new TextBlock()
    y.text = 'Y'
    y.height = '30px'
    y.color = 'green'
    adt.addControl(yPanel)
    yPanel.addControl(y)
    yPanel.linkWithMesh(yBox)

    const zBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    zBox.position = new Vector3(0, 0, 80)
    const zPanel = new StackPanel()
    zPanel.width = '20px'
    zPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    zPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const z = new TextBlock()
    z.text = 'Z'
    z.height = '30px'
    z.color = 'blue'
    adt.addControl(zPanel)
    zPanel.addControl(z)
    zPanel.linkWithMesh(zBox)
  }

  const createLine = () => {
    let uLen = 80.0
    let uTotal = 360.0
    let uVelocity = 1.0
    let uSize = 5.0
    let uTime = 0.0
    let uColor = new Color3(1, 0, 0)
    Effect.ShadersStore['lineShaderVertexShader'] = `
      // 设置着色器精度
      precision highp float;

      attribute vec3 position;
      uniform mat4 worldViewProjection;

      attribute float current;
      uniform float uSize;
      uniform float uTime;
      uniform float uLen;
      uniform float uTotal;
      uniform float uVelocity;
      uniform vec3 uColor;

      varying float vOpacity;

      void main(void) {
        float size = uSize;

        // speed*time得到当前路程,然后除以总路程,就是当前第几个粒子
        // mod(a, b) --> a / b的余数,20/100=0.2 --> 2
        float curDis = mod(uTime * uVelocity, uTotal * 2.0);

        // 判断当前像素点是否在uLen范围内
        if (current < curDis && curDis - current < uLen) {
          // 设置渐变大小,头大尾小
          float sizePct = (uLen  - (curDis - current)) / uLen;
          // clamp函数将一个值限制在另外两个值之间
          // clamp(a, min, max)
          size *= sizePct;
          vOpacity = clamp(1.0 * sizePct, 0.2, 1.0);
        } else if (current < curDis) {
          vOpacity = 0.2;
        } else {
          vOpacity = 0.2;
        }

        // gl_PointSize = size * 1.0;
        gl_Position = worldViewProjection * vec4(position, 1.0);
      }`
    Effect.ShadersStore['lineShaderFragmentShader'] = `
      precision highp float;

      varying float vOpacity;

      void main(void) {
        if (vOpacity <= 0.2) {
          gl_FragColor = vec4(0.0, 1.0, 0.0, vOpacity);
          // discard;
        } else {
          gl_FragColor = vec4(1.0, 0.0, 0.0, vOpacity);
        }
      }`

    const lineShader = new ShaderMaterial(
      'lineShader',
      scene, {
        vertex: 'lineShader',
        fragment: 'lineShader'
      }, {
        attributes: ['position', 'current'],
        uniforms: [
          'worldViewProjection',
          'uLen',
          'uTotal',
          'uSize',
          'uVelocity',
          'uTime',
          'uColor'
        ]
      }
    )

    lineShader.setFloat('uLen', uLen)
    lineShader.setFloat('uTotal', uTotal)
    lineShader.setFloat('uSize', uSize)
    lineShader.setFloat('uVelocity', uVelocity)
    lineShader.setColor3('uColor', uColor)

    let idx: any = []
    let vPos: any = []
    let index = 0
    let add = 0

    while (index < uTotal * 2) {
      const {
        x,
        y
      } = sinCosPosition(add, 2 * Math.PI)
      vPos.push(new Vector3(add, x, y))
      idx.push(index)
      index++
      add += 0.5
    }

    let me = MeshBuilder.CreateLines(
      'tex', {
        points: vPos,
        updatable: true
      },
      scene
    )
    me.position = new Vector3(0, 20, 0)
    me.setVerticesData('current', idx, true, 1)
    me.material = lineShader

    scene.registerBeforeRender(function() {
      lineShader.setFloat('uTime', uTime)
      uTime += 5
    })
  }


  const runAnimate = () => {
    engine.runRenderLoop(function() {
      if (scene && scene.activeCamera) {
        scene.render()

        fps.value = engine.getFps().toFixed(2)
      }
    })
  }

  createLight()
  createAxis()
  createGui()
  createLine()
  runAnimate()

  return {
    scene,
    engine, 
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.engine.stopRenderLoop() 
    sceneResources.engine.dispose()
    sceneResources.scene.dispose()
    sceneResources = null
  }
}

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

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

水面波纹

学习 CustomMaterial和Texture结合Fragment_DefinitionsFragment_Before_FogAddUniformMaterial.getEffect().setFloat('time', timeDiff)
fps: 0
点击运行
<template>
  <div>
    学习  
    <span style="color: red">CustomMaterial和Texture结合</span>、
    <span style="color: green">Fragment_Definitions</span>、
    <span style="color: blue">Fragment_Before_Fog</span>、
    <span style="color: pink">AddUniform</span>、
    <span style="color: orange">Material.getEffect().setFloat('time', timeDiff)</span>
    <div class="flex space-between">
      <div>fps: {{ fps }}</div>
      <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    </div>
    <canvas v-if="isRunning" id="shaderWaterRipples" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
  Engine,
  Scene,
  ArcRotateCamera,
  Vector3,
  HemisphericLight,
  MeshBuilder,
  Texture
} from 'babylonjs'
import { 
  CustomMaterial 
} from 'babylonjs-materials'

let sceneResources

const fps = ref(0)
const isRunning = ref(false)

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

const initScene = async () => {
  const ele = document.getElementById("shaderWaterRipples") as any

  ele.addEventListener('wheel', function(event) {
    // 根据需要处理滚动
    // 例如,可以修改相机的半径或角度
    event.preventDefault() // 阻止默认滚动行为
  })

  const engine: any = new Engine(ele, true, {
    preserveDrawingBuffer: true,
    stencil: true,
    disableWebGL2Support: false
  })

  const scene = new Scene(engine)
  scene.useRightHandedSystem = true

  const camera = new ArcRotateCamera('camera', -Math.PI / 1.5, Math.PI / 2.2, 15, new Vector3(0, 0, 0), scene)
  camera.upperBetaLimit = Math.PI / 2.2
  camera.wheelPrecision = 30
  camera.panningSensibility = 200
  camera.attachControl(ele, true)
  camera.setPosition(new Vector3(20, 20, 20))

  const createLight = () => {
    const light = new HemisphericLight('light',new Vector3(0, 1, 0), scene)
    return light
  }

  const materialShader = (mat) => {
    mat.Fragment_Definitions(`
      // 用于在图形渲染中创建一种称为“焦散”(caustic)效果的视觉效果
      // 焦散效果通常用于模拟光线通过透明介质(如水或玻璃)时的折射和反射
      // 产生复杂的光斑和条纹

      #define TAU 6.28318530718
      #define MAX_ITER 5
      #define SPEED 0.3
      #define SCALE 30.0

      vec4 caustic(vec2 uv) {
        // uv * TAU 将纹理坐标 (u, v) 扩展到 (0, 2 * PI) 的范围。这意味着纹理坐标被拉伸,每个分量都乘以 TAU
        // mod(uv * TAU, TAU) 将拉伸后的坐标映射回 (0, TAU) 的范围内。这是通过取模运算实现的,确保结果的每个分量都在 [0, TAU) 范围内
        // 并减去 150,进行平移
        vec2 p = mod(uv * TAU, TAU) - 150.0;

        // 根据全局时间变量 time 和定义的速度 SPEED 计算动画偏移量 t
        float t = time * SPEED + 23.0;

        // 初始化一个二维向量 i,初始值为 p
        vec2 i = vec2(p);
        // 初始化一个变量 c,用于累加计算
        float c = 1.0;
        // 定义一个强度变量 intens,用于控制分形的细节
        float intens = 0.005;

        // 开始一个循环,从 0 到 MAX_ITER-1
        for (int n = 0; n < MAX_ITER; n++) {
          // 每次迭代调整 t 的值,使分形的迭代逐渐减弱
          float nt = t * (1.0 - (3.5 / float(n + 1)));
          // 根据 t 更新 i 的值,创建分形的迭代
          i = p + vec2(cos(nt - i.x) + sin(nt + i.y), sin(nt - i.y) + cos(t + i.x));
          // 根据 i 的值更新 c,用于计算最终的颜色
          c += 1.0 / length(vec2(p.x / (sin(i.x + nt) / intens), p.y / (cos(i.y + nt) / intens)));
        }

        // 将 c 的值标准化,使其在 0 到 1 之间
        c /= float(MAX_ITER);

        // 对 c 应用非线性变换,增加对比度
        c = 1.17 - pow(c, 1.4);

        // 根据 c 的值计算颜色,使用 8 次幂函数增加对比度
        vec3 color = vec3(pow(abs(c), 8.0));

        // 将颜色限制在 [0, 1] 范围内
        color = clamp(color + vec3(0.0, 0.0, 0.0), 0.0, 1.0);

        // 定义对比度变量,对比度为 0 时无影响
        float contrast = 0.0;

        // 将颜色与白色混合,对比度为 0 时无影响
        color = mix(color, vec3(1.0, 1.0, 1.0), contrast);

        vec4 fColor = vec4(color, 0.0);

        return fColor;
      }
    `)

    // 用于在图形渲染的像素级别上混合颜色
    mat.Fragment_Before_Fog(`
      // 首先计算一个名为 coord 的二维向量
      // 使用 fract 函数和 vPositionW 变量。vPositionW 是一个包含顶点世界空间坐标的向量
      // fract 函数返回参数的小数部分,即 x - floor(x),这里用于获取坐标的小数部分,创建一种“抖动”效果
      // 然后,这些小数部分被 SCALE 缩放因子除以,进一步调整坐标的范围
      vec2 coord = vec2(fract(vPositionW.x / SCALE), fract(vPositionW.z / SCALE));
      
      // 调用了之前定义的 caustic 函数,传入 vDiffuseUV 作为参数
      // vDiffuseUV 是一个包含纹理坐标的向量
      // caustic 函数返回一个 vec4 类型的颜色值
      // 然后,使用 clamp 函数将结果的颜色值限制在 0.0 到 0.5 之间
      // clamp 函数确保颜色值不会超出这个范围
      vec4 causticColor = clamp(caustic(vDiffuseUV), 0.0, 0.5);
      
      color = vec4(clamp(mix(color, causticColor, 0.5), 0.0, 1.0).rgb, 1.0);
    `)

    mat.AddUniform('time', 'float')

    const startTime: any = new Date()

    mat.onBindObservable.add(function() {
      const endTime: any = new Date()
      const timeDiff = (endTime - startTime) / 1000.0 // in s
      mat.getEffect().setFloat('time', timeDiff)
    })
  }

  const createGround = () => {
    const ground = MeshBuilder.CreateGround(
      'ground',
      {
        width: 12,
        height: 12
      },
      scene
    )
    const grass = new CustomMaterial('grass', scene)
    grass.diffuseTexture = new Texture('/images/grass.png', scene)
    ground.material = grass
    materialShader(grass)
  }

  const createSphere = () => {
    const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 8 }, scene)
    const grass = new CustomMaterial('grass', scene)
    grass.diffuseTexture = new Texture('/images/grass.png', scene)
    sphere.material = grass
    sphere.position.y = 5
    materialShader(grass)
  }
  

  const runAnimate = () => {
    engine.runRenderLoop(function() {
      if (scene && scene.activeCamera) {
        scene.render()

        fps.value = engine.getFps().toFixed(2)
      }
    })
  }

  createLight()
  createGround()
  createSphere()
  runAnimate()

  return {
    scene,
    engine, 
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.engine.stopRenderLoop() 
    sceneResources.engine.dispose()
    sceneResources.scene.dispose()
    sceneResources = null
  }
}

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

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

sphere 的溶解

fps: 0
点击运行
<template>
  <div>
    <div class="flex space-between">
      <div>fps: {{ fps }}</div>
      <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    </div>
    <div v-if="isRunning">
      <span class="pointer" @click="changeSituation(1)">效果1</span>
      <span class="pointer m-l-20" @click="changeSituation(2)">效果2</span>
      <span class="pointer m-l-20" @click="changeSituation(3)">效果3</span>
    </div>
    <canvas v-if="isRunning" id="shaderSphereDissolve" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
  Engine,
  Scene,
  ArcRotateCamera,
  Vector3,
  Color4,
  HemisphericLight,
  MeshBuilder,
  Effect,
  ShaderMaterial,
  Texture
} from 'babylonjs'
import {
  AdvancedDynamicTexture,
  StackPanel,
  Control,
  TextBlock,
} from 'babylonjs-gui'

let sceneResources

const fps = ref(0)
const isRunning = ref(false)

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

const situation1Text = () => {
  return `
    float noiseValue = noise(p + uTime * 1.0); // 随时间变化的噪声

    float dissolve = smoothstep(baseColor.g, baseColor.b, noiseValue); // 循环
    gl_FragColor = mix(vec4(0.0), baseColor, dissolve);
  `
}

const situation2Text = () => {
  return `
    float noiseValue = noise(p + uTime * 1.0); // 随时间变化的噪声

    // 大于某个值则消失
    // baseColor.b * noiseValue应该是其他合适的值,现结果的溶解边缘不够圆润
    if (uTime > baseColor.g * noiseValue) {
    	discard;
    } else {
    	gl_FragColor = baseColor;
    }
  `
}

const situation3Text = () => {
  return `
    float noiseValue = noise(p + uRandom); // 随时间变化的噪声

    // 大于某个值则消失
    // baseColor.b * noiseValue应该是其他合适的值,现结果的溶解边缘不够圆润
    if (uTime > noiseValue) {
    	discard;
    } else {
    	gl_FragColor = baseColor;
    }
  `
}

const situationObj = {
 '1': situation1Text(),
 '2': situation2Text(),
 '3': situation3Text()
}


const curSituation = ref(1)

const changeSituation = async (cur) => {
  if (isRunning.value) {
    destroy()
    curSituation.value = cur
    await nextTick()
    setTimeout(async() => {
      sceneResources = await initScene()
    }, 500)
  }
}

const initScene = async () => {
  let time = 0

  const ele = document.getElementById("shaderSphereDissolve") as any

  ele.addEventListener('wheel', function(event) {
    // 根据需要处理滚动
    // 例如,可以修改相机的半径或角度
    event.preventDefault() // 阻止默认滚动行为
  })

  const engine: any = new Engine(ele, true, {
    preserveDrawingBuffer: true,
    stencil: true,
    disableWebGL2Support: false
  })

  const scene = new Scene(engine)
  scene.useRightHandedSystem = true

  const camera = new ArcRotateCamera('camera', -Math.PI / 1.5, Math.PI / 2.2, 15, new Vector3(0, 0, 0), scene)
  camera.upperBetaLimit = Math.PI / 2.2
  camera.wheelPrecision = 30
  camera.panningSensibility = 200
  camera.attachControl(ele, true)
  camera.setPosition(new Vector3(20, 20, 20))

  const createLight = () => {
    const light = new HemisphericLight('light',new Vector3(1, 1, 0), scene)
    return light
  }

  const createAxis = () => {
    const axisX = MeshBuilder.CreateLines(
      'axisX', {
        colors: [new Color4(1, 0, 0, 1), new Color4(1, 0, 0, 1)],
        points: [new Vector3(0, 0, 0), new Vector3(80, 0, 0)]
      },
      scene
    )

    const axisY = MeshBuilder.CreateLines(
      'axisY', {
        colors: [new Color4(0, 1, 0, 1),  new Color4(0, 1, 0, 1)  ],
        points: [new Vector3(0, 0, 0), new Vector3(0, 80, 0) ]
      },
      scene
    )

    const axisZ = MeshBuilder.CreateLines(
      'axisZ', {
        colors: [new Color4(0, 0, 1, 1), new Color4(0, 0, 1, 1)],
        points: [new Vector3(0, 0, 0), new Vector3(0, 0, 80)]
      },
      scene
    )

    return [axisX, axisY, axisZ]
  }

  const createGui = async () => {
    const adt = AdvancedDynamicTexture.CreateFullscreenUI('UI')

    const xBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    xBox.position = new Vector3(80, 0, 0)
    const xPanel = new StackPanel()
    xPanel.width = '20px'
    xPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    xPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const x = new TextBlock()
    x.text = 'X'
    x.height = '30px'
    x.color = 'red'
    adt.addControl(xPanel)
    xPanel.addControl(x)
    xPanel.linkWithMesh(xBox)

    const yBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    yBox.position = new Vector3(0, 80, 0)
    const yPanel = new StackPanel()
    yPanel.width = '20px'
    yPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    yPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const y = new TextBlock()
    y.text = 'Y'
    y.height = '30px'
    y.color = 'green'
    adt.addControl(yPanel)
    yPanel.addControl(y)
    yPanel.linkWithMesh(yBox)

    const zBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    zBox.position = new Vector3(0, 0, 80)
    const zPanel = new StackPanel()
    zPanel.width = '20px'
    zPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    zPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const z = new TextBlock()
    z.text = 'Z'
    z.height = '30px'
    z.color = 'blue'
    adt.addControl(zPanel)
    zPanel.addControl(z)
    zPanel.linkWithMesh(zBox)
  }

  const createShader = () => {
    Effect.ShadersStore['customVertexShader'] = `
      precision highp float;
    
      attribute vec3 position;
      uniform mat4 worldViewProjection;
      
      attribute vec2 uv; // 纹理坐标,通常在0到1之间
      
      varying vec2 vUv;
      
      void main(void) {
        vUv = uv;
        gl_Position = worldViewProjection * vec4(position, 1.0);
      }
    `

    Effect.ShadersStore['customFragmentShader'] = `
      precision highp float;
    
      uniform sampler2D uSampler;
      
      uniform float uTime;
      uniform float uRandom;
      
      varying vec2 vUv;
      
      vec2 grad(vec2 z) {
        int n = int(z.x) + int(z.y) * 11111;
        n = (n << 13) ^ n;
        n = (n * (n * n * 15731 + 789221) + 1376312589) >> 16;
      
        return vec2(cos(float(n)) ,sin(float(n)));
      }
      
      // 柏林噪音
      float noise(vec2 p) {
        vec2 i = vec2(floor(p));
        vec2 f = fract(p);

        vec2 u = f * f * (3.0 - 2.0 * f);
        
        float mix1 = mix(dot(grad(i + vec2(0, 0)), f - vec2(0.0, 0.0)), dot(grad(i + vec2(1, 0)), f - vec2(1.0, 0.0)), u.x);
        float mix2 = mix(dot(grad(i + vec2(0, 1)), f - vec2(0.0, 1.0)), dot(grad(i + vec2(1, 1)), f - vec2(1.0, 1.0)), u.x);
        return mix(mix1, mix2, u.y) * 0.5 + 0.5;
      }
      
      void main(void) {
        // texSampler:一个二维纹理采样器,它是纹理对象的引用
        // texCoord:纹理坐标,通常在0到1之间(如果纹理没有使用非归一化坐标的话)
        
        // OpenGL ES 2.0 或 OpenGL 3.x 之前的版本
        // vec4 color = texture2D(texSampler, texCoord);
        // gl_FragColor = texture2D(textureSampler, vUv); 
        
        // OpenGL 3.x 及更高版本
        // vec4 color = texture(texSampler, texCoord);
        
        
        // 基础颜色
        vec4 baseColor = texture(uSampler, vUv);
                  
        vec2 p = vUv * 10.0; // 缩放噪声
        
        ${situationObj[curSituation.value]}
      }
    `

    const sphereMat = new ShaderMaterial(
      'custom',
      scene, {
        vertex: 'custom',
        fragment: 'custom'
      }, {
        attributes: ['position', 'uv'],
        uniforms: ['worldViewProjection', 'uSampler', 'uTime'],
        samplers: ['uSampler'],
      }
    )

    const texture = new Texture('/images/ground.jpg', scene)
    sphereMat.setTexture('uSampler', texture)
    sphereMat.setFloat('uTime', time)
    sphereMat.setFloat('uRandom', Number((Math.random() * 123).toFixed(8)))

    const sphere = MeshBuilder.CreateSphere('sphere', {
      diameter: 10
    }, scene)
    sphere.material = sphereMat
    sphere.material.alpha = 0 // 透明度未0

    return sphereMat
  }



  const runAnimate = () => {
    engine.runRenderLoop(function() {
      if (scene && scene.activeCamera) {
        scene.render()

        fps.value = engine.getFps().toFixed(2)
      }
    })
  }

  createLight()
  createAxis()
  createGui()
  const sphereMat = createShader()
  runAnimate()

  scene.onBeforeRenderObservable.add(() => {
    time += curSituation.value === 1 ? 0.02 : 0.005
    sphereMat.setFloat('uTime', time)
  })

  return {
    scene,
    engine, 
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.engine.stopRenderLoop() 
    sceneResources.engine.dispose()
    sceneResources.scene.dispose()
    sceneResources = null
  }
}

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

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

粒子的颜色变换

fps: 0
点击运行
<template>
  <div>
    <div class="flex space-between">
      <div>fps: {{ fps }}</div>
      <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    </div>
    <canvas v-if="isRunning" id="shaderParticle" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
  Engine,
  Scene,
  ArcRotateCamera,
  Vector3,
  Color4,
  Texture,
  HemisphericLight,
  MeshBuilder,
  Effect,
  ParticleSystem
} from 'babylonjs'
import {
  AdvancedDynamicTexture,
  StackPanel,
  Control,
  TextBlock,
} from 'babylonjs-gui'

let sceneResources

const fps = ref(0)
const isRunning = ref(false)

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

const initScene = async () => {
  const ele = document.getElementById("shaderParticle") as any

  ele.addEventListener('wheel', function(event) {
    // 根据需要处理滚动
    // 例如,可以修改相机的半径或角度
    event.preventDefault() // 阻止默认滚动行为
  })

  const engine: any = new Engine(ele, true, {
    preserveDrawingBuffer: true,
    stencil: true,
    disableWebGL2Support: false
  })

  const scene = new Scene(engine)
  scene.useRightHandedSystem = true

  const camera = new ArcRotateCamera('camera', -Math.PI / 1.5, Math.PI / 2.2, 15, new Vector3(0, 0, 0), scene)
  camera.upperBetaLimit = Math.PI / 2.2
  camera.wheelPrecision = 30
  camera.panningSensibility = 200
  camera.attachControl(ele, true)
  camera.setPosition(new Vector3(10, 10, 10))

  const createLight = () => {
    const light = new HemisphericLight('light',new Vector3(1, 1, 0), scene)
    return light
  }

  const createAxis = () => {
    const axisX = MeshBuilder.CreateLines(
      'axisX', {
        colors: [new Color4(1, 0, 0, 1), new Color4(1, 0, 0, 1)],
        points: [new Vector3(0, 0, 0), new Vector3(80, 0, 0)]
      },
      scene
    )

    const axisY = MeshBuilder.CreateLines(
      'axisY', {
        colors: [new Color4(0, 1, 0, 1),  new Color4(0, 1, 0, 1)  ],
        points: [new Vector3(0, 0, 0), new Vector3(0, 80, 0) ]
      },
      scene
    )

    const axisZ = MeshBuilder.CreateLines(
      'axisZ', {
        colors: [new Color4(0, 0, 1, 1), new Color4(0, 0, 1, 1)],
        points: [new Vector3(0, 0, 0), new Vector3(0, 0, 80)]
      },
      scene
    )

    return [axisX, axisY, axisZ]
  }

  const createGui = async () => {
    const adt = AdvancedDynamicTexture.CreateFullscreenUI('UI')

    const xBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    xBox.position = new Vector3(80, 0, 0)
    const xPanel = new StackPanel()
    xPanel.width = '20px'
    xPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    xPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const x = new TextBlock()
    x.text = 'X'
    x.height = '30px'
    x.color = 'red'
    adt.addControl(xPanel)
    xPanel.addControl(x)
    xPanel.linkWithMesh(xBox)

    const yBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    yBox.position = new Vector3(0, 80, 0)
    const yPanel = new StackPanel()
    yPanel.width = '20px'
    yPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    yPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const y = new TextBlock()
    y.text = 'Y'
    y.height = '30px'
    y.color = 'green'
    adt.addControl(yPanel)
    yPanel.addControl(y)
    yPanel.linkWithMesh(yBox)

    const zBox = MeshBuilder.CreateBox('x', { size: 1 }, scene)
    zBox.position = new Vector3(0, 0, 80)
    const zPanel = new StackPanel()
    zPanel.width = '20px'
    zPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_RIGHT
    zPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM
    const z = new TextBlock()
    z.text = 'Z'
    z.height = '30px'
    z.color = 'blue'
    adt.addControl(zPanel)
    zPanel.addControl(z)
    zPanel.linkWithMesh(zBox)
  }

  const createParticle = () => {
    const emitter = MeshBuilder.CreateBox('box', { size: 10 }, scene)
    emitter.isVisible = false

    const effect = engine.createEffectForParticles('customParticle', ['time'])

    Effect.ShadersStore['customParticleFragmentShader'] = `
      precision highp float;

      varying vec2 vUV; // babylon.js提供
      varying vec4 vColor; // babylon.js提供

      uniform sampler2D diffuseSampler; // babylon.js提供

      uniform float time;

      void main() {
        vec2 position = vUV;
        
        vec2 center = vec2(0.5, 0.5);

        float color = 0.0;
        color = sin(distance(position, center) * 10.0 + time * vColor.g);

        vec4 baseColor = texture2D(diffuseSampler, vUV);

        gl_FragColor = baseColor * vColor * vec4(vec3(color, color, color), 1.0);
      }
    `

    const particleSystem = new ParticleSystem("particles", 4000, scene, effect)
    particleSystem.particleTexture = new Texture("/images/flare.png", scene)
    particleSystem.minSize = 0.1
    particleSystem.maxSize = 1.0
    particleSystem.minLifeTime = 0.5
    particleSystem.maxLifeTime = 5.0
    particleSystem.minEmitPower = 0.5
    particleSystem.maxEmitPower = 3.0
    particleSystem.emitter = emitter
    particleSystem.emitRate = 100;
    particleSystem.blendMode = ParticleSystem.BLENDMODE_ONEONE
    particleSystem.direction1 = new Vector3(-1, 1, -1)
    particleSystem.direction2 = new Vector3(1, 1, 1)
    particleSystem.color1 = new Color4(1, 1, 0, 1)
    particleSystem.color2 = new Color4(0, 1, 0, 1)
    particleSystem.gravity = new Vector3(0, -4.8, 0)
    particleSystem.start()


    let time = 0
    let order = 0.1

    effect.onBind = function () {
        effect.setFloat('time', time)

        time += order

        if (time > 100 || time < 0) {
            order *= -1
        }
    }
  }



  const runAnimate = () => {
    engine.runRenderLoop(function() {
      if (scene && scene.activeCamera) {
        scene.render()

        fps.value = engine.getFps().toFixed(2)
      }
    })
  }

  createLight()
  createAxis()
  createGui()
  createParticle()
  runAnimate()

  return {
    scene,
    engine, 
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.engine.stopRenderLoop() 
    sceneResources.engine.dispose()
    sceneResources.scene.dispose()
    sceneResources = null
  }
}

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

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