Appearance
房子
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>