Skip to content

模拟大海(学习Gerstner波)

<template>
  <div>
    <div><a target="_blank" href="https://zhuanlan.zhihu.com/p/623569022">GerstnerWave原理</a></div>
    <div><a target="_blank" href="https://gameidea.org/2023/12/01/3d-ocean-shader-using-gerstner-waves/">Gerstner Waves</a></div>
    <div class="flex space-between">
      <div>fps: {{ fps }}</div>
      <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    </div>
    <canvas v-if="isRunning" id="shaderSea1" class="stage"></canvas>
  </div>
</template>

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

let sceneResources, adt
let uTime = 0.0

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("shaderSea1") 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 = false

  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 = 1
  camera.panningSensibility = 10
  camera.attachControl(ele, true)
  camera.setPosition(new Vector3(160, 160, -160))

  const createLight = () => {
    const light = new HemisphericLight('light',new Vector3(40, 40, 40), scene)
    light.direction = new Vector3(1.0, 0.0, 1.0)
    light.diffuse = new Color3(1.0, 0.95, 0.8)
    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 () => {
    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('y', { 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('z', { 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 createGround = () => {
    const ground = MeshBuilder.CreateGroundFromHeightMap(
      "heightMap", 
      "/images/heightMap.png", 
      { width: 150, height: 150, subdivisions: 500, maxHeight: 80 }
    )
    const mat = new CustomMaterial('grass', scene)
    mat.diffuseTexture = new Texture('/images/ground.jpg', scene)
    ground.material = mat
    return ground
  }

  const createSphere = () => {
    const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 10 }, scene)
    const sphereMat = new StandardMaterial('sphere')
    sphereMat.diffuseColor = new Color3(1.0, 0.6, 0.2)
    sphere.material = sphereMat
    sphere.position.x = 70
    sphere.position.y = 70
    sphere.position.z = 70
  }

  const createSphereShader = () => {
    Effect.ShadersStore['customShaderVertexShader'] = `
      precision highp float;

      struct GerstnerResult {
        vec3 position;
        vec3 normal;
      };

      attribute vec3 position;
      attribute vec2 uv;

      uniform mat4 world; // 世界矩阵
      uniform vec3 cameraPosition; // 相机位置
      uniform mat4 worldViewProjection; // 投影


      uniform sampler2D textureSampler;
      uniform float uTime;
      uniform vec3 uLightDirection; // 光照参数
      uniform vec3 uLightColor; // 光照参数

      varying vec3 vColor;

      // fract的返回值在 0 ~ 1
      float random(vec2 uv) {
        return fract(sin(dot(uv.xy, vec2(12.354121, 91.321))) * 452361.21321);
      }

      float noise(vec2 uv) {
        vec2 i = floor(uv); // 将输入的二维向量 uv 向下取整,得到包含 uv 点所在网格块的整数坐标 i。这是为了确定 uv 点位于哪个 2D 网格单元内。
        vec2 f = fract(uv); // 得到 uv 的小数部分,即 uv 点在其所在网格单元内的相对位置 f。f 的范围是 [0, 1)。

        // 2d 块的四个角,这四行计算了 uv 所在 2D 块的四个角上的随机值。
        float a = random(i); // 当前网格块左下角的随机值 a。这里使用 random 函数和网格块的整数坐标 i 作为输入。
        float b = random(i + vec2(1.0, 0.0)); // 当前网格块右下角的随机值 b。通过给 i 的 x 坐标加 1,可以定位到相邻的右侧网格块的左下角(因为 i 是整数坐标,加 1 后仍然是整数坐标)。
        float c = random(i + vec2(0.0, 1.0)); // 当前网格块左上角的随机值 c。通过给 i 的 y 坐标加 1,可以定位到相邻的上侧网格块的左下角。
        float d = random(i + vec2(1.0, 1.0)); // 当前网格块右上角的随机值 d。通过同时给 i 的 x 和 y 坐标加 1,可以定位到相邻的右上角网格块的左下角(或者说当前网格块的右上角,取决于如何看待相邻和当前的关系)。
        
        vec2 u = f * f * (3.0 - 2.0 * f); // 计算一个平滑的插值因子 u,用于在四个角点之间进行平滑过渡。f * f * (3.0 - 2.0 * f) 是一个三次插值函数,在 [0, 1] 区间内生成一个平滑的曲线,用于在四个噪声值之间进行插值。

        return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; // 使用 mix 函数和之前计算的插值因子 u 来在水平方向(x 轴)上插值 a 和 b,并手动计算垂直方向(y 轴)以及斜对角方向的插值,得到最终的噪声值。这个计算考虑了所有四个角点的贡献,并通过插值因子 u 来平滑过渡,从而生成一个平滑的二维噪声。
      }

      float fbm(vec2 uv) {
        float value = 0.0;
        float amplitude = 0.75; // 振幅
        float frequency = 0.4; // 频率

        for (int i = 0; i < 6; i++) {
          value += amplitude * noise(uv) * frequency;
          uv *= 3.0;
          amplitude *= 0.5;
        }

        return value;
      }

      GerstnerResult getGerstner(float x, float y, float z, float time) {
        // ---------------------------------------------------------
        // 法线相关的计算,用于光照。本案例使用y的高度来显示海面的白色和蓝色,没用到法线
        // ---------------------------------------------------------
        // 以sin函数为例,求P点的切线
        // P(x, sin(x))   --->   f(x)=sin(x)  --->   f'(x)=cos(x)
        // 带入P(π/2, 1),求得 f'(π/2)=0,即P点的斜率为0
        // 对x求导的结果为1,最后得(1, 0),即切线的方向向量(切线的方向向量可以通过导数来确定)(切线的方向向量可以表示为 (1, dy/dx))
        // ---------------------------------------------------------
        // 将曲线表示为参数方程。设 x=t,y=f(t),那么曲线可以表示为 r(t)=(t,f(t))。在 t=t0时,对应的点是 (t0,f(t0))。
        // 切向量是曲线在该点的导数
        // r′(t)=(dx/dt, dy/dt)=(1,f′(t))
        // 这里, dx/dt=1 是因为 x=t,所以 dx/dt=1
        // ---------------------------------------------------------
        // 假设 y=x^2,在 x=1 处:
        // 斜率  dy/dx=2x,在 x=1 时为 2
        // 切向量可以是 (1,2)
        // 这意味着在 x 方向移动 1,y 方向移动 2,与斜率一致
        // ---------------------------------------------------------
        // 如果曲线不是以 x=t 参数化,比如 x=t^2,y=t^3,那么
        // dx/dt=2t,dy/dt=3(t^2)
        // 切向量为(2t, 3(t^2)),斜率为3(t^2)/2t=(3/2)t
        // 此时需要归一化,比如有一个方向向量 v=(2,3)(例如,在 t=1 时,x=t^2, y=t^3 的切向量为 (2⋅1,3⋅12)=(2,3))
        // 计算模:∥v∥= (2*2 + 3*3)^0.5 = 13^0.5
        // 归一化后 u=(2/(13^0.5), 3/(13^0.5))
        // u 是一个单位向量,方向与 v 相同
        // ---------------------------------------------------------
        // x轴的切线的方向向量(dx/dx, dy/dx, dz/dx)
        // z轴的切线的方向向量(dx/dz, dy/dz, dz/dz)
        // 法线是 N = Z 叉乘 X
        // 为什么是 Z 叉乘 X ?
        // 规定大拇指,食指和中指分别是x,y,z方向,伸出左手,本案例的 useRightHandedSystem 是使用左手定则的
        // 那么就要使用左手螺旋定则确定切线和副切线的叉乘顺序
        // 接着再伸出左手握拳,由于已经海面需要的法线方向朝上,因此大拇指朝上指向法线方向,发现四指的旋向从上往下看是顺时针旋转,由B旋转到T,所以叉乘顺序是B×T
        // 一定要注意使用左右手螺旋定则与左右坐标系有关,右手系要使用右手定则
        // ---------------------------------------------------------
        // 最终计算如下:
        // 设模型空间顶点变换到世界空间后的位置(x, y, z)
        // 经过SineWave偏移后顶点的位置为(x, y + A * sin(k * x - omega * t), z)
        // z的切线方向为(0, 0, 1),所以只要计算x的切线方向。z是这样的结果,是因为把摄像机绕y轴顺时针旋转90看这个正弦波,会发现z方向都是平的
        // X.x = dx/dx = x' = 1
        // X.y = dy/dx = (y + a * sin(k * x - omega * t))' = a * k * cos(k * x - omega * t)
        // X.z = dz/dx = 0' = 0
        // X = (1, a * k * cos(k * x - omega * t), 0)
        // Z = (0, 0, 1)
        // N = Z 叉乘 X
        // vec3 x = vec3(1, a * k * cos(k * x - omega * t), 0)
        // vec3 z = vec3(0, 0, 1)
        // vec3 n = normalize(cross(z, x))
        // ---------------------------------------------------------




        // ---------------------------------------------------------
        // direction = vec4(1, 1, 0, 0)
        // dir = normalize(direction.xy)
        // value = k * D_x * x + k * D_y * z - ω * t  ---> dot(dir * k, xz) - ω * t
        // P_x = x + A * D_x * cos(value)
        // P_y = A * sin(value)
        // P_z = z + A * D_y * cos(value)
        // ---------------------------------------------------------
        // speed 是波的速度(自己定义)
        // A 是波的振幅,表示波浪的高低起伏程度(自己定义)
        // λ 是波长(自己定义)
        // t 是时间,表示当前时刻(外部传参)
        // k 是波数,与波长λ的关系为 k = 2π / λ ,波长是波峰到波峰的距离
        // D 是波的传播方向向量(方向余弦),表示波的传播方向,通常是一个单位向量
        // frequency 是频率,f = speed / λ;   f = 1 / T
        // ω 是角频率,与波的周期T的关系为 ω = 2π / T ,周期是波完成一个完整周期的时间
        // ---------------------------------------------------------
        // 振幅A:决定了波浪的高低起伏程度。振幅越大,波浪越高
        // 波数k:与波长成反比,波长越长,波数越小
        // 传播方向向量D:决定了波的传播方向。例如,如果D=(1,0),则波沿x轴正方向传播
        // 角频率ω:与波的周期成反比,周期越短,角频率越高
        // ---------------------------------------------------------




        vec3 xyz = vec3(x, y, z);


        // 初始化法线为垂直向上的向量,法线的作用是光照,因为即使顶点改变了,但是法线不会改变
        // X和Z也需要累加
        // 每个波对表面的影响是独立的,但最终的表面状态是所有波的叠加结果
        // 这意味着每个波对切线和副切线的贡献也需要逐波累加,而不是在所有波的位置影响计算完成后才统一计算
        vec3 normalX = vec3(0.0); // 切线(Tangent,T)
        vec3 normalZ = vec3(0.0); // 副切线(Binormal,B)


        // 这里是单个的波
        // float A = 0.8;
        // float speed = 10.0;
        // float waveLength = 6.0; // 波长影响波的叠加,比如0.2会多个波叠到一起,太大会呈现一条线,所以尽量找合适的数值
        // float k = 2.0 * 3.14 / waveLength;
        // float f = speed / waveLength;
        // float omega = 2.0 * 3.14 / f;
        // float value = dot(dir * k, vec2(x, z)) - omega * time;
        // xyz.x = x + A * dir.x * cos(value);
        // xyz.y = A * sin(value);
        // xyz.z = z + A * dir.y * cos(value);

        const int wavesCount = 4;

        for(int i = 0; i < wavesCount; i++) {
          float step = float(i) + 0.212;

          // 随机方向分布,都是正方向
          // 这个的vec2的x和y分别对应了xyz轴的x和z,因为高度的y不用考虑方向
          // x是切线,z是副切线
          float angle = random(vec2(float(i), float(i) + 1.1234123)) * 3.1415926 * 2.0; // 0 - 2π
          vec2 dir = vec2(cos(angle), sin(angle));
          
          float A = random(vec2(step + 0.134, step + 0.42)) * 0.2 + 0.2;
          float waveLength = random(vec2(step + 0.134, step + 0.442)) * 15.0 + 10.0;
          float speed = random(vec2(step + 0.134, step + 0.2)) * 2.0 + 1.2;

          // 固定的计算公式和值
          float k = 2.0 * 3.14 / waveLength;
          float f = speed / waveLength; // 频率
          float omega = 2.0 * 3.14 * f; // 等于 k * speed
          float value = dot(dir * k, vec2(x, z)) - omega * time;
          xyz.x += A * dir.x * cos(value);
          xyz.y += A * sin(value);
          xyz.z += A * dir.y * cos(value);


          // T=dP/dx
          // Tx=dPx/dx   --->   dx * k * a * (-sin(value))
          // Ty=dPy/dx   --->   k * a * cos(value)
          // Tz=dPz/dx   --->   dy * k * a * (-sin(value))
          // ---------------------------------------------------------
          // 为什么会得到 normalX.x += dir.x * dir.x * k * A * -sin(value); 这个方程
          // 首先,Px的计算方式是 Px = x + dir.x * a * cos(value)
          // 对x求偏导 dPx / dx = 1 + d(dir.x * a * cos(value)) / dx
          // 由于 dir.x 和 a 是常数,且 value 是 x 的函数,可以进一步计算
          // d(dir.x * a * cos(value)) / dx   --->    dir.x * a * d(cos(value)) / dx 
          // 根据链式法则:
          // d(cos(value)) / dx    --->    -sin(value) * d(value) / dx
          // value 可以表示为:
          // value = k * (dir.x * x + dir.y * z) - omega * t
          // 其中,k 是波数,omega 是角频率,t 是时间
          // 对 value 求 x 的偏导数:
          // d(value) / dx = k * dir.x
          // 将上面的式子带入前面的表达式,得到:
          // d(cos(value)) / dx = -sin(value) * dir.x * k
          // 因此:
          // dPx / dx = 1 + dir.x * a * (-sin(value) * k * dir.x)   --->   dPx / dx = 1 - dir.x * a * sin(value) * k * dir.x
          // 这里的 1 表示初始的切线方向在 x 轴上的分量。在没有波的影响时,切线方向是沿着 x 轴的
          // normalX = vec3(1.0 + normalX.x, normalX.y, normalX.z);
          // ---------------------------------------------------------
          normalX.x += dir.x * dir.x * k * A * -sin(value);
          normalX.y += dir.x * k * A * cos(value);
          normalX.z += dir.x * dir.y * k * A * -sin(value);

          // T=dP/dz
          // Tx=dPx/dz   --->   dx * k * a * (-sin(value))
          // Ty=dPy/dz   --->   k * a * cos(value)
          // Tz=dPz/dz   --->   dy * k * a * (-sin(value))
          normalZ.x += dir.x * dir.y * k * A * -sin(value);
          normalZ.y += dir.y * k * A * cos(value);
          normalZ.z += dir.y * dir.y * k * A * -sin(value);
        }


        // 将所有波的切线和副切线贡献累加到初始的切线和副切线上,得到最终的切线和副切线,然后叉积计算得到该点的法向量
        // 需要注意的是,这里对切线和副切线的x和z分量分别加了1,这是因为初始的切线和副切线是单位向量,分别指向x轴和z轴方向
        normalX = vec3(1.0 + normalX.x, normalX.y, normalX.z);
        normalZ = vec3(normalZ.x, normalZ.y, 1.0 + normalZ.z);

        vec3 finalNormal = normalize(cross(normalZ, normalX));

        GerstnerResult result;
        result.position = xyz;
        result.normal = finalNormal;

        return result;
      }

      void main() {
        float x = position.x;
        float y = position.y;
        float z = position.z;

        // -------------------------------------------------------xyz和uv-------------------------------------------------------
        // 由此段可以看出,x、y、z的分布。右手坐标系的中心为中点,向右x正,向下y正。左手坐标系,向右x正,向上y正。
        // vec3 color = vec3(0.0);
        // if(x > 0.0 && x < 30.0 && z > 0.0 && z < 50.0) {
        //   color = vec3(1.0, 1.0, 0.0);
        // }
        // vColor = color;
        // gl_Position = worldViewProjection * vec4(vec3(x, y, z), 1.0);


        // 由此段可以看出,uv的x、y的分布。y从右手坐标系的左上角开始,到左下角是 0 ~ 1。x从右手坐标系的左上角开始,到右上角是 0 ~ 1。
        // vec3 color = vec3(0.0);
        // if(uv.x > 0.0 && uv.x < 0.5 && uv.y > 0.0 && uv.y < 0.8) {
        //   color = vec3(1.0, 1.0, 0.0);
        // }
        // vColor = color;
        // gl_Position = worldViewProjection * vec4(vec3(x, y, z), 1.0);
        // -------------------------------------------------------xyz和uv-------------------------------------------------------


        // -------------------------------------------------------这个是使用fbm的noise来模拟海浪-------------------------------------------------------
        // 通过将多个不同频率和振幅的噪声层叠加在一起,生成更复杂的纹理。这种技术可以模拟许多自然现象,包括云、山脉、水面等
        // 但直接使用它可能效果不够理想,因为海浪具有更复杂的动态特性,例如波浪的传播、反射、破碎等
        // vec4 baseColor = texture(textureSampler, uv);
        // float allBlackColor = baseColor.r + baseColor.g + baseColor.b;
        // // if (allBlackColor == 0.0) { // 只有黑色的区域是河流
        // vec2 move1 = vec2(0.0);
        // move1.x = fbm(uv * 3.0);
        // move1.y = fbm(uv * 2.0);

        // vec2 move2 = vec2(0.0);
        // move2.x = fbm(uv + -0.2 * uTime + move1 + vec2(0.82, 0.32));
        // move2.y = fbm(uv + 0.5 * uTime + move1 + vec2(0.42, 0.732));

        // float fbm_value = fbm(uv + move2);

        // vec3 yColor = mix(vec3(0.0, 0.0, 0.0), vec3(1.0, 1.0, 1.0), clamp((fbm_value * fbm_value) * 4.0, 0.0, 1.0));
        // y += yColor.r * yColor.g * yColor.b * 1.2; // y 的最大值是 1.2

        // vColor = mix(vec3(0.1, 0.56, 1.0), vec3(0.48, 0.75, 1.0), clamp(y, 0.0, 1.0)); // vec3(0.1, 0.56, 1.0) 蓝色  vec3(0.48, 0.75, 1.0) 蓝白色  由低到高 
        // // }

        // gl_Position = worldViewProjection * vec4(vec3(x, y, z), 1.0);
        // -------------------------------------------------------这个是使用fbm的noise来模拟海浪-------------------------------------------------------




        // -------------------------------------------------------sin,y通过sin在上下周期运动,根本没有发生水平移动-------------------------------------------------------
        // vec3 color = vec3(0.0);
        // vColor = color;
        // float Amplitude = 0.2; // 波幅
        // float waveLength = 10.0; // 波长
        // float frequency = 2.0 * 3.14 / waveLength; // 频率
        // float speed = 50.0;
        // y = Amplitude * sin(frequency * (x - speed * uTime));
        // gl_Position = worldViewProjection * vec4(vec3(x, y, z), 1.0);
        // -------------------------------------------------------sin,y通过sin在上下周期运动,根本没有发生水平移动-------------------------------------------------------




        // -------------------------------------------------------使用 Gerstner 波-------------------------------------------------------
        // vec3 color = vec3(0.0);
        // vColor = color;

        GerstnerResult waveResult = getGerstner(x, y, z, uTime);
        vec3 xyz = waveResult.position;


        // 要把光照的这些计算放入到片元着色器中计算
        // --------------------------------------------------------------------------------------------
        // 光照计算(包括漫反射、高光反射等)通常应该在片元着色器(Fragment Shader)中完成,而不是顶点着色器(Vertex Shader)。
        // 精度问题:顶点着色器对每个顶点计算一次光照,而片元着色器对每个像素计算一次。对于复杂的表面细节(如波浪的泡沫、菲涅尔效应),顶点级别的插值会导致明显的锯齿或失真。
        // 视觉效果:光照效果(如高光、反射)需要精细的逐像素计算,顶点着色器的插值无法准确捕捉这些细节。
        // 性能权衡:虽然顶点着色器计算更快,但现代GPU的片元着色器性能足够处理复杂光照,且视觉效果提升显著。



        //------------------------------这个是通过normal计算,显示海水的颜色------------------------------
        // 变换法线到世界空间(需要世界矩阵)
        // 目的:从世界矩阵(world)中提取旋转部分,生成一个3x3的矩阵(normalMatrix),用于变换法线向量
        // 为什么需要单独提取旋转部分?
        // 法线向量是方向向量(只有方向,没有位置),因此不应受平移影响(平移不会改变方向)
        // 如果直接使用完整的世界矩阵(4x4),会错误地包含平移分量,导致法线方向错误
        // 通过取mat3(world),只保留旋转和缩放信息,忽略平移
        mat3 normalMatrix = mat3(world); // world是世界矩阵



        // 将模型空间的法线向量转换到世界空间,并确保转换后的法线仍然是单位向量
        // 目的:将模型空间的法线向量(waveResult.normal)通过normalMatrix变换到世界空间,并归一化(normalize)
        // 关键细节:
        // 变换到世界空间:光照计算通常在世界空间进行,因此需要将法线从模型空间转换到世界空间
        // 归一化:如果模型有非均匀缩放(例如x轴缩放2倍,y轴缩放1倍),直接变换法线会导致其长度不再为1。归一化确保法线方向正确,避免光照计算错误(如亮度异常)
        vec3 waveNormal = normalize(normalMatrix * waveResult.normal);


        
        // 将模型空间中的顶点位置(xyz)通过世界矩阵(world)变换到世界空间
        vec3 targetPosition = (world * vec4(xyz, 1.0)).xyz;



        // 海水基础颜色
        vec3 deepWaterColor = vec3(0.0, 0.549, 0.996); // 海水的深蓝色
        vec3 shallowWaterColor = vec3(0.3, 0.7, 1.0); // 天空的浅天蓝色



        // 根据深度混合基础颜色
        // 由于上面 Gerstner 的计算中,振幅的高度是[0, 1] * 0.2 + 0.2,然后循环4次,所以最大最小值是[-0.4 * 4 , 0.4 * 4]
        float depthFactor = clamp(xyz.y, -1.6, 1.6);
        vec3 waterColor = mix(deepWaterColor, shallowWaterColor, depthFactor);



        // 漫反射:模拟粗糙表面的均匀散射,决定物体的基础颜色和明暗
        // 高光反射:模拟光滑表面的镜面反射,增强物体的立体感和反光效果
        // 结合使用:通过叠加两者,可以创建更真实、更有层次的光照效果


        
        // 计算漫反射(Lambertian)
        float diffuse = max(0.0, dot(waveNormal, uLightDirection));
        vec3 diffuseColor = uLightColor * diffuse;


        
        // ------------------------计算高光反射(Blinn-Phong):步骤 1 ~ 4------------------------
        // 1、计算相机位置,指向 camera
        vec3 viewDirection = normalize(cameraPosition - targetPosition); 

        // 2、计算半程向量(Half Vector),即光线方向与视线方向的中间方向,用于简化高光计算
        vec3 halfVector = normalize(uLightDirection + viewDirection);

        // 3、计算高光强度
        // dot(waveNormal, halfVector):计算法线与半程向量的点积,得到余弦值(范围[-1, 1])
        // max(0.0, ...):确保结果非负(避免负值导致高光错误)
        // pow(..., 2.0):对余弦值取2次幂(高光指数),控制高光的锐利度:
        //      指数越大,高光越集中(如金属表面)
        //      指数越小,高光越分散(如粗糙表面)
        // 输出:specular是高光强度(范围[0, 1]),值越大表示高光越亮
        float specular = pow(max(0.0, dot(waveNormal, halfVector)), 2.0); // 2 是高光指数

        // 4、生成最终的高光颜色
        vec3 specularColor = uLightColor * specular * 0.5;
        // ------------------------计算高光反射(Blinn-Phong):步骤 1 ~ 4------------------------


        
        // 菲涅尔效应(根据视角调整反射强度)
        float fresnel = pow(1.0 - abs(dot(viewDirection, waveNormal)), 2.0);
        fresnel = clamp(fresnel, 0.3, 0.9); // 限制反射强度范围
        vec3 reflectionColor = mix(deepWaterColor, shallowWaterColor, fresnel);


        
        // 最终颜色合成
        vec3 baseColor = mix(waterColor, reflectionColor, fresnel); // 基础水色与反射的混合
        baseColor += diffuseColor + specularColor; // 添加光照效果
        


        vColor = baseColor;
        //------------------------------这个是通过normal计算,显示海水的颜色------------------------------



        //------------------------------这个是通过y的高度来混合颜色,显示海水的颜色------------------------------
        // vColor = mix(vec3(0.1, 0.56, 1.0), vec3(0.48, 0.75, 1.0), clamp(xyz.y, 0.0, 1.0)); // vec3(0.1, 0.56, 1.0) 蓝色  vec3(0.48, 0.75, 1.0) 蓝白色  由低到高.
        //------------------------------这个是通过y的高度来混合颜色,显示海水的颜色------------------------------

        float waterY = xyz.y + 1.6;
        gl_Position = worldViewProjection * vec4(vec3(xyz.x, waterY, xyz.z), 1.0);
        // -------------------------------------------------------使用 Gerstner 波-------------------------------------------------------
      }
    `

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

      varying vec3 vColor;
      
      void main(void) {
        gl_FragColor = vec4(vColor, 1.0);
      }
    `

    const customShader = new ShaderMaterial(
      'customShader',
      scene, {
        vertex: 'customShader',
        fragment: 'customShader',
      }, {
        attributes: ['position', 'uv'],
        uniforms: ['worldViewProjection', 'cameraPosition', 'world', 'textureSampler', 'uLightDirection', 'uLightColor', 'uTime'],
        samplers: ['textureSampler'],
        needAlphaBlending: true,
      },
    )

    const texture = new Texture('/images/heightMap.png', scene)
    customShader.setTexture('textureSampler', texture)
    customShader.setFloat('uTime', uTime)
    
    return customShader
  }

  const createPlane = () => {
    const plane = MeshBuilder.CreateGround(
      'plane', 
      { 
        width: 150, 
        height: 150, 
        subdivisions: 600 
      },
      scene
    )
    const material = createSphereShader()
   
    plane.material = material
    return material
  }


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

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

  const light = createLight()
  createAxis()
  createGui()
  createGround()
  createSphere()
  const material = createPlane()
  material.setVector3('lightDirection', light.direction)
  material.setColor3('lightColor', light.diffuse)
  runAnimate()

  scene.registerBeforeRender(function() {
    material.setFloat('uTime', uTime)
    uTime += 0.04
  })

  return {
    scene,
    engine, 
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.engine.stopRenderLoop() 
    sceneResources.engine.dispose()
    sceneResources.scene.dispose()
    sceneResources = null
  }
  if (adt) {
    adt.dispose()
    adt = 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="shaderWaterReflection" class="stage"></canvas>
  </div>
</template>

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

let sceneResources, adt

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("shaderWaterReflection") 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 = false

  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 = 10
  camera.attachControl(ele, true)
  camera.setPosition(new Vector3(50, 60, -50))

  const createLight = () => {
    const light = new HemisphericLight('light',new Vector3(0, 50, 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 () => {
    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('y', { 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('z', { 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 createSkyBox = () => {
    const skyBox = MeshBuilder.CreateBox('skyBox', { size: 512 }, scene)
    const skyBoxMaterial = new StandardMaterial('skyBox', scene)
    skyBoxMaterial.backFaceCulling = false
    skyBoxMaterial.reflectionTexture = new CubeTexture('/images/seaBox', scene)
    skyBoxMaterial.reflectionTexture.coordinatesMode = Texture.SKYBOX_MODE
    skyBoxMaterial.diffuseColor = new Color3(0, 0, 0)
    skyBoxMaterial.specularColor = new Color3(0, 0, 0)
    skyBox.material = skyBoxMaterial
    return skyBox
  }

  const createObject = () => {
    const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 8 }, scene)
    const sphereMat = new StandardMaterial('sphere')
    sphereMat.diffuseColor = new Color3(0.0, 0.8, 0.0)
    sphere.material = sphereMat
    sphere.position.y = 8

    const box = MeshBuilder.CreateBox('box', { size: 10 }, scene)
    const boxMat = new StandardMaterial('sphere')
    boxMat.diffuseColor = new Color3(0.9, 0.0, 0.9)
    box.material = boxMat
    box.position.y = 8
    box.position.z = 20

    return box
  }

  const createWater = (list) =>{
    const plane = MeshBuilder.CreateGround('ground', { width: 512, height: 512 }, scene)
    const water = new WaterMaterial('water', scene)
    water.backFaceCulling = true
    water.bumpTexture = new Texture('/images/waterBump.png', scene)
    water.windForce = -15
    water.waveHeight = 0.7
    water.bumpHeight = 1.5
    water.windDirection = new Vector2(1, 1)
    water.waterColor = new Color3(200 / 255, 0, 221 / 255)
    water.colorBlendFactor = 0.0
    water.addToRenderList(list[0])
    water.addToRenderList(list[1])
    plane.material = water
  }



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

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

  createLight()
  createAxis()
  createGui()
  const skyBox = createSkyBox()
  const object = createObject()
  createWater([skyBox, object])
  runAnimate()

  scene.registerBeforeRender(function() {
  })

  return {
    scene,
    engine, 
  }
}

const destroy = () => {
  if (sceneResources) {
    sceneResources.engine.stopRenderLoop() 
    sceneResources.engine.dispose()
    sceneResources.scene.dispose()
    sceneResources = null
  }
  if (adt) {
    adt.dispose()
    adt = 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="phillips" class="stage"></canvas>
    </div>
  </template>
  
  <script lang="ts" setup>
  import { onMounted, ref, nextTick, onUnmounted } from 'vue'
  import {
    Engine,
    Scene,
    HemisphericLight,
    MeshBuilder,
    Effect,
    ShaderMaterial,
    Color4,
    ArcRotateCamera,
    Vector3,
    Vector2,
    Color3,
    StandardMaterial
  } from 'babylonjs'
  import {
    AdvancedDynamicTexture,
    StackPanel,
    Control,
    TextBlock,
  } from 'babylonjs-gui'
  
  let sceneResources, adt
  let uTime = 0.0
  
  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("phillips") 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 = false
  
    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 = 1
    camera.panningSensibility = 10
    camera.attachControl(ele, true)
    camera.setPosition(new Vector3(160, 160, -160))
  
    const createLight = () => {
      const light = new HemisphericLight('light',new Vector3(40, 40, 40), scene)
      light.direction = new Vector3(1.0, 0.0, 1.0)
      light.diffuse = new Color3(1.0, 0.95, 0.8)
      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 () => {
      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('y', { 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('z', { 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 createSphere = () => {
      const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 10 }, scene)
      const sphereMat = new StandardMaterial('sphere')
      sphereMat.diffuseColor = new Color3(1.0, 0.6, 0.2)
      sphere.material = sphereMat
      sphere.position.x = 70
      sphere.position.y = 70
      sphere.position.z = 70
    }
  
    const createSphereShader = () => {
      Effect.ShadersStore['customShaderVertexShader'] = `
        precision highp float;
  
        const float PI = 3.14159265358979323846;
        const float TWO_PI = 2.0 * PI;
        const float G = 9.8;
        const float N = 64.0;
  
        attribute vec3 position;
        attribute vec2 uv;
  
        uniform mat4 worldViewProjection; // 投影
  
        uniform float uTime;
        uniform vec2 uResolution;
  
  
        varying vec3 vColor;
        varying vec2 vResolution;
  
        // 快速的伪随机
        float randValueFast(vec2 uv, float magic) {
          vec2 random2 = (1.0 / 4320.0) * uv + vec2(0.25, 0.0);
          float random = fract(dot(random2 * random2, vec2(magic)));
          random = fract(random * random * (2.0 * magic));
  
          return random; 
        }
  
  
        // 复数乘法
        vec2 complexMultiply(vec2 a, vec2 b){
          return vec2(
            a.x * b.x - a.y * b.y,  // 实部
            a.x * b.y + a.y * b.x   // 虚部
          );
        }
  
  
        // 色散关系函数
        float dispersion(vec2 k){
          return sqrt(G * length(k));
        }
  
  
        //计算两个相互独立的高斯随机数
        vec2 gauss(vec2 uv) {
          float u1 = randValueFast(uv, 1753.0);
          float u2 = randValueFast(uv, 3571.0);
          if(u1 < 1e-6) {
            u1 = 1e-6;
          }
  
          float g1 = sqrt(-2.0 * log(u1)) * cos(TWO_PI * u2);
          float g2 = sqrt(-2.0 * log(u1)) * sin(TWO_PI * u2);
  
          return vec2(g1, g2);
        }
  
  
        // 菲利普计算,输入波数 K
        float phillips(vec2 K) {
          // vec2 K = vec2(TWO_PI * n.x / 100.0, TWO_PI * n.y / 100.0); // 计算波数向量 K
          
          vec2 W = vec2(1.0, -1.0); // 定义一个方向向量(风)
  
          float V = 10.0; // 定义一个速度变量 V
  
          float A = 10.0; // 定义一个振幅变量 A。
  
          float L = V * V / G; // 计算波长 L。
          float L2 = L * L;
          
          float kLen = length(K); // 计算波数向量 K 的模长
          kLen = max(0.0001, kLen);
          float kLen2 = kLen * kLen;
          float kLen4 = kLen2 * kLen2;
  
          vec2 K_norm = normalize(K);
          vec2 W_norm = normalize(W);
          float dot_KW = dot(K_norm, W_norm);
  
          // 基础 Phillips 谱
          float phi = A * exp(-1.0 / (kLen2 * L2)) / kLen4;
  
          // dot_KW * dot_KW(称为风向拓展函数)
          // 风向对齐项(加强沿风向的波能)
          phi *= dot_KW * dot_KW;
  
          // 这一行的思想,必须要记住★★★★★★★★★★★★★★★★★★★★★★★★★★
          // 只要与方向有关的,一律优先思考dot
          // 这个是判断dot的方向是否与风的方向一致
          // 没有这一行,则两个椭圆一样大
          // 可以通过dot来---1:uv的增大缩小、2:光照强度、3:相似度 ......
          if(dot_KW > 0.0) {
            phi *= V;
          }
          // 这一行的思想,必须要记住★★★★★★★★★★★★★★★★★★★★★★★★★★
  
          // 衰减因子
          float l = 0.001 * L;
          float kSqr = dot(K, K);
          phi *= exp(l * l * -kSqr);
  
          return phi;
        }
  
        void main() {
          float x = position.x;
          float y = position.y;
          float z = position.z;
  
          vec3 color = vec3(0.0);
  
          // ★★★★★★★★★★★★★★ 必须要注意uResolution,和纯shader的用法不一样 ★★★★★★★★★★★★★★
  
          // 如果没有 pixelCoord 则 color.xy = vec2(gaussValue2); 的高斯图会有谐波出现,而不是噪点图
          vec2 pixelCoord = uv * uResolution; // 将归一化UV转换为像素坐标
          vec2 gaussValue1 = gauss(pixelCoord + vec2(3.0, 5.0)); // 用于 h0k
          vec2 gaussValue2 = gauss(pixelCoord + vec2(7.0, 11.0)); // 用于 h0kConj
  
          
          float nx = (uv.x - 0.5) * uResolution.x; // 还原到像素尺度
          float ny = (uv.y - 0.5) * uResolution.y; // 还原到像素尺度
          
  
          // 计算波数 K
          vec2 K = vec2(TWO_PI * nx / N, TWO_PI * ny / N);
          // 计算 h0(k) 和 h0*(-k)
          // 这个使用风向拓展函数
          vec2 h0k = gaussValue1 * sqrt(phillips(K) * 0.5);  // 初始频谱(由菲利普频谱与高斯随机数生成)
          vec2 h0kConj = gaussValue2 * sqrt(phillips(-K) * 0.5); // 共轭复数
          h0kConj.y *= -1.0; // 为什么 *-1.0 , ax + i·b,其共轭复数是 ax - i·b
  
          float omega = dispersion(K) * uTime;
          float c = cos(omega);
          float s = sin(omega);
          vec2 h1 = complexMultiply(h0k, vec2(c, s));
          vec2 h2 = complexMultiply(h0kConj, vec2(c, -s));
  
          vec2 H_Tilde = h1 + h2; // H_Tilde 波浪H是数学公式中的写法
  
  
  
          // 计算 KxHTilde 和 KzHTilde 是为了获取到法线
  
          vec2 maxK = K / max(0.001, length(K));
          vec2 KxHTilde = complexMultiply(vec2(0, -maxK.x), H_Tilde);
          vec2 KzHTilde = complexMultiply(vec2(0, -maxK.y), H_Tilde);
  
  
          // 默认法线向上,即没有波浪时候
          vec3 normal = vec3(0.0, 1.0, 0.0);
          if (length(K) > 0.001) {  // 跳过直流分量
  
            // 这里取实数部分,不用虚数
            // 在数学和物理的多个领域(如波动理论、电磁学、量子力学等)中,当处理 dh/dx 和 dh/dz 等导数时,若函数 h 是复数形式,通常只取其实数部分
            vec2 dHdx = complexMultiply(vec2(0, K.x), H_Tilde);
            vec2 dHdz = complexMultiply(vec2(0, K.y), H_Tilde);
  
            // 为什么是负数?
            // 梯度是“上升最快的方向”
            // 法线是“垂直于表面”的方向
            // 法线在水平面上的投影,正好是梯度的反方向
            // 直观理解:
            // 因为法线必须垂直于斜坡表面
            // 如果斜坡向东倾斜(东高西低),那么法线必须向西倾斜(西高东低)才能垂直于斜坡
            // 因此,法线在水平面上的投影指向西方(即梯度的反方向)
            normal = normalize(vec3(-dHdx.x, 1.0, -dHdz.x));
          }
  
  
  
          color.xy = vec2(H_Tilde);
          vColor = color;
  
          gl_Position = worldViewProjection * vec4(vec3(x, y, z), 1.0);
        }
      `
  
      Effect.ShadersStore['customShaderFragmentShader'] = `
        precision highp float;
  
        varying vec3 vColor;
  
        void main(void) {
          gl_FragColor = vec4(vColor, 1.0);
        }
      `
  
      const customShader = new ShaderMaterial(
        'customShader',
        scene, {
          vertex: 'customShader',
          fragment: 'customShader',
        }, {
          attributes: ['position', 'uv'],
          uniforms: ['worldViewProjection', 'uResolution', 'uTime'],
          needAlphaBlending: true,
        },
      )
  
  
      customShader.setFloat('uDown', uTime)
      
      return customShader
    }
  
    const createPlane = () => {
      const plane = MeshBuilder.CreateGround(
        'plane', 
        { 
          width: 256, 
          height: 256, 
          subdivisions: 256 
        },
        scene
      )
      const material = createSphereShader()
     
      plane.material = material
      return material
    }
  
  
    const runAnimate = () => {
      engine.runRenderLoop(function() {
        if (scene && scene.activeCamera) {
          scene.render()
  
          fps.value = engine.getFps().toFixed(2)
        }
      })
    }
  
    createLight()
    createAxis()
    createGui()
    createSphere()
    const material = createPlane()
    material.setVector2('uResolution', new Vector2(256, 256))
    runAnimate()
  
    scene.registerBeforeRender(function() {
      material.setFloat('uTime', uTime)
      uTime += 0.02
    })
  
    return {
      scene,
      engine, 
    }
  }
  
  const destroy = () => {
    if (sceneResources) {
      sceneResources.engine.stopRenderLoop() 
      sceneResources.engine.dispose()
      sceneResources.scene.dispose()
      sceneResources = null
    }
    if (adt) {
      adt.dispose()
      adt = null
    }
  }
  
  onMounted(async() => {
    await nextTick()
  })
  
  onUnmounted(() => {
    destroy()
  })
  </script>

proceduralTexture1(继承renderTargetTexture)(注意二者区别)

proceduralTexture1和proceduralTexture2的区别:
如果第一个proceduralTexture传入了rawTextureY这些数据,想通过uTime改变texture(uSampler, vUV)是不起效果的,还会报错。
只有等创建了一份ProceduralTexture后,才能在第二份的ProceduralTexture传入uTime改变texture(uSampler, vUV)。
fps: 0
点击运行
<template>
  <div>
    <div>proceduralTexture1和proceduralTexture2的区别:</div>
    <div>如果第一个proceduralTexture传入了rawTextureY这些数据,想通过uTime改变texture(uSampler, vUV)是不起效果的,还会报错。</div>
    <div>只有等创建了一份ProceduralTexture后,才能在第二份的ProceduralTexture传入uTime改变texture(uSampler, vUV)。</div>
    <div class="flex space-between">
      <div>fps: {{ fps }}</div>
      <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    </div>
    <canvas v-if="isRunning" id="proceduralTexture1" class="stage"></canvas>
  </div>
</template>

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

let sceneResources, adt

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()
  }
}

let uTime = 0.2

const initScene = async () => {
  const ele = document.getElementById("proceduralTexture1") 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 = false

  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 = 1
  camera.panningSensibility = 10
  camera.attachControl(ele, true)
  camera.setPosition(new Vector3(0, 560, -560))

  const createLight = () => {
    const light = new HemisphericLight('light',new Vector3(40, 40, 40), scene)
    light.direction = new Vector3(1.0, 0.0, 1.0)
    light.diffuse = new Color3(1.0, 0.95, 0.8)
    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 () => {
    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('y', { 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('z', { 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)
  }


  Effect.ShadersStore['firstShaderVertexShader'] = `
    precision highp float;
    
    attribute vec3 position;
    attribute vec2 uv;

    uniform mat4 worldViewProjection;

    varying vec2 vUV;

    void main() {
      vUV = uv;
      gl_Position = worldViewProjection * vec4(position, 1.0);
    }
  `

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

    varying vec2 vUV;
    uniform float uTime;

    void main() {
      vec3 c = vec3(0.0);
      if(vUV.x < 0.5) c.r = cos(uTime);
      gl_FragColor = vec4(c, 1.0);
    }
  `

  Effect.ShadersStore['secondShaderVertexShader'] = `
    precision highp float;
    
    attribute vec3 position;
    uniform mat4 worldViewProjection;
    
    attribute vec2 uv;

    varying vec2 vUV;

    void main() {
      vUV = uv;
      gl_Position = worldViewProjection * vec4(position, 1.0);
    }
  `

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

    uniform sampler2D uSampler;

    varying vec2 vUV;
    uniform float uTime;

    void main() {

      vec4 baseColor = texture(uSampler, vUV);

      if(vUV.y > 0.5) baseColor.g = 0.5;
      baseColor.b += sin(uTime);

      gl_FragColor = vec4(baseColor);
    }
  `

  Effect.ShadersStore['thirdShaderVertexShader'] = `
    precision highp float;
    
    attribute vec3 position;
    uniform mat4 worldViewProjection;
    
    attribute vec2 uv;

    varying vec2 vUV;

    void main(void) {
      vUV = uv;
      gl_Position = worldViewProjection * vec4(position, 1.0);
    }
  `

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

    uniform sampler2D uSampler;

    varying vec2 vUV;

    void main(void) {

      vec4 baseColor = texture(uSampler, vUV);

      if(vUV.y < 0.5) baseColor.b = 0.3;

      gl_FragColor = vec4(baseColor);
    }
  `

  const createProceduralTexture = () => {
    const finalShader = new ShaderMaterial(
      'thirdShader',
      scene, {
        vertex: 'thirdShader',
        fragment: 'thirdShader',
      }, {
        attributes: ['position', 'uv'],
        uniforms: ['worldViewProjection', 'uSampler'],
        samplers: ['uSampler'],
        needAlphaBlending: true,
      },
    )

    const procTex1 = new ProceduralTexture('first', 256, 'firstShader', scene)
    const procTex2 = new ProceduralTexture('second', 256, 'secondShader', scene)
    
    const finalPlane = MeshBuilder.CreateGround('plane', { width: 256, height: 256, subdivisions: 256 },  scene)
    finalPlane.material = finalShader

    procTex2.setTexture('uSampler', procTex1)
    finalShader.setTexture('uSampler', procTex2)

    return { procTex1, procTex2 }
  }

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

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

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


  const { procTex1, procTex2  } = createProceduralTexture()
  scene.registerBeforeRender(() => {
    procTex1.setFloat('uTime', uTime)
    procTex2.setFloat('uTime', uTime)
    uTime += 0.02
  })

  return {
    scene,
    engine, 
  }
}

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

onMounted(async() => {
  await nextTick()
  // isRunning.value = true
  // await nextTick()
  // sceneResources = await initScene()
})

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

proceduralTexture2(继承renderTargetTexture)(注意二者区别)

注意:vUV的值会经过插值,导致获取到片元着色器vUV.x不等于顶点着色器的uv.x,如果要一致,需要用flat来固定
fps: 0
点击运行
<template>
  <div>
    <div>注意:vUV的值会经过插值,导致获取到片元着色器vUV.x不等于顶点着色器的uv.x,如果要一致,需要用flat来固定</div>
    <div class="flex space-between">
      <div>fps: {{ fps }}</div>
      <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    </div>
    <canvas v-if="isRunning" id="proceduralTexture2" class="stage"></canvas>
  </div>
</template>

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

let sceneResources, adt
let uTime = 0.2

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("proceduralTexture2") 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 = false

  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 = 1
  camera.panningSensibility = 10
  camera.attachControl(ele, true)
  camera.setPosition(new Vector3(0, 560, -560))

  const createLight = () => {
    const light = new HemisphericLight('light',new Vector3(40, 40, 40), scene)
    light.direction = new Vector3(1.0, 0.0, 1.0)
    light.diffuse = new Color3(1.0, 0.95, 0.8)
    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 () => {
    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('y', { 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('z', { 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 createSphere = () => {
    const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 10 }, scene)
    const sphereMat = new StandardMaterial('sphere')
    sphereMat.diffuseColor = new Color3(1.0, 0.6, 0.2)
    sphere.material = sphereMat
    sphere.position.x = 70
    sphere.position.y = 70
    sphere.position.z = 70
  }


  const PI = 3.14159265358979323846
  const TWO_PI = 2 * PI
  const G = 9.8
  const N = 512

  // 复数乘法
  function complexMultiply(a, b) {
    return {
      x: a.x * b.x - a.y * b.y,  // 实部
      y: a.x * b.y + a.y * b.x   // 虚部
    }
  }
 
  // 色散关系函数
  function dispersion(k) {
    return Math.sqrt(G * vectorLength(k))
  }
 
  // 计算向量长度(模)
  function vectorLength(v) {
    return Math.sqrt(v.x * v.x + v.y * v.y)
  }
 
  // 归一化向量
  function normalize(v) {
    const len = vectorLength(v)
    return { x: v.x / len, y: v.y / len }
  }
 
  // 点积计算
  function dot(a, b) {
    return a.x * b.x + a.y * b.y
  }
 
  // 快速随机值生成函数
  function randValueFast(uv, seed) {
    return fract(Math.sin(dot(uv, {x: 12.9898, y: 78.233}) + seed) * 43758.5453)
  }
 
  // fract函数
  function fract(x) {
    return x - Math.floor(x)
  }
 
  // 计算两个相互独立的高斯随机数
  function gauss(uv) {
    let u1 = randValueFast(uv, 1753.0)
    let u2 = randValueFast(uv, 3571.0)
    
    if (u1 < 1e-6) {
      u1 = 1e-6
    }
 
    const g1 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(TWO_PI * u2)
    const g2 = Math.sqrt(-2.0 * Math.log(u1)) * Math.sin(TWO_PI * u2)
 
    return { x: g1, y: g2 }
  }
 
  // 菲利普频谱计算,输入波数 K
  function phillips(K) {
    const W = { x: 1.0, y: -1.0 } // 风的方向向量
    const V = 10.0 // 风速
    const A = 10.0 // 振幅参数
    
    const L = V * V / G // 最大波长
    const L2 = L * L
    
    const kLen = Math.max(0.0001, vectorLength(K))
    const kLen2 = kLen * kLen
    const kLen4 = kLen2 * kLen2
 
    const K_norm = normalize(K)
    const W_norm = normalize(W)
    const dot_KW = dot(K_norm, W_norm)
 
    // 基础 Phillips 谱
    let phi = A * Math.exp(-1.0 / (kLen2 * L2)) / kLen4
 
    // 风向对齐项
    phi *= dot_KW * dot_KW
 
    // 仅保留与风向同方向的波
    if (dot_KW > 0.0) {
        phi *= V
    }
 
    // 衰减因子(减少长波)
    const l = 0.001 * L
    const kSqr = dot(K, K)
    phi *= Math.exp(l * l * -kSqr)
 
    return phi
  }

  function createXyzTexture (scene) {
    const xData = new Uint8Array(N * N * 4)
    const yData = new Uint8Array(N * N * 4)
    const zData = new Uint8Array(N * N * 4)

    for (let y = 0; y < N; y++) {
      for (let x = 0; x < N; x++) {
        const index = (x + y * N) * 4
        const gaussValue1 = gauss({x: x + 3, y: y + 5})
        const gaussValue2 = gauss({x: x + 7, y: y + 11})

        const nx = x - N / 2
        const ny = y - N / 2

        const K = {
          x: TWO_PI * nx / N,
          y: TWO_PI * ny / N,
        }

        const phillipsRes1 = Math.sqrt(phillips(K) * 0.5)
        const h0k = {
          x: gaussValue1.x * phillipsRes1,
          y: gaussValue1.y * phillipsRes1
        }

        const phillipsRes2 = Math.sqrt(phillips({x: K.x * -1, y: K.y * -1}) * 0.5)
        const h0kConj = {
          x: gaussValue2.x * phillipsRes2,
          y: gaussValue2.y * phillipsRes2 * -1
        }

        const omega = dispersion(K)
        const c = Math.cos(omega)
        const s = Math.sin(omega)
        const h1 = complexMultiply(h0k, {x: c, y: s})
        const h2 = complexMultiply(h0kConj, {x: c, y: s * -1})

        const H_Tilde = {
          x: h1.x + h2.x,
          y: h1.y + h2.y
        }

        let kLen = vectorLength(K)
        kLen = kLen < 0.001 ? 0.001 : kLen
        const maxK = {x: K.x / kLen, y: K.y / kLen}

        const KxHTilde = complexMultiply({x: 0, y: -1 * maxK.x}, H_Tilde);
        const KzHTilde = complexMultiply({x: 0, y: -1 * maxK.y}, H_Tilde);

        yData[index] = H_Tilde.x
        yData[index + 1] = H_Tilde.y
        yData[index + 2] = 0
        yData[index + 3] = 255


        xData[index] = KxHTilde.x
        xData[index + 1] = KxHTilde.y
        xData[index + 2] = 0
        xData[index + 3] = 255


        zData[index] = KzHTilde.x
        zData[index + 1] = KzHTilde.y
        zData[index + 2] = 0
        zData[index + 3] = 255

      }
    }

    const rawTextureY = new RawTexture(
      yData,
      N,
      N,
      Constants.TEXTUREFORMAT_RGBA,
      scene,
      false, // 不生成 mipmap
      false, // 不使用线性空间
      Constants.TEXTURE_NEAREST_SAMPLINGMODE
    )

    const rawTextureX = new RawTexture(
      xData,
      N,
      N,
      Constants.TEXTUREFORMAT_RGBA,
      scene,
      false, // 不生成 mipmap
      false, // 不使用线性空间
      Constants.TEXTURE_NEAREST_SAMPLINGMODE
    )

    const rawTextureZ = new RawTexture(
      zData,
      N,
      N,
      Constants.TEXTUREFORMAT_RGBA,
      scene,
      false, // 不生成 mipmap
      false, // 不使用线性空间
      Constants.TEXTURE_NEAREST_SAMPLINGMODE
    )

    return {
      rawTextureY,
      rawTextureX,
      rawTextureZ
    }
  }

  const createXyzPlane = ({ rawTextureX, rawTextureY, rawTextureZ }) => {
    const planeX = MeshBuilder.CreateGround('planeX', { width: N, height: N, subdivisions: N }, scene)
    planeX.position = new Vector3(-N - 10, 0, N + 10)
    const materialX = new StandardMaterial("planeMaterial", scene)
    materialX.diffuseTexture = rawTextureX
    planeX.material = materialX

    const planeY = MeshBuilder.CreateGround('planeY', { width: N, height: N, subdivisions: N }, scene)
    planeY.position = new Vector3(0, 0, N + 10)
    const materialY = new StandardMaterial("planeMaterial", scene)
    materialY.diffuseTexture = rawTextureY
    planeY.material = materialY

    const planeZ = MeshBuilder.CreateGround('planeZ', { width: N, height: N, subdivisions: N }, scene)
    planeZ.position = new Vector3(N + 10, 0, N + 10)
    const materialZ = new StandardMaterial("planeMaterial", scene)
    materialZ.diffuseTexture = rawTextureZ
    planeZ.material = materialZ
  }

  Effect.ShadersStore['firstShaderVertexShader'] = `
    precision highp float;
    
    attribute vec3 position;
    attribute vec2 uv;

    uniform mat4 worldViewProjection;

    uniform sampler2D uSampler;

    void main() {
      // texture的参数含义
      // 纹理采样器(sampler2D):这是一个指向纹理的引用,包含了整个纹理的信息
      // 纹理坐标(vec2):这是一个二维向量,表示在纹理中要采样的位置。通常,纹理坐标(uv)的范围是 [0, 1],其中 (0, 0) 表示纹理的左下角,(1, 1) 表示纹理的右上角  
      // vec4 baseColor = texture(uSampler, uv);

      vUV = uv;
      gl_Position = worldViewProjection * vec4(position, 1.0);
    }
  `

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

    uniform sampler2D uSampler;

    varying vec2 vUV;
    uniform float uTime;

    void main() {
      vec4 baseColor = texture(uSampler, vUV);
      gl_FragColor = vec4(baseColor);
    }
  `

   Effect.ShadersStore['secondShaderVertexShader'] = `
    precision highp float;
    
    attribute vec3 position;
    uniform mat4 worldViewProjection;
    
    attribute vec2 uv;

    varying vec2 vUV;

    void main() {
      vUV = uv;
      gl_Position = worldViewProjection * vec4(position, 1.0);
    }
  `

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

    uniform sampler2D uSampler;

    varying vec2 vUV;
    uniform float uTime;

    void main() {

      vec4 baseColor = texture(uSampler, vUV);

      if(vUV.y > 0.5) baseColor.g = 0.5;
      baseColor.b += sin(uTime);

      gl_FragColor = vec4(baseColor);
    }
  `

  Effect.ShadersStore['finalIfftVertexShader'] = `
    precision highp float;
    
    attribute vec3 position;
    uniform mat4 worldViewProjection;
    
    attribute vec2 uv;

    varying vec2 vUV;

    void main(void) {
      vUV = uv;
      gl_Position = worldViewProjection * vec4(position, 1.0);
    }
  `

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

    uniform sampler2D uSampler;

    varying vec2 vUV;

    void main(void) {

      vec4 baseColor = texture(uSampler, vUV);

      gl_FragColor = vec4(baseColor);
    }
  `

  const ifftComputed = ({ rawTextureX, rawTextureY, rawTextureZ }) => {
    const finalShader = new ShaderMaterial(
      'finalIfft',
      scene, {
        vertex: 'finalIfft',
        fragment: 'finalIfft',
      }, {
        attributes: ['position', 'uv'],
        uniforms: ['worldViewProjection', 'uSampler'],
        samplers: ['uSampler'],
        needAlphaBlending: true,
      },
    )

    const procTex1 = new ProceduralTexture('first', N, 'firstShader', scene)
    const procTex2 = new ProceduralTexture('second', 256, 'secondShader', scene)
    const finalPlane = MeshBuilder.CreateGround('plane', { width: N, height: N, subdivisions: N },  scene)
    finalPlane.material = finalShader
    
    procTex1.setTexture('uSampler', rawTextureY)
    procTex2.setTexture('uSampler', procTex1)
    finalShader.setTexture('uSampler', procTex2)

    scene.registerBeforeRender(() => {
      procTex2.setFloat('uTime', uTime)
      uTime += 0.02
    })
  }

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

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

  createLight()
  createAxis()
  createGui()
  createSphere()
  const { rawTextureX, rawTextureY, rawTextureZ } = createXyzTexture(scene)
  createXyzPlane({ rawTextureX, rawTextureY, rawTextureZ })
  ifftComputed({ rawTextureX, rawTextureY, rawTextureZ })
  runAnimate()

  scene.registerBeforeRender(function() {
    uTime += 0.04
  })

  return {
    scene,
    engine, 
  }
}

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

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

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