three
Version:
JavaScript 3D library
942 lines (629 loc) • 24.5 kB
JavaScript
import { BlendColorFactor, OneMinusBlendColorFactor, } from '../../common/Constants.js';
import {
GPUFrontFace, GPUCullMode, GPUColorWriteFlags, GPUCompareFunction, GPUBlendFactor, GPUBlendOperation, GPUIndexFormat, GPUStencilOperation, GPUPrimitiveTopology
} from './WebGPUConstants.js';
import {
BackSide, DoubleSide,
NeverDepth, AlwaysDepth, LessDepth, LessEqualDepth, EqualDepth, GreaterEqualDepth, GreaterDepth, NotEqualDepth,
NoBlending, NormalBlending, AdditiveBlending, SubtractiveBlending, MultiplyBlending, CustomBlending, MaterialBlending,
ZeroFactor, OneFactor, SrcColorFactor, OneMinusSrcColorFactor, SrcAlphaFactor, OneMinusSrcAlphaFactor, DstColorFactor,
OneMinusDstColorFactor, DstAlphaFactor, OneMinusDstAlphaFactor, SrcAlphaSaturateFactor,
AddEquation, SubtractEquation, ReverseSubtractEquation, MinEquation, MaxEquation,
KeepStencilOp, ZeroStencilOp, ReplaceStencilOp, InvertStencilOp, IncrementStencilOp, DecrementStencilOp, IncrementWrapStencilOp, DecrementWrapStencilOp,
NeverStencilFunc, AlwaysStencilFunc, LessStencilFunc, LessEqualStencilFunc, EqualStencilFunc, GreaterEqualStencilFunc, GreaterStencilFunc, NotEqualStencilFunc
} from '../../../constants.js';
import { error, ReversedDepthFuncs, warn, warnOnce } from '../../../utils.js';
import GPUComputePipelineDescriptor from '../descriptors/GPUComputePipelineDescriptor.js';
import GPUPipelineLayoutDescriptor from '../descriptors/GPUPipelineLayoutDescriptor.js';
import GPURenderBundleEncoderDescriptor from '../descriptors/GPURenderBundleEncoderDescriptor.js';
import GPURenderPipelineDescriptor from '../descriptors/GPURenderPipelineDescriptor.js';
const _computePipelineDescriptor = new GPUComputePipelineDescriptor();
const _pipelineLayoutDescriptor = new GPUPipelineLayoutDescriptor();
const _renderBundleEncoderDescriptor = new GPURenderBundleEncoderDescriptor();
const _renderPipelineDescriptor = new GPURenderPipelineDescriptor();
/**
* A WebGPU backend utility module for managing pipelines.
*
* @private
*/
class WebGPUPipelineUtils {
/**
* Constructs a new utility object.
*
* @param {WebGPUBackend} backend - The WebGPU backend.
*/
constructor( backend ) {
/**
* A reference to the WebGPU backend.
*
* @type {WebGPUBackend}
*/
this.backend = backend;
}
/**
* Returns the sample count derived from the given render context.
*
* @private
* @param {RenderContext} renderContext - The render context.
* @return {number} The sample count.
*/
_getSampleCount( renderContext ) {
return this.backend.utils.getSampleCountRenderContext( renderContext );
}
/**
* Creates a render pipeline for the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @param {Array<Promise>} promises - An array of compilation promises which are used in `compileAsync()`.
*/
createRenderPipeline( renderObject, promises ) {
const { object, material, geometry, pipeline } = renderObject;
const { vertexProgram, fragmentProgram } = pipeline;
const backend = this.backend;
const device = backend.device;
const utils = backend.utils;
const pipelineData = backend.get( pipeline );
// bind group layouts
const bindGroupLayouts = [];
for ( const bindGroup of renderObject.getBindings() ) {
const bindingsData = backend.get( bindGroup );
const { layoutGPU } = bindingsData.layout;
bindGroupLayouts.push( layoutGPU );
}
// vertex buffers
const vertexBuffers = backend.attributeUtils.createShaderVertexBuffers( renderObject );
// material blending
let materialBlending;
if ( material.blending !== NoBlending && ( material.blending !== NormalBlending || material.transparent !== false ) ) {
materialBlending = this._getBlending( material );
}
// stencil
let stencilFront = {};
if ( material.stencilWrite === true ) {
stencilFront = {
compare: this._getStencilCompare( material ),
failOp: this._getStencilOperation( material.stencilFail ),
depthFailOp: this._getStencilOperation( material.stencilZFail ),
passOp: this._getStencilOperation( material.stencilZPass )
};
}
const colorWriteMask = this._getColorWriteMask( material );
const targets = [];
if ( renderObject.context.textures !== null ) {
const textures = renderObject.context.textures;
const mrt = renderObject.context.mrt;
for ( let i = 0; i < textures.length; i ++ ) {
const texture = textures[ i ];
const colorFormat = utils.getTextureFormatGPU( texture );
// mrt blending
let blending;
if ( mrt !== null ) {
if ( this.backend.compatibilityMode !== true ) {
const blendMode = mrt.getBlendMode( texture.name );
if ( blendMode.blending === MaterialBlending ) {
blending = materialBlending;
} else if ( blendMode.blending !== NoBlending ) {
blending = this._getBlending( blendMode );
}
} else {
warnOnce( 'WebGPURenderer: Multiple Render Targets (MRT) blending configuration is not fully supported in compatibility mode. The material blending will be used for all render targets.' );
blending = materialBlending;
}
} else {
blending = materialBlending;
}
targets.push( {
format: colorFormat,
blend: blending,
writeMask: colorWriteMask
} );
}
} else {
const colorFormat = utils.getCurrentColorFormat( renderObject.context );
targets.push( {
format: colorFormat,
blend: materialBlending,
writeMask: colorWriteMask
} );
}
const vertexModule = backend.get( vertexProgram ).module;
const fragmentModule = backend.get( fragmentProgram ).module;
const primitiveState = this._getPrimitiveState( object, geometry, material );
const depthCompare = this._getDepthCompare( material );
const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context );
const sampleCount = this._getSampleCount( renderObject.context );
_pipelineLayoutDescriptor.bindGroupLayouts = bindGroupLayouts;
const pipelineLayout = device.createPipelineLayout( _pipelineLayoutDescriptor );
_pipelineLayoutDescriptor.reset();
_renderPipelineDescriptor.label = `renderPipeline_${ material.name || material.type }_${ material.id }`;
_renderPipelineDescriptor.vertex = Object.assign( {}, vertexModule, { buffers: vertexBuffers } );
_renderPipelineDescriptor.fragment = Object.assign( {}, fragmentModule, { targets } );
_renderPipelineDescriptor.primitive = primitiveState;
_renderPipelineDescriptor.multisample.count = sampleCount;
_renderPipelineDescriptor.multisample.alphaToCoverageEnabled = material.alphaToCoverage && sampleCount > 1;
_renderPipelineDescriptor.layout = pipelineLayout;
const depthStencil = {};
const renderDepth = renderObject.context.depth;
const renderStencil = renderObject.context.stencil;
if ( renderDepth === true || renderStencil === true ) {
if ( renderDepth === true ) {
depthStencil.format = depthStencilFormat;
depthStencil.depthWriteEnabled = material.depthWrite;
depthStencil.depthCompare = depthCompare;
}
if ( renderStencil === true ) {
depthStencil.stencilFront = stencilFront;
depthStencil.stencilBack = stencilFront; // apply the same stencil ops to both faces, matching gl.stencilOp() which is not face-separated
depthStencil.stencilReadMask = material.stencilFuncMask;
depthStencil.stencilWriteMask = material.stencilWriteMask;
}
if ( material.polygonOffset === true && ( primitiveState.topology === GPUPrimitiveTopology.TriangleList ) ) {
depthStencil.depthBias = material.polygonOffsetUnits;
depthStencil.depthBiasSlopeScale = material.polygonOffsetFactor;
depthStencil.depthBiasClamp = 0; // three.js does not provide an API to configure this value
}
_renderPipelineDescriptor.depthStencil = depthStencil;
}
// create pipeline
device.pushErrorScope( 'validation' );
const stages = [
{ program: vertexProgram, module: vertexModule.module },
{ program: fragmentProgram, module: fragmentModule.module }
];
const pipelineLabel = _renderPipelineDescriptor.label;
if ( promises === null ) {
pipelineData.pipeline = device.createRenderPipeline( _renderPipelineDescriptor );
_renderPipelineDescriptor.reset();
device.popErrorScope().then( ( err ) => {
if ( err !== null ) {
pipelineData.error = true;
error( `WebGPURenderer: Render pipeline creation failed (${ pipelineLabel }): ${ err.message }` );
this._reportShaderDiagnostics( stages, pipelineLabel );
}
} );
} else {
const p = new Promise( async ( resolve /*, reject*/ ) => {
try {
let asyncError = null;
let pipelinePromise = null;
try {
pipelinePromise = device.createRenderPipelineAsync( _renderPipelineDescriptor );
} catch ( err ) {
asyncError = err;
}
_renderPipelineDescriptor.reset();
if ( pipelinePromise !== null ) {
try {
pipelineData.pipeline = await pipelinePromise;
} catch ( err ) {
asyncError = err;
}
}
const errorScope = await device.popErrorScope();
if ( errorScope !== null || asyncError !== null ) {
pipelineData.error = true;
const reason = ( errorScope && errorScope.message ) || ( asyncError && asyncError.message ) || 'unknown';
error( `WebGPURenderer: Async render pipeline creation failed (${ pipelineLabel }): ${ reason }` );
await this._reportShaderDiagnostics( stages, pipelineLabel );
}
} finally {
// Guarantee resolution so `compileAsync`'s Promise.all cannot hang on an
// unexpected throw from any await above.
resolve();
}
} );
promises.push( p );
}
}
/**
* Creates GPU render bundle encoder for the given render context.
*
* @param {RenderContext} renderContext - The render context.
* @param {?string} [label='renderBundleEncoder'] - The label.
* @return {GPURenderBundleEncoder} The GPU render bundle encoder.
*/
createBundleEncoder( renderContext, label = 'renderBundleEncoder' ) {
const backend = this.backend;
const { utils, device } = backend;
const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderContext );
const colorFormats = utils.getCurrentColorFormats( renderContext );
const sampleCount = this._getSampleCount( renderContext );
_renderBundleEncoderDescriptor.label = label;
_renderBundleEncoderDescriptor.colorFormats = colorFormats;
_renderBundleEncoderDescriptor.depthStencilFormat = depthStencilFormat;
_renderBundleEncoderDescriptor.sampleCount = sampleCount;
const bundleEncoder = device.createRenderBundleEncoder( _renderBundleEncoderDescriptor );
_renderBundleEncoderDescriptor.reset();
return bundleEncoder;
}
/**
* Creates a compute pipeline for the given compute node.
*
* @param {ComputePipeline} pipeline - The compute pipeline.
* @param {Array<BindGroup>} bindings - The bindings.
*/
createComputePipeline( pipeline, bindings ) {
const backend = this.backend;
const device = backend.device;
const computeProgram = backend.get( pipeline.computeProgram ).module;
const pipelineGPU = backend.get( pipeline );
// bind group layouts
const bindGroupLayouts = [];
for ( const bindingsGroup of bindings ) {
const bindingsData = backend.get( bindingsGroup );
const { layoutGPU } = bindingsData.layout;
bindGroupLayouts.push( layoutGPU );
}
const computeStage = pipeline.computeProgram;
const pipelineLabel = `computePipeline_${ computeStage.stage }${ computeStage.name ? `_${ computeStage.name }` : '' }`;
device.pushErrorScope( 'validation' );
_pipelineLayoutDescriptor.bindGroupLayouts = bindGroupLayouts;
const pipelineLayout = device.createPipelineLayout( _pipelineLayoutDescriptor );
_pipelineLayoutDescriptor.reset();
_computePipelineDescriptor.label = pipelineLabel;
_computePipelineDescriptor.compute = computeProgram;
_computePipelineDescriptor.layout = pipelineLayout;
pipelineGPU.pipeline = device.createComputePipeline( _computePipelineDescriptor );
_computePipelineDescriptor.reset();
device.popErrorScope().then( ( err ) => {
if ( err !== null ) {
pipelineGPU.error = true;
error( `WebGPURenderer: Compute pipeline creation failed (${ pipelineLabel }): ${ err.message }` );
this._reportShaderDiagnostics( [ { program: computeStage, module: computeProgram.module } ], pipelineLabel );
}
} );
}
/**
* Reads line-accurate diagnostics from shader modules and logs them.
* Called from pipeline creation error paths to turn opaque validation
* failures into actionable WGSL feedback.
*
* @private
* @param {Array<{program: ProgrammableStage, module: GPUShaderModule}>} stages - Pairs of program + compiled shader module.
* @param {string} pipelineLabel - Label of the owning pipeline, used as log prefix.
* @return {Promise<void>}
*/
async _reportShaderDiagnostics( stages, pipelineLabel ) {
for ( const { program, module } of stages ) {
const info = await module.getCompilationInfo();
if ( info.messages.length === 0 ) continue;
const sourceLines = program.code.split( '\n' );
for ( const msg of info.messages ) {
const location = msg.lineNum > 0
? ` at line ${ msg.lineNum }${ msg.linePos > 0 ? `:${ msg.linePos }` : '' }`
: '';
const header = `WebGPURenderer [${ pipelineLabel } / ${ program.stage } ${ msg.type }]${ location }: ${ msg.message }`;
let excerpt = '';
if ( msg.lineNum > 0 && msg.lineNum <= sourceLines.length ) {
excerpt = `\n ${ sourceLines[ msg.lineNum - 1 ] }`;
if ( msg.linePos > 0 ) excerpt += `\n ${ ' '.repeat( msg.linePos - 1 ) }^`;
}
( msg.type === 'error' ? error : warn )( header + excerpt );
}
}
}
/**
* Returns the blending state as a descriptor object required
* for the pipeline creation.
*
* @private
* @param {Material|BlendMode} object - The object containing blending information.
* @return {Object} The blending state.
*/
_getBlending( object ) {
let color, alpha;
const blending = object.blending;
const blendSrc = object.blendSrc;
const blendDst = object.blendDst;
const blendEquation = object.blendEquation;
if ( blending === CustomBlending ) {
const blendSrcAlpha = object.blendSrcAlpha !== null ? object.blendSrcAlpha : blendSrc;
const blendDstAlpha = object.blendDstAlpha !== null ? object.blendDstAlpha : blendDst;
const blendEquationAlpha = object.blendEquationAlpha !== null ? object.blendEquationAlpha : blendEquation;
color = {
srcFactor: this._getBlendFactor( blendSrc ),
dstFactor: this._getBlendFactor( blendDst ),
operation: this._getBlendOperation( blendEquation )
};
alpha = {
srcFactor: this._getBlendFactor( blendSrcAlpha ),
dstFactor: this._getBlendFactor( blendDstAlpha ),
operation: this._getBlendOperation( blendEquationAlpha )
};
} else {
const premultipliedAlpha = object.premultipliedAlpha;
const setBlend = ( srcRGB, dstRGB, srcAlpha, dstAlpha ) => {
color = {
srcFactor: srcRGB,
dstFactor: dstRGB,
operation: GPUBlendOperation.Add
};
alpha = {
srcFactor: srcAlpha,
dstFactor: dstAlpha,
operation: GPUBlendOperation.Add
};
};
if ( premultipliedAlpha ) {
switch ( blending ) {
case NormalBlending:
setBlend( GPUBlendFactor.One, GPUBlendFactor.OneMinusSrcAlpha, GPUBlendFactor.One, GPUBlendFactor.OneMinusSrcAlpha );
break;
case AdditiveBlending:
setBlend( GPUBlendFactor.One, GPUBlendFactor.One, GPUBlendFactor.One, GPUBlendFactor.One );
break;
case SubtractiveBlending:
setBlend( GPUBlendFactor.Zero, GPUBlendFactor.OneMinusSrc, GPUBlendFactor.Zero, GPUBlendFactor.One );
break;
case MultiplyBlending:
setBlend( GPUBlendFactor.Dst, GPUBlendFactor.OneMinusSrcAlpha, GPUBlendFactor.Zero, GPUBlendFactor.One );
break;
}
} else {
switch ( blending ) {
case NormalBlending:
setBlend( GPUBlendFactor.SrcAlpha, GPUBlendFactor.OneMinusSrcAlpha, GPUBlendFactor.One, GPUBlendFactor.OneMinusSrcAlpha );
break;
case AdditiveBlending:
setBlend( GPUBlendFactor.SrcAlpha, GPUBlendFactor.One, GPUBlendFactor.One, GPUBlendFactor.One );
break;
case SubtractiveBlending:
error( `WebGPURenderer: "SubtractiveBlending" requires "${ object.isMaterial ? 'material' : 'blendMode' }.premultipliedAlpha = true".` );
break;
case MultiplyBlending:
error( `WebGPURenderer: "MultiplyBlending" requires "${ object.isMaterial ? 'material' : 'blendMode' }.premultipliedAlpha = true".` );
break;
}
}
}
if ( color !== undefined && alpha !== undefined ) {
return { color, alpha };
} else {
error( 'WebGPURenderer: Invalid blending: ', blending );
}
}
/**
* Returns the GPU blend factor which is required for the pipeline creation.
*
* @private
* @param {number} blend - The blend factor as a three.js constant.
* @return {string} The GPU blend factor.
*/
_getBlendFactor( blend ) {
let blendFactor;
switch ( blend ) {
case ZeroFactor:
blendFactor = GPUBlendFactor.Zero;
break;
case OneFactor:
blendFactor = GPUBlendFactor.One;
break;
case SrcColorFactor:
blendFactor = GPUBlendFactor.Src;
break;
case OneMinusSrcColorFactor:
blendFactor = GPUBlendFactor.OneMinusSrc;
break;
case SrcAlphaFactor:
blendFactor = GPUBlendFactor.SrcAlpha;
break;
case OneMinusSrcAlphaFactor:
blendFactor = GPUBlendFactor.OneMinusSrcAlpha;
break;
case DstColorFactor:
blendFactor = GPUBlendFactor.Dst;
break;
case OneMinusDstColorFactor:
blendFactor = GPUBlendFactor.OneMinusDst;
break;
case DstAlphaFactor:
blendFactor = GPUBlendFactor.DstAlpha;
break;
case OneMinusDstAlphaFactor:
blendFactor = GPUBlendFactor.OneMinusDstAlpha;
break;
case SrcAlphaSaturateFactor:
blendFactor = GPUBlendFactor.SrcAlphaSaturated;
break;
case BlendColorFactor:
blendFactor = GPUBlendFactor.Constant;
break;
case OneMinusBlendColorFactor:
blendFactor = GPUBlendFactor.OneMinusConstant;
break;
default:
error( 'WebGPURenderer: Blend factor not supported.', blend );
}
return blendFactor;
}
/**
* Returns the GPU stencil compare function which is required for the pipeline creation.
*
* @private
* @param {Material} material - The material.
* @return {string} The GPU stencil compare function.
*/
_getStencilCompare( material ) {
let stencilCompare;
const stencilFunc = material.stencilFunc;
switch ( stencilFunc ) {
case NeverStencilFunc:
stencilCompare = GPUCompareFunction.Never;
break;
case AlwaysStencilFunc:
stencilCompare = GPUCompareFunction.Always;
break;
case LessStencilFunc:
stencilCompare = GPUCompareFunction.Less;
break;
case LessEqualStencilFunc:
stencilCompare = GPUCompareFunction.LessEqual;
break;
case EqualStencilFunc:
stencilCompare = GPUCompareFunction.Equal;
break;
case GreaterEqualStencilFunc:
stencilCompare = GPUCompareFunction.GreaterEqual;
break;
case GreaterStencilFunc:
stencilCompare = GPUCompareFunction.Greater;
break;
case NotEqualStencilFunc:
stencilCompare = GPUCompareFunction.NotEqual;
break;
default:
error( 'WebGPURenderer: Invalid stencil function.', stencilFunc );
}
return stencilCompare;
}
/**
* Returns the GPU stencil operation which is required for the pipeline creation.
*
* @private
* @param {number} op - A three.js constant defining the stencil operation.
* @return {string} The GPU stencil operation.
*/
_getStencilOperation( op ) {
let stencilOperation;
switch ( op ) {
case KeepStencilOp:
stencilOperation = GPUStencilOperation.Keep;
break;
case ZeroStencilOp:
stencilOperation = GPUStencilOperation.Zero;
break;
case ReplaceStencilOp:
stencilOperation = GPUStencilOperation.Replace;
break;
case InvertStencilOp:
stencilOperation = GPUStencilOperation.Invert;
break;
case IncrementStencilOp:
stencilOperation = GPUStencilOperation.IncrementClamp;
break;
case DecrementStencilOp:
stencilOperation = GPUStencilOperation.DecrementClamp;
break;
case IncrementWrapStencilOp:
stencilOperation = GPUStencilOperation.IncrementWrap;
break;
case DecrementWrapStencilOp:
stencilOperation = GPUStencilOperation.DecrementWrap;
break;
default:
error( 'WebGPURenderer: Invalid stencil operation.', stencilOperation );
}
return stencilOperation;
}
/**
* Returns the GPU blend operation which is required for the pipeline creation.
*
* @private
* @param {number} blendEquation - A three.js constant defining the blend equation.
* @return {string} The GPU blend operation.
*/
_getBlendOperation( blendEquation ) {
let blendOperation;
switch ( blendEquation ) {
case AddEquation:
blendOperation = GPUBlendOperation.Add;
break;
case SubtractEquation:
blendOperation = GPUBlendOperation.Subtract;
break;
case ReverseSubtractEquation:
blendOperation = GPUBlendOperation.ReverseSubtract;
break;
case MinEquation:
blendOperation = GPUBlendOperation.Min;
break;
case MaxEquation:
blendOperation = GPUBlendOperation.Max;
break;
default:
error( 'WebGPUPipelineUtils: Blend equation not supported.', blendEquation );
}
return blendOperation;
}
/**
* Returns the primitive state as a descriptor object required
* for the pipeline creation.
*
* @private
* @param {Object3D} object - The 3D object.
* @param {BufferGeometry} geometry - The geometry.
* @param {Material} material - The material.
* @return {Object} The primitive state.
*/
_getPrimitiveState( object, geometry, material ) {
const descriptor = {};
const utils = this.backend.utils;
//
descriptor.topology = utils.getPrimitiveTopology( object, material );
if ( geometry.index !== null && object.isLine === true && object.isLineSegments !== true ) {
descriptor.stripIndexFormat = ( geometry.index.array instanceof Uint16Array ) ? GPUIndexFormat.Uint16 : GPUIndexFormat.Uint32;
}
//
let flipSided = ( material.side === BackSide );
if ( object.isMesh && object.matrixWorld.determinantAffine() < 0 ) flipSided = ! flipSided;
descriptor.frontFace = ( flipSided === true ) ? GPUFrontFace.CW : GPUFrontFace.CCW;
//
descriptor.cullMode = ( material.side === DoubleSide ) ? GPUCullMode.None : GPUCullMode.Back;
return descriptor;
}
/**
* Returns the GPU color write mask which is required for the pipeline creation.
*
* @private
* @param {Material} material - The material.
* @return {number} The GPU color write mask.
*/
_getColorWriteMask( material ) {
return ( material.colorWrite === true ) ? GPUColorWriteFlags.All : GPUColorWriteFlags.None;
}
/**
* Returns the GPU depth compare function which is required for the pipeline creation.
*
* @private
* @param {Material} material - The material.
* @return {string} The GPU depth compare function.
*/
_getDepthCompare( material ) {
let depthCompare;
if ( material.depthTest === false ) {
depthCompare = GPUCompareFunction.Always;
} else {
const depthFunc = ( this.backend.parameters.reversedDepthBuffer ) ? ReversedDepthFuncs[ material.depthFunc ] : material.depthFunc;
switch ( depthFunc ) {
case NeverDepth:
depthCompare = GPUCompareFunction.Never;
break;
case AlwaysDepth:
depthCompare = GPUCompareFunction.Always;
break;
case LessDepth:
depthCompare = GPUCompareFunction.Less;
break;
case LessEqualDepth:
depthCompare = GPUCompareFunction.LessEqual;
break;
case EqualDepth:
depthCompare = GPUCompareFunction.Equal;
break;
case GreaterEqualDepth:
depthCompare = GPUCompareFunction.GreaterEqual;
break;
case GreaterDepth:
depthCompare = GPUCompareFunction.Greater;
break;
case NotEqualDepth:
depthCompare = GPUCompareFunction.NotEqual;
break;
default:
error( 'WebGPUPipelineUtils: Invalid depth function.', depthFunc );
}
}
return depthCompare;
}
}
export default WebGPUPipelineUtils;