Appearance
效果-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="postProcess1" class="stage"></canvas>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, onUnmounted } from 'vue'
import 'babylonjs-loaders'
import {
Engine,
Scene,
ArcRotateCamera,
Vector3,
HemisphericLight,
MeshBuilder,
PostProcess,
Effect,
PostProcessRenderPipeline,
PostProcessRenderEffect,
} 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("postProcess1") 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, 5, 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(5, 5, 5))
const createLight = () => {
const light = new HemisphericLight('light', new Vector3(1, 1, 0), scene)
return light
}
const createEffect = () => {
const box = MeshBuilder.CreateBox("box", { size: 2 }, scene)
box.position.y = -1
const sphere = MeshBuilder.CreateSphere('sphere', { diameter: 1 }, scene)
sphere.position.y = 2
Effect.ShadersStore["customFragmentShader"] = `
#ifdef GL_ES
precision highp float;
#endif
// 像素化的本质:通过量化UV坐标实现"块状"视觉效果
// Samplers
varying vec2 vUV;
uniform sampler2D textureSampler; // 输入的屏幕纹理采样器(Babylon.js 自动传入的渲染结果)
// Parameters
uniform vec2 screenSize; // 屏幕分辨率(像素单位)
uniform float pixelSize; // 用户定义的像素块大小
void main(void) {
// 将用户设定的像素块大小转换为纹理空间中的尺寸
// 例如:pixelSize=5, screenSize=1920×1080 → 每个大像素占 (5/1920, 5/1080) 的纹理空间
vec2 pixelSize = pixelSize / screenSize;
// 将平滑的 UV 坐标" snapping"(对齐)到最近的像素网格
// vUV / pixelSize:将坐标转换到"像素块"坐标系
// floor(...):向下取整,锁定到整数网格(关键!这导致同一"像素块"内的所有片元得到相同坐标)
// * pixelSize:转换回纹理空间
// 效果:一个像素块内的所有片元采样同一个颜色,形成块状像素感
vec2 snappedTexCoord = floor(vUV / pixelSize) * pixelSize;
// 使用对齐后的坐标对屏幕纹理进行采样
// 同一像素块内的所有片元将返回相同的颜色值
vec4 color = texture2D(textureSampler, snappedTexCoord);
gl_FragColor = color;
}
`
const postProcess = new PostProcess('Pixelation Post Process', 'custom', ['screenSize', 'pixelSize'], null, 1, null, undefined, engine)
postProcess.onApply = function (effect) {
effect.setFloat2('screenSize', postProcess.width, postProcess.height)
effect.setFloat('pixelSize', 5)
}
// 创建并应用一个后期处理渲染管线(PostProcess Render Pipeline),将自定义像素化效果应用到相机画面上
// 优点:
// -- 便于管理:多个效果可以打包成一个管线,统一控制
// -- 可重用:一个管线可以附加到多个相机
// -- 可切换:可以动态启用/禁用整个管线或单个效果
// -- 顺序控制:效果按添加顺序依次执行
// 缺点:
// -- 稍显繁琐:单个效果时代码量比直接附加更复杂
// 1. 创建一个渲染管线容器
const pipeline1 = new PostProcessRenderPipeline(engine, "pipeline1")
// 2. 将后期处理包装成"效果"
const pixelate = new PostProcessRenderEffect(engine, "pixelate", function () {
return [postProcess] // 返回包含像素化处理器的数组
})
// 3. 将效果添加到管线
pipeline1.addEffect(pixelate)
// 4. 注册管线到场景
scene.postProcessRenderPipelineManager.addPipeline(pipeline1)
// 5. 将管线绑定到相机
scene.postProcessRenderPipelineManager.attachCamerasToRenderPipeline("pipeline1", camera)
// 更简单的写法(适合单个效果)
// camera.attachPostProcess(postProcess)
}
const runAnimate = () => {
engine.runRenderLoop(function() {
if (scene && scene.activeCamera) {
scene.render()
fps.value = engine.getFps().toFixed(2)
}
})
}
createLight()
createEffect()
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>