From 4e27bc26d69f224cf5dfd218ccc1cead61d2ac56 Mon Sep 17 00:00:00 2001 From: Gregg Tavares Date: Fri, 21 Mar 2025 12:10:22 -0700 Subject: [PATCH] Add generate mipmap sample We've got hello triangle. Generating mipmaps seems like a pretty common feature. Maybe a sample for it is good? --- sample/generateMipmap/generateMipmap.ts | 164 +++++++++ sample/generateMipmap/generateMipmap.wgsl | 63 ++++ sample/generateMipmap/index.html | 30 ++ sample/generateMipmap/main.ts | 361 ++++++++++++++++++++ sample/generateMipmap/makeCanvasImage.ts | 47 +++ sample/generateMipmap/meta.ts | 15 + sample/generateMipmap/texturedGeometry.wgsl | 70 ++++ src/samples.ts | 2 + 8 files changed, 752 insertions(+) create mode 100644 sample/generateMipmap/generateMipmap.ts create mode 100644 sample/generateMipmap/generateMipmap.wgsl create mode 100644 sample/generateMipmap/index.html create mode 100644 sample/generateMipmap/main.ts create mode 100644 sample/generateMipmap/makeCanvasImage.ts create mode 100644 sample/generateMipmap/meta.ts create mode 100644 sample/generateMipmap/texturedGeometry.wgsl diff --git a/sample/generateMipmap/generateMipmap.ts b/sample/generateMipmap/generateMipmap.ts new file mode 100644 index 00000000..fd98f7ca --- /dev/null +++ b/sample/generateMipmap/generateMipmap.ts @@ -0,0 +1,164 @@ +import generateMipmapWGSL from './generateMipmap.wgsl'; + +export function numMipLevels(...sizes: number[]) { + const maxSize = Math.max(...sizes); + return (1 + Math.log2(maxSize)) | 0; +} + +/** + * Get the default viewDimension + * Note: It's only a guess. The user needs to tell us to be + * correct in all cases because we can't distinguish between + * a 2d texture and a 2d-array texture with 1 layer, nor can + * we distinguish between a 2d-array texture with 6 layers and + * a cubemap. + */ +export function getDefaultViewDimensionForTexture( + dimension: GPUTextureDimension, + depthOrArrayLayers: number +) { + switch (dimension) { + case '1d': + return '1d'; + default: + case '2d': + return depthOrArrayLayers > 1 ? '2d-array' : '2d'; + case '3d': + return '3d'; + } +} + +type DeviceSpecificInfo = { + sampler: GPUSampler; + module: GPUShaderModule; + pipelineByFormatAndView: Map; +}; + +function createDeviceSpecificInfo(device: GPUDevice): DeviceSpecificInfo { + const module = device.createShaderModule({ + label: 'textured quad shaders for mip level generation', + code: generateMipmapWGSL, + }); + + const sampler = device.createSampler({ + minFilter: 'linear', + magFilter: 'linear', + }); + + return { + module, + sampler, + pipelineByFormatAndView: new Map(), + }; +} + +const s_deviceToDeviceSpecificInfo = new WeakMap< + GPUDevice, + DeviceSpecificInfo +>(); + +/** + * Generates mip levels 1 to texture.mipLevelCount - 1 using + * mip level 0 as the source. + * @param device The device + * @param texture The texture to generate mips for + * @param textureBindingViewDimension The view dimension, needed for + * compatibility mode if the texture is a cube map OR if the texture + * is a 1 layer 2d-array. + */ +export function generateMips( + device: GPUDevice, + texture: GPUTexture, + textureBindingViewDimension?: GPUTextureViewDimension +) { + textureBindingViewDimension = + textureBindingViewDimension ?? + getDefaultViewDimensionForTexture( + texture.dimension, + texture.depthOrArrayLayers + ); + const deviceSpecificInfo = + s_deviceToDeviceSpecificInfo.get(device) ?? + createDeviceSpecificInfo(device); + s_deviceToDeviceSpecificInfo.set(device, deviceSpecificInfo); + const { sampler, module, pipelineByFormatAndView } = deviceSpecificInfo; + + const id = `${texture.format}.${textureBindingViewDimension}`; + + if (!pipelineByFormatAndView.get(id)) { + // Choose an fragment shader based on the viewDimension (removes the '-' from 2d-array and cube-array) + const entryPoint = `fs${textureBindingViewDimension.replace('-', '')}`; + pipelineByFormatAndView.set( + id, + device.createRenderPipeline({ + label: `mip level generator pipeline for ${textureBindingViewDimension}, format: ${texture.format}`, + layout: 'auto', + vertex: { + module, + }, + fragment: { + module, + entryPoint, + targets: [{ format: texture.format }], + }, + }) + ); + } + + const pipeline = pipelineByFormatAndView.get(id); + const encoder = device.createCommandEncoder({ + label: 'mip gen encoder', + }); + + // For each mip level > 0, sample the previous mip level + // while rendering into this one. + for ( + let baseMipLevel = 1; + baseMipLevel < texture.mipLevelCount; + ++baseMipLevel + ) { + for (let layer = 0; layer < texture.depthOrArrayLayers; ++layer) { + const bindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: sampler }, + { + binding: 1, + resource: texture.createView({ + dimension: textureBindingViewDimension, + baseMipLevel: baseMipLevel - 1, + mipLevelCount: 1, + }), + }, + ], + }); + + const renderPassDescriptor: GPURenderPassDescriptor = { + label: 'our basic canvas renderPass', + colorAttachments: [ + { + view: texture.createView({ + dimension: '2d', + baseMipLevel, + mipLevelCount: 1, + baseArrayLayer: layer, + arrayLayerCount: 1, + }), + loadOp: 'clear', + storeOp: 'store', + }, + ], + }; + + const pass = encoder.beginRenderPass(renderPassDescriptor); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + // draw 3 vertices, 1 instance, first instance (instance_index) = layer + pass.draw(3, 1, 0, layer); + pass.end(); + } + } + + const commandBuffer = encoder.finish(); + device.queue.submit([commandBuffer]); +} diff --git a/sample/generateMipmap/generateMipmap.wgsl b/sample/generateMipmap/generateMipmap.wgsl new file mode 100644 index 00000000..6cc79667 --- /dev/null +++ b/sample/generateMipmap/generateMipmap.wgsl @@ -0,0 +1,63 @@ +const faceMat = array( + mat3x3f( 0, 0, -2, 0, -2, 0, 1, 1, 1), // pos-x + mat3x3f( 0, 0, 2, 0, -2, 0, -1, 1, -1), // neg-x + mat3x3f( 2, 0, 0, 0, 0, 2, -1, 1, -1), // pos-y + mat3x3f( 2, 0, 0, 0, 0, -2, -1, -1, 1), // neg-y + mat3x3f( 2, 0, 0, 0, -2, 0, -1, 1, 1), // pos-z + mat3x3f(-2, 0, 0, 0, -2, 0, 1, 1, -1)); // neg-z + +struct VSOutput { + @builtin(position) position: vec4f, + @location(0) texcoord: vec2f, + @location(1) @interpolate(flat, either) baseArrayLayer: u32, +}; + +@vertex fn vs( + @builtin(vertex_index) vertexIndex : u32, + @builtin(instance_index) baseArrayLayer: u32, +) -> VSOutput { + var pos = array( + vec2f(-1.0, -1.0), + vec2f(-1.0, 3.0), + vec2f( 3.0, -1.0), + ); + + var vsOutput: VSOutput; + let xy = pos[vertexIndex]; + vsOutput.position = vec4f(xy, 0.0, 1.0); + vsOutput.texcoord = xy * vec2f(0.5, -0.5) + vec2f(0.5); + vsOutput.baseArrayLayer = baseArrayLayer; + return vsOutput; +} + +@group(0) @binding(0) var ourSampler: sampler; + +@group(0) @binding(1) var ourTexture2d: texture_2d; +@fragment fn fs2d(fsInput: VSOutput) -> @location(0) vec4f { + return textureSample(ourTexture2d, ourSampler, fsInput.texcoord); +} + +@group(0) @binding(1) var ourTexture2dArray: texture_2d_array; +@fragment fn fs2darray(fsInput: VSOutput) -> @location(0) vec4f { + return textureSample( + ourTexture2dArray, + ourSampler, + fsInput.texcoord, + fsInput.baseArrayLayer); +} + +@group(0) @binding(1) var ourTextureCube: texture_cube; +@fragment fn fscube(fsInput: VSOutput) -> @location(0) vec4f { + return textureSample( + ourTextureCube, + ourSampler, + faceMat[fsInput.baseArrayLayer] * vec3f(fract(fsInput.texcoord), 1)); +} + +@group(0) @binding(1) var ourTextureCubeArray: texture_cube_array; +@fragment fn fscubearray(fsInput: VSOutput) -> @location(0) vec4f { + return textureSample( + ourTextureCubeArray, + ourSampler, + faceMat[fsInput.baseArrayLayer] * vec3f(fract(fsInput.texcoord), 1), fsInput.baseArrayLayer); +} \ No newline at end of file diff --git a/sample/generateMipmap/index.html b/sample/generateMipmap/index.html new file mode 100644 index 00000000..ca2f73b4 --- /dev/null +++ b/sample/generateMipmap/index.html @@ -0,0 +1,30 @@ + + + + + + webgpu-samples: generateMipmap + + + + + + + + diff --git a/sample/generateMipmap/main.ts b/sample/generateMipmap/main.ts new file mode 100644 index 00000000..154c0894 --- /dev/null +++ b/sample/generateMipmap/main.ts @@ -0,0 +1,361 @@ +import { mat4 } from 'wgpu-matrix'; +import { generateMips } from './generateMipmap'; +import { quitIfWebGPUNotAvailable } from '../util'; +import { makeCanvasImage } from './makeCanvasImage'; +import { + cubeVertexArray, + cubeVertexSize, + cubeUVOffset, + cubePositionOffset, + cubeVertexCount, +} from '../../meshes/cube'; + +import textureGeometryWGSL from './texturedGeometry.wgsl'; + +const hsl = (h: number, s: number, l: number) => + `hsl(${h * 360} ${s * 100}% ${l * 100}%)`; + +const adapter = await navigator.gpu?.requestAdapter({ + featureLevel: 'compatibility', +}); +const device = await adapter?.requestDevice({ + requiredFeatures: [ + ...(adapter.features.has('core-features-and-limits') + ? ['core-features-and-limits' as GPUFeatureName] + : []), + ], +}); +quitIfWebGPUNotAvailable(adapter, device); + +const canvas = document.querySelector('canvas') as HTMLCanvasElement; +const context = canvas.getContext('webgpu') as GPUCanvasContext; +const devicePixelRatio = window.devicePixelRatio; +canvas.width = canvas.clientWidth * devicePixelRatio; +canvas.height = canvas.clientHeight * devicePixelRatio; +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); +context.configure({ + device, + format: presentationFormat, +}); + +const textures: { + texture: GPUTexture; + viewDimension: GPUTextureViewDimension; +}[] = []; + +// make a 2d texture, put an image in it, generate mips +{ + const texture = device.createTexture({ + size: [256, 256], + mipLevelCount: 9, + format: 'rgba8unorm', + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + putDataInTexture2d(device, texture); + generateMips(device, texture); + textures.push({ texture, viewDimension: '2d' }); +} + +// Make a 2d array texture, put an image in each layer, generate mips +{ + const texture = device.createTexture({ + size: [256, 256, 10], + mipLevelCount: 9, + format: 'rgba8unorm', + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + putDataInTexture2dArray(device, texture); + generateMips(device, texture); + textures.push({ texture, viewDimension: '2d' }); +} + +// Make a cube texture, put an image in each face, generate mips +{ + const texture = device.createTexture({ + size: [256, 256, 6], + textureBindingViewDimension: 'cube', + mipLevelCount: 9, + format: 'rgba8unorm', + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + putDataInTextureCube(device, texture); + generateMips(device, texture, 'cube'); + textures.push({ texture, viewDimension: 'cube' }); +} + +// Compatibility mode might not support 'core-features-and-limits' +if (device.features.has('core-features-and-limits')) { + // Make a cube array texture, put a different image in each layer, generate mips + const texture = device.createTexture({ + size: [256, 256, 24], + mipLevelCount: 9, + format: 'rgba8unorm', + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + putDataInTextureCubeArray(device, texture); + generateMips(device, texture); + textures.push({ texture, viewDimension: 'cube-array' }); +} else { + // Make a cube texture as a fallback since we can't make a cube-array, + // put a different image in each face, generate mips + const texture = device.createTexture({ + size: [256, 256, 6], + mipLevelCount: 9, + format: 'rgba8unorm', + usage: + GPUTextureUsage.TEXTURE_BINDING | + GPUTextureUsage.COPY_DST | + GPUTextureUsage.RENDER_ATTACHMENT, + }); + putDataInTextureCubeFallback(device, texture); + generateMips(device, texture); + textures.push({ texture, viewDimension: 'cube' }); +} + +const module = device.createShaderModule({ + code: textureGeometryWGSL, +}); + +const sampler = device.createSampler({ + minFilter: 'linear', + magFilter: 'linear', + mipmapFilter: 'linear', +}); + +const uniformBuffer = device.createBuffer({ + size: 16 * 4, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, +}); +const uniformValues = new ArrayBuffer(uniformBuffer.size); +const matrix = new Float32Array(uniformValues); + +// Make pipelines using shaders for each type of texture (2d, 2d-array, cube, cube-array) and, +// make a bindGroup that uses that texture. +const objects = textures.map(({ texture, viewDimension }) => { + const pipeline = device.createRenderPipeline({ + layout: 'auto', + vertex: { + module, + entryPoint: `vs_${viewDimension.replace('-', '_')}`, + buffers: [ + { + arrayStride: cubeVertexSize, + attributes: [ + { + // position + shaderLocation: 0, + offset: cubePositionOffset, + format: 'float32x4', + }, + { + // uv + shaderLocation: 1, + offset: cubeUVOffset, + format: 'float32x2', + }, + ], + }, + ], + }, + fragment: { + module, + entryPoint: `fs_${viewDimension.replace('-', '_')}`, + targets: [{ format: presentationFormat }], + }, + primitive: { + cullMode: 'back', + }, + }); + const bindGroup = device.createBindGroup({ + layout: pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: uniformBuffer } }, + { binding: 1, resource: sampler }, + { + binding: 2, + resource: texture.createView({ dimension: viewDimension }), + }, + ], + }); + return { pipeline, bindGroup }; +}); + +// Create a vertex buffer from the cube data. +const verticesBuffer = device.createBuffer({ + size: cubeVertexArray.byteLength, + usage: GPUBufferUsage.VERTEX, + mappedAtCreation: true, +}); +new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray); +verticesBuffer.unmap(); + +const renderPassDescriptor: GPURenderPassDescriptor = { + label: 'our basic canvas renderPass', + colorAttachments: [ + { + view: undefined, // <- to be filled out when we render + clearValue: [0.3, 0.3, 0.3, 1], + loadOp: 'clear', + storeOp: 'store', + }, + ], +}; + +function render(time: number) { + time *= 0.001; + + const canvasTexture = context.getCurrentTexture(); + renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView(); + + const aspect = canvas.clientWidth / canvas.clientHeight; + const projection = mat4.perspective((90 * Math.PI) / 180, aspect, 0.1, 20); + const view = mat4.lookAt( + [0, 4 + 2.5 * Math.sin(time), 2], // eye + [0, 0, 0], // target + [0, 1, 0] // up + ); + const viewProjection = mat4.multiply(projection, view); + mat4.rotateX(viewProjection, time, matrix); + mat4.rotateY(matrix, time * 0.7, matrix); + + device.queue.writeBuffer(uniformBuffer, 0, uniformValues); + + const encoder = device.createCommandEncoder(); + const pass = encoder.beginRenderPass(renderPassDescriptor); + pass.setVertexBuffer(0, verticesBuffer); + + // draw each cube in a different quadrant of the canvas texture + // by using setViewport to select an area. + objects.forEach(({ pipeline, bindGroup }, i) => { + const x = i % 2; + const y = (i / 2) | 0; + const w = canvasTexture.width; + const h = canvasTexture.height; + pass.setViewport((x * w) / 2, (y * h) / 2, w / 2, h / 2, 0, 1); + pass.setPipeline(pipeline); + pass.setBindGroup(0, bindGroup); + pass.draw(cubeVertexCount, 1, 0, time | 0); + }); + + pass.end(); + device.queue.submit([encoder.finish()]); + + requestAnimationFrame(render); +} +requestAnimationFrame(render); + +// --- + +function putDataInTexture2d(device: GPUDevice, texture: GPUTexture) { + const canvas = makeCanvasImage({ + width: 256, + height: 256, + backgroundColor: '#f00', + borderColor: '#ff0', + foregroundColor: '#fff', + text: ['😅'], + }); + device.queue.copyExternalImageToTexture( + { source: canvas }, + { texture: texture }, + [256, 256] + ); +} + +function putDataInTexture2dArray(device: GPUDevice, texture: GPUTexture) { + // prettier-ignore + const faces = ['🐵', '🐶', '🦊', '🐱', '🐷', '🐮', '🐸', '🐙', '🐹', '🐼', '🐨', '🐭', '🦁', '🐯']; + for (let layer = 0; layer < texture.depthOrArrayLayers; ++layer) { + const h = layer / texture.depthOrArrayLayers; + const canvas = makeCanvasImage({ + width: 256, + height: 256, + backgroundColor: hsl(h, 0.5, 0.5), + borderColor: hsl(h + 0.5, 1, 0.75), + foregroundColor: '#fff', + text: [faces[layer]], + }); + device.queue.copyExternalImageToTexture( + { source: canvas }, + { texture: texture, origin: [0, 0, layer] }, + [256, 256] + ); + } +} + +function putDataInTextureCube(device: GPUDevice, texture: GPUTexture) { + const kFaces = ['+x', '-x', '+y', '-y', '+z', '-z']; + for (let layer = 0; layer < texture.depthOrArrayLayers; ++layer) { + const h = layer / texture.depthOrArrayLayers; + const canvas = makeCanvasImage({ + width: 256, + height: 256, + backgroundColor: hsl(h, 0.5, 0.5), + borderColor: hsl(h + 0.5, 1, 0.75), + foregroundColor: '#f00', + text: ['🌞', kFaces[layer]], + }); + device.queue.copyExternalImageToTexture( + { source: canvas }, + { texture: texture, origin: [0, 0, layer] }, + [256, 256] + ); + } +} + +function putDataInTextureCubeArray(device: GPUDevice, texture: GPUTexture) { + const kFaces = ['+x', '-x', '+y', '-y', '+z', '-z']; + const kLayers = ['💐', '🌸', '🪷', '🏵️', '🌹', '🥀', '🌺', '🌻', '🌼']; + const cubeLayers = texture.depthOrArrayLayers / 6; + for (let layer = 0; layer < texture.depthOrArrayLayers; ++layer) { + const cubeLayer = (layer / 6) | 0; + const face = layer % 6; + const h = cubeLayer / cubeLayers; + const canvas = makeCanvasImage({ + width: 256, + height: 256, + backgroundColor: hsl(h, 0.5, 0.5), + borderColor: hsl(h + 0.5, 1, 0.75), + foregroundColor: '#fff', + text: [kLayers[cubeLayer], kFaces[face]], + }); + device.queue.copyExternalImageToTexture( + { source: canvas }, + { texture: texture, origin: [0, 0, layer] }, + [256, 256] + ); + } +} + +// Used when cube-array does not exist. +function putDataInTextureCubeFallback(device: GPUDevice, texture: GPUTexture) { + const kFaces = ['+x', '-x', '+y', '-y', '+z', '-z']; + const kNo = ['⛔️', '🚫', '❌', '👎', '🤬', '😱']; + for (let layer = 0; layer < texture.depthOrArrayLayers; ++layer) { + const canvas = makeCanvasImage({ + width: 256, + height: 256, + backgroundColor: '#400', + borderColor: '#f00', + foregroundColor: '#8ff', + text: [kNo[layer], kFaces[layer]], + }); + device.queue.copyExternalImageToTexture( + { source: canvas }, + { texture: texture, origin: [0, 0, layer] }, + [256, 256] + ); + } +} diff --git a/sample/generateMipmap/makeCanvasImage.ts b/sample/generateMipmap/makeCanvasImage.ts new file mode 100644 index 00000000..8eb24f2d --- /dev/null +++ b/sample/generateMipmap/makeCanvasImage.ts @@ -0,0 +1,47 @@ +/** + * Makes a canvas with a border and centered text. + */ +export function makeCanvasImage({ + width, + height, + borderColor, + backgroundColor, + foregroundColor, + font, + text, +}: { + width: number; + height: number; + borderColor: string; + backgroundColor: string; + foregroundColor: string; + font?: string; + text: string[]; +}) { + const canvas = new OffscreenCanvas(width, height); + const ctx = canvas.getContext('2d'); + ctx.fillStyle = borderColor; + ctx.fillRect(0, 0, width, height); + const borderSize = 10; + ctx.fillStyle = backgroundColor; + ctx.fillRect( + borderSize, + borderSize, + width - borderSize * 2, + height - borderSize * 2 + ); + ctx.fillStyle = foregroundColor; + ctx.font = + font ?? `${Math.ceil(Math.min(width, height) * 0.8)}px bold monospace`; + ctx.textAlign = 'left'; + ctx.textBaseline = 'top'; + for (const t of text) { + const m = ctx.measureText(t); + ctx.fillText( + t, + (width - m.actualBoundingBoxRight + m.actualBoundingBoxLeft) / 2, + (height - m.actualBoundingBoxDescent + m.actualBoundingBoxAscent) / 2 + ); + } + return canvas; +} diff --git a/sample/generateMipmap/meta.ts b/sample/generateMipmap/meta.ts new file mode 100644 index 00000000..078cf34f --- /dev/null +++ b/sample/generateMipmap/meta.ts @@ -0,0 +1,15 @@ +export default { + name: 'Generate Mipmap', + description: `\ +This example shows one way to generate a mipmap in WebGPU in a compatibility mode compatible way. +For more info [see this article](https://webgpufundamentals.org/webgpu/lessons/webgpu-compatibility-mode.html#a-generating-mipmaps). +`, + filename: __DIRNAME__, + sources: [ + { path: 'main.ts' }, + { path: 'generateMipmap.ts' }, + { path: 'generateMipmap.wgsl' }, + { path: 'texturedGeometry.wgsl' }, + { path: 'makeCanvasImage.ts' }, + ], +}; diff --git a/sample/generateMipmap/texturedGeometry.wgsl b/sample/generateMipmap/texturedGeometry.wgsl new file mode 100644 index 00000000..a0306c8a --- /dev/null +++ b/sample/generateMipmap/texturedGeometry.wgsl @@ -0,0 +1,70 @@ +struct VSInput { + @location(0) position: vec4f, + @location(1) texcoord: vec3f, + @builtin(instance_index) iNdx: u32, +}; + +struct VSOutputFSInput { + @builtin(position) position: vec4f, + @location(0) texcoord: vec3f, + @location(1) @interpolate(flat, either) instanceNdx: u32, +}; + +struct Uniforms { + matrix: mat4x4f, +} + +@group(0) @binding(0) var uni: Uniforms; +@group(0) @binding(1) var s: sampler; +@group(0) @binding(2) var t_2d: texture_2d; +@group(0) @binding(2) var t_2d_array: texture_2d_array; +@group(0) @binding(2) var t_cube: texture_cube; +@group(0) @binding(2) var t_cube_array: texture_cube_array; + +@vertex fn vs_2d(v: VSInput) -> VSOutputFSInput { + return VSOutputFSInput( + uni.matrix * v.position, + v.texcoord, + v.iNdx, + ); +} + +@vertex fn vs_2d_array(v: VSInput) -> VSOutputFSInput { + return VSOutputFSInput( + uni.matrix * v.position, + v.texcoord, + v.iNdx, + ); +} + +@vertex fn vs_cube(v: VSInput) -> VSOutputFSInput { + return VSOutputFSInput( + uni.matrix * v.position, + v.position.xyz, + v.iNdx, + ); +} + +@vertex fn vs_cube_array(v: VSInput) -> VSOutputFSInput { + return VSOutputFSInput( + uni.matrix * v.position, + v.position.xyz, + v.iNdx, + ); +} + +@fragment fn fs_2d(f: VSOutputFSInput) -> @location(0) vec4f { + return textureSample(t_2d, s, f.texcoord.xy); +} + +@fragment fn fs_2d_array(f: VSOutputFSInput) -> @location(0) vec4f { + return textureSample(t_2d_array, s, f.texcoord.xy, f.instanceNdx % textureNumLayers(t_2d_array)); +} + +@fragment fn fs_cube(f: VSOutputFSInput) -> @location(0) vec4f { + return textureSample(t_cube, s, f.texcoord); +} + +@fragment fn fs_cube_array(f: VSOutputFSInput) -> @location(0) vec4f { + return textureSample(t_cube_array, s, f.texcoord, f.instanceNdx % textureNumLayers(t_cube_array)); +} \ No newline at end of file diff --git a/src/samples.ts b/src/samples.ts index b420c874..8a9ee1ab 100644 --- a/src/samples.ts +++ b/src/samples.ts @@ -12,6 +12,7 @@ import cubemap from '../sample/cubemap/meta'; import deferredRendering from '../sample/deferredRendering/meta'; import fractalCube from '../sample/fractalCube/meta'; import gameOfLife from '../sample/gameOfLife/meta'; +import generateMipmap from '../sample/generateMipmap/meta'; import helloTriangle from '../sample/helloTriangle/meta'; import helloTriangleMSAA from '../sample/helloTriangleMSAA/meta'; import imageBlur from '../sample/imageBlur/meta'; @@ -129,6 +130,7 @@ export const pageCategories: PageCategory[] = [ particles, points, imageBlur, + generateMipmap, cornell, 'a-buffer': aBuffer, skinnedMesh,