wgpu/src/utils/mip-generator.ts
2025-04-21 23:36:59 -05:00

188 lines
4.5 KiB
TypeScript

import code from './mip-shader.wgsl' with { type: 'text' }
const mip = (n: number) => Math.max(1, n >>> 1)
export class MipGenerator {
_device: GPUDevice
_sampler: GPUSampler
_pipelines: Record<string, GPURenderPipeline>
_shader?: GPUShaderModule
_bindGroupLayout?: GPUBindGroupLayout
_pipelineLayout?: GPUPipelineLayout
constructor(device: GPUDevice) {
this._device = device
this._sampler = device.createSampler({ minFilter: 'linear' })
this._pipelines = {}
}
_getShader() {
if (!this._shader) {
this._shader = this._device.createShaderModule({
code
})
}
return this._shader
}
_getBindGroupLayout() {
if (!this._bindGroupLayout) {
this._bindGroupLayout = this._device.createBindGroupLayout({
entries: [{
binding: 0,
visibility: GPUShaderStage.FRAGMENT,
sampler: {}
}, {
binding: 1,
visibility: GPUShaderStage.FRAGMENT,
texture: {}
}]
})
}
return this._bindGroupLayout
}
_getPipelineLayout() {
if (!this._pipelineLayout) {
this._pipelineLayout = this._device.createPipelineLayout({
label: 'Mipmap Generator',
bindGroupLayouts: [this._getBindGroupLayout()],
})
}
return this._pipelineLayout
}
getPipeline(format: GPUTextureFormat) {
let pipeline = this._pipelines[format]
if (!pipeline) {
const shader = this._getShader()
pipeline = this._device.createRenderPipeline({
layout: this._getPipelineLayout(),
vertex: {
module: shader,
entryPoint: 'vs_main'
},
fragment: {
module: shader,
entryPoint: 'fs_main',
targets: [{ format }]
}
})
this._pipelines[format] = pipeline
}
return pipeline
}
generateMipmap(texture: GPUTexture, descriptor: GPUTextureDescriptor) {
const pipeline = this.getPipeline(descriptor.format)
if (descriptor.dimension !== '2d') {
throw new Error('Generating mipmaps for anything except 2d is unsupported.')
}
let mipTexture = texture
const { width, height, depthOrArrayLayers } = descriptor.size as GPUExtent3DDict
const renderToSource = descriptor.usage & GPUTextureUsage.RENDER_ATTACHMENT
if (!renderToSource) {
mipTexture = this._device.createTexture({
size: {
width: mip(width),
height: mip(height),
depthOrArrayLayers
},
format: descriptor.format,
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_SRC | GPUTextureUsage.RENDER_ATTACHMENT,
mipLevelCount: descriptor.mipLevelCount - 1
})
}
const encoder = this._device.createCommandEncoder({})
for (let layer = 0; layer < depthOrArrayLayers; ++layer) {
let srcView = texture.createView({
baseMipLevel: 0,
mipLevelCount: 1,
dimension: '2d',
baseArrayLayer: layer,
arrayLayerCount: 1
})
let dstMipLevel = renderToSource ? 1 : 0
for (let i = 1; i < descriptor.mipLevelCount; ++i) {
const dstView = mipTexture.createView({
baseMipLevel: dstMipLevel++,
mipLevelCount: 1,
dimension: '2d',
baseArrayLayer: layer,
arrayLayerCount: 1
})
const passEncoder = encoder.beginRenderPass({
colorAttachments: [{
view: dstView,
loadOp: 'clear',
storeOp: 'store'
}]
})
const bindGroup = this._device.createBindGroup({
layout: this._bindGroupLayout,
entries: [{
binding: 0,
resource: this._sampler
}, {
binding: 1,
resource: srcView
}]
})
passEncoder.setPipeline(pipeline)
passEncoder.setBindGroup(0, bindGroup)
passEncoder.draw(3, 1, 0, 0)
passEncoder.end()
srcView = dstView
}
}
if (!renderToSource) {
const mipLevelSize = {
width: mip(width),
height: mip(height),
depthOrArrayLayers
}
for (let i = 1; i < descriptor.mipLevelCount; ++i) {
encoder.copyTextureToTexture({
texture: mipTexture,
mipLevel: i - 1
}, {
texture,
mipLevel: i,
}, mipLevelSize)
mipLevelSize.width = mip(mipLevelSize.width)
mipLevelSize.height = mip(mipLevelSize.height)
}
}
this._device.queue.submit([encoder.finish()])
if (!renderToSource) {
mipTexture.destroy()
}
return texture
}
}