{
const context = html`<canvas width=400 height=400>`.getContext('webgpu');
const adapter = await navigator.gpu?.requestAdapter();
const device = await adapter?.requestDevice();
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({device, format});
const code = `
struct Time {
frame: u32,
elapsed : f32
}
@group(0) @binding(0) var<uniform> time : Time;
@group(3) @binding(0) var screen : texture_storage_2d<rgba16float, write>;
//==========================================================================================
// hashes (low quality, do NOT use in production)
//==========================================================================================
fn hash1(p: float2) -> float
{
let p2 = 50.0*fract( p*0.3183099 );
return fract( p2.x*p2.y*(p2.x+p2.y) );
}
fn noise(x: float2 ) -> float
{
let p = floor(x);
let w = fract(x);
let u = w * w * (3.0 - 2.0 * w);
let a = hash1(p+vec2(0.,0.));
let b = hash1(p+vec2(1.,0.));
let c = hash1(p+vec2(0.,1.));
let d = hash1(p+vec2(1.,1.));
return -1.0 + 2.0 * (a + (b-a) * u.x + (c-a) * u.y + (a - b - c + d) * u.x * u.y);
}
//------------------------------------------------------------------------------------------
// global
//------------------------------------------------------------------------------------------
const kMaxTreeHeight = 4.8;
const kMaxHeight = 840.0;
//------------------------------------------------------------------------------------------
// terrain
//------------------------------------------------------------------------------------------
fn terrainMap(p: float2) -> float
{
var e = noise( p/2000.0 + vec2(1.0,-2.0) );
// let a = 1.0-smoothstep( 0.12, 0.13, abs(e+0.12) ); // flag high-slope areas (-0.25, 0.0)
e = 600.0*e + 600.0;
// cliff
e += 90.0*smoothstep( 552.0, 594.0, e );
//e += 90.0*smoothstep( 550.0, 600.0, e );
// return vec2(e,a);
return e;
}
// ro is ray origin, rd is ray direction
fn raymarchTerrain(ro: float3, rd: float3, tmin: float, tmax: float) -> float2
{
// bounding plane
let tp = (kMaxHeight+kMaxTreeHeight-ro.y)/rd.y;
var tmax2 = tmax;
if( tp>0.0 ) { tmax2 = min( tmax, tp ); };
// raymarch
var dis: float;
var th: float;
var t2 = -1.0;
var t = tmin;
var ot = t;
var odis = 0.0;
var odis2 = 0.0;
for(var i=0; i<400; i++ )
{
th = 0.001*t;
let pos = ro + t*rd;
let env = terrainMap( pos.xz );
// for now use a terrainMap that only returns height
// float hei = env.x;
let hei = env;
// tree envelope
let dis2 = pos.y - (hei+kMaxTreeHeight*1.1);
if( dis2<th )
{
if( t2<0.0 )
{
t2 = ot + (th-odis2)*(t-ot)/(dis2-odis2); // linear interpolation for better accuracy
}
}
odis2 = dis2;
// terrain
dis = pos.y - hei;
if( dis<th ) { break; };
ot = t;
odis = dis;
// ignore this for now
// t += dis*0.8*(1.0-0.75*env.y); // slow down in step areas
if( t>tmax2 ) { break; };
}
if( t>tmax2 ) { t = -1.0; }
else { t = ot + (th-odis)*(t-ot)/(dis-odis); }; // linear interpolation for better accuracy
return vec2(t,t2);
}
@compute @workgroup_size(16, 16)
fn main_image(@builtin(global_invocation_id) id: uint3) {
let screen_size = uint2(textureDimensions(screen));
if (id.x >= screen_size.x || id.y >= screen_size.y) { return; }
let fragCoord = float2(id.xy) + .5;
let resolution = float2(screen_size);
let p = fragCoord / resolution.x;
var ignore = time.elapsed / time.elapsed; // to avoid rewriting run without the time bindings.
var uv = p * float2(resolution.x / resolution.y, 1.) + ignore;
var f = noise( 8.0*uv );
// camera
let ro = vec3(0.0, 401.5, 6.0);
let rd = normalize(vec3(uv, 1.));
//----------------------------------
// raycast terrain and tree envelope
//----------------------------------
const tmax = 2000.0;
let col = raymarchTerrain( ro, rd, 15.0, tmax );
textureStore(screen, int2(id.xy), float4(f,f,f, 1.));
}
`;
const module = device.createShaderModule({code});
const pipeline = device.createRenderPipeline({
layout: 'auto',
vertex: {
module,
entryPoint: 'vertexMain',
buffers: [{
arrayStride: 4 * 4,
attributes: [{shaderLocation: 0, offset: 0, format: 'float32x4'}]
}]
},
fragment: {
module,
entryPoint: 'main_image',
targets: [{format}]
},
primitive: {topology: 'triangle-strip'}
});
const uniformValues = new Float32Array(1);
const uniformBuffer = device.createBuffer({size: uniformValues.byteLength, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [{binding: 0, resource: {buffer: uniformBuffer}}]
});
const vertexData = Float32Array.of(
-0.7, 0.7, 0, 1,
-0.7, -0.7, 0, 0,
0.7, 0.7, 1, 1,
0.7, -0.7, 1, 0
);
const vertexBuffer = device.createBuffer({size: vertexData.byteLength, usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST});
device.queue.writeBuffer(vertexBuffer, 0, vertexData);
let start = performance.now();
(function render(time) {
uniformValues[0] = (time - start) / 1000;
device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
const encoder = device.createCommandEncoder();
const output = {
clearValue: [1, 1, 1, 1],
loadOp: 'clear',
storeOp: 'store',
view: context.getCurrentTexture().createView()
};
const pass = encoder.beginRenderPass({colorAttachments: [output]});
pass.setPipeline(pipeline);
pass.setVertexBuffer(0, vertexBuffer);
pass.setBindGroup(0, bindGroup);
pass.draw(4);
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(render);
})(start);
return context.canvas;
}