Created
November 5, 2024 22:13
-
-
Save greggman/e60d7e7967962f9b3119a0fd3d622f9a to your computer and use it in GitHub Desktop.
WebGPU: textureGather from r16uint cubemap (filtering fixed)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
:root { color-scheme: light dark; } | |
pre { margin: 0;} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*bug-in-github-api-content-can-not-be-empty*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{"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