Skip to content

Instantly share code, notes, and snippets.

@greggman
Created November 5, 2024 22:13
Show Gist options
  • Save greggman/e60d7e7967962f9b3119a0fd3d622f9a to your computer and use it in GitHub Desktop.
Save greggman/e60d7e7967962f9b3119a0fd3d622f9a to your computer and use it in GitHub Desktop.
WebGPU: textureGather from r16uint cubemap (filtering fixed)

WebGPU: textureGather from r16uint cubemap (filtering fixed)

view on jsgist

:root { color-scheme: light dark; }
pre { margin: 0;}
/*bug-in-github-api-content-can-not-be-empty*/
async function test(forceFallbackAdapter) {
const adapter = await navigator.gpu.requestAdapter({forceFallbackAdapter});
const device = await adapter.requestDevice();
log(adapter.info?.vendor, adapter.info?.architecture);
const texture = device.createTexture({
format: 'r16uint',
size: [8, 8, 6],
textureBindingViewDimension: 'cube',
usage: GPUTextureUsage.COPY_DST |
GPUTextureUsage.TEXTURE_BINDING,
mipLevelCount: 3,
});
const uvw = [-0.7084864819326223, 0.705718956612574, 0.002767525320049306];
log('cube coord :', uvw.join(', '));
const uvwAs3D = convertCubeCoordToNormalized3DTextureCoord(uvw);
log('3d coord :', uvwAs3D.join(', '));
const t = convertNormalized3DTextureCoordinateToTexelAndLayerCoordinate(
texture, 0, uvwAs3D);
const faceNdx = Math.floor(t[2]);
const kFaceNames = ['+x', '-x', '+y', '-y', '+z', '-z'];
log('texel coord:', t.slice(0, 2).join(', '), ' layer:', faceNdx, 'face:', kFaceNames[faceNdx]);
const data = new Uint16Array(texture.width * texture.height * texture.depthOrArrayLayers * 2);
for (let z = 0; z < texture.depthOrArrayLayers; ++z) {
for (let y = 0; y < texture.height; ++y) {
for (let x = 0; x < texture.width; ++x) {
const offset = (z * texture.width * texture.height + y * texture.width + x);
data[offset ] = ((y << 6) + x + (z << 12));
}
}
}
device.queue.writeTexture(
{texture},
data,
{ bytesPerRow: texture.width * 2, rowsPerImage: texture.height },
[texture.width, texture.height, texture.depthOrArrayLayers],
);
const sampler = device.createSampler({
});
const code = `
struct Args0 {
@align(16) coords : vec3f
}
struct Data {
args0 : array<Args0, 1>,
}
@vertex
fn vs_main(@builtin(vertex_index) vertex_index : u32) -> @builtin(position) vec4f {
let positions = array(
vec4f(-1, 1, 0, 1), vec4f( 1, 1, 0, 1),
vec4f(-1, -1, 0, 1), vec4f( 1, -1, 0, 1),
);
return positions[vertex_index];
}
@group(0) @binding(0) var T : texture_cube<u32>;
@group(0) @binding(1) var S : sampler;
@group(0) @binding(2) var<storage> data : Data;
//@group(0) @binding(3) var<storage, read_write> check : array<vec4f>;
@fragment
fn fs_main(@builtin(position) frag_pos : vec4f) -> @location(0) vec4<u32> {
let frag_idx = u32(frag_pos.x) + u32(frag_pos.y) * 256;
var result : vec4u;
{
let is_active = (frag_idx >= 0) & (frag_idx < 1);
let args = data.args0[frag_idx - 0];
let call = textureGather(/* component */ 0, T, S, args.coords);
result = select(result, call, is_active);
//check[frag_idx] = vec4f(args.coords, 0);
}
return vec4<u32>(result);
}
`;
const module = device.createShaderModule({code});
const bgl = device.createBindGroupLayout( {
entries: [
{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
texture: {
sampleType: "uint",
viewDimension: "cube",
multisampled: false,
},
},
{
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
sampler: {
type: "non-filtering",
},
},
{
binding: 2,
visibility: GPUShaderStage.FRAGMENT,
buffer: {
type: "read-only-storage",
hasDynamicOffset: false,
minBindingSize: 16,
},
},
],
});
const pipeline = device.createRenderPipeline({
layout: device.createPipelineLayout({bindGroupLayouts: [bgl]}),
vertex: { module },
fragment: { module, targets: [{format: 'rgba32uint'}] },
primitive: { topology: 'triangle-strip' },
});
const numResults = uvw.length / 2;
const uniformBuffer = device.createBuffer({
size: numResults * 16,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST,
});
device.queue.writeBuffer(
uniformBuffer,
0,
new Float32Array(uvw),
);
const height = Math.ceil(numResults / 256);
const target = device.createTexture({
size: [256, height, 1],
format: 'rgba32uint',
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
});
const resultBuffer = device.createBuffer({
size: 256 * 4 * 4 * height,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
const checkBuffer = device.createBuffer({
size: (numResults + 1) * 4 * 4,
usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_SRC,
});
const checkMapBuffer = device.createBuffer({
size: checkBuffer.size,
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST,
});
const bindGroup = device.createBindGroup({
layout: pipeline.getBindGroupLayout(0),
entries: [
{ binding: 0, resource: texture.createView({dimension: 'cube'}) },
{ binding: 1, resource: sampler },
{ binding: 2, resource: { buffer: uniformBuffer }},
// { binding: 3, resource: { buffer: checkBuffer }},
],
});
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [
{
view: target.createView(),
loadOp: 'clear',
storeOp: 'store',
},
],
});
pass.setPipeline(pipeline);
pass.setBindGroup(0, bindGroup);
pass.draw(4);
pass.end();
encoder.copyTextureToBuffer(
{ texture: target },
{ buffer: resultBuffer, bytesPerRow: 256 * 4 * 4 },
target,
);
encoder.copyBufferToBuffer(checkBuffer, 0, checkMapBuffer, 0, checkMapBuffer.size);
device.queue.submit([encoder.finish()]);
//await checkMapBuffer.mapAsync(GPUMapMode.READ);
//const check = new Float32Array(checkMapBuffer.getMappedRange());
//for (let i = 0; i < numResults; i += 4) {
// log(`check[${i}] = ${[...check.subarray(i, i + 4)].map(v => v).join(',')}`);
//}
await resultBuffer.mapAsync(GPUMapMode.READ);
const result = new Uint32Array(resultBuffer.getMappedRange());
const format = v => `F${v >> 12}:X${v & 0x3F}:Y${(v >> 6) & 0x3F}`;
for (let i = 0; i < numResults; i += 4) {
log(`result[${i}] = ${[...result.subarray(i, i + 4)].map(v => v).join(',')}`);
}
[...'R'].forEach((c, i) => {
log(c, 'gather =', [...result.subarray(i * 4, i * 4 + 4)].map(format).join(', '));
});
function show(result) {
const m = new Map([...result].map((v, i) =>
[ `${v & 0x3F},${(v >> 6) & 0x3F}`, {c: v >> 12, i} ],
));
console.log(m);
const l1 = ['+'];
for (let x = 0; x < texture.width; ++x) {
l1.push('----+');
};
const sep = l1.join('');
const labels = 'RGBA';
for (let y = 0; y < texture.height; ++y) {
const l2 = ['|'];
for (let x = 0; x < texture.width; ++x) {
const f = m.get(`${x},${y}`);
l2.push(` ${f ? `${labels[f.i]}${f.c}` : ' '} |`);
}
log(sep);
log(l2.join(''));
}
log(sep);
}
for (let i = 0; i < 1; ++i) {
show(result.subarray(i * 4, i * 4 + 4));
}
}
await test(false);
//await test(true);
function log(...args) {
const elem = document.createElement('pre');
elem.textContent = args.join(' ');
document.body.appendChild(elem);
}
function assert(c, msg) {
if (!c) {
throw new Error(msg);
}
}
function normalize(v/*: vec3*/)/*: vec3*/ {
const length = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
assert(length > 0);
return v.map(v => v / length)/* as vec3*/;
}
function convertCubeCoordToNormalized3DTextureCoord(v/*: vec3*/)/*: vec3*/ {
let uvw;
let layer;
// normalize the coord.
// MAINTENANCE_TODO: handle(0, 0, 0)
const r = normalize(v);
const absR = r.map(v => Math.abs(v));
if (absR[0] > absR[1] && absR[0] > absR[2]) {
// x major
const negX = r[0] < 0.0 ? 1 : 0;
uvw = [negX ? r[2] : -r[2], -r[1], absR[0]];
layer = negX;
} else if (absR[1] > absR[2]) {
// y major
const negY = r[1] < 0.0 ? 1 : 0;
uvw = [r[0], negY ? -r[2] : r[2], absR[1]];
layer = 2 + negY;
} else {
// z major
const negZ = r[2] < 0.0 ? 1 : 0;
uvw = [negZ ? -r[0] : r[0], -r[1], absR[2]];
layer = 4 + negZ;
}
return [(uvw[0] / uvw[2] + 1) * 0.5, (uvw[1] / uvw[2] + 1) * 0.5, (layer + 0.5) / 6];
}
function reifyExtent3D(
val/*: Readonly<GPUExtent3DDict> | Iterable<number>*/
)/*: Required<GPUExtent3DDict>*/ {
if (Symbol.iterator in val) {
const v = Array.from(val);
return {
width: (v[0] ?? 1) | 0,
height: (v[1] ?? 1) | 0,
depthOrArrayLayers: (v[2] ?? 1) | 0,
};
} else {
const v = val;
return {
width: (v.width ?? 1) | 0,
height: (v.height ?? 1) | 0,
depthOrArrayLayers: (v.depthOrArrayLayers ?? 1) | 0,
};
}
}
function virtualMipSize(
dimension/*: GPUTextureDimension*/,
size/*: GPUExtent3D*/,
mipLevel/*: number*/
)/*: [number, number, number]*/ {
const { width, height, depthOrArrayLayers } = reifyExtent3D(size);
const shiftMinOne = (n/*: number*/) => Math.max(1, n >> mipLevel);
switch (dimension) {
case '1d':
return [shiftMinOne(width), height, depthOrArrayLayers];
case '2d':
return [shiftMinOne(width), shiftMinOne(height), depthOrArrayLayers];
case '3d':
return [shiftMinOne(width), shiftMinOne(height), shiftMinOne(depthOrArrayLayers)];
default:
unreachable();
}
}
function convertNormalized3DTextureCoordinateToTexelAndLayerCoordinate(texture, mipLevel, coord) {
const mipSize = virtualMipSize(
texture.dimension,
texture,
mipLevel
);
return coord.map((v, i) => (v * mipSize[i]).toFixed(3));
}
{"name":"WebGPU: textureGather from r16uint cubemap (filtering fixed)","settings":{},"filenames":["index.html","index.css","index.js"]}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment