console
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>WebGPU Octree AMR Dust Field Demo</title>
<style>
canvas {width:100vw;height:100vh;display:block;background:#000;}
</style>
</head>
<body>
<canvas id="gpuCanvas"></canvas>
<script type="module">
const canvas = document.getElementById('gpuCanvas');
const ctx = canvas.getContext('webgpu');
// ===================== 1. WebGPU 初始化 =====================
let adapter = await navigator.gpu?.requestAdapter();
let device = await adapter?.requestDevice();
if(!device) throw new Error("浏览器不支持WebGPU,请使用Chrome 113+ / Edge");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const format = navigator.gpu.getPreferredCanvasFormat();
const config = {device, format, alphaMode:"opaque"};
ctx.configure(config);
// ===================== 2. 八叉节点类型(CPU侧) =====================
interface OctNode {
min: [number,number,number],
max: [number,number,number],
level: number,
isLeaf: boolean,
children: OctNode[] | null,
// 如果是叶子:对应GPU里场数据索引
gpuIdx: number,
}
const ROOT_SIZE = 10.0; // 根盒子 10m x10m x10m,匹配你房间尺度
const MIN_LEVEL = 4; // 最小细分层级 2^4 = 16等分,粒度 10/16 ≈0.625m 接近0.5m目标
function createNode(min:[number,number,number], max:[number,number,number], level:number):OctNode {
return {min,max,level,isLeaf:true,children:null,gpuIdx:-1};
}
const root:OctNode = createNode([-ROOT_SIZE/2,-ROOT_SIZE/2,-ROOT_SIZE/2],[ROOT_SIZE/2,ROOT_SIZE/2,ROOT_SIZE/2],0);
// 递归八叉分裂
function splitNode(node:OctNode){
if(!node.isLeaf || node.level >= MIN_LEVEL) return;
node.isLeaf = false;
node.children = [];
const [x0,y0,z0] = node.min;
const [x1,y1,z1] = node.max;
const mx = (x0+x1)/2, my=(y0+y1)/2, mz=(z0+z1)/2;
// 8个子块
node.children.push( createNode([x0,y0,z0],[mx,my,mz], node.level+1) );
node.children.push( createNode([mx,y0,z0],[x1,my,mz], node.level+1) );
node.children.push( createNode([x0,my,z0],[mx,y1,mz], node.level+1) );
node.children.push( createNode([mx,my,z0],[x1,y1,mz], node.level+1) );
node.children.push( createNode([x0,y0,mz],[mx,my,z1], node.level+1) );
node.children.push( createNode([mx,y0,mz],[x1,my,z1], node.level+1) );
node.children.push( createNode([x0,my,mz],[mx,y1,z1], node.level+1) );
node.children.push( createNode([mx,my,mz],[x1,y1,z1], node.level+1) );
}
// 收集所有叶子节点(扁平化上传GPU)
function gatherLeaves(n:OctNode, out:OctNode[]){
if(n.isLeaf) { out.push(n); return; }
for(const c of n.children!) gatherLeaves(c,out);
}
// ===================== 3. WGSL 着色器 =====================
const computeCode = `
struct LeafAABB {
min: vec3f,
max: vec3f,
dust: f32, // 简化:每个叶子体心的尘埃浓度
};
@group(0) @binding(0) var<storage,read_write> leaves: array<LeafAABB>;
@group(0) @binding(1) var<uniform> dt: f32;
const DIFFUSION = 0.008;
const GRAV_SETTLE = 0.012; // Z向下沉降速度
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) id:vec3u){
let idx = id.x;
if(idx >= arrayLength(&leaves)) { return; }
var cell = leaves[idx];
var d = cell.dust;
// 简单源:中心喷灰
let center = (cell.min + cell.max)*0.5;
let src = smoothstep(1.2,0.2,length(center));
d += src * dt * 0.06;
// 重力沉降(简化版本,完整版需要轴向邻域通量交换)
d = d * (1.0 - GRAV_SETTLE * dt);
// 边界衰减
cell.dust = clamp(d,0.0,1.0);
leaves[idx] = cell;
}
`;
const renderCode = `
struct LeafAABB {
min: vec3f,
max: vec3f,
dust: f32,
};
@group(0) @binding(0) var<storage,read> leaves: array<LeafAABB>;
@group(0) @binding(1) var<uniform> camPos: vec3f;
@group(0) @binding(2) var<uniform> camDir: vec3f;
fn aabbHit(ro:vec3f, rd:vec3f, bmin:vec3f, bmax:vec3f) -> f32 {
var tmin = (bmin - ro)/rd;
var tmax = (bmax - ro)/rd;
let t1 = min(tmin,tmax);
let t2 = max(tmin,tmax);
let tNear = max(max(t1.x,t1.y),t1.z);
let tFar = min(min(t2.x,t2.y),t2.z);
if(tNear > tFar || tFar < 0) { return -1.0; }
return max(tNear,0.0);
}
@fragment
fn fs(@builtin(position) uv:vec4f) -> @location(0) vec4f {
let res = vec2f(textureDimensions(renderTarget));
let p = (uv.xy * 2.0 - res) / res.y;
var ro = camPos;
var rd = normalize( vec3f(p, 1.8) );
var acc = 0.0;
var t = 0.0;
const MAX_STEP = 128;
for(var s=0u;s<MAX_STEP;s++){
var hitT = 9999.0;
var hitDust = 0.0;
for(var i=0u;i<arrayLength(&leaves);i++){
let l = leaves[i];
let ht = aabbHit(ro + rd*t, rd, l.min, l.max);
if(ht>0.0 && ht < hitT){
hitT = ht;
hitDust = l.dust;
}
}
if(hitT > 9000.0) break;
acc += hitDust * 0.04;
t += max(hitT,0.05);
}
let col = vec3(acc * vec3(1.0,0.98,0.92));
return vec4(col,1.0);
}
`;
// ===================== 4. Pipeline & Buffer =====================
// 先手动分裂一部分节点做演示:把根的一个子块分裂
splitNode(root.children![0]);
let leaves:OctNode[] = [];
gatherLeaves(root, leaves);
console.log("活跃叶子数量:", leaves.length);
const leafSize = (3*4 + 3*4 + 4); // min(3f32),max(3f32),dust(f32)
const leafBuffer = device.createBuffer({
size: leaves.length * leafSize,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
});
function uploadLeaves(){
const arr = new Float32Array(leaves.length * 7);
for(let i=0;i<leaves.length;i++){
const n = leaves[i];
arr.set([...n.min, ...n.max, 0.01], i*7);
}
device.queue.writeBuffer(leafBuffer,0,arr);
}
uploadLeaves();
// Compute Pipeline
const computeModule = device.createShaderModule({code:computeCode});
const computeBindLayout = device.createBindGroupLayout({entries:[
{binding:0,visibility:GPUShaderStage.COMPUTE, buffer:{type:"storage"}},
{binding:1,visibility:GPUShaderStage.COMPUTE, buffer:{type:"uniform"}},
]});
const computePipeline = device.createComputePipeline({
layout: device.createPipelineLayout({bindGroupLayouts:[computeBindLayout]}),
compute: {module:computeModule, entryPoint:"main"}
});
const dtBuf = device.createBuffer({size:4, usage:GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST});
// Render Pipeline
const renderModule = device.createShaderModule({code:renderCode});
const renderBindLayout = device.createBindGroupLayout({entries:[
{binding:0,visibility:GPUShaderStage.FRAGMENT,buffer:{type:"read-only-storage"}},
{binding:1,visibility:GPUShaderStage.FRAGMENT,buffer:{type:"uniform"}},
{binding:2,visibility:GPUShaderStage.FRAGMENT,buffer:{type:"uniform"}},
]});
const renderPipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({bindGroupLayouts:[renderBindLayout]}),
vertex:{module:device.createShaderModule({code:`@vertex fn v()->@builtin(position)vec4f{return vec4(0,0,0,1);}`}),entryPoint:"v"},
fragment:{module:renderModule,entryPoint:"fs",targets:[{format}]}
});
const camBuf = device.createBuffer({size:24, usage:GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST});
const computeBind = device.createBindGroup({layout:computeBindLayout, entries:[
{binding:0,resource:{buffer:leafBuffer}},
{binding:1,resource:{buffer:dtBuf}},
]});
const renderBind = device.createBindGroup({layout:renderBindLayout, entries:[
{binding:0,resource:{buffer:leafBuffer}},
{binding:1,resource:{buffer:camBuf}},
{binding:2,resource:{buffer:camBuf,offset:12}},
]});
// ===================== 5. 渲染循环 =====================
let t = 0;
function frame(){
t += 0.016;
const camPos = new Float32Array([Math.sin(t*0.3)*7, 2, Math.cos(t*0.3)*7]);
const camDir = new Float32Array([-camPos[0], -1.5, -camPos[2]]);
device.queue.writeBuffer(dtBuf,0,new Float32Array([0.016]));
device.queue.writeBuffer(camBuf,0,camPos);
device.queue.writeBuffer(camBuf,12,camDir);
const enc = device.createCommandEncoder();
// Compute pass:场演化
const cpass = enc.beginComputePass();
cpass.setPipeline(computePipeline);
cpass.setBindGroup(0, computeBind);
cpass.dispatchWorkgroups(Math.ceil(leaves.length / 64));
cpass.end();
// Render pass:光线步进体积渲染
const rpass = enc.beginRenderPass({
colorAttachments:[{view: ctx.getCurrentTexture().createView(), loadOp:"clear", storeOp:"store", clearValue:[0,0,0,1]}]
});
rpass.setPipeline(renderPipeline);
rpass.setBindGroup(0, renderBind);
rpass.draw(3);
rpass.end();
device.queue.submit([enc.finish()]);
requestAnimationFrame(frame);
}
frame();
</script>
</body>
</html>