Skip to content

Commit fe8a2ce

Browse files
authored
Add generate mipmap sample (#497)
We've got hello triangle. Generating mipmaps seems like a pretty common feature. Maybe a sample for it is good?
1 parent 8a7776c commit fe8a2ce

File tree

8 files changed

+752
-0
lines changed

8 files changed

+752
-0
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import generateMipmapWGSL from './generateMipmap.wgsl';
2+
3+
export function numMipLevels(...sizes: number[]) {
4+
const maxSize = Math.max(...sizes);
5+
return (1 + Math.log2(maxSize)) | 0;
6+
}
7+
8+
/**
9+
* Get the default viewDimension
10+
* Note: It's only a guess. The user needs to tell us to be
11+
* correct in all cases because we can't distinguish between
12+
* a 2d texture and a 2d-array texture with 1 layer, nor can
13+
* we distinguish between a 2d-array texture with 6 layers and
14+
* a cubemap.
15+
*/
16+
export function getDefaultViewDimensionForTexture(
17+
dimension: GPUTextureDimension,
18+
depthOrArrayLayers: number
19+
) {
20+
switch (dimension) {
21+
case '1d':
22+
return '1d';
23+
default:
24+
case '2d':
25+
return depthOrArrayLayers > 1 ? '2d-array' : '2d';
26+
case '3d':
27+
return '3d';
28+
}
29+
}
30+
31+
type DeviceSpecificInfo = {
32+
sampler: GPUSampler;
33+
module: GPUShaderModule;
34+
pipelineByFormatAndView: Map<string, GPURenderPipeline>;
35+
};
36+
37+
function createDeviceSpecificInfo(device: GPUDevice): DeviceSpecificInfo {
38+
const module = device.createShaderModule({
39+
label: 'textured quad shaders for mip level generation',
40+
code: generateMipmapWGSL,
41+
});
42+
43+
const sampler = device.createSampler({
44+
minFilter: 'linear',
45+
magFilter: 'linear',
46+
});
47+
48+
return {
49+
module,
50+
sampler,
51+
pipelineByFormatAndView: new Map(),
52+
};
53+
}
54+
55+
const s_deviceToDeviceSpecificInfo = new WeakMap<
56+
GPUDevice,
57+
DeviceSpecificInfo
58+
>();
59+
60+
/**
61+
* Generates mip levels 1 to texture.mipLevelCount - 1 using
62+
* mip level 0 as the source.
63+
* @param device The device
64+
* @param texture The texture to generate mips for
65+
* @param textureBindingViewDimension The view dimension, needed for
66+
* compatibility mode if the texture is a cube map OR if the texture
67+
* is a 1 layer 2d-array.
68+
*/
69+
export function generateMips(
70+
device: GPUDevice,
71+
texture: GPUTexture,
72+
textureBindingViewDimension?: GPUTextureViewDimension
73+
) {
74+
textureBindingViewDimension =
75+
textureBindingViewDimension ??
76+
getDefaultViewDimensionForTexture(
77+
texture.dimension,
78+
texture.depthOrArrayLayers
79+
);
80+
const deviceSpecificInfo =
81+
s_deviceToDeviceSpecificInfo.get(device) ??
82+
createDeviceSpecificInfo(device);
83+
s_deviceToDeviceSpecificInfo.set(device, deviceSpecificInfo);
84+
const { sampler, module, pipelineByFormatAndView } = deviceSpecificInfo;
85+
86+
const id = `${texture.format}.${textureBindingViewDimension}`;
87+
88+
if (!pipelineByFormatAndView.get(id)) {
89+
// Choose an fragment shader based on the viewDimension (removes the '-' from 2d-array and cube-array)
90+
const entryPoint = `fs${textureBindingViewDimension.replace('-', '')}`;
91+
pipelineByFormatAndView.set(
92+
id,
93+
device.createRenderPipeline({
94+
label: `mip level generator pipeline for ${textureBindingViewDimension}, format: ${texture.format}`,
95+
layout: 'auto',
96+
vertex: {
97+
module,
98+
},
99+
fragment: {
100+
module,
101+
entryPoint,
102+
targets: [{ format: texture.format }],
103+
},
104+
})
105+
);
106+
}
107+
108+
const pipeline = pipelineByFormatAndView.get(id);
109+
const encoder = device.createCommandEncoder({
110+
label: 'mip gen encoder',
111+
});
112+
113+
// For each mip level > 0, sample the previous mip level
114+
// while rendering into this one.
115+
for (
116+
let baseMipLevel = 1;
117+
baseMipLevel < texture.mipLevelCount;
118+
++baseMipLevel
119+
) {
120+
for (let layer = 0; layer < texture.depthOrArrayLayers; ++layer) {
121+
const bindGroup = device.createBindGroup({
122+
layout: pipeline.getBindGroupLayout(0),
123+
entries: [
124+
{ binding: 0, resource: sampler },
125+
{
126+
binding: 1,
127+
resource: texture.createView({
128+
dimension: textureBindingViewDimension,
129+
baseMipLevel: baseMipLevel - 1,
130+
mipLevelCount: 1,
131+
}),
132+
},
133+
],
134+
});
135+
136+
const renderPassDescriptor: GPURenderPassDescriptor = {
137+
label: 'our basic canvas renderPass',
138+
colorAttachments: [
139+
{
140+
view: texture.createView({
141+
dimension: '2d',
142+
baseMipLevel,
143+
mipLevelCount: 1,
144+
baseArrayLayer: layer,
145+
arrayLayerCount: 1,
146+
}),
147+
loadOp: 'clear',
148+
storeOp: 'store',
149+
},
150+
],
151+
};
152+
153+
const pass = encoder.beginRenderPass(renderPassDescriptor);
154+
pass.setPipeline(pipeline);
155+
pass.setBindGroup(0, bindGroup);
156+
// draw 3 vertices, 1 instance, first instance (instance_index) = layer
157+
pass.draw(3, 1, 0, layer);
158+
pass.end();
159+
}
160+
}
161+
162+
const commandBuffer = encoder.finish();
163+
device.queue.submit([commandBuffer]);
164+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const faceMat = array(
2+
mat3x3f( 0, 0, -2, 0, -2, 0, 1, 1, 1), // pos-x
3+
mat3x3f( 0, 0, 2, 0, -2, 0, -1, 1, -1), // neg-x
4+
mat3x3f( 2, 0, 0, 0, 0, 2, -1, 1, -1), // pos-y
5+
mat3x3f( 2, 0, 0, 0, 0, -2, -1, -1, 1), // neg-y
6+
mat3x3f( 2, 0, 0, 0, -2, 0, -1, 1, 1), // pos-z
7+
mat3x3f(-2, 0, 0, 0, -2, 0, 1, 1, -1)); // neg-z
8+
9+
struct VSOutput {
10+
@builtin(position) position: vec4f,
11+
@location(0) texcoord: vec2f,
12+
@location(1) @interpolate(flat, either) baseArrayLayer: u32,
13+
};
14+
15+
@vertex fn vs(
16+
@builtin(vertex_index) vertexIndex : u32,
17+
@builtin(instance_index) baseArrayLayer: u32,
18+
) -> VSOutput {
19+
var pos = array<vec2f, 3>(
20+
vec2f(-1.0, -1.0),
21+
vec2f(-1.0, 3.0),
22+
vec2f( 3.0, -1.0),
23+
);
24+
25+
var vsOutput: VSOutput;
26+
let xy = pos[vertexIndex];
27+
vsOutput.position = vec4f(xy, 0.0, 1.0);
28+
vsOutput.texcoord = xy * vec2f(0.5, -0.5) + vec2f(0.5);
29+
vsOutput.baseArrayLayer = baseArrayLayer;
30+
return vsOutput;
31+
}
32+
33+
@group(0) @binding(0) var ourSampler: sampler;
34+
35+
@group(0) @binding(1) var ourTexture2d: texture_2d<f32>;
36+
@fragment fn fs2d(fsInput: VSOutput) -> @location(0) vec4f {
37+
return textureSample(ourTexture2d, ourSampler, fsInput.texcoord);
38+
}
39+
40+
@group(0) @binding(1) var ourTexture2dArray: texture_2d_array<f32>;
41+
@fragment fn fs2darray(fsInput: VSOutput) -> @location(0) vec4f {
42+
return textureSample(
43+
ourTexture2dArray,
44+
ourSampler,
45+
fsInput.texcoord,
46+
fsInput.baseArrayLayer);
47+
}
48+
49+
@group(0) @binding(1) var ourTextureCube: texture_cube<f32>;
50+
@fragment fn fscube(fsInput: VSOutput) -> @location(0) vec4f {
51+
return textureSample(
52+
ourTextureCube,
53+
ourSampler,
54+
faceMat[fsInput.baseArrayLayer] * vec3f(fract(fsInput.texcoord), 1));
55+
}
56+
57+
@group(0) @binding(1) var ourTextureCubeArray: texture_cube_array<f32>;
58+
@fragment fn fscubearray(fsInput: VSOutput) -> @location(0) vec4f {
59+
return textureSample(
60+
ourTextureCubeArray,
61+
ourSampler,
62+
faceMat[fsInput.baseArrayLayer] * vec3f(fract(fsInput.texcoord), 1), fsInput.baseArrayLayer);
63+
}

sample/generateMipmap/index.html

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<title>webgpu-samples: generateMipmap</title>
7+
<style>
8+
:root {
9+
color-scheme: light dark;
10+
}
11+
html, body {
12+
margin: 0; /* remove default margin */
13+
height: 100%; /* make body fill the browser window */
14+
display: flex;
15+
place-content: center center;
16+
}
17+
canvas {
18+
width: 600px;
19+
height: 600px;
20+
max-width: 100%;
21+
display: block;
22+
}
23+
</style>
24+
<script defer src="main.js" type="module"></script>
25+
<script defer type="module" src="../../js/iframe-helper.js"></script>
26+
</head>
27+
<body>
28+
<canvas></canvas>
29+
</body>
30+
</html>

0 commit comments

Comments
 (0)