WebGPU renderer complete: dirty uploads, default limits, legacy removed, WebGPU required, warm-up (t88 phase 2d)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 23:17:17 -04:00
co-authored by Claude Opus 5.5
parent 452b6aed32
commit 20963c776e
83 changed files with 776 additions and 1240 deletions
+6 -2
View File
@@ -1,4 +1,8 @@
// Shared CPU colour rules, used by legacy and the rare-cell WebGPU override.
// Rare-cell shading also treats built pieces as transparent surroundings.
function builtForShade(i, x, y) {
return (PH[mat[i]] === PH_SOLID && !(flags[i] & F_NAT)) || blockP[(y >> 2) * BW + (x >> 2)] || mach[i] || part[i];
}
// CPU colour rules for the rare-cell WebGPU overrides.
function cellPixel(x, y) {
const { bw, bh, px, vx0, vy0, smoothOn: sm, rain: rk } = R;
const bx = x - vx0, by = y - vy0, row = by * bw;
@@ -35,7 +39,7 @@ function cellPixel(x, y) {
const T = temp[i];
// Also used by the thermal lens, which always draws in pixels.
let looseShade = 1;
if (!sm && m !== EMPTY && !builtForLight(i, x, y)) {
if (!sm && m !== EMPTY && !builtForShade(i, x, y)) {
if (PH[m] === PH_POWDER) looseShade = 1.1 + nz * 0.00065;
else if (PH[m] === PH_SOLID && (
(x > 0 && PH[mat[i - 1]] === PH_POWDER && !part[i - 1]) ||
+2 -2
View File
@@ -17,7 +17,7 @@ ${smooth ? `@group(0) @binding(16) var backOut: texture_storage_2d<rgba8unorm, w
@group(0) @binding(21) var fieldOver: texture_2d<f32>;
@group(0) @binding(22) var maskOver: texture_2d<u32>;
` : ""}
@group(0) @binding(15) var glowT: texture_storage_2d<rgba8unorm, write>;
${smooth ? "" : "@group(0) @binding(15) var glowT: texture_storage_2d<rgba8unorm, write>;"}
fn phase(p: vec2i) -> i32 { return i32(lut[textureLoad(matT,p,0).r*3u+1u].x); }
fn powder(p: vec2i) -> bool {
return all(p >= vec2i(0)) && all(p < vec2i(u.world.xy)) && phase(p)==${PH_POWDER} && textureLoad(partT,p,0).r==0u;
@@ -94,7 +94,7 @@ fn colour(@builtin(global_invocation_id) id: vec3u) {
if (${smooth ? "textureLoad(maskOver,q,0).r>0u" : "over.a>0."}) { rgb=over.rgb; ${smooth ? "cl=over.a*255.; backValue=textureLoad(backOver,q,0); treeValue=textureLoad(treeOver,q,0); fieldValue=textureLoad(fieldOver,q,0);" : ""} bloom=max(bloom,textureLoad(overrideGlow,q/2,0).rgb); }
${smooth ? "textureStore(backOut,q,backValue); textureStore(treeOut,q,treeValue); textureStore(fieldOut,q,fieldValue);" : ""}
textureStore(outputT,q,vec4f(rgb,${smooth ? "cl/255." : "1"}));
if (((q.x | q.y) & 1)==0) { textureStore(glowT,q/2,vec4f(bloom,1)); }
${smooth ? "" : "if (((q.x | q.y) & 1)==0) { textureStore(glowT,q/2,vec4f(bloom,1)); }"}
}`;
}
const GPU_COLOUR = gpuColourShader(false);
+79
View File
@@ -0,0 +1,79 @@
// The drill's three seeded Perlin contour fields, uploaded once with the material tables.
function gpuCrackTables() {
const out = new Float32Array(3 * 512 * 4);
for (let l=0;l<3;l++) {
const rng=mulberry32(0xc7ac1+l), p=Array.from({length:256},(_,i)=>i);
for(let i=255;i>0;i--) { const j=Math.floor(rng()*(i+1)); [p[i],p[j]]=[p[j],p[i]]; }
for(let i=0;i<512;i++) out[(l*512+i)*4]=p[i&255];
for(let i=0;i<256;i++) { const a=rng()*Math.PI*2; out[(l*512+i)*4+1]=Math.cos(a); out[(l*512+i)*4+2]=Math.sin(a); }
}
return out;
}
const GPU_CRACKS = `
fn crackNoise(p: vec2f, l: u32) -> f32 {
let base=${NMAT*3+700+551*3}u+l*512u;
let v=vec2u(vec2i(floor(p)) & vec2i(255)); let f=fract(p);
let p0=u32(lut[base+v.y].x); let p1=u32(lut[base+v.y+1u].x);
let a=lut[base+u32(lut[base+v.x+p0].x)].yz;
let b=lut[base+u32(lut[base+v.x+1u+p0].x)].yz;
let c=lut[base+u32(lut[base+v.x+p1].x)].yz;
let d=lut[base+u32(lut[base+v.x+1u+p1].x)].yz;
let t=f*f*f*(f*(f*6.-15.)+10.);
return mix(mix(dot(a,f),dot(b,f-vec2f(1,0)),t.x),mix(dot(c,f-vec2f(0,1)),dot(d,f-vec2f(1)),t.x),t.y)*1.41;
}
`;
// Screen-resolution colour output preserves the old sub-grain marching-square strokes.
const GPU_CRACK_COLOUR = `
struct Params { view:vec4f, world:vec4f, style:vec4f, effects:vec4f, camera:vec4f, light:vec4f, screen:vec4f, extra:vec4f }
@group(0) @binding(0) var<uniform> u:Params;
@group(0) @binding(1) var<storage,read> lut:array<vec4f>;
@group(0) @binding(2) var matT:texture_2d<u32>;
@group(0) @binding(3) var digpT:texture_2d<u32>;
@group(0) @binding(4) var outputT:texture_storage_2d<rgba8unorm,write>;
${GPU_CRACKS}
fn segmentDistance(p:vec2f,a:vec2f,b:vec2f)->f32 {
let d=b-a;return length(p-a-d*clamp(dot(p-a,d)/max(0.000001,dot(d,d)),0.,1.));
}
@compute @workgroup_size(8,8)
fn main(@builtin(global_invocation_id) id:vec3u) {
let q=vec2i(id.xy);if(any(q>=vec2i(u.screen.xy))){return;}
let pos=u.extra.xy+(vec2f(q)+0.5)/u.camera.z; let p=vec2i(floor(pos));
var colour=vec4f(0);
if(u.camera.z>=2.5 && all(p>=vec2i(0)) && all(p<vec2i(u.world.xy))) {
let m=textureLoad(matT,p,0).r;let damage=f32(textureLoad(digpT,p,0).r)/255.;
if(lut[m*3u+1u].x==${PH_SOLID}.) {
for(var l=0u;l<3u;l++) {
if(damage<array<f32,3>(0.1,0.42,0.72)[l]){continue;}
let scale=array<f32,3>(1./7.,1./3.6,1./2.)[l];
let at=vec2f(p)*scale+vec2f(0.37,0.61);
let a=crackNoise(at,l);let b=crackNoise(at+vec2f(scale,0),l);
let c=crackNoise(at+vec2f(scale),l);let d=crackNoise(at+vec2f(0,scale),l);
var pts:array<vec2f,4>;var n=0u;
if((a<0.)!=(b<0.)){pts[n]=vec2f(a/(a-b),0);n++;}
if((b<0.)!=(c<0.)){pts[n]=vec2f(1,b/(b-c));n++;}
if((d<0.)!=(c<0.)){pts[n]=vec2f(d/(d-c),1);n++;}
if((a<0.)!=(d<0.)){pts[n]=vec2f(0,a/(a-d));n++;}
let width=array<f32,3>(0.2,0.14,0.09)[l];
for(var stroke=0u;stroke<2u;stroke++) {
let lip=stroke==0u;let offset=select(0.,max(1./u.camera.z,0.12),lip);
var distance=100.;
for(var k=0u;k+1u<n;k+=2u) {
distance=min(distance,segmentDistance(pos-vec2f(p)-vec2f(0,offset),pts[k],pts[k+1u]));
}
let w=max(1./u.camera.z,width*select(1.,1.3,lip))*0.5;
let alpha=clamp((w-distance)*u.camera.z+0.5,0.,1.)*select(0.78-f32(l)*0.12,0.13,lip);
let rgb=select(vec3f(18,12,10),vec3f(255,240,220),lip)/255.;
colour=vec4f(rgb*alpha+colour.rgb*(1.-alpha),alpha+colour.a*(1.-alpha));
}
}
}
}
textureStore(outputT,q,colour);
}`;
const GPU_CRACK_DRAW = `
@group(0) @binding(0) var cracks:texture_2d<f32>;
@vertex fn vs(@builtin(vertex_index) i:u32)->@builtin(position) vec4f {
let p=array<vec2f,3>(vec2f(-1,-1),vec2f(3,-1),vec2f(-1,3));return vec4f(p[i],0,1);
}
@fragment fn fs(@builtin(position) p:vec4f)->@location(0) vec4f {return textureLoad(cracks,vec2i(p.xy),0);}
`;
+50 -9
View File
@@ -1,23 +1,30 @@
// Async startup shares the title screen's adapter; failures leave a usable legacy renderer.
// All pipelines compile during startup; rendering never falls back.
const GPU_LIMITS = { maxStorageTexturesPerShaderStage: 4, maxSampledTexturesPerShaderStage: 16, maxStorageBuffersPerShaderStage: 8, maxTextureDimension2D: 8192 };
function gpuLimitsOK(adapter) { return Object.entries(GPU_LIMITS).every(([k,v]) => adapter.limits[k] >= v); }
const GPU = { ok: false, canvas: document.getElementById('gpu'), error: '', gpuMs: null };
async function gpuInit(adapter) {
const started = performance.now();
try {
if (!gpuLimitsOK(adapter)) throw Error("Graphics adapter does not meet the required WebGPU limits.");
const features = adapter.features.has('timestamp-query') ? ['timestamp-query'] : [];
const d = GPU.device = await adapter.requestDevice({ requiredFeatures: features, requiredLimits: { maxStorageTexturesPerShaderStage: 5 } });
const d = GPU.device = await adapter.requestDevice({ requiredFeatures: features, requiredLimits: GPU_LIMITS });
d.addEventListener('uncapturederror', e => gpuFail(e.error.message));
d.lost.then(info => gpuFail(info.message || 'Graphics device lost'));
d.lost.then(info => { if (GPU.device === d) gpuRecover(info.message || 'Graphics device lost'); });
GPU.context = GPU.canvas.getContext('webgpu');
GPU.format = navigator.gpu.getPreferredCanvasFormat();
GPU.context.configure({ device: d, format: GPU.format, alphaMode: 'opaque' });
d.pushErrorScope('validation');
GPU.colour = await d.createComputePipelineAsync({ layout: 'auto', compute: { module: d.createShaderModule({ code: GPU_COLOUR }), entryPoint: 'colour' } });
GPU.colourSmooth = await d.createComputePipelineAsync({ layout: 'auto', compute: { module: d.createShaderModule({ code: GPU_COLOUR_SMOOTH }), entryPoint: 'colour' } });
gpuSmoothInit();
await gpuSmoothInit();
for (const [name, code, entryPoint] of [['lightRoof',GPU_LIGHT,'roof'],['lightSources',GPU_LIGHT,'sources'],['lightSpread',GPU_LIGHT_SPREAD,'main'],['lightPack',GPU_LIGHT_PACK,'main']]) {
GPU[name] = await d.createComputePipelineAsync({layout:'auto',compute:{module:d.createShaderModule({code}),entryPoint}});
}
GPU.blur = await d.createComputePipelineAsync({ layout: 'auto', compute: { module: d.createShaderModule({ code: GPU_BLUR }), entryPoint: 'main' } });
GPU.draw = await d.createRenderPipelineAsync({ layout: 'auto', vertex: { module: d.createShaderModule({ code: GPU_DRAW }), entryPoint: 'vs' }, fragment: { module: d.createShaderModule({ code: GPU_DRAW }), entryPoint: 'fs', targets: [{ format: GPU.format }] }, primitive: { topology: 'triangle-list' } });
GPU.cracks = await d.createComputePipelineAsync({layout:'auto',compute:{module:d.createShaderModule({code:GPU_CRACK_COLOUR}),entryPoint:'main'}});
const cracks = d.createShaderModule({code:GPU_CRACK_DRAW});
GPU.crackDraw = await d.createRenderPipelineAsync({layout:'auto',vertex:{module:cracks,entryPoint:'vs'},fragment:{module:cracks,entryPoint:'fs',targets:[{format:GPU.format,blend:{color:{srcFactor:'one',dstFactor:'one-minus-src-alpha'},alpha:{srcFactor:'one',dstFactor:'one-minus-src-alpha'}}}]},primitive:{topology:'triangle-list'}});
const error = await d.popErrorScope(); if (error) throw error;
GPU.uniform = d.createBuffer({ size: 128, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
GPU.params = new Float32Array(32);
@@ -27,14 +34,48 @@ async function gpuInit(adapter) {
GPU.resolve = d.createBuffer({ size: 256, usage: GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC });
GPU.readback = d.createBuffer({ size: 32, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ });
}
GPU.ok = true;
} catch (e) { gpuFail(e.message); }
GPU.ok = true; GPU.error = ''; GPU.initMs = performance.now() - started;
return true;
} catch (e) { GPU.ok = false; GPU.error = e.message; return false; }
}
function gpuFail(message) {
GPU.ok = false; GPU.error = message; SET.renderer = 'legacy';
GPU.canvas.hidden = true;
GPU.ok = false; GPU.error = message;
console.warn('WebGPU renderer: ' + message);
if (GAME.running) toast('WebGPU stopped drawing. Legacy renderer is on.');
if (GAME.running) gpuRecover(message);
}
async function gpuRecover(message) {
if (GPU.recovering || GPU.stopped) return;
GPU.ok = false; GPU.recovering = true;
const saved = !GAME.running || await saveWorld('auto', true);
const old = GPU.device;
// Resources belong to the lost device; force every retained cache to rebuild.
GPU.world = GPU.sky = GPU.viewKey = GPU.lightWorld = GPU.lightKey = null;
GPU.bindColour = GPU.lightDirections = GPU.queries = null;
GPU.timingBusy = false; GPU.lightFrame = undefined;
let restored = false;
try {
const adapter = navigator.gpu && await navigator.gpu.requestAdapter();
if (adapter) restored = await gpuInit(adapter);
} catch (e) { GPU.error = e.message; }
if (restored) { if (old) old.destroy(); GPU.recovering = false; lastT = performance.now(); acc = 0; return; }
GPU.device = null; if (old) old.destroy();
GPU.stopped = true; GPU.recovering = false; GAME.paused = true;
dialog('Graphics stopped', '<p>The graphics device could not restart. '+
(saved ? 'Your world was autosaved. Reload to continue.' : 'Saving failed. Export your world before reloading.')+'</p>', [
...(!saved ? [{label:'Export world',fn:exportWorld}] : []),
{label:'Reload',fn:()=>location.reload()}
]);
}
// Submit both looks behind the loading screen, including their first real bindings.
async function gpuWarmWorld() {
buildSky(); resizeRender(); snapCamera();
const look = SET.look, started = performance.now();
try {
for (const next of [look === 'smooth' ? 'pixel' : 'smooth', look]) {
SET.look = next; drawWorld(); await GPU.device.queue.onSubmittedWorkDone();
}
GPU.warmMs = performance.now() - started;
} finally { SET.look = look; }
}
// Tests sample this composite so a transparent overlay cannot pass world-colour checks.
function worldPixelContext() {
+12 -3
View File
@@ -48,8 +48,14 @@ function gpuDraw(lighting, glowOn, lightUpdate) {
const blit = performance.now(), d = GPU.device, encoder = d.createCommandEncoder();
const timed = GPU.queries && !GPU.timingBusy && (PERF_MARKS || SET.showPerf);
const compute = encoder.beginComputePass(timed ? { timestampWrites: { querySet: GPU.queries, beginningOfPassWriteIndex: 0 } } : {});
compute.setPipeline(R.smoothOn ? GPU.colourSmooth : GPU.colour); compute.setBindGroup(0, R.smoothOn ? GPU.bindColourSmooth : GPU.bindColour);
compute.dispatchWorkgroups(Math.ceil(R.bw / 8), Math.ceil(R.bh / 8)); compute.end();
compute.setPipeline(GPU.colour); compute.setBindGroup(0, GPU.bindColour);
if (R.smoothOn) {
compute.dispatchWorkgroups(Math.ceil(R.bw / 8), Math.ceil(R.bh / 8));
compute.setPipeline(GPU.colourSmooth); compute.setBindGroup(0, GPU.bindColourSmooth);
}
compute.dispatchWorkgroups(Math.ceil(R.bw / 8), Math.ceil(R.bh / 8));
compute.setPipeline(GPU.cracks); compute.setBindGroup(0, GPU.bindCracks);
compute.dispatchWorkgroups(Math.ceil(R.canvas.width / 8), Math.ceil(R.canvas.height / 8)); compute.end();
if (glowOn && !R.smoothOn) for (let i = 0; i < 2; i++) {
const pass = encoder.beginComputePass(); pass.setPipeline(GPU.blur); pass.setBindGroup(0, GPU.bindBlur[i]);
pass.dispatchWorkgroups(Math.ceil(R.glowC.width / 8), Math.ceil(R.glowC.height / 8)); pass.end();
@@ -78,6 +84,9 @@ function gpuDraw(lighting, glowOn, lightUpdate) {
// Also used for readback after the browser has expired the presented canvas texture.
function gpuPresent(encoder, timed = false) {
const pass = encoder.beginRenderPass({ colorAttachments: [{ view: GPU.context.getCurrentTexture().createView(), loadOp: 'clear', storeOp: 'store', clearValue: [0, 0, 0, 1] }],
...(timed ? { timestampWrites: { querySet: GPU.queries, endOfPassWriteIndex: 1 } } : {}) });
});
pass.setPipeline(R.smoothOn ? GPU.smooth : GPU.draw); pass.setBindGroup(0, R.smoothOn ? GPU.bindSmooth : GPU.bindDraw); pass.draw(3); pass.end();
const cracks = encoder.beginRenderPass({colorAttachments:[{view:GPU.context.getCurrentTexture().createView(),loadOp:'load',storeOp:'store'}],
...(timed ? {timestampWrites:{querySet:GPU.queries,endOfPassWriteIndex:1}} : {})});
cracks.setPipeline(GPU.crackDraw);cracks.setBindGroup(0,GPU.bindCrackDraw);cracks.draw(3);cracks.end();
}
+6 -5
View File
@@ -9,11 +9,11 @@ const GPU_FIELD_MIP = `
let size=max(vec2u(1),textureDimensions(source)/2u);
return textureSampleLevel(source,linear,p.xy/vec2f(size),0);
}`;
function gpuSmoothInit() {
async function gpuSmoothInit() {
const d=GPU.device;
const pipeline=(code,vertex,format)=>d.createRenderPipeline({layout:'auto',vertex:{module:d.createShaderModule({code:vertex}),entryPoint:'vs'},fragment:{module:d.createShaderModule({code}),entryPoint:'fs',targets:[{format}]},primitive:{topology:'triangle-list'}});
GPU.smooth=pipeline(GPU_SMOOTH,GPU_DRAW,GPU.format);
GPU.fieldMip=pipeline(GPU_FIELD_MIP,GPU_FIELD_MIP,'rgba8unorm');
const pipeline=(code,vertex,format)=>d.createRenderPipelineAsync({layout:'auto',vertex:{module:d.createShaderModule({code:vertex}),entryPoint:'vs'},fragment:{module:d.createShaderModule({code}),entryPoint:'fs',targets:[{format}]},primitive:{topology:'triangle-list'}});
GPU.smooth=await pipeline(GPU_SMOOTH,GPU_DRAW,GPU.format);
GPU.fieldMip=await pipeline(GPU_FIELD_MIP,GPU_FIELD_MIP,'rgba8unorm');
GPU.smoothUniform=d.createBuffer({size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST});
}
function gpuSmoothTextures() {
@@ -26,7 +26,8 @@ function gpuSmoothTextures() {
GPU.viewTextures.push(...GPU.smoothOver,GPU.smoothMask,GPU.backTex,GPU.treeTex,GPU.fieldTex);
}
function gpuSmoothBindings(group,view,buffer) {
GPU.bindColourSmooth=group(GPU.colourSmooth,[buffer(GPU.uniform),buffer(GPU.lut),buffer(GPU.land),...GPU.worldTextures.slice(0,9).map(view),view(GPU.override),view(GPU.overrideGlow),view(GPU.colourTex),view(GPU.glowTex),view(GPU.backTex),view(GPU.treeTex),GPU.fieldTex.createView({baseMipLevel:0,mipLevelCount:1}),...GPU.smoothOver.map(view),view(GPU.smoothMask)]);
const colourResources=[buffer(GPU.uniform),buffer(GPU.lut),buffer(GPU.land),...GPU.worldTextures.slice(0,9).map(view),view(GPU.override),view(GPU.overrideGlow),view(GPU.colourTex),null,view(GPU.backTex),view(GPU.treeTex),GPU.fieldTex.createView({baseMipLevel:0,mipLevelCount:1}),...GPU.smoothOver.map(view),view(GPU.smoothMask)];
GPU.bindColourSmooth=GPU.device.createBindGroup({layout:GPU.colourSmooth.getBindGroupLayout(0),entries:colourResources.map((resource,binding)=>({binding,resource})).filter(e=>e.resource)});
GPU.bindSmooth=group(GPU.smooth,[buffer(GPU.uniform),view(GPU.colourTex),view(GPU.backTex),view(GPU.treeTex),view(GPU.fieldTex),view(GPU.lightTex),view(GPU.glowTex),GPU.sampler,buffer(GPU.smoothUniform)]);
GPU.fieldMips=[];
for(let i=1;i<GPU.fieldTex.mipLevelCount;i++) GPU.fieldMips.push({
+5
View File
@@ -0,0 +1,5 @@
const SM = { t0: performance.now(), surf: -1 };
function smoothSurface() {
if (SM.surf < 0 || SM.surfSeed !== worldSeed || SM.surfW !== W) { SM.surf = meanSurface(); SM.surfW = W; SM.surfSeed = worldSeed; }
return SM.surf;
}
+13 -19
View File
@@ -1,6 +1,6 @@
// Whole-world allocation, direct strided uploads; no cell packing or dirty tracking.
// Whole-world textures retain clean chunks across camera moves.
function gpuTexture(w, h, format = 'rgba8unorm', storage = false) {
return GPU.device.createTexture({ size: [w, h], format, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | (storage ? GPUTextureUsage.STORAGE_BINDING : 0) });
return GPU.device.createTexture({ size: [w, h], format, usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC | (storage ? GPUTextureUsage.STORAGE_BINDING : 0) });
}
function gpuWrite(texture, data, stride, x, y, w, h) {
if (w <= 0 || h <= 0) return;
@@ -16,15 +16,17 @@ function gpuWorldTextures() {
if (GPU.worldTextures) for (const t of GPU.worldTextures) t.destroy();
if (GPU.lut) GPU.lut.destroy(); if (GPU.land) GPU.land.destroy();
GPU.world = mat; GPU.sky = SKY_R;
GPU.resident = new Uint8Array(NCH);
GPU.worldTextures = Array.from({ length: 8 }, (_, i) => gpuTexture(i === 7 ? FW : W, i === 7 ? FH : H, 'r8uint'));
GPU.temp = gpuTexture(W, H, 'r32float'); GPU.worldTextures.push(GPU.temp);
gpuWrite(GPU.worldTextures[6], noiseB, W, 0, 0, W, H);
const tables = new Float32Array((NMAT * 3 + 700 + 551 * 3) * 4);
const tables = new Float32Array((NMAT * 3 + 700 + 551 * 3 + 3 * 512) * 4);
for (let m = 0; m < NMAT; m++) {
tables.set([COLR[m], COLG[m], COLB[m], CVAR[m], PH[m], ALPHA[m], 0, 0, TREE_R[m], TREE_G[m], TREE_B[m], 0], m * 12);
}
for (let i = 0; i < 700; i++) tables.set([GLOW_R[i], GLOW_G[i], GLOW_B[i], GLOW_W[i]], (NMAT * 3 + i) * 4);
for (const [j, palette] of Object.values(THERM).entries()) for (let i = 0; i < 551; i++) tables.set([palette[i * 3], palette[i * 3 + 1], palette[i * 3 + 2], 0], (NMAT * 3 + 700 + j * 551 + i) * 4);
tables.set(gpuCrackTables(), (NMAT * 3 + 700 + 551 * 3) * 4);
GPU.lut = gpuBuffer(tables);
GPU.landData = new Float32Array((W + H) * 4);
GPU.land = gpuBuffer(GPU.landData);
@@ -35,18 +37,19 @@ function gpuWorldTextures() {
GPU.bindColour = null;
}
function gpuViewTextures() {
const key = `${R.bw}/${R.bh}/${R.lw}/${R.lh}/${+R.smoothOn}`;
const key = `${R.bw}/${R.bh}/${R.lw}/${R.lh}/${+R.smoothOn}/${R.canvas.width}/${R.canvas.height}`;
if (GPU.viewKey === key) return;
GPU.viewKey = key; GPU.lightFrame = undefined;
if (GPU.viewTextures) for (const t of GPU.viewTextures) t.destroy();
const bw = R.bw, bh = R.bh, gw = R.glowC.width, gh = R.glowC.height;
GPU.crackTex = gpuTexture(R.canvas.width, R.canvas.height, 'rgba8unorm', true);
GPU.override = gpuTexture(bw, bh); GPU.overrideGlow = gpuTexture(gw, gh);
GPU.colourTex = gpuTexture(bw, bh, 'rgba8unorm', true);
GPU.glowTex = gpuTexture(gw, gh, 'rgba8unorm', true);
GPU.blurX = gpuTexture(gw, gh, 'rgba8unorm', true); GPU.blurY = gpuTexture(gw, gh, 'rgba8unorm', true);
GPU.lightTex = gpuTexture(Math.max(1, R.lw), Math.max(1, R.lh), 'rgba8unorm', true);
GPU.lightWork = [0,1].map(() => gpuTexture(Math.max(1,R.lw),Math.max(1,R.lh),'rgba16float',true));
GPU.viewTextures = [GPU.override, GPU.overrideGlow, GPU.colourTex, GPU.glowTex, GPU.blurX, GPU.blurY, GPU.lightTex, ...GPU.lightWork];
GPU.viewTextures = [GPU.crackTex, GPU.override, GPU.overrideGlow, GPU.colourTex, GPU.glowTex, GPU.blurX, GPU.blurY, GPU.lightTex, ...GPU.lightWork];
if (R.smoothOn) gpuSmoothTextures();
GPU.bindColour = null;
}
@@ -57,6 +60,8 @@ function gpuBindings() {
GPU.bindColour = group(GPU.colour, [buffer(GPU.uniform), buffer(GPU.lut), buffer(GPU.land), ...GPU.worldTextures.slice(0,9).map(view), view(GPU.override), view(GPU.overrideGlow), view(GPU.colourTex), view(GPU.glowTex)]);
if (R.smoothOn) gpuSmoothBindings(group, view, buffer);
gpuLightBindings(group, view, buffer);
GPU.bindCracks = group(GPU.cracks, [buffer(GPU.uniform),buffer(GPU.lut),view(GPU.worldTextures[0]),view(GPU.worldTextures[5]),view(GPU.crackTex)]);
GPU.bindCrackDraw = group(GPU.crackDraw, [view(GPU.crackTex)]);
GPU.bindDraw = group(GPU.draw, [buffer(GPU.uniform), view(GPU.colourTex), view(GPU.lightTex), view(GPU.blurY), GPU.sampler]);
if (GPU.blurUniforms) for (const b of GPU.blurUniforms) b.destroy();
GPU.blurUniforms = [0, 1].map(() => d.createBuffer({ size: 16, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST }));
@@ -67,20 +72,9 @@ function gpuUpload(lighting, glowOn) {
const margin = lighting ? Math.ceil(28 / R.lres) * R.lres + R.lres : 2;
const x = Math.max(0, R.vx0 - margin), y = Math.max(0, R.vy0 - margin);
const w = Math.min(W, R.vx0 + R.bw + margin) - x, h = Math.min(H, R.vy0 + R.bh + margin + 12) - y;
const arrays = [mat, flags, life, part, bgm, digp];
arrays.forEach((a, i) => gpuWrite(GPU.worldTextures[i], a, W, x, y, w, h));
gpuWrite(GPU.temp, temp, W, x, y, w, h);
if (lighting) {
let top=y;
for(let xx=x;xx<x+w;xx++) top=Math.min(top,roofY[xx]);
// Roof scans can start above the view; loose loads also inspect twelve grains below.
for(const [t,a] of [[GPU.worldTextures[0],mat],[GPU.worldTextures[1],flags],[GPU.worldTextures[3],part]]) gpuWrite(t,a,W,x,top,w,y+h-top);
gpuWrite(GPU.mach,mach,W,x,top,w,y+h-top);
const bx=x>>2, by=top>>2;
gpuWrite(GPU.block,blockP,BW,bx,by,Math.min(BW,Math.ceil((x+w)/4))-bx,Math.min(BH,Math.ceil((y+h)/4))-by);
}
const fx = Math.max(0, Math.floor(x / FOG)), fy = Math.max(0, Math.floor(y / FOG));
gpuWrite(GPU.worldTextures[7], fog, FW, fx, fy, Math.min(FW, Math.ceil((x + w) / FOG) + 1) - fx, Math.min(FH, Math.ceil((y + h) / FOG) + 1) - fy);
let top = y;
if (lighting) for (let xx = x; xx < x + w; xx++) top = Math.min(top, roofY[xx]);
gpuUploadChunks(x, top, x + w, y + h);
// Small per-row/column tables may change in scene tools independently of world allocation.
const land = GPU.landData;
for (let i = 0; i < W; i++) { const k = i * 4; land[k] = surfY[i]; land[k + 1] = dirtY[i]; land[k + 2] = graniteY[i]; land[k + 3] = basaltY[i]; }
+48
View File
@@ -0,0 +1,48 @@
// Upload full chunks so scrolling within a resident chunk needs no extra copy.
function gpuUploadChunks(x0, y0, x1, y1) {
const cx0 = Math.max(0, x0 >> 5), cy0 = Math.max(0, y0 >> 5);
const cx1 = Math.min(CW, Math.ceil(x1 / 32)), cy1 = Math.min(CH, Math.ceil(y1 / 32));
GPU.uploadStrips = 0; GPU.uploadCells = 0;
for (let cy = cy0; cy < cy1; cy++) for (let cx = cx0; cx < cx1;) {
const c = cy * CW + cx;
if (!renderDirty[c] && GPU.resident[c]) { cx++; continue; }
const first = cx++;
while (cx < cx1 && (renderDirty[cy * CW + cx] || !GPU.resident[cy * CW + cx])) cx++;
const x = first * 32, y = cy * 32, w = Math.min(W, cx * 32) - x, h = Math.min(32, H - y);
gpuUploadStrip(x, y, w, h);
GPU.uploadStrips++; GPU.uploadCells += w * h;
for (let k = first; k < cx; k++) { renderDirty[cy * CW + k] = 0; GPU.resident[cy * CW + k] = 1; }
}
}
function gpuUploadStrip(x, y, w, h) {
for (const [i, a] of [mat, flags, life, part, bgm, digp].entries()) gpuWrite(GPU.worldTextures[i], a, W, x, y, w, h);
gpuWrite(GPU.temp, temp, W, x, y, w, h);
gpuWrite(GPU.mach, mach, W, x, y, w, h);
gpuWrite(GPU.block, blockP, BW, x >> 2, y >> 2, Math.ceil(w / BS), Math.ceil(h / BS));
// Fog interpolation reads the next sample, possibly in a neighbouring chunk.
gpuWrite(GPU.worldTextures[7], fog, FW, x / FOG, y / FOG, Math.min(FW - x / FOG, Math.ceil(w / FOG) + 1), Math.min(FH - y / FOG, Math.ceil(h / FOG) + 1));
}
// Debug-only readback: compares resident GPU texels to CPU arrays without refreshing them.
async function gpuCheckUploads() {
const sources = [...[mat, flags, life, part, bgm, digp].map((a,i)=>[GPU.worldTextures[i],a,W,1]),
[GPU.temp,temp,W,1], [GPU.mach,mach,W,1], [GPU.block,blockP,BW,BS], [GPU.worldTextures[7],fog,FW,FOG]];
let checked = 0;
for (const [texture, data, stride, scale] of sources) {
const bytes = data.BYTES_PER_ELEMENT, row = Math.ceil(texture.width * bytes / 256) * 256;
const buffer = GPU.device.createBuffer({size:row*texture.height,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ});
const encoder = GPU.device.createCommandEncoder();
encoder.copyTextureToBuffer({texture},{buffer,bytesPerRow:row},[texture.width,texture.height]);
GPU.device.queue.submit([encoder.finish()]); await buffer.mapAsync(GPUMapMode.READ);
const actual = new Uint8Array(buffer.getMappedRange()), expected = new Uint8Array(data.buffer,data.byteOffset,data.byteLength);
let mismatch = '';
for (let cy=0;cy<CH;cy++) for(let cx=0;cx<CW;cx++) {
const c=cy*CW+cx; if(!GPU.resident[c] || renderDirty[c]) continue;
for(let y=cy*32/scale;y<Math.min(texture.height,(cy+1)*32/scale);y++)
for(let x=cx*32/scale;x<Math.min(texture.width,(cx+1)*32/scale);x++) for(let b=0;b<bytes;b++) {
checked++; if(actual[y*row+x*bytes+b]!==expected[(y*stride+x)*bytes+b] && !mismatch) mismatch=`${texture.format} at ${x},${y}`;
}
}
buffer.unmap(); buffer.destroy(); if(mismatch) throw Error('Stale GPU upload: '+mismatch);
}
return checked;
}
-103
View File
@@ -1,103 +0,0 @@
// Propagated coloured light from glowing and burning cells.
// ---------- Lighting ----------
// Light is computed on a coarse grid (2 or 4 grains per cell), written into a
// small canvas, then the GPU upscales it smoothly and multiplies it over the world.
function builtForLight(i, x, y) {
return (PH[mat[i]] === PH_SOLID && !(flags[i] & F_NAT)) || blockP[(y >> 2) * BW + (x >> 2)] || mach[i] || part[i];
}
function looseForLight(i) {
for (let depth = 0; depth <= 12 && i < N; depth++, i += W) {
const m = mat[i], ph = PH[m];
if (builtForLight(i, i % W, (i / W) | 0)) return true;
if (ph !== PH_POWDER && ph !== PH_LIQUID) return m === EMPTY || ph === PH_GAS;
}
return false;
}
function computeLight() {
const res = SET.lighting === 'fast' ? 4 : 2;
const M = Math.ceil(28 / res);
const lx0 = Math.floor(R.vx0 / res) - M, ly0 = Math.floor(R.vy0 / res) - M;
const lw = Math.ceil(R.bw / res) + 2 * M, lh = Math.ceil(R.bh / res) + 2 * M;
if (lw !== R.lw || lh !== R.lh || res !== R.lres || !R.lr || R.lr.length !== lw * lh || R.lightC.width !== lw || R.lightC.height !== lh) {
R.lw = lw; R.lh = lh; R.lres = res;
R.lr = new Float32Array(lw * lh); R.lg = new Float32Array(lw * lh); R.lb = new Float32Array(lw * lh); R.latt = new Float32Array(lw * lh);
R.lightC.width = lw; R.lightC.height = lh;
R.lightImg = R.lightX.createImageData(lw, lh);
R.lightPx = new Uint32Array(R.lightImg.data.buffer);
R.lightRoof = new Int32Array(lw);
}
R.lx0 = lx0; R.ly0 = ly0;
const lr = R.lr, lg = R.lg, lb = R.lb, la = R.latt;
// per-grain transmittance: air .978, gas .96, liquid .93, solid .935 (light soaks a little way into rock)
const P = res;
const aAir = Math.pow(0.978, P), aGas = Math.pow(0.96, P), aLiq = Math.pow(0.93, P), aSol = Math.pow(0.9, P);
const skyK = SET.daylight * (1 - 0.32 * (R.rain || 0)); // overcast while it rains
// Only sampled view columns, starting at the simulation roof; nothing below the view matters.
const roofEnd = Math.min(H, (ly0 + lh) * res), lightRoof = R.lightRoof;
for (let lx = 0; lx < lw; lx++) {
const x = (lx0 + lx) * res;
if (x < 0 || x >= W) continue;
let y = roofY[x];
for (; y < roofEnd; y++) {
const i = y * W + x, ph = PH[mat[i]];
if ((ph === PH_SOLID || ph === PH_POWDER || ph === PH_LIQUID) && !builtForLight(i, x, y) && !((ph === PH_POWDER || ph === PH_LIQUID) && looseForLight(i))) break;
}
lightRoof[lx] = y;
}
for (let ly = 0; ly < lh; ly++) {
const wy = (ly0 + ly) * res;
for (let lx = 0; lx < lw; lx++) {
const wx = (lx0 + lx) * res, k = ly * lw + lx;
if (wx < 0 || wy < 0 || wx >= W || wy >= H) { la[k] = 0; lr[k] = lg[k] = lb[k] = 0; continue; }
const i = wy * W + wx, m = mat[i], ph = PH[m];
la[k] = builtForLight(i, wx, wy) || ((ph === PH_POWDER || ph === PH_LIQUID) && looseForLight(i)) ? aAir : ph === PH_EMPTY ? (bgm[i] ? aGas : aAir) : ph === PH_GAS ? aGas : ph === PH_LIQUID ? aLiq : m === GLASS || m === ICE ? aGas : aSol;
let r = 0, g = 0, b = 0;
if (wy < lightRoof[lx]) { r = skyK; g = skyK * 0.98; b = skyK * 0.94; }
else if (wy < surfY[wx] && ph !== PH_SOLID) { const v = skyK * 0.9; r = v; g = v * 0.98; b = v * 0.95; }
// emission from heat: hottest grain in the block
let T = 0;
const xe = Math.min(W, wx + res), ye = Math.min(H, wy + res);
for (let yy = wy; yy < ye; yy++) { const row = yy * W; for (let xx = wx; xx < xe; xx++) { const tt = temp[row + xx]; if (tt > T) T = tt; } }
if (T > 520) {
const gi = glowIndex(T), e = GLOW_W[gi] * 1.35;
r += GLOW_R[gi] / 255 * e; g += GLOW_G[gi] / 255 * e; b += GLOW_B[gi] / 255 * e;
}
lr[k] = r; lg[k] = g; lb[k] = b;
}
}
// bot lamp
if (bot.lamp) {
const cx = (bot.x + bot.w / 2) / res - lx0, cy = (bot.y + bot.h / 2) / res - ly0, rad = 34 / res;
for (let ly = Math.max(0, Math.floor(cy - rad)); ly < Math.min(lh, cy + rad); ly++)
for (let lx = Math.max(0, Math.floor(cx - rad)); lx < Math.min(lw, cx + rad); lx++) {
const d = Math.hypot(lx - cx, ly - cy) / rad;
if (d >= 1) continue;
const v = (1 - d) * (1 - d) * 1.05, k = ly * lw + lx;
if (lr[k] < v) lr[k] = v; if (lg[k] < v * 0.93) lg[k] = v * 0.93; if (lb[k] < v * 0.8) lb[k] = v * 0.8;
}
}
// propagate: forward sweep (from left/up) then backward sweep (from right/down)
for (let ly = 0; ly < lh; ly++) for (let lx = 0; lx < lw; lx++) {
const k = ly * lw + lx, a = la[k];
let r = lr[k], g = lg[k], b = lb[k];
if (lx > 0) { const j = k - 1; if (lr[j] * a > r) r = lr[j] * a; if (lg[j] * a > g) g = lg[j] * a; if (lb[j] * a > b) b = lb[j] * a; }
if (ly > 0) { const j = k - lw; if (lr[j] * a > r) r = lr[j] * a; if (lg[j] * a > g) g = lg[j] * a; if (lb[j] * a > b) b = lb[j] * a; }
lr[k] = r; lg[k] = g; lb[k] = b;
}
for (let ly = lh - 1; ly >= 0; ly--) for (let lx = lw - 1; lx >= 0; lx--) {
const k = ly * lw + lx, a = la[k];
let r = lr[k], g = lg[k], b = lb[k];
if (lx < lw - 1) { const j = k + 1; if (lr[j] * a > r) r = lr[j] * a; if (lg[j] * a > g) g = lg[j] * a; if (lb[j] * a > b) b = lb[j] * a; }
if (ly < lh - 1) { const j = k + lw; if (lr[j] * a > r) r = lr[j] * a; if (lg[j] * a > g) g = lg[j] * a; if (lb[j] * a > b) b = lb[j] * a; }
lr[k] = r; lg[k] = g; lb[k] = b;
}
// write the light canvas
const fl = SET.ambient, px = R.lightPx, n = lw * lh;
for (let k = 0; k < n; k++) {
let r = lr[k], g = lg[k], b = lb[k];
if (r < fl) r = fl; if (g < fl) g = fl; if (b < fl) b = fl;
r = r >= 1 ? 255 : (r * 255) | 0; g = g >= 1 ? 255 : (g * 255) | 0; b = b >= 1 ? 255 : (b * 255) | 0;
px[k] = 0xff000000 | (b << 16) | (g << 8) | r;
}
if (!R.smoothOn && !R.gpuOn) R.lightX.putImageData(R.lightImg, 0, 0);
}
+1 -3
View File
@@ -5,7 +5,7 @@ const R = {
canvas: null, ctx: null, buf: null, bctx: null, img: null, px: null,
glowC: null, glowX: null, glowImg: null, glowPx: null,
bw: 0, bh: 0, zp: 3, camX: 0, camY: 0, vx0: 0, vy0: 0,
lr: null, lg: null, lb: null, latt: null, lw: 0, lh: 0, lx0: 0, ly0: 0, lres: 1,
lw: 0, lh: 0, lx0: 0, ly0: 0, lres: 1,
frame: 0, lens: false, particles: [],
};
@@ -143,8 +143,6 @@ function initRender(canvas) {
R.bctx = R.buf.getContext('2d');
R.glowC = document.createElement('canvas');
R.glowX = R.glowC.getContext('2d');
R.lightC = document.createElement('canvas');
R.lightX = R.lightC.getContext('2d');
}
function resizeRender() {
-111
View File
@@ -1,111 +0,0 @@
// Smooth slope shading, kept beside the shared CPU geometry in slope-belts.js.
const SM_SLOPE_HELPERS = `
// The same rounded strokes as slopeBeltShape; y/z carry distance along/across the band.
vec3 slopeStroke(vec2 p, vec2 a, vec2 b, float origin) {
vec2 ray = b - a; float len = length(ray); ray /= len;
float along = dot(p - a, ray), across = dot(p - a, vec2(-ray.y, ray.x));
vec2 q = abs(vec2(along - len * 0.5, across)) - vec2(len * 0.5 - 0.75, 1.1);
float sd = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - 0.75;
return vec3(sd, origin + along, across + 2.0);
}
vec3 slopeCorner(vec2 p, vec2 a, float origin) {
vec2 d = p - a; float radius = length(d);
return vec3(radius - 1.85, origin + d.x, 2.0 + d.y);
}
vec3 slopeShape(vec2 p, vec2 worldOrigin, float grade, int joins) {
if ((joins & 64) != 0) p.y -= 0.45;
float sy = grade < 0.0 ? 4.0 : 0.0;
vec2 a = vec2(-grade * 1.30814755, sy + 1.30814755), b = vec2(4.0 - grade * 1.30814755, 4.0 - sy + 1.30814755);
if ((joins & 1) != 0) { a.y = sy + 2.0; a.x = (a.y - sy - 2.61629509) / grade; }
else if ((joins & 4) != 0) a -= vec2(4.0, grade * 4.0);
if ((joins & 2) != 0) { b.y = 6.0 - sy; b.x = (b.y - sy - 2.61629509) / grade; }
else if ((joins & 8) != 0) b += vec2(4.0, grade * 4.0);
vec2 axis = vec2(1.0, grade) * 0.70710678;
vec3 best = slopeStroke(p, a, b, dot(worldOrigin + a, axis));
if ((joins & 1) != 0) {
vec3 h = slopeStroke(p, vec2(-2.0, a.y), a, worldOrigin.x - 2.0);
if (h.x < best.x) best = h;
h = slopeCorner(p, a, worldOrigin.x + a.x);
if (h.x < best.x) best = h;
}
if ((joins & 2) != 0) {
vec3 h = slopeStroke(p, b, vec2(6, b.y), worldOrigin.x + b.x);
if (h.x < best.x) best = h;
h = slopeCorner(p, b, worldOrigin.x + b.x);
if (h.x < best.x) best = h;
}
return best;
}
`;
const SM_SLOPE_DRAW = `
if (nc == 31 || nc == 32) {
vec4 tag = cellAt(tTree, c);
float grade = nc == 31 ? -1.0 : 1.0;
vec2 local = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w);
float sy = grade < 0.0 ? 4.0 : 0.0;
float foot = (local.x + grade * (local.y - sy)) * 0.5;
float depth = (grade * local.x - local.y + sy) * 0.70710678;
int wallFlags = int(tag.a * 255.0 + 0.5);
float ends = max((wallFlags & 8) == 0 ? -foot : -100.0, (wallFlags & 16) == 0 ? foot - 4.0 : -100.0);
float sd = max(ends * 1.41421356, max(-depth, depth - ${CLEAT_HEIGHT.toFixed(1)}));
float phase = float(wallFlags & 7);
float wave = 0.5 + 0.5 * sin((foot + w.x - local.x - phase) * 3.14159265);
vec3 rubber = mix(vec3(0.20,0.22,0.25), vec3(0.25,0.27,0.31), wave);
C = mix(C, rubber, 1.0 - smoothstep(-px, px, sd));
// the ribs, where the simulation has them: rubber bars standing straight out of an uphill belt,
// one every ${CLEAT_SPACING} face cells, moved along by the belt's phase
float dirS = (wallFlags & 32) != 0 ? 1.0 : -1.0;
if (grade * dirS < 0.0) {
float rc = (w.x - local.x) + foot - (grade < 0.0 ? 1.0 : 0.0) - dirS * phase;
float off = (mod(rc + ${(CLEAT_SPACING / 2).toFixed(1)}, ${CLEAT_SPACING.toFixed(1)}) - ${(CLEAT_SPACING / 2).toFixed(1)}) * 1.41421356;
float ribTop = ${((2 * CLEAT_REACH + 1) * Math.SQRT1_2).toFixed(3)};
float rd = length(vec2(off, depth - clamp(depth, 0.0, ribTop))) - 0.5;
vec3 rib = mix(vec3(0.50,0.52,0.56), vec3(0.70,0.72,0.76), smoothstep(ribTop - 1.2, ribTop, depth));
rib *= mix(0.78, 1.0, smoothstep(-0.5, 0.2, off * -dirS * grade));
C = mix(C, rib, 1.0 - smoothstep(-px, px, rd));
}
}
// Slopes use the flat belt's band, rollers and mesh in coordinates along the run.
if ((nc == 2 || (nc >= 23 && nc <= 30)) &&
(int(cellAt(tTree, c).a * 255.0 + 0.5) & 128) != 0 &&
cellAt(tTree, c).r * 255.0 >= 22.5 && cellAt(tTree, c).r * 255.0 <= 30.5) {
int slopeClass = int(cellAt(tTree, c).r * 255.0 + 0.5);
int shape = (slopeClass - 23) / 2;
bool mesh = shape >= 2;
float grade = (shape == 0 || shape == 2) ? -1.0 : 1.0;
float dir = ((slopeClass - 23) % 2) == 1 ? 1.0 : -1.0;
vec4 tag = cellAt(tTree, c);
int joins = int(tag.a * 255.0 + 0.5);
vec2 local = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w);
vec3 shapePos = slopeShape(local, w - local, grade, joins);
float sd = shapePos.x, axis = shapePos.y, depth = shapePos.z;
float time = !mesh && (joins & 16) != 0 ? uTime : 0.0;
vec2 lb = vec2(axis, depth), alongWorld = vec2(axis, depth);
float rg = step(0.72, fract((axis - dir * (depth < 2.0 ? 1.0 : -1.0) * time * 30.0) * 0.5));
vec3 belt;
if (-sd < (mesh ? 1.1 : 0.8)) {
belt = mesh ? mix(vec3(0.22,0.25,0.29), vec3(0.48,0.53,0.59), depth < 2.0 ? 0.85 : 0.35) : bandColor(lb, rg);
belt *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), -sd));
} else if (mesh) {
vec2 cg = abs(fract(alongWorld) - 0.5);
float bw2 = max(px * 0.9, 0.09);
float hole = 1.0 - smoothstep(0.5 - bw2 - px, 0.5 - bw2, max(cg.x, cg.y));
vec3 steel = dot(own.rgb, vec3(1)) > 0.01 ? mix(vec3(0.42, 0.44, 0.5), own.rgb, 0.4) : vec3(0.42, 0.44, 0.5);
belt = mix(steel * 0.78, C * 0.8, hole);
} else {
vec2 dv = vec2(mod(axis, 2.0) - 1.0, depth - 2.0);
float rd = length(dv);
belt = vec3(0.15, 0.16, 0.18);
if (rd < 0.95) {
float ang = atan(dv.y, dv.x) - dir * time * 30.0 / 0.95;
float spoke = smoothstep(0.55, 0.85, cos(ang * 3.0));
belt = mix(vec3(0.5, 0.36, 0.25), vec3(0.72, 0.53, 0.36), 0.5 + 0.5 * dot(normalize(dv + 1e-4), vec2(-0.6, -0.8)) * smoothstep(0.2, 0.95, rd));
belt *= mix(1.0, 0.72, spoke * smoothstep(0.25, 0.4, rd));
belt *= mix(1.0, 0.55, smoothstep(0.82, 0.95, rd));
}
}
C = mix(C, belt, 1.0 - smoothstep(-px, px, sd));
}
`;
-377
View File
@@ -1,377 +0,0 @@
// Smooth shaders: vertex and fragment programs for the rounded world look.
const SM_VS = `#version 300 es
void main() {
vec2 p = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
gl_Position = vec4(p * 2.0 - 1.0, 0.0, 1.0);
}`;
const SM_FS = `#version 300 es
precision highp float;
precision highp int;
uniform sampler2D tCol, tColL, tBack, tTree, tField, tLight, tGlow;
uniform vec2 uSize, uOrigin, uLightSize, uGlowSize;
uniform vec4 uView, uLightMap;
uniform float uGlowK, uTime, uSurf, uRain;
out vec4 o;
float h21(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }
float vn(vec2 p) {
vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);
return mix(mix(h21(i), h21(i + vec2(1, 0)), f.x), mix(h21(i + vec2(0, 1)), h21(i + vec2(1, 1)), f.x), f.y);
}
float fbm(vec2 p) { float a = 0.5, s = 0.0; for (int k = 0; k < 4; k++) { s += a * vn(p); p = p * 2.03 + 17.1; a *= 0.5; } return s / 0.9375; }
vec4 lin(sampler2D t, vec2 q) { return textureLod(t, q / uSize, 0.0); }
// cubic B-spline from four bilinear taps: rounds a field of 0s and 1s into smooth shapes
vec4 bsp(sampler2D t, vec2 q) {
vec2 tc = q - 0.5, i = floor(tc), f = tc - i, f2 = f * f, f3 = f2 * f;
vec2 w0 = (1.0 - 3.0 * f + 3.0 * f2 - f3) / 6.0, w1 = (4.0 - 6.0 * f2 + 3.0 * f3) / 6.0;
vec2 w2 = (1.0 + 3.0 * f + 3.0 * f2 - 3.0 * f3) / 6.0, w3 = f3 / 6.0;
vec2 g0 = w0 + w1, g1 = w2 + w3;
vec2 h0 = (i - 0.5 + w1 / g0) / uSize, h1 = (i + 1.5 + w3 / g1) / uSize;
return g0.y * (g0.x * textureLod(t, h0, 0.0) + g1.x * textureLod(t, vec2(h1.x, h0.y), 0.0))
+ g1.y * (g0.x * textureLod(t, vec2(h0.x, h1.y), 0.0) + g1.x * textureLod(t, h1, 0.0));
}
vec4 cellAt(sampler2D t, ivec2 c) { return texelFetch(t, clamp(c, ivec2(0), ivec2(uSize) - 1), 0); }
int cls(vec4 c) { return int(c.a * 255.0 + 0.5); }
float isT(int n) { return (n == 1 || n == 4 || n == 6 || (n >= 7 && n <= 12)) ? 1.0 : 0.0; }
float isM(int n) { return n == 2 ? 1.0 : isT(n); } // matter: ground, built pieces or loose grains
float isF(int n) { return (n == 13 || n == 14) ? 1.0 : 0.0; }
float isBelt(int n) { return (n == 7 || n == 8) ? 1.0 : 0.0; }
float isN(int n, int k) { return n == k ? 1.0 : 0.0; }
// signed distance (in grains) to the outline of one block of belt: a band 3.7 grains tall; where
// the line of belts ends, the end is square with rounded corners. Negative inside.
float beltSD(vec2 lb, bool endL, bool endR) {
float x0 = endL ? 0.15 : -2.0, x1 = endR ? 3.85 : 6.0, r = 0.75;
vec2 c = vec2((x0 + x1) * 0.5, 2.0), hs = vec2((x1 - x0) * 0.5, 1.85);
vec2 q = abs(lb - c) - hs + r;
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}
// The outline of the belt or filter in cell c. Belts and filters side by side are one belt, so
// the line only rounds off where the whole run ends.
float beltLineSD(ivec2 c, vec2 lb) {
int caps = int(cellAt(tTree, c).a * 255.0 + 0.5);
return beltSD(lb, (caps & 1) != 0, (caps & 2) != 0);
}
// The rubber band of a belt or filter: 1 on a ridge. Ridges travel along the top run one way and
// back along the bottom.
float bandRidge(vec2 w, vec2 lb, float dir) {
float run = lb.y < 2.0 ? 1.0 : -1.0;
return step(0.72, fract((w.x - dir * run * uTime * 30.0) * 0.5));
}
vec3 bandColor(vec2 lb, float rg) { return mix(vec3(0.2, 0.21, 0.24), vec3(0.34, 0.35, 0.39), rg) * (lb.y < 2.0 ? 1.0 : 0.72); }
${SM_SLOPE_HELPERS}
void main() {
float zp = uView.z, px = 1.0 / zp;
vec2 sp = vec2(gl_FragCoord.x, uView.w - gl_FragCoord.y);
vec2 q = sp * px + uView.xy; // position in grains, in the window of grains uploaded
vec2 w = q + uOrigin; // position in the world
vec2 cam = uOrigin + uView.xy; // world position of the screen's top-left corner
vec2 scr = q - uView.xy; // grains from the screen's top-left corner
float shc = uView.w * px; // screen height in grains
ivec2 c = ivec2(floor(q));
vec2 qq = q - 0.5; ivec2 c0 = ivec2(floor(qq)); vec2 t = qq - floor(qq);
vec4 k0 = cellAt(tCol, c0), k1 = cellAt(tCol, c0 + ivec2(1, 0)), k2 = cellAt(tCol, c0 + ivec2(0, 1)), k3 = cellAt(tCol, c0 + ivec2(1, 1));
int n0 = cls(k0), n1 = cls(k1), n2 = cls(k2), n3 = cls(k3);
vec4 own = cellAt(tCol, c); int nc = cls(own);
vec2 s = smoothstep(0.1, 0.9, t);
vec4 wq = vec4((1.0 - s.x) * (1.0 - s.y), s.x * (1.0 - s.y), (1.0 - s.x) * s.y, s.x * s.y);
// fields, and everything that needs screen derivatives (worked out before any branching)
vec4 Fb = bsp(tField, q);
vec4 F = mix(lin(tField, q), Fb, 0.62);
// loose-grain occupancy from the four grains around (bilinear): ground and grains together make "matter"
vec4 wl = vec4((1.0 - t.x) * (1.0 - t.y), t.x * (1.0 - t.y), (1.0 - t.x) * t.y, t.x * t.y);
float pw = dot(wl, vec4(isN(n0, 2), isN(n1, 2), isN(n2, 2), isN(n3, 2)));
float M = clamp(F.r + pw, 0.0, 1.0);
float lf = clamp(mix(F.g, Fb.g, 0.6) + F.r + pw, 0.0, 1.0);
float fwT = max(fwidth(F.r), 1e-4), fwM = max(fwidth(M), 1e-4), fwA = max(fwidth(F.a), 1e-4), fwL = max(fwidth(lf), 1e-4);
vec2 gT = vec2(dFdx(mix(Fb.r, M, 0.5)), dFdy(mix(Fb.r, M, 0.5))), gL = vec2(dFdx(F.g), dFdy(F.g));
// ---- behind everything: sky with clouds and far hills, or the cave's back wall
vec4 back = lin(tBack, q);
vec3 C = back.rgb;
float sky = back.a;
if (sky > 0.01) {
vec3 S = C;
vec2 P0 = scr + cam * 0.08;
// clouds; while it rains they thicken and turn grey
float cl = smoothstep(0.56 - 0.3 * uRain, 0.78 - 0.2 * uRain, fbm(vec2(P0.x * 0.016 + uTime * 0.006, P0.y * 0.05)));
float hi = clamp((uSurf - 40.0 - w.y) / 50.0, 0.0, 1.0);
S = mix(S, mix(vec3(0.96, 0.95, 0.93), vec3(0.5, 0.52, 0.56), uRain), cl * (0.6 + 0.3 * uRain) * hi);
float k1 = 0.16; vec2 P1 = scr + cam * k1;
float base1 = shc * 0.5 + 8.0 + (uSurf - shc * 0.5) * k1;
float top1 = base1 - 36.0 * (0.25 + 0.9 * fbm(vec2(P1.x * 0.009, 1.7)));
float m1 = smoothstep(top1 - px * 1.5, top1 + px * 1.5, P1.y);
vec3 far = mix(vec3(0.50, 0.60, 0.72), S, 0.35) * (0.94 + 0.08 * fbm(P1 * 0.05));
S = mix(S, far, m1);
float k2 = 0.38; vec2 P2 = scr + cam * k2;
float base2 = shc * 0.5 + 14.0 + (uSurf - shc * 0.5) * k2;
float top2 = base2 - 20.0 * (0.2 + 0.9 * fbm(vec2(P2.x * 0.016, 7.3)));
float m2 = smoothstep(top2 - px * 1.5, top2 + px * 1.5, P2.y);
vec3 near = vec3(0.36, 0.5, 0.47) * (0.92 + 0.14 * fbm(P2 * 0.08));
near = mix(near, S, 0.42 - 0.2 * smoothstep(top2, top2 + 30.0, P2.y));
S = mix(S, near, m2);
C = mix(C, S, sky);
}
// the back wall: rough rock, and darker close to the tunnel walls (ambient occlusion)
float notSky = 1.0 - sky;
C *= mix(1.0, 1.08 + 0.4 * fbm(w * 0.21), notSky);
float near = textureLod(tField, q / uSize, 2.2).r;
C *= 1.0 - 0.34 * smoothstep(0.12, 0.85, near) * (1.0 - 0.75 * sky);
// ---- trees (behind the ground and everything on it)
if (F.a > 0.2) {
vec4 a0 = cellAt(tTree, c0), a1 = cellAt(tTree, c0 + ivec2(1, 0)), a2 = cellAt(tTree, c0 + ivec2(0, 1)), a3 = cellAt(tTree, c0 + ivec2(1, 1));
vec4 m = vec4(step(0.4, a0.a), step(0.4, a1.a), step(0.4, a2.a), step(0.4, a3.a)) * wq;
float sm = m.x + m.y + m.z + m.w;
if (sm > 1e-4) {
vec3 tc = (a0.rgb * m.x + a1.rgb * m.y + a2.rgb * m.z + a3.rgb * m.w) / sm;
float leaf = (a0.a * m.x + a1.a * m.y + a2.a * m.z + a3.a * m.w) / sm;
float lv = smoothstep(0.6, 0.9, leaf);
tc *= mix(0.86 + 0.22 * vn(vec2(w.x * 1.4, w.y * 0.25)), 0.72 + 0.5 * fbm(w * 0.42), lv);
float d = (F.a - 0.36) / fwA;
tc *= mix(0.55, 1.0, smoothstep(0.4, 1.6, d));
C = mix(C, tc, smoothstep(-0.7, 0.7, d));
}
}
// ---- smoke, steam and flame: soft, see-through puffs
if (Fb.b > 0.02) {
vec4 m = vec4(isN(n0, 5), isN(n1, 5), isN(n2, 5), isN(n3, 5)) * wq;
float sm = m.x + m.y + m.z + m.w;
vec3 gc = sm > 1e-4 ? (k0.rgb * m.x + k1.rgb * m.y + k2.rgb * m.z + k3.rgb * m.w) / sm : own.rgb;
if (sm > 1e-4 || nc == 5) C = mix(C, gc, smoothstep(0.03, 0.55, Fb.b) * 0.92);
}
// ---- liquids: smooth surfaces that fill right up against the ground, with a glossy top line
float anyL = isN(n0, 3) + isN(n1, 3) + isN(n2, 3) + isN(n3, 3);
if (anyL > 0.0 && lf > 0.3) {
vec4 m = vec4(isN(n0, 3), isN(n1, 3), isN(n2, 3), isN(n3, 3)) * (wq + 0.02);
float sm = m.x + m.y + m.z + m.w;
vec3 lc = (k0.rgb * m.x + k1.rgb * m.y + k2.rgb * m.z + k3.rgb * m.w) / sm;
lc *= 0.94 + 0.1 * vn(w * 0.55 + vec2(uTime * 0.7, uTime * 0.25));
float gl = length(gL);
float topSide = gl > 1e-5 ? clamp(gL.y / gl, 0.0, 1.0) : 0.0;
float band = 1.0 - smoothstep(0.5, 0.75, F.g);
lc += vec3(0.16, 0.18, 0.2) * topSide * band;
C = mix(C, lc, smoothstep(0.5 - fwL, 0.5 + fwL, lf));
}
// ---- the ground: rounded, outlined, with a lit top edge and a shaded underside
if (F.r > 0.3) {
vec4 m = vec4(isT(n0), isT(n1), isT(n2), isT(n3)) * wq;
float sm = m.x + m.y + m.z + m.w;
vec3 blur = bsp(tColL, q).rgb, tc = own.rgb;
float bd = 1e9;
if (m.x > 0.0) { vec3 e = k0.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k0.rgb; } }
if (m.y > 0.0) { vec3 e = k1.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k1.rgb; } }
if (m.z > 0.0) { vec3 e = k2.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k2.rgb; } }
if (m.w > 0.0) { vec3 e = k3.rgb - blur; float dd = dot(e, e); if (dd < bd) { bd = dd; tc = k3.rgb; } }
if (sm < 1e-4) tc = own.rgb;
tc *= 0.84 + 0.3 * fbm(w * 0.28);
tc *= 0.95 + 0.1 * vn(w * 1.6);
float gl = length(gT);
vec2 nrm = gl > 1e-6 ? -gT / gl : vec2(0.0);
float band = 1.0 - smoothstep(0.5, 0.9, M);
float lit = dot(nrm, normalize(vec2(-0.5, -1.0)));
tc *= 1.0 + 0.42 * lit * band;
// Keep the air outline; a softer, screen-width seam also separates rock from loose grains.
float dm = (M - 0.5) / fwM;
tc *= mix(0.4, 1.0, smoothstep(0.5, 1.4 + zp * 0.08, dm));
float dt = (F.r - 0.5) / fwT;
if (nc == 1 && pw > 0.0) tc *= mix(0.65, 1.0, smoothstep(0.0, 1.5, dt));
C = mix(C, tc, smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT));
}
${SM_SLOPE_DRAW}
// ---- loose grains: little round pebbles; a pocket closed in by four grains is filled
int np = int(isN(n0, 2) + isN(n1, 2) + isN(n2, 2) + isN(n3, 2));
bool closed = isM(n0) + isM(n1) + isM(n2) + isM(n3) > 3.5;
if (np > 0) {
float best = 1e9; vec3 gc = vec3(0.0); vec2 gd = vec2(0.0); float gr = 0.6;
for (int k = 0; k < 4; k++) {
ivec2 o4 = ivec2(k & 1, k >> 1);
int nk = k == 0 ? n0 : k == 1 ? n1 : k == 2 ? n2 : n3;
if (nk != 2) continue;
vec4 ck = k == 0 ? k0 : k == 1 ? k1 : k == 2 ? k2 : k3;
vec2 ce = vec2(c0 + o4) + 0.5;
float r = 0.6 + 0.1 * (h21(ce + uOrigin) - 0.5);
vec2 dv = (q - ce) / r;
float dd = length(dv);
if (dd < best) { best = dd; gc = ck.rgb; gd = dv; gr = r; }
}
float cover = np == 4 ? 1.0 : 1.0 - smoothstep(1.0 - px / gr, 1.0 + px / gr, best);
if (closed && F.r < 0.5) cover = 1.0;
// Pebbles stop at the rock face so they cannot paint over its thin seam.
cover *= 1.0 - smoothstep(-0.6, 0.6, (F.r - 0.5) / fwT);
if (cover > 0.0) {
float r2 = min(1.0, dot(gd, gd));
vec3 n3 = normalize(vec3(gd * 0.85, sqrt(1.0 - r2) + 0.25));
float sh = 0.98 + 0.18 * max(0.0, dot(n3, normalize(vec3(-0.45, -0.8, 0.6))));
sh *= mix(1.0, 0.94, smoothstep(0.7 - px * 0.2, 1.15 + px * 0.2, best));
vec3 pc = (np == 4 ? (k0.rgb * wq.x + k1.rgb * wq.y + k2.rgb * wq.z + k3.rgb * wq.w) : gc) * sh;
if (!closed) pc *= mix(1.0, 0.72, smoothstep(0.7, 1.0, best));
C = mix(C, pc, cover);
}
}
// ---- built pieces: crisp, and wall blocks drawn as bevelled tiles with mortar lines
if (nc == 4) C = own.rgb;
else if (nc == 7 || nc == 8) {
// belt: a rubber band round two copper rollers per block; the top run moves the load
float dir = nc == 8 ? 1.0 : -1.0;
vec2 lb = fract(w * 0.25) * 4.0;
// outline: square ends with rounded corners where the line of belts (and filters) ends
float sd = beltLineSD(c, lb);
float inside = 1.0 - smoothstep(-px, px, sd);
if (inside > 0.0) {
float edge = -sd;
vec3 bc;
if (edge < 0.8) {
bc = bandColor(lb, bandRidge(w, lb, dir));
bc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge));
} else {
vec2 rc = vec2(lb.x < 2.0 ? 1.0 : 3.0, 2.0), dv = lb - rc;
float rd = length(dv);
bc = vec3(0.15, 0.16, 0.18);
if (rd < 0.95) {
float ang = atan(dv.y, dv.x) - dir * uTime * 30.0 / 0.95;
float spoke = smoothstep(0.55, 0.85, cos(ang * 3.0));
bc = mix(vec3(0.5, 0.36, 0.25), vec3(0.72, 0.53, 0.36), 0.5 + 0.5 * dot(normalize(dv + 1e-4), vec2(-0.6, -0.8)) * smoothstep(0.2, 0.95, rd));
bc *= mix(1.0, 0.72, spoke * smoothstep(0.25, 0.4, rd));
bc *= mix(1.0, 0.55, smoothstep(0.82, 0.95, rd));
}
}
C = mix(C, bc, inside);
}
}
else if ((nc >= 9 && nc <= 12) || (nc >= 19 && nc <= 22)) {
// screw conveyor: a copper pipe; what is inside shows through, and the screw's flights turn
bool vert = nc == 9 || nc == 11;
float sp2 = nc == 9 ? -1.0 : nc == 10 ? 1.0 : nc == 11 ? 1.0 : -1.0;
float a = (vert ? fract(w.x * 0.25) : fract(w.y * 0.25)) * 4.0, along = vert ? w.y : w.x;
float screwSD = -1.0, capRing = 0.0;
vec4 screwTag = cellAt(tTree, c);
int screwJoins = int(screwTag.a * 255.0 + 0.5);
// Straight bend tags use six bits; background trees use 128/255.
bool shaped = nc >= 19 || (screwJoins > 0 && screwJoins < 64);
if (shaped) {
vec2 d = vec2(1.0, nc == 19 || nc == 21 ? -1.0 : 1.0);
vec4 tag = screwTag;
vec2 local = floor(tag.gb * 255.0 + 0.5) - 8.0 + fract(w) - 2.0;
int joins = screwJoins;
screwSD = 100.0;
for (int end = 0; end < 2; end++) {
int join = (joins >> (end * 3)) & 7;
float side = end == 0 ? -1.0 : 1.0;
bool diagonal = nc >= 19 ? join == 0 || join >= 5 : join != 0;
float grade = nc >= 19 ? d.y : join == 1 ? -1.0 : 1.0;
vec2 ray = diagonal ? (vert ? vec2(side * grade, side) : vec2(side, side * grade)) * 0.70710678 :
nc < 19 ? (vert ? vec2(0, side) : vec2(side, 0)) :
join == 1 ? vec2(0, -1) : join == 2 ? vec2(1, 0) : join == 3 ? vec2(0, 1) : vec2(-1, 0);
float t = dot(local, ray), cross2 = dot(local, vec2(-ray.y, ray.x));
float radius = length(vec2(cross2, min(0.0, t))), cap = t - (diagonal ? 2.82842712 : 2.0);
// A connected mouth meets another opaque pipe; fading its edge leaves a seam.
float sd = max(radius - 2.0, (joins & (64 << end)) != 0 ? cap : -10.0);
if (sd < screwSD) {
screwSD = sd;
a = 2.0 + (side * cross2 < 0.0 ? -radius : radius);
along = (nc >= 19 ? dot(w - local, d) * 0.70710678 : vert ? w.y - local.y : w.x - local.x) + side * t;
capRing = (joins & (64 << end)) != 0 ? smoothstep(-0.55, -0.15, cap) : 0.0;
}
}
if (nc >= 19) sp2 = nc == 19 || nc == 20 ? 1.0 : -1.0;
}
float wall = 0.62;
vec3 tc;
if (a < wall || a > 4.0 - wall || capRing > 0.5) {
float u2 = a < wall ? a / wall : (4.0 - a) / wall; // 0 at the outside, 1 at the bore
float cyl = sin(clamp(u2, 0.0, 1.0) * 1.5708);
tc = vec3(0.62, 0.41, 0.27) * (0.6 + 0.55 * cyl);
if (a < wall) tc *= 1.08;
float ring = 1.0 - smoothstep(0.0, 0.3, min(fract(along * 0.25), 1.0 - fract(along * 0.25)) * 4.0);
if (nc < 19) tc *= 1.0 - 0.3 * ring;
tc *= mix(0.5, 1.0, smoothstep(0.0, max(px * 1.3, 0.1), min(a, 4.0 - a)));
} else {
bool full = own.r + own.g + own.b > 0.0;
float fl = fract((along + a * 0.9 - sp2 * uTime * 14.0) * 0.5);
float flight = smoothstep(0.82, 0.92, fl) * (1.0 - smoothstep(0.92, 1.0, fl));
vec3 bore = vec3(0.11, 0.1, 0.11) * (0.8 + 0.4 * (1.0 - abs(a - 2.0) / 1.4));
tc = full ? own.rgb * (0.9 + 0.1 * vn(w * 1.3)) : bore;
tc = mix(tc, vec3(0.6, 0.48, 0.38), flight * (full ? 0.35 : 0.8));
tc *= 1.0 - 0.25 * smoothstep(0.9, 1.3, abs(a - 2.0));
}
if (shaped) {
bool full = own.r + own.g + own.b > 0.0;
if (full) C = mix(C, own.rgb, 1.0 - smoothstep(0.36, 0.5, length(fract(w) - 0.5)));
C = mix(C, tc, 1.0 - smoothstep(2.0 - px, 2.0 + px, screwSD + 2.0));
} else C = tc;
}
else if (nc == 6 || (nc >= 15 && nc <= 18)) {
vec2 lb = fract(w * 0.25) * 4.0, rb = 4.0 - lb;
float e = min(min(lb.x, lb.y), min(rb.x, rb.y));
vec3 tc = own.rgb * (0.92 + 0.14 * fbm(w * 0.55));
float hl = max(1.0 - smoothstep(0.0, 0.8, lb.x), 1.0 - smoothstep(0.0, 0.8, lb.y));
float shd = max(1.0 - smoothstep(0.0, 0.8, rb.x), 1.0 - smoothstep(0.0, 0.8, rb.y));
tc *= 1.0 + 0.2 * hl - 0.26 * shd;
tc *= mix(0.5, 1.0, smoothstep(px * 0.5, px * 0.5 + max(px * 1.2, 0.14), e));
if (nc >= 15) {
vec2 sl = lb;
if (nc == 16 || nc == 18) sl.x = 4.0 - sl.x;
if (nc >= 17) sl.y = 4.0 - sl.y;
float d = (sl.y - sl.x) * 0.70710678;
tc *= 1.0 + 0.2 * (1.0 - smoothstep(0.0, 0.6, d));
C = mix(C, tc, smoothstep(-px * 0.5, px * 0.5, d));
} else C = tc;
}
else if (nc == 13 || nc == 14) {
// filter: a belt whose middle is a see-through steel mesh. It has the same band as a belt, so a
// row of belts and filters reads as one belt, rounded only where the whole line ends. The
// band is perforated; its colour tag sits on the top run for what rides along, on the
// bottom run for what drops through.
float dir = nc == 14 ? 1.0 : -1.0;
vec2 lb = fract(w * 0.25) * 4.0;
vec4 tag = cellAt(tTree, c);
bool carry = tag.a > 0.16, tinted = tag.r + tag.g + tag.b > 0.0;
float sd = beltLineSD(c, lb);
float inside = 1.0 - smoothstep(-px, px, sd);
if (inside > 0.0) {
float edge = -sd;
vec3 steel = vec3(0.42, 0.44, 0.5);
// through the holes: whatever is behind (water, sky, rock), or what is passing through
vec3 behind = own.r + own.g + own.b > 0.0 ? own.rgb : C * 0.8;
vec2 cg = abs(fract(w) - 0.5);
float bw2 = max(px * 0.9, 0.09);
float hole = 1.0 - smoothstep(0.5 - bw2 - px, 0.5 - bw2, max(cg.x, cg.y));
vec3 fc;
if (edge < 0.8) {
float rg = bandRidge(w, lb, dir);
vec3 band = bandColor(lb, rg);
if (tinted && (carry == (lb.y < 2.0))) band = mix(band, tag.rgb * (lb.y < 2.0 ? 1.0 : 0.8), 0.75 * rg);
fc = mix(band, behind * 0.85, hole * (1.0 - rg) * 0.75);
fc *= mix(0.55, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge));
} else {
fc = mix(steel * 0.78, behind, hole);
fc *= mix(0.7, 1.0, smoothstep(0.0, max(px * 1.4, 0.12), edge - 0.8));
}
C = mix(C, fc, inside);
}
}
// ---- light, then the glow of hot things on top
if (uLightMap.w > 0.5) C *= textureLod(tLight, (q - uLightMap.xy) / uLightMap.z / uLightSize, 0.0).rgb;
if (uGlowK > 0.0) {
vec2 gu = q / (2.0 * uGlowSize), d = 1.0 / uGlowSize;
vec3 g = textureLod(tGlow, gu, 0.0).rgb * 0.25;
g += (textureLod(tGlow, gu + vec2(d.x, 0), 0.0).rgb + textureLod(tGlow, gu - vec2(d.x, 0), 0.0).rgb + textureLod(tGlow, gu + vec2(0, d.y), 0.0).rgb + textureLod(tGlow, gu - vec2(0, d.y), 0.0).rgb) * 0.125;
g += (textureLod(tGlow, gu + d, 0.0).rgb + textureLod(tGlow, gu - d, 0.0).rgb + textureLod(tGlow, gu + vec2(d.x, -d.y), 0.0).rgb + textureLod(tGlow, gu + vec2(-d.x, d.y), 0.0).rgb) * 0.0625;
C += g * uGlowK;
}
o = vec4(C, 1.0);
}`;
-108
View File
@@ -1,108 +0,0 @@
// Smooth look: WebGL setup, texture uploads, drawing and adaptive resolution.
const SM = { tried: false, ok: false, why: '', gl: null, canvas: null, prog: null, tex: {}, u: {}, t0: performance.now(), white: null, surf: -1, scale: 1, ms: 0, n: 0 };
function smCompile(gl, type, src) {
const sh = gl.createShader(type);
gl.shaderSource(sh, src); gl.compileShader(sh);
if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(sh));
return sh;
}
// Is the smooth look possible here (WebGL2)? Set up once, on first use.
function smoothAvailable() {
if (SM.tried) return SM.ok;
SM.tried = true;
try {
const cv = document.createElement('canvas');
const gl = cv.getContext('webgl2', { alpha: false, antialias: false, depth: false, stencil: false, preserveDrawingBuffer: true, premultipliedAlpha: false });
if (!gl) { SM.why = 'nogl'; throw new Error('WebGL2 is not available'); }
// a browser drawing without the graphics card would crawl: keep the pixel look there
const dbg = gl.getExtension('WEBGL_debug_renderer_info');
const rend = dbg ? String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL)) : '';
if (/swiftshader|llvmpipe|softpipe|software/i.test(rend) && !window.__allowSoftwareGL) { SM.why = 'software'; throw new Error('software rendering: ' + rend); }
const prog = gl.createProgram();
gl.attachShader(prog, smCompile(gl, gl.VERTEX_SHADER, SM_VS));
gl.attachShader(prog, smCompile(gl, gl.FRAGMENT_SHADER, SM_FS));
gl.linkProgram(prog);
if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) throw new Error(gl.getProgramInfoLog(prog));
gl.useProgram(prog);
for (const n of ['uSize', 'uOrigin', 'uLightSize', 'uGlowSize', 'uView', 'uLightMap', 'uGlowK', 'uTime', 'uSurf', 'uRain']) SM.u[n] = gl.getUniformLocation(prog, n);
// texture unit for each sampler; linear ones are sampled between grains
const texs = [['tCol', false, false], ['tColL', true, false], ['tBack', true, false], ['tTree', false, false], ['tField', true, true], ['tLight', true, false], ['tGlow', true, false]];
texs.forEach(([name, linear, mip], unit) => {
const t = gl.createTexture();
gl.activeTexture(gl.TEXTURE0 + unit); gl.bindTexture(gl.TEXTURE_2D, t);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, mip ? gl.LINEAR_MIPMAP_LINEAR : linear ? gl.LINEAR : gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, linear ? gl.LINEAR : gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.uniform1i(gl.getUniformLocation(prog, name), unit);
SM.tex[name] = { t, unit, mip, w: 0, h: 0 };
});
gl.bindVertexArray(gl.createVertexArray());
SM.canvas = cv; SM.gl = gl; SM.prog = prog; SM.ok = true;
SM.white = new Uint8Array([255, 255, 255, 255]);
cv.addEventListener('webglcontextlost', e => { e.preventDefault(); SM.ok = false; });
} catch (e) {
console.warn('Smooth look unavailable, using pixels:', e && e.message);
SM.ok = false;
}
return SM.ok;
}
function smUpload(name, w, h, data) {
const gl = SM.gl, T = SM.tex[name];
gl.activeTexture(gl.TEXTURE0 + T.unit); gl.bindTexture(gl.TEXTURE_2D, T.t);
if (T.w !== w || T.h !== h) { gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, data); T.w = w; T.h = h; }
else gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, data);
if (T.mip) gl.generateMipmap(gl.TEXTURE_2D);
}
const smBytes = a => new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
// Both renderers share the same cached parallax horizon.
function smoothSurface() {
if (SM.surf < 0 || SM.surfSeed !== worldSeed || SM.surfW !== W) { SM.surf = meanSurface(); SM.surfW = W; SM.surfSeed = worldSeed; }
return SM.surf;
}
function smoothDraw(lighting, glowOn) {
const t0 = performance.now();
// the GPU pass can run below full resolution on a slow graphics card; it is scaled up smoothly
const gl = SM.gl, cv = SM.canvas, sc = SM.scale, cw = Math.max(1, Math.round(R.canvas.width * sc)), ch = Math.max(1, Math.round(R.canvas.height * sc)), u = SM.u;
if (cv.width !== cw || cv.height !== ch) { cv.width = cw; cv.height = ch; }
const surf = smoothSurface();
gl.viewport(0, 0, cw, ch);
gl.useProgram(SM.prog);
const bw = R.bw, bh = R.bh;
const uploadStart = PERF_MARKS ? performance.now() : 0;
smUpload('tCol', bw, bh, smBytes(R.px));
smUpload('tColL', bw, bh, smBytes(R.px));
smUpload('tBack', bw, bh, smBytes(R.backPx));
smUpload('tTree', bw, bh, smBytes(R.treePx));
smUpload('tField', bw, bh, smBytes(R.fieldPx));
if (lighting && R.lightImg) smUpload('tLight', R.lw, R.lh, smBytes(R.lightImg.data)); else smUpload('tLight', 1, 1, SM.white);
if (glowOn) smUpload('tGlow', R.glowC.width, R.glowC.height, smBytes(R.glowImg.data)); else smUpload('tGlow', 1, 1, SM.white);
const blitStart = PERF_MARKS ? performance.now() : 0;
if (PERF_MARKS) { perfMarks.upload = blitStart - uploadStart; perfMarks.prepare += uploadStart - t0; }
gl.uniform2f(u.uSize, bw, bh);
gl.uniform2f(u.uOrigin, R.vx0, R.vy0);
gl.uniform4f(u.uView, R.camX - R.vx0, R.camY - R.vy0, R.zp * (ch / R.canvas.height), ch);
gl.uniform4f(u.uLightMap, lighting ? R.lx0 * R.lres - R.vx0 : 0, lighting ? R.ly0 * R.lres - R.vy0 : 0, R.lres || 1, lighting ? 1 : 0);
gl.uniform2f(u.uLightSize, R.lw || 1, R.lh || 1);
gl.uniform2f(u.uGlowSize, R.glowC.width || 1, R.glowC.height || 1);
gl.uniform1f(u.uGlowK, glowOn ? (SET.glow === 'high' ? 0.9 : 0.55) : 0);
gl.uniform1f(u.uTime, (performance.now() - SM.t0) / 1000);
gl.uniform1f(u.uSurf, surf);
gl.uniform1f(u.uRain, R.rain || 0);
gl.drawArrays(gl.TRIANGLES, 0, 3);
const ctx = R.ctx;
ctx.imageSmoothingEnabled = true;
ctx.drawImage(cv, 0, 0, cw, ch, 0, 0, R.canvas.width, R.canvas.height);
if (PERF_MARKS) perfMarks.blit = performance.now() - blitStart;
smoothPace(performance.now() - t0);
}
// Keep the frame rate up: drop the GPU pass's resolution when it is slow, raise it again when there
// is room, and if even half resolution is too slow, give up on the smooth look for this session.
function smoothPace(ms) {
SM.ms = SM.n ? SM.ms * 0.8 + ms * 0.2 : ms;
if (++SM.n < 15) return;
SM.n = 1;
if (SM.ms > 40 && SM.scale <= 0.5) { SM.ok = false; SM.why = 'slow'; return; }
if (SM.ms > 18 && SM.scale > 0.5) SM.scale = Math.max(0.5, SM.scale - (SM.ms > 60 ? 0.5 : 0.25));
else if (SM.ms < 6 && SM.scale < 1) SM.scale = Math.min(1, SM.scale + 0.25);
}
+7 -46
View File
@@ -5,18 +5,16 @@ function fogColor(c, k) { return (c & 0xff000000) | (((c >>> 16 & 255) * k) << 1
function drawWorld() {
const perfStart = (PERF_MARKS || SET.showPerf) ? performance.now() : 0;
if (PERF_MARKS || SET.showPerf) { perfMarks.light = 0; perfMarks.override = 0; delete perfMarks.gpu; }
const wasGpu = R.gpuOn;
R.gpuOn = SET.renderer === 'webgpu' && GPU.ok;
if (R.gpuOn && !wasGpu) GPU.lightFrame = undefined;
GPU.canvas.hidden = !R.gpuOn;
if (R.gpuOn) R.ctx.clearRect(0, 0, R.canvas.width, R.canvas.height);
if (!GPU.ok) return;
R.gpuOn = true; GPU.canvas.hidden = false;
R.ctx.clearRect(0, 0, R.canvas.width, R.canvas.height);
const bw = R.bw, bh = R.bh;
R.vx0 = Math.floor(R.camX); R.vy0 = Math.floor(R.camY);
const vx0 = R.vx0, vy0 = R.vy0;
// how hard it is raining around the camera, eased so the sky greys over gently
R.rain = (R.rain || 0) * 0.98 + rainAt(Math.max(0, Math.min(W - 1, Math.floor(R.camX + bw / 2)))) * 0.02;
// Smooth inputs carry a class in alpha; only exceptional cells use the CPU in WebGPU mode.
const sm = R.smoothOn = SET.look === 'smooth' && !R.lens && (R.gpuOn || smoothAvailable());
const sm = R.smoothOn = SET.look === 'smooth' && !R.lens;
const mmask = R.moldMask;
if (sm) {
mmask.fill(0);
@@ -34,52 +32,15 @@ function drawWorld() {
}
prepareSlopeBelts(vx0, vy0, vx0 + bw, vy0 + bh);
const lighting = SET.lighting !== 'off' && !R.lens;
const lightUpdate = R.gpuOn && lighting ? gpuLightGrid() : false;
if (!R.gpuOn && lighting && (R.frame % SET.lightEvery === 0 || !R.lw || wasGpu)) {
const lightStart = PERF_MARKS ? performance.now() : 0;
computeLight();
if (PERF_MARKS) perfMarks.light = performance.now() - lightStart;
}
const lightUpdate = lighting ? gpuLightGrid() : false;
const colorStart = (PERF_MARKS || SET.showPerf) ? performance.now() : 0;
const glowOn = SET.glow !== 'off' && !R.lens;
if (glowOn) R.glowPx.fill(0);
if (R.gpuOn) gpuOverrides();
else for (let by = 0; by < bh; by++) for (let bx = 0; bx < bw; bx++) cellPixel(vx0 + bx, vy0 + by);
gpuOverrides();
worldObjects();
if (PERF_MARKS || SET.showPerf) {
perfMarks.color = performance.now() - colorStart;
perfMarks.prepare = colorStart - perfStart - perfMarks.light;
}
if (R.gpuOn) { gpuDraw(lighting, glowOn, lightUpdate); return; }
if (sm) { smoothDraw(lighting, glowOn); return; }
const uploadStart = PERF_MARKS ? performance.now() : 0;
R.bctx.putImageData(R.img, 0, 0);
const blitStart = PERF_MARKS ? performance.now() : 0;
if (PERF_MARKS) perfMarks.upload = blitStart - uploadStart;
const ctx = R.ctx, zp = R.zp;
ctx.imageSmoothingEnabled = false;
const ox = -(R.camX - vx0) * zp, oy = -(R.camY - vy0) * zp;
ctx.drawImage(R.buf, 0, 0, bw, bh, Math.round(ox), Math.round(oy), bw * zp, bh * zp);
if (lighting) {
const res = R.lres;
ctx.save();
ctx.globalCompositeOperation = 'multiply';
ctx.imageSmoothingEnabled = true;
const lx = (R.lx0 * res - R.camX) * zp, ly = (R.ly0 * res - R.camY) * zp;
ctx.drawImage(R.lightC, 0, 0, R.lw, R.lh, lx, ly, R.lw * res * zp, R.lh * res * zp);
ctx.restore();
}
if (glowOn) {
R.glowX.putImageData(R.glowImg, 0, 0);
ctx.save();
ctx.globalCompositeOperation = 'lighter';
ctx.globalAlpha = SET.glow === 'high' ? 0.9 : 0.5;
ctx.imageSmoothingEnabled = true;
const blur = Math.max(2, zp * (SET.glow === 'high' ? 3.2 : 2.2));
try { ctx.filter = `blur(${blur.toFixed(1)}px)`; } catch (e) { /* no filter support */ }
ctx.drawImage(R.glowC, 0, 0, R.glowC.width, R.glowC.height, Math.round(ox), Math.round(oy), R.glowC.width * 2 * zp, R.glowC.height * 2 * zp);
ctx.filter = 'none';
ctx.restore();
}
if (PERF_MARKS) perfMarks.blit = performance.now() - blitStart;
gpuDraw(lighting, glowOn, lightUpdate);
}