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="textureHouse" class="stage"></canvas>
  </div>
</template>

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

let sceneResources: any

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

const semiHouseWidth = 2
const cubeHouseWidth = 1

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("textureHouse") as any

  ele.addEventListener('wheel', function(event: any) {
    // 根据需要处理滚动
    // 例如,可以修改相机的半径或角度
    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 = 10
  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 createGround = () => {
    const groundMat = new StandardMaterial('ground')
    groundMat.diffuseColor = new Color3(0.2, 0.5, 0.6)
    const ground = MeshBuilder.CreateGround('ground', {
      width: 20,
      height: 20
    })
    ground.material = groundMat
  }

  const createBox = (width: any) => {
    const boxMat = new StandardMaterial('box')
    if (width === semiHouseWidth) {
      boxMat.diffuseTexture = new Texture('/images/semiHouse.png')
    } else {
      boxMat.diffuseTexture = new Texture('/images/cubeHouse.png')
    }

    const faceUV: any = []
    if (width === semiHouseWidth) {
      // 后面
      faceUV[0] = new Vector4(0.6, 0.0, 1.0, 1.0)
      // 前面
      faceUV[1] = new Vector4(0.0, 0.0, 0.4, 1.0)
      // 右面
      faceUV[2] = new Vector4(0.4, 0, 0.6, 1.0)
      // 左面
      faceUV[3] = new Vector4(0.4, 0, 0.6, 1.0)
    } else {
      // 后面
      faceUV[0] = new Vector4(0.5, 0.0, 0.75, 1.0)
      // 前面
      faceUV[1] = new Vector4(0.0, 0.0, 0.25, 1.0)
      // 右面
      faceUV[2] = new Vector4(0.25, 0, 0.5, 1.0)
      // 左面
      faceUV[3] = new Vector4(0.75, 0, 1.0, 1.0)
    }

    const box = MeshBuilder.CreateBox('box', {
      width: width,
      faceUV: faceUV,
      wrap: true
    })
    box.material = boxMat
    box.position.y = 0.5
    return box
  }

  const createRoof = (width: any) => {
    const roofMat = new StandardMaterial('roof')
    roofMat.diffuseTexture = new Texture('/images/roof.jpg')
    const roof = MeshBuilder.CreateCylinder('roof', {
      diameter: 1.3,
      height: 1.2,
      tessellation: 3
    })
    roof.material = roofMat
    roof.scaling.x = 0.75
    roof.scaling.y = width
    roof.rotation.z = Math.PI / 2
    roof.position.y = 1.22
    return roof
  }

  const mergeHouse = (width: any) => {
    const box = createBox(width)
    const roof = createRoof(width)
    return Mesh.MergeMeshes([box, roof], true, false, undefined, false, true)
  }

  const creteHouse = () => {
    const semiHouse = mergeHouse(semiHouseWidth)
    const cubeHouse = mergeHouse(cubeHouseWidth)

    // 每项都是一个数组[房屋类型,旋转,x,z]
    const places: any = []
    
    places.push([1, -Math.PI / 16, -6.8, 2.5])
    places.push([1, 15 * Math.PI / 16, -4.1, -1])
    places.push([1, 5 * Math.PI / 4, 0, -1])
    places.push([1, Math.PI + Math.PI / 2.5, 0.5, -3])
    places.push([1, Math.PI + Math.PI / 2.25, 0.75, -7])
    places.push([1, Math.PI / 1.95, 4.5, -3])
    places.push([1, Math.PI / 1.9, 4.75, -7])
    places.push([1, -Math.PI / 3, 6, 4])

    places.push([2, -Math.PI / 3, 5.25, 2])
    places.push([2, 15 * Math.PI / 16, -2.1, -0.5])
    places.push([2, Math.PI / 1.9, 4.75, -1])
    places.push([2, Math.PI + Math.PI / 2.1, 0.75, -5])
    places.push([2, -Math.PI / 16, -4.5, 3])
    places.push([2, -Math.PI / 16, -1.5, 4])
    places.push([2, -Math.PI / 3, 1.5, 6])
    places.push([2, 15 * Math.PI / 16, -6.4, -1.5])
    places.push([2, Math.PI / 1.9, 4.75, -5])

    const houses: any = []
    for (let i = 0; i < places.length; i++) {
      if (places[i][0] === 1) {
        houses[i] = cubeHouse?.createInstance('house' + i)
      } else {
        houses[i] = semiHouse?.createInstance('house' + i)
      }
      houses[i].rotation.y = places[i][1]
      houses[i].position.x = places[i][2]
      houses[i].position.z = places[i][3]
    }
  }


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

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

  createLight()
  createGround()
  creteHouse()
  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>

折射

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="refractionTexture" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
  Engine,
  Scene,
  ArcRotateCamera,
  Vector3,
  ReflectionProbe,
  RenderTargetTexture,
  Color3,
  HemisphericLight,
  MeshBuilder,
  StandardMaterial,
} from 'babylonjs'

let sceneResources: any

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("refractionTexture") as any

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

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

  const scene = new Scene(engine)

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

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

  const createRefractionTexture = () => {
    const redMaterial = new StandardMaterial('red', scene)
    redMaterial.diffuseColor = new Color3(1, -1, 0)

    const sphere = MeshBuilder.CreateSphere('Sphere', {}, scene)
    sphere.position = new Vector3(-1, -2, 0)
    sphere.material = redMaterial

    const greenMaterial = new StandardMaterial('green', scene)
    greenMaterial.diffuseColor = new Color3(0, 1, 0)

    const box = MeshBuilder.CreateBox('Box', {}, scene)
    box.position = new Vector3(2, -4, 0)
    box.material = greenMaterial

    const glass = MeshBuilder.CreatePlane('glass', { width: 15, height: 15 }, scene)
    glass.position = new Vector3(0, 0, 0)
    glass.rotation = new Vector3(Math.PI / 2, 0, 0)

    const probe: any = new ReflectionProbe('main', 512, scene)
    probe.renderList.push(sphere)
    probe.renderList.push(box)

    const renderTargetTexture: any = new RenderTargetTexture('th', 1024, scene)
    renderTargetTexture.renderList.push(box)
    renderTargetTexture.renderList.push(sphere)


    // Babylon 的 StandardMaterial 在启用 indexOfRefraction 后,会:
    // 		用 refractionTexture 当作“背景颜色”
    // 		用 indexOfRefraction 计算一张固定 UV 偏移图(即环境贴图坐标)
    // 		把 refractionTexture 采样结果填进去
    const mirrorMaterial = new StandardMaterial('mirror', scene)
    mirrorMaterial.refractionTexture = renderTargetTexture
    mirrorMaterial.indexOfRefraction = 0.2
    mirrorMaterial.diffuseColor = Color3.White()

    glass.material = mirrorMaterial

    glass.material.alpha = 0.8
  }

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

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

  createLight()
  createRefractionTexture()
  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>

反射 -1

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="mirrorTexture1" 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,
  StandardMaterial,
  MirrorTexture,
  Plane
} from 'babylonjs'

let sceneResources: any

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("mirrorTexture1") as any

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

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

  const scene = new Scene(engine)

  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(0, 600, -600))

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

  const createMirrorTexture = () => {
    const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 50 }, scene)
    sphere.position = new Vector3(360, 40, -120)
    const sMat = new StandardMaterial('sMat', scene)
    sMat.diffuseColor = new Color3(0.0, 0.9, 0.7)
    sphere.material = sMat

    const box = MeshBuilder.CreateBox('box', {
      size: 20,
      faceColors: [
        new Color4(1, 0, 0, 1), new Color4(0, 1, 0, 1),
        new Color4(0, 0, 1, 1), new Color4(1, 1, 0, 1),
        new Color4(1, 0, 1, 1), new Color4(0, 1, 1, 1)
      ]
    }, scene)
    box.position.y = 30

    const ground = MeshBuilder.CreateGround('ground', { width: 1000, height: 1000 }, scene)
    ground.position.y = 0 // 正好落在 y=0 平面

    const mirror: any = new MirrorTexture('mirror', 1024, scene, true)
    mirror.mirrorPlane = new Plane(0, -1, 0, 0) // 地面 y=0,法线朝下
    mirror.renderList.push(sphere) // 只反射小球
    mirror.renderList.push(box) // 只反射小球

    const mat = new StandardMaterial('groundMat', scene)
    mat.diffuseColor = new Color3(0.1, 0.1, 0.1)
    mat.specularColor = new Color3(1, 1, 1)
    mat.reflectionTexture = mirror // ← 关键
    ground.material = mat
  }

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

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

  createLight()
  createMirrorTexture()
  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>

反射 -2【主要是原理】【StandardMaterial】

fps: 0
点击运行
巧妙的创建镜像世界,把view变成镜像世界的矩阵,并不是真的new一个相机来观察
<template>
  <div>
    <div class="flex space-between">
      <div>fps: {{ fps }}</div>
      <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    </div>
    <div>巧妙的创建镜像世界,把view变成镜像世界的矩阵,并不是真的new一个相机来观察</div>
    <canvas v-if="isRunning" id="mirrorTexture2" class="stage"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
  Engine,
  Scene,
  ArcRotateCamera,
  Vector3,
  Color4,
  Color3,
  HemisphericLight,
  DirectionalLight,
  MeshBuilder,
  StandardMaterial,
  RenderTargetTexture,
  Plane,
  Matrix,
} from 'babylonjs'

let sceneResources: any

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("mirrorTexture2") as any

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

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

  const scene: any = new Scene(engine)

  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(0, 30, -200))
  scene.activeCamera = camera

  const createLight = () => {
    const dir = new DirectionalLight('dir', new Vector3(-1, -2, -1), scene)
    dir.position.set(10, 15, 10)
    dir.intensity = 0.7
    const hemispheric = new HemisphericLight('hemispheric', new Vector3(0, 1, 0), scene)
    hemispheric.intensity = 0.8
    hemispheric.groundColor = new Color3(0.2, 0.5, 0.2)
  }

  const createMirrorTexture = () => {
    const box = MeshBuilder.CreateBox('box', {
      size: 20,
      faceColors: [
        new Color4(1, 0, 0, 1), new Color4(0, 1, 0, 1),
        new Color4(0, 0, 1, 1), new Color4(1, 1, 0, 1),
        new Color4(1, 0, 1, 1), new Color4(1, 1, 1, 1)
      ]
    }, scene)
    box.position.y = 30

    const ground: any = MeshBuilder.CreateGround('ground', { width: 1000, height: 1000 }, scene)
    ground.position.y = 0


    const name = 'mirror'

    // 在 onBeforeRenderObservable 里,临时保存主场景当前的裁剪平面,以便在 onAfterRenderObservable 里恢复
    // 画镜像时,必须只渲染镜子平面以上的物体(否则会出现“地下”的倒影),因此需要启用 裁剪平面(clipPlane)——即只保留平面一侧的像素
    // saveClipPlane 就是用来 临时保存场景原来的裁剪平面,以便在画完镜像后再恢复回去,避免影响后续正常渲染
    // 渲染镜像前
    //       saveClipPlane = scene.clipPlane   // 备份
    //       scene.clipPlane = rrt.mirrorPlane // 设为镜面平面,启用裁剪
    // 渲染镜像后
    //         scene.clipPlane = saveClipPlane   // 还原备份,恢复现场
    // saveClipPlane 起到“现场保护”作用,保证镜像渲染不会破坏场景原有的裁剪状态。
    let saveClipPlane: any = null

    const rrt: any = new RenderTargetTexture('mirror', { width: 1000, height: 1000 }, scene, false, true)

    // 定义镜子的平面,用于计算反射矩阵和裁剪平面
    rrt.mirrorPlane = new Plane(0, -1, 0, 0)

    // 临时存储反射后的视图矩阵,避免每帧 new 新矩阵
    rrt._transformMatrix = Matrix.Zero()

    // 临时存储反射矩阵,避免每帧 new 新矩阵
    rrt._mirrorMatrix = Matrix.Zero()

    // 告诉 RenderTargetTexture:只画这个物体,别的都不画
    rrt.renderList.push(box)

    scene.customRenderTargets.push(rrt)

    // 防止主场景和反射场景之间的 Uniform 数据冲突
    if (engine.supportsUniformBuffers) {
      rrt._sceneUBO = scene.createSceneUniformBuffer(`Scene for Mirror Texture (name "${name}")`)
    }


     
    // 时序图
    // 时刻:scene 状态
    // 进入 onBefore:主场景 UBO、V 矩阵、clipPlane 都是“正常”
    // 镜子绘制期间:全部被换成“反射专用”
    // 离开 onAfter:一次性全部还原,主场景完全感知不到



    // 这 10 行代码在 GPU 里“临时造了一个平行宇宙”:
    // 世界被沿地面翻过去,相机矩阵被替换,裁剪平面被启用,
    // 画完再把所有状态还原,主场景完全感知不到。
    rrt.onBeforeRenderObservable.add(() => {
      // UBO 是 Uniform Buffer Object 的缩写,直译为“统一缓冲区对象”。
      // 是 OpenGL / WebGL2 的一项底层功能,Babylon.js 把“一大块 uniform 变量”打包成一次 GPU 上传,而不是逐个 uniform* 调用。
      // 切换 UniformBuffer(如果支持)
      // 在 WebGL2 下,给镜子单独准备一块 uniform 内存,画完再换回去
      if (rrt._sceneUBO) {
        // 把主场景当前正在用的 UBO暂存到 rrt._currentSceneUBO
        // 后面画完镜子还要换回来,否则主场景会拿到错误的 uniform 数据
        rrt._currentSceneUBO = scene.getSceneUniformBuffer()

        // 把镜子专用 UBO设为场景当前 UBO
        // 后续所有 scene.uniformBuffer.update() 都会写进这块 buffer,不会污染主场景
        scene.setSceneUniformBuffer(rrt._sceneUBO)

        // 先强制解绑 shader 与 UBO 的关联
        // 避免 Babylon 的缓存逻辑以为“同一 buffer 已绑定”而跳过真正的 glBindBufferBase 调用
        scene.getSceneUniformBuffer().unbindEffect()
      }

      // 计算反射矩阵
      // rrt.mirrorPlane,平面方程 y = 0(法线朝下)
      // rrt._mirrorMatrix,输出矩阵,原地写入
      // 得到 4×4 矩阵
      // R = I – 2 · n⊗n / (n·n)
      // 其中 n = (0,–1,0,0)ᵀ,所以
      // R = diag(1, –1, 1, 1)
      // 对任意点 (x,y,z) 变换后变成 (x,–y,z)——这就是镜面反射
      // 一句话:用一条指令算出“把世界沿镜子翻过去”的矩阵
      // ---------------------------------------------------------------------
      // 第一步:镜子平面是哪?
      //     平面方程给出法线向量
      //     n = (0, -1, 0)
      //     意思:镜子就是 地面(y = 0),法线竖直朝下
      // 第二步:公式在算什么?
      //    R = I – 2 · n⊗n / (n·n):是“对任意一个点,沿这个平面翻过去”的通用反射矩阵
      //    I 是单位矩阵,什么都不变
      //    n⊗n 叫“外积”,当 n 已经是单位长度时,n⊗n 就是一个能把向量“投影到法线方向”的小机器
      //    整个式子就是在说:先把待反射的向量投影到法线方向,再两倍减掉,就得到了镜像
      // 第三步:代入具体数字
      //       n = (0, -1, 0)
      //       n·n = 0² + (-1)² + 0² = 1【点乘 == 点积 == 内积】“乘出来是个数”
      //       所以分母消失,公式变成
      //       R = I – 2 · n⊗n
      //       把 n⊗n 写出来【外积】“乘出来是个矩阵”
      //       n⊗n = [ +0 ] [ 0 -1 0 ] = [ 0  0  0 ]
      //              [ -1 ]              [ 0  1  0 ]
      //              [ +0 ]              [ 0  0  0 ]
      //       乘以 -2 后
      //       2·n⊗n = [ 0  0  0 ]
      //                [ 0  2  0 ]
      //                [ 0  0  0 ]
      //       单位矩阵减去:
      //       R = I - 2·n⊗n = [ 1 0 0 ] - [ 0 0 0 ] = [ 1  0  0 ]
      //                        [ 0 1 0 ]   [ 0 2 0 ]   [ 0 -1  0 ]
      //                        [ 0 0 1 ]   [ 0 0 0 ]   [ 0  0  1 ]
      //       这就是 diag(1, –1, 1) 的来历。
      // 第四步:到底对点干了什么?
      //         拿任意点 (x, y, z) 乘这个矩阵:
      //         [ 1  0  0 ] [x]   [ x ]
      //         [ 0 -1  0 ] [y] = [-y ]
      //         [ 0  0  1 ] [z]   [ z ]
      //         y 坐标被取反,x 和 z 不变。
      //         换句话说:把点沿地面(y=0)翻过去,上半空间变下半空间,这就是镜面反射。
      // 那个公式只是“沿地面照镜子”的数学写法,算出来的是个简单对角矩阵,作用就是把 y 变成 -y,别的啥也没干
      Matrix.ReflectionToRef(rrt.mirrorPlane, rrt._mirrorMatrix)

      // 计算反射后的视图矩阵
      // scene.getViewMatrix(),主相机视图矩阵 V(世界→相机)
      // rrt._mirrorMatrix,反射矩阵 R(世界→反射世界)
      // rrt._transformMatrix,输出 V' = R · V
      // 几何意义:
      //       不是“把相机移到镜子下面”,而是把整个世界先沿镜子翻过去,再用原来的相机去看。
      //       这样做不需要第二台相机,只用矩阵乘法就得到“反射视角”。
      // 一句话:“虚拟相机”不是 new 出来的,是矩阵乘出来的。
      rrt._mirrorMatrix.multiplyToRef(scene.getViewMatrix(), rrt._transformMatrix)

      // 强制设置场景的视图矩阵
      // scene.setTransformMatrix 是 Babylon.js 中用于手动覆盖场景视图矩阵的低阶 API
      // 直接把“世界→视图→投影”链条里最上游的视图矩阵(以及可选的投影矩阵)换成你传进去的值
      // 之后整个场景在那一帧就会按你给的矩阵去渲染
      // 换句话说:它绕开了 Babylon 自带的相机系统,让你“劫持”了摄像机。
      // ---------------------------------------------------------------------
      // 作用:
      //       “把当前场景里所有后续绘制命令的 viewMatrix(和 projectionMatrix)换成我指定的矩阵,直到我再次调用 setTransformMatrix 或 Babylon 在下一帧自动重置它。”
      // ---------------------------------------------------------------------
      // 调用后:场景对象 scene._viewMatrix / scene._projectionMatrix 被立即覆盖
      // 所有 mesh.getWorldMatrix() 依然正常算世界矩阵,但最终 MVP 里的 V 和 P 就是所给的
      // ---------------------------------------------------------------------
      // 什么时候用
      //       需要把 Babylon 场景嵌入到已有引擎/AR/VR 框架里,而头部姿态矩阵由外部 SDK 给出(如 WebXR、OpenCV、ARKit、Kinect)。
      //       做离线渲染、截图、立方体贴图生成时,想一次性把 6 个方向的视图矩阵塞进去,而懒得创建 6 个相机。
      //       做特殊投影(斜投影、非对称视锥、浮雕投影、光场显示)而 Babylon 相机参数 UI 里调不出来。
      //       做“画中画”分屏、多眼渲染:同一帧里先 setTransformMatrix(eye0View, eye0Proj) 画一遍,再 setTransformMatrix(eye1View, eye1Proj) 画第二遍,只需一个场景、一个相机对象即可。
      // ---------------------------------------------------------------------
      // 最小可运行示例:
      //       // 假设外部已经给你算好了 view / proj
      //       const customView = BABYLON.Matrix.LookAtLH(eye, target, up);
      //       const customProj = BABYLON.Matrix.PerspectiveFovLH(fov, aspect, zn, zf);
      //       // 每帧刷新,劫持摄像机
      //       scene.registerBeforeRender(() => {
      //           scene.setTransformMatrix(customView, customProj);
      //       });
      // ---------------------------------------------------------------------
      // 参数 1:	视图矩阵已被换成 V' = R·V
      // 参数 2:	投影矩阵保持原样(P)
      // WebGL 侧实际动作:
      //       立即把 uniform mat4 view 换成 V'
      //       把 uniform mat4 viewProjection 换成 P·V'
      //       下一帧所有 draw call 都会用这套新矩阵
      // 一句话:“欺骗”整个场景,以为“相机已经在镜子下方”。
      scene.setTransformMatrix(rrt._transformMatrix, scene.getProjectionMatrix())

      // 临时设置裁剪平面为镜子平面
      // 备份	主场景可能已有别的裁剪平面(例如水面、UI 裁剪),必须先存起来
      // 启用	把 WebGL 裁剪平面设为 0x –1y +0z +0w ≥ 0(即 y ≤ 0)
      // WebGL 侧实际动作:
      //       若扩展 GL_ARB_clip_distance 可用,Babylon 会编译一份带 gl_ClipDistance[0] 的 shader
      //       在顶点着色器里写入 dot(worldPos, plane),GPU 自动丢弃 y>0 的片元
      //       避免把镜子以上的物体画到纹理里(否则地面会出现“重影”)
      // 一句话:“只画镜子下方的世界”,上半部分直接裁掉。
      saveClipPlane = scene.clipPlane // 备份
      scene.clipPlane = rrt.mirrorPlane // 启用

      // 记录反射后的相机位置
      // globalPosition	主相机在世界坐标系的真实位置
      // rrt._mirrorMatrix	反射矩阵 R
      // 输出	镜子里的“虚拟相机”位置
      // 用途:
      //       后续材质计算反射向量时,直接用这个世界坐标当做“眼睛”位置,不用再算一次反射
      //       水面 Fresnel、镜面高光、SSR 等效果都会读这个只读属性
      //       不写也行,但写了可以省一次矩阵乘法
      // 一句话:“提前帮后面所有 shader 算好虚拟眼睛在哪”。
      scene._mirroredCameraPosition = Vector3.TransformCoordinates(scene.activeCamera.globalPosition, rrt
        ._mirrorMatrix)
    })

    rrt.onAfterRenderObservable.add(() => {
      // 恢复 UniformBuffer(如果支持)
      // 之前做了什么	onBeforeRender 里把场景的 UBO 换成了镜子专用 UBO (rrt._sceneUBO),防止 uniform 数据被覆盖。
      // 现在做什么	把场景 UBO 指针换回去,让主场景继续用自己的那块 GPU 内存。
      // 不恢复的后果	主场景会永远用镜子的 uniform 数据(视图矩阵、相机位置等),画面瞬间错乱。
      if (rrt._sceneUBO) {
        scene.setSceneUniformBuffer(rrt._currentSceneUBO)
      }
      
      // 恢复场景的视图矩阵
      // 之前做了什么:onBeforeRender 里手动调了 scene.setTransformMatrix(R·V, P),把视图矩阵换成了“反射视角”。
      // 现在做什么:updateTransformMatrix() 让 Babylon 重新计算:主相机的视图矩阵 V × 投影矩阵 P,恢复成正常视角。
      // 不恢复的后果:主场景会一直用“镜子视角”渲染,整个世界上下颠倒。
      scene.updateTransformMatrix()

      // 清空临时相机位置
      // 之前做了什么:onBeforeRender 里写了 scene._mirroredCameraPosition = ...,供后续 shader 或材质使用。
      // 现在做什么:把它清掉,标记“当前不在反射通道”。
      // 不恢复的后果:后续材质或后处理如果依赖这个字段,会误以为仍在反射通道,可能算错反射向量或高光。
      scene._mirroredCameraPosition = null

      // 恢复裁剪平面
      // 之前做了什么:onBeforeRender 里把 scene.clipPlane 设成镜子平面 y=0,只画镜子下方的物体。
      // 现在做什么:恢复成进入镜子前的原始裁剪平面(通常是 null,也就是不裁剪)。
      // 不恢复的后果:主场景会继续用 y=0 裁剪,上半部分世界被切掉,画面缺一块。
      scene.clipPlane = saveClipPlane
    })

    const mat = new StandardMaterial('groundMat', scene)
    mat.diffuseColor = new Color3(0.1, 0.1, 0.1)
    mat.specularColor = new Color3(1, 1, 1)
    mat.reflectionTexture = rrt // ← 关键
    ground.material = mat

  }

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

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

  createLight()
  createMirrorTexture()
  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>

反射 -3【主要是原理】【ShaderMaterial】

fps: 0
点击运行
巧妙的创建镜像世界,把view变成镜像世界的矩阵,并不是真的new一个相机来观察
这个案例特殊点,需要高宽一致
<template>
  <div>
    <div class="flex space-between">
      <div>fps: {{ fps }}</div>
      <div @click="onTrigger" class="pointer">点击{{ !isRunning ? '运行' : '关闭' }}</div>
    </div>
    <div>巧妙的创建镜像世界,把view变成镜像世界的矩阵,并不是真的new一个相机来观察</div>
    <div>这个案例特殊点,需要高宽一致</div>
    <canvas v-if="isRunning" id="mirrorTexture3" class="stage" style="height: 648px;width: 648px;"></canvas>
  </div>
</template>

<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import {
  Engine,
  Scene,
  ArcRotateCamera,
  Vector3,
  Color4,
  Color3,
  HemisphericLight,
  DirectionalLight,
  MeshBuilder,
  ShaderMaterial,
  RenderTargetTexture,
  Plane,
  Matrix,
} from 'babylonjs'

let sceneResources: any

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("mirrorTexture3") as any

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

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

  const scene: any = new Scene(engine)

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

  const createLight = () => {
    const dir = new DirectionalLight('dir', new Vector3(-1, -2, -1), scene)
    dir.position.set(10, 15, 10)
    dir.intensity = 0.7
    const hemispheric = new HemisphericLight('hemispheric', new Vector3(0, 1, 0), scene)
    hemispheric.intensity = 0.8
    hemispheric.groundColor = new Color3(0.2, 0.5, 0.2)
  }

  const createMirrorTexture = () => {
    const rect = engine.getRenderHeight()

    const box = MeshBuilder.CreateBox('box', {
      size: 20,
      faceColors: [
        new Color4(1, 0, 0, 1), new Color4(0, 1, 0, 1),
        new Color4(0, 0, 1, 1), new Color4(1, 1, 0, 1),
        new Color4(1, 0, 1, 1), new Color4(1, 1, 1, 1)
      ]
    }, scene)
    box.position.y = 30

    const ground: any = MeshBuilder.CreateGround('ground', { width: rect, height: rect, subdivisions: rect }, scene)
    ground.position.y = 0


    const name = 'test'

    // 在 onBeforeRenderObservable 里,临时保存主场景当前的裁剪平面,以便在 onAfterRenderObservable 里恢复
    // 画镜像时,必须只渲染镜子平面以上的物体(否则会出现“地下”的倒影),因此需要启用 裁剪平面(clipPlane)——即只保留平面一侧的像素
    // saveClipPlane 就是用来 临时保存场景原来的裁剪平面,以便在画完镜像后再恢复回去,避免影响后续正常渲染
    // 渲染镜像前
    //       saveClipPlane = scene.clipPlane   // 备份
    //       scene.clipPlane = rrt.testPlane // 设为镜面平面,启用裁剪
    // 渲染镜像后
    //         scene.clipPlane = saveClipPlane   // 还原备份,恢复现场
    // saveClipPlane 起到“现场保护”作用,保证镜像渲染不会破坏场景原有的裁剪状态。
    let saveClipPlane: any = null

    const rrt: any = new RenderTargetTexture('test', { width: rect, height: rect }, scene, false, true)

    // 定义镜子的平面,用于计算反射矩阵和裁剪平面
    rrt.testPlane = new Plane(0, -1, 0, 0)

    // 临时存储反射后的视图矩阵,避免每帧 new 新矩阵
    rrt._transformMatrix = Matrix.Zero()

    // 临时存储反射矩阵,避免每帧 new 新矩阵
    rrt._testMatrix = Matrix.Zero()

    // 告诉 RenderTargetTexture:只画这个物体,别的都不画
    rrt.renderList.push(box)

    scene.customRenderTargets.push(rrt)

    // 防止主场景和反射场景之间的 Uniform 数据冲突
    if (engine.supportsUniformBuffers) {
      rrt._sceneUBO = scene.createSceneUniformBuffer(`Scene for Mirror Texture (name "${name}")`)
    }


     
    // 时序图
    // 时刻:scene 状态
    // 进入 onBefore:主场景 UBO、V 矩阵、clipPlane 都是“正常”
    // 镜子绘制期间:全部被换成“反射专用”
    // 离开 onAfter:一次性全部还原,主场景完全感知不到



    // 这 10 行代码在 GPU 里“临时造了一个平行宇宙”:
    // 世界被沿地面翻过去,相机矩阵被替换,裁剪平面被启用,
    // 画完再把所有状态还原,主场景完全感知不到。
    rrt.onBeforeRenderObservable.add(() => {
      // UBO 是 Uniform Buffer Object 的缩写,直译为“统一缓冲区对象”。
      // 是 OpenGL / WebGL2 的一项底层功能,Babylon.js 把“一大块 uniform 变量”打包成一次 GPU 上传,而不是逐个 uniform* 调用。
      // 切换 UniformBuffer(如果支持)
      // 在 WebGL2 下,给镜子单独准备一块 uniform 内存,画完再换回去
      if (rrt._sceneUBO) {
        // 把主场景当前正在用的 UBO暂存到 rrt._currentSceneUBO
        // 后面画完镜子还要换回来,否则主场景会拿到错误的 uniform 数据
        rrt._currentSceneUBO = scene.getSceneUniformBuffer()

        // 把镜子专用 UBO设为场景当前 UBO
        // 后续所有 scene.uniformBuffer.update() 都会写进这块 buffer,不会污染主场景
        scene.setSceneUniformBuffer(rrt._sceneUBO)

        // 先强制解绑 shader 与 UBO 的关联
        // 避免 Babylon 的缓存逻辑以为“同一 buffer 已绑定”而跳过真正的 glBindBufferBase 调用
        scene.getSceneUniformBuffer().unbindEffect()
      }

      // 计算反射矩阵
      // rrt.testPlane,平面方程 y = 0(法线朝下)
      // rrt._testMatrix,输出矩阵,原地写入
      // 得到 4×4 矩阵
      // R = I – 2 · n⊗n / (n·n)
      // 其中 n = (0,–1,0,0)ᵀ,所以
      // R = diag(1, –1, 1, 1)
      // 对任意点 (x,y,z) 变换后变成 (x,–y,z)——这就是镜面反射
      // 一句话:用一条指令算出“把世界沿镜子翻过去”的矩阵
      // ---------------------------------------------------------------------
      // 第一步:镜子平面是哪?
      //     平面方程给出法线向量
      //     n = (0, -1, 0)
      //     意思:镜子就是 地面(y = 0),法线竖直朝下
      // 第二步:公式在算什么?
      //    R = I – 2 · n⊗n / (n·n):是“对任意一个点,沿这个平面翻过去”的通用反射矩阵
      //    I 是单位矩阵,什么都不变
      //    n⊗n 叫“外积”,当 n 已经是单位长度时,n⊗n 就是一个能把向量“投影到法线方向”的小机器
      //    整个式子就是在说:先把待反射的向量投影到法线方向,再两倍减掉,就得到了镜像
      // 第三步:代入具体数字
      //       n = (0, -1, 0)
      //       n·n = 0² + (-1)² + 0² = 1【点乘 == 点积 == 内积】“乘出来是个数”
      //       所以分母消失,公式变成
      //       R = I – 2 · n⊗n
      //       把 n⊗n 写出来【外积】“乘出来是个矩阵”
      //       n⊗n = [ +0 ] [ 0 -1 0 ] = [ 0  0  0 ]
      //              [ -1 ]              [ 0  1  0 ]
      //              [ +0 ]              [ 0  0  0 ]
      //       乘以 -2 后
      //       2·n⊗n = [ 0  0  0 ]
      //                [ 0  2  0 ]
      //                [ 0  0  0 ]
      //       单位矩阵减去:
      //       R = I - 2·n⊗n = [ 1 0 0 ] - [ 0 0 0 ] = [ 1  0  0 ]
      //                        [ 0 1 0 ]   [ 0 2 0 ]   [ 0 -1  0 ]
      //                        [ 0 0 1 ]   [ 0 0 0 ]   [ 0  0  1 ]
      //       这就是 diag(1, –1, 1) 的来历。
      // 第四步:到底对点干了什么?
      //         拿任意点 (x, y, z) 乘这个矩阵:
      //         [ 1  0  0 ] [x]   [ x ]
      //         [ 0 -1  0 ] [y] = [-y ]
      //         [ 0  0  1 ] [z]   [ z ]
      //         y 坐标被取反,x 和 z 不变。
      //         换句话说:把点沿地面(y=0)翻过去,上半空间变下半空间,这就是镜面反射。
      // 那个公式只是“沿地面照镜子”的数学写法,算出来的是个简单对角矩阵,作用就是把 y 变成 -y,别的啥也没干
      Matrix.ReflectionToRef(rrt.testPlane, rrt._testMatrix)

      // 计算反射后的视图矩阵
      // scene.getViewMatrix(),主相机视图矩阵 V(世界→相机)
      // rrt._testMatrix,反射矩阵 R(世界→反射世界)
      // rrt._transformMatrix,输出 V' = R · V
      // 几何意义:
      //       不是“把相机移到镜子下面”,而是把整个世界先沿镜子翻过去,再用原来的相机去看。
      //       这样做不需要第二台相机,只用矩阵乘法就得到“反射视角”。
      // 一句话:“虚拟相机”不是 new 出来的,是矩阵乘出来的。
      rrt._testMatrix.multiplyToRef(scene.getViewMatrix(), rrt._transformMatrix)

      // 强制设置场景的视图矩阵
      // scene.setTransformMatrix 是 Babylon.js 中用于手动覆盖场景视图矩阵的低阶 API
      // 直接把“世界→视图→投影”链条里最上游的视图矩阵(以及可选的投影矩阵)换成你传进去的值
      // 之后整个场景在那一帧就会按你给的矩阵去渲染
      // 换句话说:它绕开了 Babylon 自带的相机系统,让你“劫持”了摄像机。
      // ---------------------------------------------------------------------
      // 作用:
      //       “把当前场景里所有后续绘制命令的 viewMatrix(和 projectionMatrix)换成我指定的矩阵,直到我再次调用 setTransformMatrix 或 Babylon 在下一帧自动重置它。”
      // ---------------------------------------------------------------------
      // 调用后:场景对象 scene._viewMatrix / scene._projectionMatrix 被立即覆盖
      // 所有 mesh.getWorldMatrix() 依然正常算世界矩阵,但最终 MVP 里的 V 和 P 就是所给的
      // ---------------------------------------------------------------------
      // 什么时候用
      //       需要把 Babylon 场景嵌入到已有引擎/AR/VR 框架里,而头部姿态矩阵由外部 SDK 给出(如 WebXR、OpenCV、ARKit、Kinect)。
      //       做离线渲染、截图、立方体贴图生成时,想一次性把 6 个方向的视图矩阵塞进去,而懒得创建 6 个相机。
      //       做特殊投影(斜投影、非对称视锥、浮雕投影、光场显示)而 Babylon 相机参数 UI 里调不出来。
      //       做“画中画”分屏、多眼渲染:同一帧里先 setTransformMatrix(eye0View, eye0Proj) 画一遍,再 setTransformMatrix(eye1View, eye1Proj) 画第二遍,只需一个场景、一个相机对象即可。
      // ---------------------------------------------------------------------
      // 最小可运行示例:
      //       // 假设外部已经给你算好了 view / proj
      //       const customView = BABYLON.Matrix.LookAtLH(eye, target, up);
      //       const customProj = BABYLON.Matrix.PerspectiveFovLH(fov, aspect, zn, zf);
      //       // 每帧刷新,劫持摄像机
      //       scene.registerBeforeRender(() => {
      //           scene.setTransformMatrix(customView, customProj);
      //       });
      // ---------------------------------------------------------------------
      // 参数 1:	视图矩阵已被换成 V' = R·V
      // 参数 2:	投影矩阵保持原样(P)
      // WebGL 侧实际动作:
      //       立即把 uniform mat4 view 换成 V'
      //       把 uniform mat4 viewProjection 换成 P·V'
      //       下一帧所有 draw call 都会用这套新矩阵
      // 一句话:“欺骗”整个场景,以为“相机已经在镜子下方”。
      scene.setTransformMatrix(rrt._transformMatrix, scene.getProjectionMatrix())

      // 临时设置裁剪平面为镜子平面
      // 备份	主场景可能已有别的裁剪平面(例如水面、UI 裁剪),必须先存起来
      // 启用	把 WebGL 裁剪平面设为 0x –1y +0z +0w ≥ 0(即 y ≤ 0)
      // WebGL 侧实际动作:
      //       若扩展 GL_ARB_clip_distance 可用,Babylon 会编译一份带 gl_ClipDistance[0] 的 shader
      //       在顶点着色器里写入 dot(worldPos, plane),GPU 自动丢弃 y>0 的片元
      //       避免把镜子以上的物体画到纹理里(否则地面会出现“重影”)
      // 一句话:“只画镜子下方的世界”,上半部分直接裁掉。
      saveClipPlane = scene.clipPlane // 备份
      scene.clipPlane = rrt.testPlane // 启用

      // 记录反射后的相机位置
      // globalPosition	主相机在世界坐标系的真实位置
      // rrt._testMatrix	反射矩阵 R
      // 输出	镜子里的“虚拟相机”位置
      // 用途:
      //       后续材质计算反射向量时,直接用这个世界坐标当做“眼睛”位置,不用再算一次反射
      //       水面 Fresnel、镜面高光、SSR 等效果都会读这个只读属性
      //       不写也行,但写了可以省一次矩阵乘法
      // 一句话:“提前帮后面所有 shader 算好虚拟眼睛在哪”。
      scene._mirroredCameraPosition = Vector3.TransformCoordinates(scene.activeCamera.globalPosition, rrt
        ._testMatrix)
    })

    rrt.onAfterRenderObservable.add(() => {
      // 恢复 UniformBuffer(如果支持)
      // 之前做了什么	onBeforeRender 里把场景的 UBO 换成了镜子专用 UBO (rrt._sceneUBO),防止 uniform 数据被覆盖。
      // 现在做什么	把场景 UBO 指针换回去,让主场景继续用自己的那块 GPU 内存。
      // 不恢复的后果	主场景会永远用镜子的 uniform 数据(视图矩阵、相机位置等),画面瞬间错乱。
      if (rrt._sceneUBO) {
        scene.setSceneUniformBuffer(rrt._currentSceneUBO)
      }

      // 恢复场景的视图矩阵
      // 之前做了什么:onBeforeRender 里手动调了 scene.setTransformMatrix(R·V, P),把视图矩阵换成了“反射视角”。
      // 现在做什么:updateTransformMatrix() 让 Babylon 重新计算:主相机的视图矩阵 V × 投影矩阵 P,恢复成正常视角。
      // 不恢复的后果:主场景会一直用“镜子视角”渲染,整个世界上下颠倒。
      scene.updateTransformMatrix()

      // 清空临时相机位置
      // 之前做了什么:onBeforeRender 里写了 scene._mirroredCameraPosition = ...,供后续 shader 或材质使用。
      // 现在做什么:把它清掉,标记“当前不在反射通道”。
      // 不恢复的后果:后续材质或后处理如果依赖这个字段,会误以为仍在反射通道,可能算错反射向量或高光。
      scene._mirroredCameraPosition = null

      // 恢复裁剪平面
      // 之前做了什么:onBeforeRender 里把 scene.clipPlane 设成镜子平面 y=0,只画镜子下方的物体。
      // 现在做什么:恢复成进入镜子前的原始裁剪平面(通常是 null,也就是不裁剪)。
      // 不恢复的后果:主场景会继续用 y=0 裁剪,上半部分世界被切掉,画面缺一块。
      scene.clipPlane = saveClipPlane
    })

    const matShader = new ShaderMaterial('matShader', scene, {
      vertexSource: `
        precision highp float;
        
        attribute vec3 position;
        
        uniform mat4 worldViewProjection;
        
        void main() {
          gl_Position = worldViewProjection * vec4(position, 1.0);
        }

      `,
      fragmentSource: `
        precision highp float;

        uniform sampler2D testSampler;
        uniform float texSize;
        
        void main(){
          vec2 texelCenter = (floor(gl_FragCoord.xy) + 0.5) / texSize;
      
          vec4 refl = texture2D(testSampler, texelCenter);

          gl_FragColor = vec4(mix(vec3(0.0, 0.549, 0.996), refl.rgb, 0.3), 1.0);
        }
      `
    }, {
      attributes: ['position'],
      uniforms: ['world', 'worldViewProjection', 'testSampler', 'texSize'],
      samplers: ['testSampler'],
    })

    matShader.setTexture('testSampler', rrt)
    matShader.setFloat('texSize', rect)

    ground.material = matShader

  }

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

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

  createLight()
  createMirrorTexture()
  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>

rawTexture3d-1

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="rawTexture3d_1" class="stage"></canvas>
  </div>
</template>

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

let sceneResources: any

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

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

const generateWorleyNoise3D = (width: any, height: any, depth: any) => {
  const size = width * height * depth * 4 // RGBA
  const data = new Uint8Array(size)

  // 随机特征点生成器
  function randomF(p: any) {
    return ((Math.sin(p[0] * 12.9898 + p[1] * 78.233 + p[2] * 53.53) * 43758.5453) % 1 + 1) % 1
  }

  function randomV3(p: any) {
    return [
      randomF([p[0], p[1], p[2]]),
      randomF([p[0] + 1.0, p[1] + 2.0, p[2] + 3.0]),
      randomF([p[0] + 4.0, p[1] + 5.0, p[2] + 6.0])
    ]
  }

  // 生成特征点网格
  // 3D 空间被划分成 4×4×4 的网格:
  // 每个维度被分成4段,共64个小立方体
  //   X轴: [0,1] [1,2] [2,3] [3,4]
  //   Y轴: [0,1] [1,2] [2,3] [3,4]
  //   Z轴: [0,1] [1,2] [2,3] [3,4]
  // 每个小格子内有1个随机偏移的特征点
  // 噪声值 = 到最近特征点的距离
  const gridSize = 4 // 每个维度的网格数量
  const featurePoints = []

  for (let z = 0; z < gridSize; z++) {
    for (let y = 0; y < gridSize; y++) {
      for (let x = 0; x < gridSize; x++) {
        const seed = [x * 1.5, y * 1.5, z * 1.5]
        const offset = randomV3(seed)
        featurePoints.push({
          x: x + offset[0],
          y: y + offset[1],
          z: z + offset[2]
        })
      }
    }
  }

  let index = 0
  for (let z = 0; z < depth; z++) {
    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        // 归一化坐标到 [0, gridSize]
        const u = (x / width) * gridSize
        const v = (y / height) * gridSize
        const w = (z / depth) * gridSize

        // 更优雅的写法(现代 JS)
        // let minDist = Infinity
        let minDist = 999999.0 // 初始最大值技巧,用于查找最小距离;确保第一次比较能成功,任何实际距离都会小于 999999,所以第一个 dist 一定会替换它

        // 查找最近的特征点(考虑边界环绕)
        for (let fp of featurePoints) {
          let dx = Math.abs(u - fp.x)
          let dy = Math.abs(v - fp.y)
          let dz = Math.abs(w - fp.z)

          // 考虑周期性边界
          if (dx > gridSize * 0.5) dx = gridSize - dx
          if (dy > gridSize * 0.5) dy = gridSize - dy
          if (dz > gridSize * 0.5) dz = gridSize - dz

          const dist = Math.sqrt(dx * dx + dy * dy + dz * dz)
          if (dist < minDist) {
            minDist = dist
          }
        }

        // 归一化距离到 [0, 1] 并转换为灰度
        const intensity = Math.min(minDist / (gridSize * 0.8), 1.0)
        const gray = Math.floor(intensity * 255)

        data[index++] = gray // R
        data[index++] = gray // G
        data[index++] = gray // B
        data[index++] = 255 // A
      }
    }
  }

  return data
}


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

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

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

  const scene = new Scene(engine)

  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(0, 30, 30))

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

  // ============================================
  // 3D Worley Noise Shader
  // ============================================
  Effect.ShadersStore['worley3DVertexShader'] = `
    precision highp float;
    
    attribute vec3 position;
    attribute vec3 normal;
    attribute vec2 uv;
    
    uniform mat4 worldViewProjection;
    uniform mat4 world;
    
    varying vec3 vPosition;
    varying vec3 vNormal;
    varying vec3 vUV3D;  // 3D纹理坐标
    
    void main(void) {
      vec4 worldPos = world * vec4(position, 1.0);
      vPosition = worldPos.xyz;
      vNormal = normalize(mat3(world) * normal);
      
      // 使用世界坐标作为3D纹理坐标,可以缩放调整密度
      vUV3D = worldPos.xyz * 0.15;
      
      gl_Position = worldViewProjection * vec4(position, 1.0);
    }
  `

  Effect.ShadersStore['worley3DFragmentShader'] = `
    precision highp float;
    precision highp sampler3D;
    
    varying vec3 vPosition;
    varying vec3 vNormal;
    varying vec3 vUV3D;
    
    // 3D纹理采样器 - 必须使用 sampler3D
    uniform sampler3D worleyTexture;
    uniform vec3 cameraPosition;
    uniform float time;
    
    void main(void) {
      // 采样3D Worley噪声纹理
      // 使用 fract 实现周期性重复
      vec3 uvw = fract(vUV3D + vec3(time * 0.05));
      
      // 使用 texture 函数采样3D纹理 (WebGL 2.0)
      float noise = texture(worleyTexture, uvw).r;
      
      // 添加一些颜色变化
      vec3 baseColor = vec3(0.2, 0.4, 0.8); // 蓝色基调
      vec3 highlightColor = vec3(0.9, 0.95, 1.0); // 白色高光
      
      // 基于噪声混合颜色
      vec3 color = mix(baseColor, highlightColor, noise);
      
      // 简单的光照计算
      vec3 lightDir = normalize(vec3(0.5, 1.0, 0.5));
      float diff = max(dot(vNormal, lightDir), 0.0);
      vec3 ambient = vec3(0.3);
      
      // 边缘发光效果(基于噪声)
      float rim = 1.0 - max(dot(vNormal, normalize(cameraPosition - vPosition)), 0.0);
      rim = pow(rim, 3.0) * noise * 2.0;
      
      gl_FragColor = vec4(color * (diff + ambient) + vec3(rim), 1.0);
    }
  `

  const createWorleyTexture3D = () => {
    const textureSize = 32 // 3D纹理尺寸 (32x32x32)
    const noiseData = generateWorleyNoise3D(textureSize, textureSize, textureSize)

    // 使用 RawTexture3D 创建 3D 纹理
    // 参数: data, width, height, depth, format, scene, generateMipMaps, invertY, samplingMode, textureType
    const worleyTexture3D = new RawTexture3D(
      noiseData,
      textureSize, // width
      textureSize, // height
      textureSize, // depth
      Engine.TEXTUREFORMAT_RGBA,
      scene,
      false, // generateMipMaps
      false, // invertY
      Texture.TRILINEAR_SAMPLINGMODE,
      Engine.TEXTURETYPE_UNSIGNED_BYTE // 8位无符号整数(0-255),Uint8Array 数据;TEXTURETYPE_UNSIGNED_INTEGER,32位无符号整数
    )

    // 这三个属性控制 3D 纹理在三个轴向上的寻址/包装模式,决定当纹理坐标超出 [0, 1] 范围时如何采样。
    // wrapU	X 轴	水平方向(左右)	2D 纹理的 U
    // wrapV	Y 轴	垂直方向(上下)	2D 纹理的 V
    // wrapR	Z 轴	深度方向(前后)	3D 纹理特有
    // 配合 shader:
    //   vec3 uvw = fract(vUV3D + vec3(time * 0.05));  // fract 将坐标限制在 [0,1]
    // 实际上 fract() 已经处理了越界,但 wrap 模式确保万一有浮点误差或直接使用 uvw > 1.0 时,纹理会无缝重复而不是截断或报错。
    // -------------------------------------------------------------------------------
    // // 1. WRAP - 重复/平铺(你的设置)
    // // 坐标 1.2 → 采样 0.2 的位置,形成无缝循环
    // Texture.WRAP_ADDRESSMODE
    // // 2. CLAMP - 边缘拉伸
    // // 坐标 1.2 → 采样 1.0 的边缘像素,边缘拉伸效果
    // Texture.CLAMP_ADDRESSMODE  
    // // 3. MIRROR - 镜像重复
    // // 坐标 1.2 → 采样 0.8 的位置(反向),1.8 → 0.2,形成镜像
    // Texture.MIRROR_ADDRESSMODE
    worleyTexture3D.wrapU = Texture.WRAP_ADDRESSMODE // X: 左右重复
    worleyTexture3D.wrapV = Texture.WRAP_ADDRESSMODE // Y: 上下重复  
    worleyTexture3D.wrapR = Texture.WRAP_ADDRESSMODE // Z: 前后重复

    const worley3DMaterial = new ShaderMaterial('worley3D', scene, {
      vertex: 'worley3D',
      fragment: 'worley3D'
    }, {
      attributes: ['position', 'normal', 'uv'],
      uniforms: ['worldViewProjection', 'world', 'cameraPosition', 'time'],
      samplers: ['worleyTexture'], // 必须声明 3D 纹理采样器
    })

    // 设置 3D 纹理到材质
    worley3DMaterial.setTexture('worleyTexture', worleyTexture3D)
    worley3DMaterial.setFloat('time', 0)


    // 球体
    const sphere = MeshBuilder.CreateSphere('sphere', {
      diameter: 10,
      segments: 64
    }, scene)
    sphere.position.x = -6
    sphere.material = worley3DMaterial

    // 圆环结
    const torus = MeshBuilder.CreateTorusKnot('torus', {
      radius: 4,
      tube: 1.5,
      radialSegments: 128,
      tubularSegments: 64
    }, scene)
    torus.position.x = 6
    torus.material = worley3DMaterial

    // 盒子
    const box = MeshBuilder.CreateBox('box', {
      size: 7,
    }, scene)
    box.position.z = -8
    box.rotation.y = Math.PI / 4
    box.material = worley3DMaterial

    return { worley3DMaterial, box, torus }
  }

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

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

  createLight()
  const { worley3DMaterial, box, torus } = createWorleyTexture3D()
  runAnimate()
  
  scene.registerBeforeRender(() => {
    time += engine.getDeltaTime() * 0.001
    worley3DMaterial.setFloat('time', time)

    // 缓慢旋转物体以展示3D效果
    box.rotation.y = time * 1.2
    torus.rotation.x = time * 0.3
    torus.rotation.y = time * 0.5
  })

  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>