Skip to content

Commit

Permalink
WebGPUTextures: Add support for mipmap computation.
Browse files Browse the repository at this point in the history
  • Loading branch information
Mugen87 committed Sep 7, 2020
1 parent 0dd9f90 commit 9e136bb
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 17 deletions.
2 changes: 1 addition & 1 deletion examples/jsm/renderers/webgpu/WebGPURenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -684,7 +684,7 @@ async function initWebGPU( scope ) {
scope._properties = new WebGPUProperties();
scope._attributes = new WebGPUAttributes( device );
scope._geometries = new WebGPUGeometries( scope._attributes, scope._info );
scope._textures = new WebGPUTextures( device, scope._properties, scope._info );
scope._textures = new WebGPUTextures( device, scope._properties, scope._info, compiler );
scope._bindings = new WebGPUBindings( device, scope._info, scope._properties, scope._textures );
scope._objects = new WebGPUObjects( scope._geometries, scope._info );
scope._renderPipelines = new WebGPURenderPipelines( device, compiler, scope._bindings );
Expand Down
148 changes: 148 additions & 0 deletions examples/jsm/renderers/webgpu/WebGPUTextureUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright 2020 Brandon Jones
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import { GPUIndexFormat, GPUFilterMode, GPUPrimitiveTopology } from './constants.js';

// ported from https://github.com/toji/web-texture-tool/blob/master/src/webgpu-mipmap-generator.js

class WebGPUTextureUtils {

constructor( device, glslang ) {

this.device = device;

const mipmapVertexSource = `#version 450
const vec2 pos[4] = vec2[4](vec2(-1.0f, 1.0f), vec2(1.0f, 1.0f), vec2(-1.0f, -1.0f), vec2(1.0f, -1.0f));
const vec2 tex[4] = vec2[4](vec2(0.0f, 0.0f), vec2(1.0f, 0.0f), vec2(0.0f, 1.0f), vec2(1.0f, 1.0f));
layout(location = 0) out vec2 vTex;
void main() {
vTex = tex[gl_VertexIndex];
gl_Position = vec4(pos[gl_VertexIndex], 0.0, 1.0);
}
`;

const mipmapFragmentSource = `#version 450
layout(set = 0, binding = 0) uniform sampler imgSampler;
layout(set = 0, binding = 1) uniform texture2D img;
layout(location = 0) in vec2 vTex;
layout(location = 0) out vec4 outColor;
void main() {
outColor = texture(sampler2D(img, imgSampler), vTex);
}`;

this.sampler = device.createSampler( { minFilter: GPUFilterMode.Linear } );

// We'll need a new pipeline for every texture format used.
this.pipelines = {};

this.mipmapVertexShaderModule = device.createShaderModule( {
code: glslang.compileGLSL( mipmapVertexSource, 'vertex' ),
} );
this.mipmapFragmentShaderModule = device.createShaderModule( {
code: glslang.compileGLSL( mipmapFragmentSource, 'fragment' ),
} );

}

getMipmapPipeline( format ) {

let pipeline = this.pipelines[ format ];

if ( pipeline === undefined ) {

pipeline = this.device.createRenderPipeline( {
vertexStage: {
module: this.mipmapVertexShaderModule,
entryPoint: 'main',
},
fragmentStage: {
module: this.mipmapFragmentShaderModule,
entryPoint: 'main',
},
primitiveTopology: GPUPrimitiveTopology.TriangleStrip,
vertexState: {
indexFormat: GPUIndexFormat.Uint32
},
colorStates: [ { format } ],
} );
this.pipelines[ format ] = pipeline;

}

return pipeline;

}

generateMipmappedTexture( imageBitmap, textureGPU, textureGPUDescriptor ) {

// mipmaps have to computed manually right now, see https://github.com/gpuweb/gpuweb/issues/386

const pipeline = this.getMipmapPipeline( textureGPUDescriptor.format );

const commandEncoder = this.device.createCommandEncoder( {} );
// @TODO: Consider making this static.
const bindGroupLayout = pipeline.getBindGroupLayout( 0 );

let srcView = textureGPU.createView( {
baseMipLevel: 0,
mipLevelCount: 1,
} );

for ( let i = 1; i < textureGPUDescriptor.mipLevelCount; i ++ ) {

const dstView = textureGPU.createView( {
baseMipLevel: i,
mipLevelCount: 1,
} );

const passEncoder = commandEncoder.beginRenderPass( {
colorAttachments: [ {
attachment: dstView,
loadValue: [ 0, 0, 0, 0 ],
} ],
} );

const bindGroup = this.device.createBindGroup( {
layout: bindGroupLayout,
entries: [ {
binding: 0,
resource: this.sampler,
}, {
binding: 1,
resource: srcView,
} ],
} );

passEncoder.setPipeline( pipeline );
passEncoder.setBindGroup( 0, bindGroup );
passEncoder.draw( 4, 1, 0, 0 );
passEncoder.endPass();

srcView = dstView;

}

this.device.defaultQueue.submit( [ commandEncoder.finish() ] );

}

}

export default WebGPUTextureUtils;
77 changes: 61 additions & 16 deletions examples/jsm/renderers/webgpu/WebGPUTextures.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { GPUTextureFormat, GPUAddressMode, GPUFilterMode } from './constants.js';
import { Texture, NearestFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, RepeatWrapping, MirroredRepeatWrapping, FloatType, HalfFloatType } from '../../../../build/three.module.js';
import { Texture, NearestFilter, NearestMipmapNearestFilter, NearestMipmapLinearFilter, LinearFilter, RepeatWrapping, MirroredRepeatWrapping, FloatType, HalfFloatType } from '../../../../build/three.module.js';
import WebGPUTextureUtils from './WebGPUTextureUtils.js';

class WebGPUTextures {

constructor( device, properties, info ) {
constructor( device, properties, info, glslang ) {

this.device = device;
this.properties = properties;
this.info = info;
this.glslang = glslang;

this.defaultTexture = null;
this.defaultSampler = null;

this.samplerCache = new Map();
this.utils = null;

}

Expand Down Expand Up @@ -229,6 +232,12 @@ class WebGPUTextures {

}

_computeMipLevelCount( width, height ) {

return Math.floor( Math.log2( Math.max( width, height ) ) ) + 1;

}

_convertAddressMode( value ) {

let addressMode = GPUAddressMode.ClampToEdge;
Expand Down Expand Up @@ -288,16 +297,32 @@ class WebGPUTextures {
const height = ( image !== undefined ) ? image.height : 1;

const format = this._convertFormat( texture.type );
const needsMipmaps = this._needsMipmaps( texture );
const mipLevelCount = ( needsMipmaps === true ) ? this._computeMipLevelCount( width, height ) : undefined;

let usage = GPUTextureUsage.SAMPLED | GPUTextureUsage.COPY_DST;

if ( needsMipmaps === true ) {

const textureGPU = device.createTexture( {
usage |= GPUTextureUsage.OUTPUT_ATTACHMENT;

}

// texture creation

const textureGPUDescriptor = {
size: {
width: width,
height: height,
depth: 1,
},
format: format,
usage: GPUTextureUsage.SAMPLED | GPUTextureUsage.COPY_DST,
} );
usage: usage,
mipLevelCount: mipLevelCount
};
const textureGPU = device.createTexture( textureGPUDescriptor );

// transfer texture data

if ( texture.isDataTexture ) {

Expand All @@ -317,7 +342,7 @@ class WebGPUTextures {

createImageBitmap( image, 0, 0, width, height, options ).then( imageBitmap => {

this._copyImageBitmapToTexture( imageBitmap, textureGPU );
this._copyImageBitmapToTexture( imageBitmap, textureGPU, needsMipmaps, textureGPUDescriptor );

} );

Expand All @@ -327,7 +352,7 @@ class WebGPUTextures {

// assuming ImageBitmap. Directly start copy operation of the contents of ImageBitmap into the destination texture

this._copyImageBitmapToTexture( image, textureGPU );
this._copyImageBitmapToTexture( image, textureGPU, needsMipmaps, textureGPUDescriptor );

}

Expand All @@ -342,7 +367,9 @@ class WebGPUTextures {
_copyBufferToTexture( image, format, textureGPU ) {

// this code assumes data textures in RGBA format

// @TODO: Consider to support valid buffer layouts with other formats like RGB
// @TODO: Support mipmaps

const device = this.device;
const data = image.data;
Expand Down Expand Up @@ -377,15 +404,7 @@ class WebGPUTextures {

}

_getBytesPerTexel( format ) {

if ( format === GPUTextureFormat.RGBA8Unorm ) return 4;
if ( format === GPUTextureFormat.RGBA16Float ) return 8;
if ( format === GPUTextureFormat.RGBA32Float ) return 16;

}

_copyImageBitmapToTexture( imageBitmap, textureGPU ) {
_copyImageBitmapToTexture( imageBitmap, textureGPU, needsMipmaps, textureGPUDescriptor ) {

const device = this.device;

Expand All @@ -401,6 +420,32 @@ class WebGPUTextures {
}
);

if ( needsMipmaps === true ) {

if ( this.utils === null ) {

this.utils = new WebGPUTextureUtils( this.device, this.glslang ); // only create this helper if necessary

}

this.utils.generateMipmappedTexture( imageBitmap, textureGPU, textureGPUDescriptor );

}

}

_getBytesPerTexel( format ) {

if ( format === GPUTextureFormat.RGBA8Unorm ) return 4;
if ( format === GPUTextureFormat.RGBA16Float ) return 8;
if ( format === GPUTextureFormat.RGBA32Float ) return 16;

}

_needsMipmaps( texture ) {

return ( texture.generateMipmaps === true ) && ( texture.minFilter !== NearestFilter ) && ( texture.minFilter !== LinearFilter );

}

}
Expand Down

0 comments on commit 9e136bb

Please sign in to comment.