SOURCE

console 命令行工具 X clear

                    
>
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>