three
Version:
JavaScript 3D library
53,715 lines • 1.35 MB
JavaScript
/**
* @license
* Copyright 2010-2026 Three.js Authors
* SPDX-License-Identifier: MIT
*/
import { Material, LineBasicMaterial, LineDashedMaterial, NoBlending, warnOnce, MeshNormalMaterial, SRGBColorSpace, CubeTexture, EquirectangularReflectionMapping, EquirectangularRefractionMapping, CubeReflectionMapping, CubeRefractionMapping, warn, AddOperation, MixOperation, MultiplyOperation, MeshBasicMaterial, MeshLambertMaterial, MeshPhongMaterial, BackSide, MeshStandardMaterial, MeshPhysicalMaterial, MeshToonMaterial, MeshMatcapMaterial, SpriteMaterial, PointsMaterial, Vector2, ShadowMaterial, DynamicDrawUsage, Uint32BufferAttribute, Uint16BufferAttribute, error, ByteType, UnsignedByteType, ShortType, UnsignedShortType, HalfFloatType, IntType, UnsignedIntType, FloatType, AlphaFormat, RedFormat, RedIntegerFormat, DepthFormat, DepthStencilFormat, RGFormat, RGIntegerFormat, RGBFormat, RGBIntegerFormat, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedInt248Type, UnsignedInt5999Type, UnsignedInt101111Type, DoubleSide, Vector4, DepthTexture, ColorManagement, SRGBTransfer, Vector3, Mesh, SphereGeometry, Color, Matrix2, Matrix3, Matrix4, NormalBlending, RenderTarget, LinearFilter, LinearMipmapNearestFilter, NearestMipmapLinearFilter, LinearMipmapLinearFilter, Float16BufferAttribute, yieldToMain, REVISION, Plane, EventDispatcher, PerspectiveCamera, ArrayCamera, PlaneGeometry, RGBAFormat, FrontSide, CustomBlending, AddEquation, ZeroFactor, CylinderGeometry, Quaternion, WebXRController, RAD2DEG, FramebufferTexture, Frustum, FrustumArray, NoToneMapping, PCFShadowMap, PCFSoftShadowMap, Scene, RenderObjectRefreshType, VSMShadowMap, NoColorSpace, DataTexture, RGBAIntegerFormat, TimestampQuery, createCanvasElement, MaxEquation, MinEquation, ReverseSubtractEquation, SubtractEquation, OneMinusConstantAlphaFactor, ConstantAlphaFactor, OneMinusConstantColorFactor, ConstantColorFactor, OneMinusDstAlphaFactor, OneMinusDstColorFactor, OneMinusSrcAlphaFactor, OneMinusSrcColorFactor, DstAlphaFactor, DstColorFactor, SrcAlphaSaturateFactor, SrcAlphaFactor, SrcColorFactor, OneFactor, CullFaceNone, CullFaceBack, CullFaceFront, MaterialBlending, MultiplyBlending, SubtractiveBlending, AdditiveBlending, NotEqualDepth, GreaterDepth, GreaterEqualDepth, EqualDepth, LessEqualDepth, LessDepth, AlwaysDepth, NeverDepth, ReversedDepthFuncs, RGB_S3TC_DXT1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGB_PVRTC_4BPPV1_Format, RGB_PVRTC_2BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGBA_PVRTC_2BPPV1_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGBA_ETC2_EAC_Format, R11_EAC_Format, SIGNED_R11_EAC_Format, RG11_EAC_Format, SIGNED_RG11_EAC_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_10x10_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_BPTC_Format, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RED_RGTC1_Format, SIGNED_RED_RGTC1_Format, RED_GREEN_RGTC2_Format, SIGNED_RED_GREEN_RGTC2_Format, MirroredRepeatWrapping, ClampToEdgeWrapping, RepeatWrapping, NearestMipmapNearestFilter, NearestFilter, NotEqualCompare, GreaterCompare, GreaterEqualCompare, EqualCompare, LessEqualCompare, LessCompare, AlwaysCompare, NeverCompare, LinearTransfer, getByteLength, WebGLCoordinateSystem, Compatibility, isTypedArray, Texture, NotEqualStencilFunc, GreaterStencilFunc, GreaterEqualStencilFunc, EqualStencilFunc, LessEqualStencilFunc, LessStencilFunc, AlwaysStencilFunc, NeverStencilFunc, DecrementWrapStencilOp, IncrementWrapStencilOp, DecrementStencilOp, IncrementStencilOp, InvertStencilOp, ReplaceStencilOp, ZeroStencilOp, KeepStencilOp, WebGPUCoordinateSystem, SpotLight, PointLight, DirectionalLight, RectAreaLight, AmbientLight, HemisphereLight, LightProbe, LinearToneMapping, ReinhardToneMapping, CineonToneMapping, ACESFilmicToneMapping, AgXToneMapping, NeutralToneMapping, Group, Loader, FileLoader, MaterialLoader, ObjectLoader } from './three.core.js';
export { AdditiveAnimationBlendMode, AnimationAction, AnimationClip, AnimationLoader, AnimationMixer, AnimationObjectGroup, AnimationUtils, ArcCurve, ArrowHelper, AttachedBindMode, Audio, AudioAnalyser, AudioContext, AudioListener, AudioLoader, AxesHelper, BasicDepthPacking, BasicShadowMap, BatchedMesh, BezierInterpolant, Bone, BooleanKeyframeTrack, Box2, Box3, Box3Helper, BoxGeometry, BoxHelper, BufferAttribute, BufferGeometry, BufferGeometryLoader, Cache, Camera, CameraHelper, CanvasTexture, CapsuleGeometry, CatmullRomCurve3, CircleGeometry, Clock, ColorKeyframeTrack, CompressedArrayTexture, CompressedCubeTexture, CompressedTexture, CompressedTextureLoader, ConeGeometry, Controls, CubeCamera, CubeDepthTexture, CubeTextureLoader, CubicBezierCurve, CubicBezierCurve3, CubicInterpolant, CullFaceFrontBack, Curve, CurvePath, CustomToneMapping, Cylindrical, Data3DTexture, DataArrayTexture, DataTextureLoader, DataUtils, DefaultLoadingManager, DetachedBindMode, DirectionalLightHelper, DiscreteInterpolant, DodecahedronGeometry, DynamicCopyUsage, DynamicReadUsage, EdgesGeometry, EllipseCurve, Euler, ExternalTexture, ExtrudeGeometry, Float32BufferAttribute, Fog, FogExp2, GLBufferAttribute, GLSL1, GLSL3, GridHelper, HTMLTexture, HemisphereLightHelper, IcosahedronGeometry, ImageBitmapLoader, ImageLoader, ImageUtils, InstancedBufferAttribute, InstancedBufferGeometry, InstancedInterleavedBuffer, InstancedMesh, Int16BufferAttribute, Int32BufferAttribute, Int8BufferAttribute, InterleavedBuffer, InterleavedBufferAttribute, Interpolant, InterpolateBezier, InterpolateDiscrete, InterpolateLinear, InterpolateSmooth, InterpolationSamplingMode, InterpolationSamplingType, KeyframeTrack, LOD, LatheGeometry, Layers, Light, LightShadow, Line, Line3, LineCurve, LineCurve3, LineLoop, LineSegments, LinearInterpolant, LinearMipMapLinearFilter, LinearMipMapNearestFilter, LinearSRGBColorSpace, LoaderUtils, LoadingManager, LoopOnce, LoopPingPong, LoopRepeat, MOUSE, MathUtils, MeshDepthMaterial, MeshDistanceMaterial, NearestMipMapLinearFilter, NearestMipMapNearestFilter, NoNormalPacking, NormalAnimationBlendMode, NormalGAPacking, NormalRGPacking, NumberKeyframeTrack, Object3D, ObjectSpaceNormalMap, OctahedronGeometry, OrthographicCamera, Path, PlaneHelper, PointLightHelper, Points, PolarGridHelper, PolyhedronGeometry, PositionalAudio, PropertyBinding, PropertyMixer, QuadraticBezierCurve, QuadraticBezierCurve3, QuaternionKeyframeTrack, QuaternionLinearInterpolant, RGBADepthPacking, RGBDepthPacking, RGDepthPacking, RawShaderMaterial, Ray, Raycaster, RenderTarget3D, RingGeometry, ShaderMaterial, Shape, ShapeGeometry, ShapePath, ShapeUtils, Skeleton, SkeletonHelper, SkinnedMesh, Source, Sphere, Spherical, SphericalHarmonics3, SplineCurve, SpotLightHelper, Sprite, StaticCopyUsage, StaticDrawUsage, StaticReadUsage, StereoCamera, StreamCopyUsage, StreamDrawUsage, StreamReadUsage, StringKeyframeTrack, TOUCH, TangentSpaceNormalMap, TetrahedronGeometry, TextureLoader, TextureSource, TextureUtils, Timer, TorusGeometry, TorusKnotGeometry, Triangle, TriangleFanDrawMode, TriangleStripDrawMode, TrianglesDrawMode, TubeGeometry, UVMapping, Uint8BufferAttribute, Uint8ClampedBufferAttribute, Uniform, UniformsGroup, VectorKeyframeTrack, VideoFrameTexture, VideoTexture, WebGL3DRenderTarget, WebGLArrayRenderTarget, WebGLRenderTarget, WireframeGeometry, WrapAroundEnding, ZeroCurvatureEnding, ZeroSlopeEnding, getConsoleFunction, log, setConsoleFunction } from './three.core.js';
import { NodeMaterial, float, materialLineDashOffset, materialLineScale, materialLineDashSize, materialLineGapSize, dashSize, gapSize, varying, attribute, varyingProperty, Fn, cameraProjectionMatrix, vec2, vec4, modelViewMatrix, viewport, If, positionGeometry, mix, vec3, materialLineWidth, screenDPR, uv, smoothstep, diffuseColor, viewportOpaqueMipTexture, modelWorldMatrixInverse, cameraWorldMatrix, cameraProjectionMatrixInverse, positionLocal, positionPrevious, materialOpacity, colorSpaceToWorking, packNormalToRGB, normalView, Node, cubeTexture, NodeUpdateType, CubeRenderTarget, nodeProxy, LightingNode, materialEnvRotation, materialSpecularStrength, materialReflectivity, negateOnBackSide, normalViewGeometry, materialLightMap, BRDF_Lambert, positionViewDirection, F_Schlick, specularColor, shininess, materialShininess, materialSpecular, iridescenceThickness, iridescenceIOR, metalness, Schlick_to_F0, cameraPosition, positionWorld, normalWorld, roughness, diffuseContribution, specularColorBlended, specularF90, modelWorldMatrix, cameraViewMatrix, ior, thickness, attenuationColor, attenuationDistance, dispersion, transmission, DFGLUT, EON_DirectionalAlbedo, diffuseRoughness, iridescence, BRDF_Sheen, sheenRoughness, sheen, clearcoatNormalView, BRDF_GGX, clearcoatRoughness, retroreflectivity, BRDF_EON, positionView, LTC_Uv, mat3, LTC_Evaluate, EnvironmentBRDF, clearcoat, Loop, length, refract, normalize, div, cameraViewport, screenSize, log2, textureBicubicLevel, log, exp, viewportMipTexture, clamp, pmremTexture, isolate, materialEnvIntensity, pow4, bentNormalView, materialMetalness, materialRoughness, getRoughness, materialIOR, materialSpecularColor, materialSpecularIntensity, min, pow2, materialDiffuseRoughness, materialClearcoat, materialClearcoatRoughness, materialSheen, materialSheenRoughness, materialRetroreflectivity, materialIridescence, materialIridescenceIOR, materialIridescenceThickness, materialAnisotropy, anisotropy, alphaT, anisotropyT, TBNViewMatrix, anisotropyB, materialTransmission, materialThickness, materialAttenuationDistance, materialAttenuationColor, materialDispersion, materialClearcoatNormal, subBuild, materialReference, matcapUV, reference$1 as reference, materialRotation, rotate, materialPointSize, viewportSize, uniform, renderGroup, property, modelRadius, linearDepth, viewZToPerspectiveDepth, cameraFar, cameraNear, LTC_Evaluate_Volume, hashString, hashArray, ChainMap, Color4, backgroundIntensity, backgroundBlurriness, backgroundRotation, normalWorldGeometry, stack, getTextureType, shaderStages, mergeContext, getTypeFromLength, setCurrentStack, FunctionNode, ParameterNode, isArrayAsParameter, defaultBuildStages, getCurrentStack, AnalyticLightNode, lightTargetDirection, lightPosition, lightProjectionUV, getDistanceAttenuation, texture, textureSize, remap, atan, uniformArray, getShIrradianceAt, lightShadowMatrix, sub, acos, saturate, max, lightViewPosition, StackTrace, screenUV, reference as reference$1, fog, densityFogFactor, rangeFogFactor, builtin, LightsNode, context, renderOutput, InspectorBase, highpModelViewMatrix, highpModelNormalViewMatrix, QuadMesh, CodeNode, TextureNode, vectorComponents, NodeAccess, VarNode, ExpressionNode, PointLightNode, linearToneMapping, reinhardToneMapping, cineonToneMapping, acesFilmicToneMapping, agxToneMapping, neutralToneMapping, output, StorageBufferAttribute } from './three.tsl.js';
export { AONode, ArrayElementNode, ArrayNode, AssignNode, AtomicFunctionNode, AttributeNode, BarrierNode, BitcastNode, BitcountNode, BlendMode, BufferAttributeNode, BufferNode, BuiltinNode, BumpMapNode, BypassNode, ClippingNode, ColorSpaceNode, ComputeBuiltinNode, ComputeNode, ConditionalNode, ConstNode, ContextNode, ConvertNode, CubeTextureNode, DebugNode, EventNode, FlipNode, FrontFacingNode, FunctionCallNode, FunctionOverloadingNode, IndexNode, InputNode, InspectorNode, IrradianceNode, IsolateNode, JoinNode, LightingContextNode, LoopNode, MRTNode, MaterialNode, MaterialReferenceNode, MathNode, MaxMipLevelNode, MemberNode, ModelNode, NodeError, NodeMaterialObserver, NodeShaderStage, NodeType, NodeUtils, NormalMapNode, Object3DNode, OperatorNode, OutputStructNode, OverrideContextNode, PMREMGenerator, PMREMNode, PackFloatNode, Packed4x8IntegerNode, PassNode, PointShadowNode, PointUVNode, PropertyNode, RTTNode, RangeNode, ReferenceAttributeNode, ReferenceBaseNode, ReferenceElementNode, ReferenceNode, ReflectorNode, RenderOutputNode, RendererReferenceNode, RendererUtils, RotateNode, SampleNode, ScreenNode, SetNode, ShadowBaseNode, ShadowNode, SplitNode, StackNode, StorageArrayElementNode, StorageBufferNode, StorageInstancedBufferAttribute, StorageTexture3DNode, StorageTextureNode, StructNode, StructTypeNode, SubBuildNode, SubgroupFunctionNode, Three_TSL as TSL, Texture3DNode, TextureSizeNode, ToneMappingNode, ToonOutlinePassNode, UniformArrayNode, UniformGroupNode, UniformNode, UnpackFloatNode, UserDataNode, VaryingNode, VelocityNode, VertexColorNode, ViewportDepthNode, ViewportDepthTextureNode, ViewportSharedTextureNode, ViewportTextureNode, WorkgroupInfoNode, defaultShaderStages } from './three.tsl.js';
/**
* A material instance that reuses the shader of a node material with its own properties.
* All instances created from the same node material share a single shader build, so
* configure the node material before creating instances. Instance values are read in
* the shared node graph with `materialReference( name, type )`. Node properties such as
* `colorNode` are read-only and always read from the node material.
*
* ```js
* const nodeMaterial = new MeshBasicNodeMaterial();
* nodeMaterial.colorNode = materialReference( 'myColor', 'color' );
*
* const material = new NodeMaterialInstance( nodeMaterial );
* material.myColor = new Color( 1, 0, 0 );
* ```
*
* @augments Material
*/
class NodeMaterialInstance extends Material {
/**
* Constructs a new node material instance.
*
* @param {NodeMaterial} nodeMaterial - The node material defining the shared shader.
* @throws {TypeError} When the given material is not a node material.
*/
constructor( nodeMaterial ) {
super();
if ( nodeMaterial?.isNodeMaterial !== true ) {
throw new TypeError( 'THREE.NodeMaterialInstance: The parameter must be a NodeMaterial.' );
}
copyProperties( this, nodeMaterial );
// Instance values often live in `userData`, so each instance starts with its own copy.
this.userData = cloneUserData( nodeMaterial.userData );
this.type = 'NodeMaterialInstance';
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isNodeMaterialInstance = true;
/**
* The node material defining the shared shader.
*
* @type {NodeMaterial}
*/
this.nodeMaterial = nodeMaterial;
}
/**
* Returns a cache key that identifies the shared shader, without traversing
* the node graph for each instance.
*
* @return {string} The custom program cache key.
*/
customProgramCacheKey() {
return this.nodeMaterial.uuid + ',' + this.nodeMaterial.version;
}
/**
* Builds the shader of the node material while keeping this instance
* as `builder.material`.
*
* @param {NodeBuilder} builder - The current node builder.
*/
build( builder ) {
this.nodeMaterial.build( builder );
}
/**
* The version of the node material. Setting `needsUpdate` on the node material
* updates all its instances, while setting it on an instance has no effect.
*
* @type {number}
*/
get version() {
return this.nodeMaterial.version;
}
set version( value ) {}
/**
* Copies the values of the given node material instance to this instance.
* Object values such as colors are shared with the source, except in `userData`.
* Unlike {@link Material#copy}, `userData` is not copied via JSON: its first level
* values are cloned when possible, so types such as colors are preserved. Textures
* remain shared.
*
* @param {NodeMaterialInstance} source - The material to copy.
* @return {NodeMaterialInstance} A reference to this instance.
*/
copy( source ) {
copyProperties( this, source );
this.userData = cloneUserData( source.userData );
return this;
}
/**
* Returns a new node material instance that shares the same node material.
*
* @return {NodeMaterialInstance} A clone of this instance.
*/
clone() {
return new this.constructor( this.nodeMaterial ).copy( this );
}
}
// Read-only accessors shared by all instances, so node properties always reflect the node material.
// They must be enumerable since the renderer detects node materials by iterating their properties.
const _nodeDescriptors = {};
function getNodeDescriptor( property ) {
return _nodeDescriptors[ property ] ??= {
enumerable: true,
get() {
return this.nodeMaterial[ property ];
}
};
}
// Copies the first level of user data, cloning values such as colors and sharing textures.
function cloneUserData( userData ) {
const result = {};
for ( const key in userData ) {
const value = userData[ key ];
result[ key ] = value?.clone !== undefined && value.isTexture !== true ? value.clone() : value;
}
return result;
}
// Copies configuration while preserving the target's identity and events.
function copyProperties( target, source ) {
for ( const property of Object.keys( source ) ) {
if ( property === 'uuid' || property === 'version' || property === '_listeners' ) continue;
if ( property.endsWith( 'Node' ) ) {
if ( Object.hasOwn( target, property ) === false ) Object.defineProperty( target, property, getNodeDescriptor( property ) );
} else {
target[ property ] = source[ property ];
}
}
}
const _defaultValues$d = /*@__PURE__*/ new LineBasicMaterial();
/**
* Node material version of {@link LineBasicMaterial}.
*
* @augments NodeMaterial
*/
class LineBasicNodeMaterial extends NodeMaterial {
static get type() {
return 'LineBasicNodeMaterial';
}
/**
* Constructs a new line basic node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isLineBasicNodeMaterial = true;
this.setDefaultValues( _defaultValues$d );
this.setValues( parameters );
}
}
const _defaultValues$c = /*@__PURE__*/ new LineDashedMaterial();
/**
* Node material version of {@link LineDashedMaterial}.
*
* @augments NodeMaterial
*/
class LineDashedNodeMaterial extends NodeMaterial {
static get type() {
return 'LineDashedNodeMaterial';
}
/**
* Constructs a new line dashed node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isLineDashedNodeMaterial = true;
this.setDefaultValues( _defaultValues$c );
/**
* The dash offset.
*
* @type {number}
* @default 0
*/
this.dashOffset = 0;
/**
* The offset of dash materials is by default inferred from the `dashOffset`
* property. This node property allows to overwrite the default
* and define the offset with a node instead.
*
* If you don't want to overwrite the offset but modify the existing
* value instead, use {@link materialLineDashOffset}.
*
* @type {?Node<float>}
* @default null
*/
this.offsetNode = null;
/**
* The scale of dash materials is by default inferred from the `scale`
* property. This node property allows to overwrite the default
* and define the scale with a node instead.
*
* If you don't want to overwrite the scale but modify the existing
* value instead, use {@link materialLineScale}.
*
* @type {?Node<float>}
* @default null
*/
this.dashScaleNode = null;
/**
* The dash size of dash materials is by default inferred from the `dashSize`
* property. This node property allows to overwrite the default
* and define the dash size with a node instead.
*
* If you don't want to overwrite the dash size but modify the existing
* value instead, use {@link materialLineDashSize}.
*
* @type {?Node<float>}
* @default null
*/
this.dashSizeNode = null;
/**
* The gap size of dash materials is by default inferred from the `gapSize`
* property. This node property allows to overwrite the default
* and define the gap size with a node instead.
*
* If you don't want to overwrite the gap size but modify the existing
* value instead, use {@link materialLineGapSize}.
*
* @type {?Node<float>}
* @default null
*/
this.gapSizeNode = null;
this.setValues( parameters );
}
/**
* Setups the dash specific node variables.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setupVariants( /* builder */ ) {
const offsetNode = this.offsetNode ? float( this.offsetNode ) : materialLineDashOffset;
const dashScaleNode = this.dashScaleNode ? float( this.dashScaleNode ) : materialLineScale;
const dashSizeNode = this.dashSizeNode ? float( this.dashSizeNode ) : materialLineDashSize;
const gapSizeNode = this.gapSizeNode ? float( this.gapSizeNode ) : materialLineGapSize;
dashSize.assign( dashSizeNode );
gapSize.assign( gapSizeNode );
const vLineDistance = varying( attribute( 'lineDistance' ).mul( dashScaleNode ) );
const vLineDistanceOffset = offsetNode ? vLineDistance.add( offsetNode ) : vLineDistance;
vLineDistanceOffset.mod( dashSize.add( gapSize ) ).greaterThan( dashSize ).discard();
}
}
const _defaultValues$b = /*@__PURE__*/ new LineDashedMaterial();
/**
* Varying node representing the world position of the segment start in view space.
* Used for distance and coordinate calculations across the fragment shader.
* @type {VaryingNode<vec3>}
*/
const worldStart = varyingProperty( 'vec3', 'worldStart' );
/**
* Varying node representing the world position of the segment end in view space.
* Used for distance and coordinate calculations across the fragment shader.
* @type {VaryingNode<vec3>}
*/
const worldEnd = varyingProperty( 'vec3', 'worldEnd' );
/**
* Varying node representing the accumulated distance along the line.
* Crucial for correctly computing dashed line intervals in fragment stage.
* @type {VaryingNode<float>}
*/
const lineDistance = varyingProperty( 'float', 'lineDistance' );
/**
* Varying node representing the interpolated world/view position of the current fragment.
* Used for line/ray distance checks under perspective projection.
* @type {VaryingNode<vec4>}
*/
const worldPos = varyingProperty( 'vec4', 'worldPos' );
/**
* Trims the line segment to avoid rendering behind the camera near plane.
* Computes an interpolation factor (alpha) to clamp the segment's coordinate.
*
* @param {Object} inputs
* @param {Node<vec4>} inputs.start - Segment start position in view space.
* @param {Node<vec4>} inputs.end - Segment end position in view space.
* @returns {Node<float>} The interpolation factor (alpha) to trim the segment.
*/
const trimSegmentAlpha = Fn( ( { start, end } ) => {
const a = cameraProjectionMatrix.element( 2 ).element( 2 ); // 3nd entry in 3th column
const b = cameraProjectionMatrix.element( 3 ).element( 2 ); // 3nd entry in 4th column
// we need different nearEstimate formula for reversed and default depth buffer
// a is positive with a reversed depth buffer so it can be used for controlling the code flow
const nearEstimate = a.greaterThan( 0 ).select( b.negate().div( a.add( 1 ) ), b.mul( -0.5 ).div( a ) );
return nearEstimate.sub( start.z ).div( end.z.sub( start.z ) );
}, { start: 'vec4', end: 'vec4', return: 'float' } );
/**
* Calculates the closest points on two 3D lines.
* Used for perspective-correct line rendering and coordinates interpolation.
*
* @param {Object} inputs
* @param {Node<vec3>} inputs.p1 - Start of line 1.
* @param {Node<vec3>} inputs.p2 - End of line 1.
* @param {Node<vec3>} inputs.p3 - Start of line 2.
* @param {Node<vec3>} inputs.p4 - End of line 2.
* @returns {Node<vec2>} A vec2 containing the parametric coordinates (mua, mub) of the closest points on line 1 and line 2.
*/
const closestLineToLine = Fn( ( { p1, p2, p3, p4 } ) => {
const p13 = p1.sub( p3 );
const p43 = p4.sub( p3 );
const p21 = p2.sub( p1 );
const d1343 = p13.dot( p43 );
const d4321 = p43.dot( p21 );
const d1321 = p13.dot( p21 );
const d4343 = p43.dot( p43 );
const d2121 = p21.dot( p21 );
const denom = d2121.mul( d4343 ).sub( d4321.mul( d4321 ) );
const numer = d1343.mul( d4321 ).sub( d1321.mul( d4343 ) );
const mua = numer.div( denom ).clamp();
const mub = d1343.add( d4321.mul( mua ) ).div( d4343 ).clamp();
return vec2( mua, mub );
}, { p1: 'vec3', p2: 'vec3', p3: 'vec3', p4: 'vec3', return: 'vec2' } );
/**
* TSL node acting as a custom Model-View-Projection (MVP) for fat lines,
* expanding 3D segments into screen/world-facing ribbons of a specified width.
*
* @tsl
* @type {Node<vec4>}
*/
const mvpLine = Fn( ( { material } ) => {
const useDash = material._useDash;
const useWorldUnits = material._useWorldUnits;
const instanceStart = attribute( 'instanceStart' );
const instanceEnd = attribute( 'instanceEnd' );
// camera space
const start = vec4( modelViewMatrix.mul( vec4( instanceStart, 1.0 ) ) ).toVar( 'start' );
const end = vec4( modelViewMatrix.mul( vec4( instanceEnd, 1.0 ) ) ).toVar( 'end' );
let distanceStart, distanceEnd;
if ( useDash ) {
distanceStart = float( attribute( 'instanceDistanceStart' ) ).toVar( 'distanceStart' );
distanceEnd = float( attribute( 'instanceDistanceEnd' ) ).toVar( 'distanceEnd' );
}
if ( useWorldUnits ) {
worldStart.assign( start.xyz );
worldEnd.assign( end.xyz );
}
const aspect = viewport.z.div( viewport.w );
// special case for perspective projection, and segments that terminate either in, or behind, the camera plane
// clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
// but we need to perform ndc-space calculations in the shader, so we must address this issue directly
// perhaps there is a more elegant solution -- WestLangley
const perspective = cameraProjectionMatrix.element( 2 ).element( 3 ).equal( -1 ); // 4th entry in the 3rd column
If( perspective, () => {
If( start.z.lessThan( 0.0 ).and( end.z.greaterThan( 0.0 ) ), () => {
const alpha = trimSegmentAlpha( { start, end } );
end.assign( vec4( mix( start.xyz, end.xyz, alpha ), end.w ) );
if ( useDash ) {
distanceEnd.assign( mix( distanceStart, distanceEnd, alpha ) );
}
} ).ElseIf( end.z.lessThan( 0.0 ).and( start.z.greaterThanEqual( 0.0 ) ), () => {
const alpha = trimSegmentAlpha( { start: end, end: start } );
start.assign( vec4( mix( end.xyz, start.xyz, alpha ), start.w ) );
if ( useDash ) {
distanceStart.assign( mix( distanceEnd, distanceStart, alpha ) );
}
} );
} );
if ( useDash ) {
const dashScaleNode = material.dashScaleNode ? float( material.dashScaleNode ) : materialLineScale;
const offsetNode = material.offsetNode ? float( material.offsetNode ) : materialLineDashOffset;
let lineDist = positionGeometry.y.lessThan( 0.5 ).select( dashScaleNode.mul( distanceStart ), dashScaleNode.mul( distanceEnd ) );
lineDist = lineDist.add( offsetNode );
lineDistance.assign( lineDist );
}
// clip space
const clipStart = cameraProjectionMatrix.mul( start );
const clipEnd = cameraProjectionMatrix.mul( end );
// ndc space
const ndcStart = clipStart.xyz.div( clipStart.w );
const ndcEnd = clipEnd.xyz.div( clipEnd.w );
// direction
const dir = ndcEnd.xy.sub( ndcStart.xy ).toVar();
// account for clip-space aspect ratio
dir.x.assign( dir.x.mul( aspect ) );
dir.assign( dir.normalize() );
const clip = vec4().toVar();
if ( useWorldUnits ) {
// get the offset direction as perpendicular to the view vector
const worldDir = end.xyz.sub( start.xyz ).normalize();
const tmpFwd = perspective.select( mix( start.xyz, end.xyz, 0.5 ).normalize(), vec3( 0.0, 0.0, -1 ) );
const worldUp = worldDir.cross( tmpFwd ).normalize();
const worldFwd = worldDir.cross( worldUp );
worldPos.assign( positionGeometry.y.lessThan( 0.5 ).select( start, end ) );
// height offset
const hw = materialLineWidth.mul( 0.5 );
worldPos.addAssign( vec4( positionGeometry.x.lessThan( 0.0 ).select( worldUp.mul( hw ), worldUp.mul( hw ).negate() ), 0 ) );
// don't extend the line if we're rendering dashes because we
// won't be rendering the endcaps
if ( ! useDash ) {
// cap extension
worldPos.addAssign( vec4( positionGeometry.y.lessThan( 0.5 ).select( worldDir.mul( hw ).negate(), worldDir.mul( hw ) ), 0 ) );
// add width to the box
worldPos.addAssign( vec4( worldFwd.mul( hw ), 0 ) );
// endcaps
If( positionGeometry.y.greaterThan( 1.0 ).or( positionGeometry.y.lessThan( 0.0 ) ), () => {
worldPos.subAssign( vec4( worldFwd.mul( 2.0 ).mul( hw ), 0 ) );
} );
}
// project the worldpos
clip.assign( cameraProjectionMatrix.mul( worldPos ) );
// shift the depth of the projected points so the line
// segments overlap neatly
const clipPose = vec3().toVar();
clipPose.assign( positionGeometry.y.lessThan( 0.5 ).select( ndcStart, ndcEnd ) );
If( clip.w.greaterThan( 0.0 ), () => {
// only shift the depth of vertices in front of the camera (#35137)
clip.z.assign( clipPose.z.mul( clip.w ) );
} );
} else {
const offset = vec2( dir.y, dir.x.negate() ).toVar( 'offset' );
// undo aspect ratio adjustment
dir.x.assign( dir.x.div( aspect ) );
offset.x.assign( offset.x.div( aspect ) );
// sign flip
offset.assign( positionGeometry.x.lessThan( 0.0 ).select( offset.negate(), offset ) );
// endcaps
If( positionGeometry.y.lessThan( 0.0 ), () => {
offset.assign( offset.sub( dir ) );
} ).ElseIf( positionGeometry.y.greaterThan( 1.0 ), () => {
offset.assign( offset.add( dir ) );
} );
// adjust for linewidth
offset.assign( offset.mul( materialLineWidth ) );
// adjust for clip-space to screen-space conversion // maybe resolution should be based on viewport ...
offset.assign( offset.div( viewport.w.div( screenDPR ) ) );
// select end
clip.assign( positionGeometry.y.lessThan( 0.5 ).select( clipStart, clipEnd ) );
// back to clip space
offset.assign( offset.mul( clip.w ) );
clip.assign( clip.add( vec4( offset, 0, 0 ) ) );
}
return clip;
} )();
/**
* TSL fragment node that computes the shape/coverage (alpha) of the fat line segment.
* Handles dash/gap generation, alpha-to-coverage rendering, and round endcaps.
*
* @tsl
* @type {Node<float>}
*/
const alphaLine = Fn( ( { material, renderer } ) => {
const useAlphaToCoverage = material._useAlphaToCoverage;
const useDash = material._useDash;
const useWorldUnits = material._useWorldUnits;
const vUv = uv();
if ( useDash ) {
const dashSizeNode = material.dashSizeNode ? float( material.dashSizeNode ) : materialLineDashSize;
const gapSizeNode = material.gapSizeNode ? float( material.gapSizeNode ) : materialLineGapSize;
dashSize.assign( dashSizeNode );
gapSize.assign( gapSizeNode );
vUv.y.lessThan( -1 ).or( vUv.y.greaterThan( 1.0 ) ).discard(); // discard endcaps
lineDistance.mod( dashSize.add( gapSize ) ).greaterThan( dashSize ).discard(); // todo - FIX
}
const alpha = float( 1 ).toVar( 'alpha' );
if ( useWorldUnits ) {
const len = float().toVar();
const orthographic = cameraProjectionMatrix.element( 2 ).element( 3 ).notEqual( -1 ).toConst();
If( orthographic, () => {
// View rays are parallel to the z axis so the distance reduces to camera-space XY
const lineDir = worldEnd.xy.sub( worldStart.xy );
const t = worldPos.xy.sub( worldStart.xy ).dot( lineDir ).div( lineDir.dot( lineDir ) ).clamp();
len.assign( worldStart.xy.add( lineDir.mul( t ) ).sub( worldPos.xy ).length() );
} ).Else( () => {
// Find the closest points on the view ray and the line segment
const rayEnd = worldPos.xyz.normalize().mul( 1e5 );
const lineDir = worldEnd.sub( worldStart );
const params = closestLineToLine( { p1: worldStart, p2: worldEnd, p3: vec3( 0.0, 0.0, 0.0 ), p4: rayEnd } );
const p1 = worldStart.add( lineDir.mul( params.x ) );
const p2 = rayEnd.mul( params.y );
const delta = p1.sub( p2 );
len.assign( delta.length() );
} );
const norm = len.div( materialLineWidth );
if ( ! useDash ) {
if ( useAlphaToCoverage && renderer.currentSamples > 0 ) {
const dnorm = norm.fwidth();
alpha.assign( smoothstep( dnorm.negate().add( 0.5 ), dnorm.add( 0.5 ), norm ).oneMinus() );
} else {
norm.greaterThan( 0.5 ).discard();
}
}
} else {
// round endcaps
if ( useAlphaToCoverage && renderer.currentSamples > 0 ) {
const a = vUv.x;
const b = vUv.y.greaterThan( 0.0 ).select( vUv.y.sub( 1.0 ), vUv.y.add( 1.0 ) );
const len2 = a.mul( a ).add( b.mul( b ) );
const dlen = float( len2.fwidth() ).toVar( 'dlen' );
If( vUv.y.abs().greaterThan( 1.0 ), () => {
alpha.assign( smoothstep( dlen.oneMinus(), dlen.add( 1 ), len2 ).oneMinus() );
} );
} else {
If( vUv.y.abs().greaterThan( 1.0 ), () => {
const a = vUv.x;
const b = vUv.y.greaterThan( 0.0 ).select( vUv.y.sub( 1.0 ), vUv.y.add( 1.0 ) );
const len2 = a.mul( a ).add( b.mul( b ) );
len2.greaterThan( 1.0 ).discard();
} );
}
}
return alpha;
} )();
/**
* This node material can be used to render lines with a size larger than one
* by representing them as instanced meshes.
*
* @augments NodeMaterial
*/
class Line2NodeMaterial extends NodeMaterial {
static get type() {
return 'Line2NodeMaterial';
}
/**
* Constructs a new node material for wide line rendering.
*
* @param {Object} [parameters={}] - The configuration parameter.
*/
constructor( parameters = {} ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isLine2NodeMaterial = true;
this.setDefaultValues( _defaultValues$b );
/**
* Whether vertex colors should be used or not.
*
* @type {boolean}
* @default false
*/
this.vertexColors = parameters.vertexColors;
/**
* The dash offset.
*
* @type {number}
* @default 0
*/
this.dashOffset = 0;
/**
* Defines the offset.
*
* @type {?Node<float>}
* @default null
*/
this.offsetNode = null;
/**
* Defines the dash scale.
*
* @type {?Node<float>}
* @default null
*/
this.dashScaleNode = null;
/**
* Defines the dash size.
*
* @type {?Node<float>}
* @default null
*/
this.dashSizeNode = null;
/**
* Defines the gap size.
*
* @type {?Node<float>}
* @default null
*/
this.gapSizeNode = null;
/**
* Blending is set to `NoBlending` since transparency
* is not supported, yet.
*
* @type {number}
* @default 0
*/
this.blending = NoBlending;
this._useDash = parameters.dashed;
this._useAlphaToCoverage = true;
this._useWorldUnits = false;
this.setValues( parameters );
}
/**
* Setups the diffuse color of the line material in the fragment stage.
* Overrides the base setup to incorporate line/dash rendering and blending.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setupDiffuseColor( builder ) {
super.setupDiffuseColor( builder );
diffuseColor.a.mulAssign( alphaLine );
if ( this.vertexColors === true && builder.geometry.hasAttribute( 'instanceColorStart' ) ) {
const instanceColorStart = attribute( 'instanceColorStart' );
const instanceColorEnd = attribute( 'instanceColorEnd' );
const instanceColor = positionGeometry.y.lessThan( 0.5 ).select( instanceColorStart, instanceColorEnd );
diffuseColor.rgb.mulAssign( instanceColor );
}
if ( this.transparent ) {
diffuseColor.rgb.assign( diffuseColor.rgb.mul( diffuseColor.a ).add( viewportOpaqueMipTexture().rgb.mul( diffuseColor.a.oneMinus() ) ) );
}
}
/**
* Setups the position of the expanded fat line vertex in local space.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node<vec3>} The position of the fat line vertex in local space.
*/
setupPosition( builder ) {
const localPosition = modelWorldMatrixInverse.mul( cameraWorldMatrix ).mul( cameraProjectionMatrixInverse ).mul( mvpLine );
positionLocal.assign( localPosition.xyz.div( localPosition.w ) );
if ( builder.needsPreviousData() ) {
positionPrevious.assign( positionLocal );
}
return super.setupPosition( builder );
}
/**
* Defines the lines color.
*
* @deprecated since r185. Use {@link NodeMaterial#colorNode} instead.
* @type {?Node<vec3>}
*/
get lineColorNode() {
return this.colorNode;
}
set lineColorNode( value ) {
warnOnce( 'Line2NodeMaterial: "lineColorNode" has been deprecated. Use "colorNode" instead.' ); // @deprecated r185
this.colorNode = value;
}
/**
* Whether the lines should sized in world units or not.
* When set to `false` the unit is pixel.
*
* @type {boolean}
* @default false
*/
get worldUnits() {
return this._useWorldUnits;
}
set worldUnits( value ) {
if ( this._useWorldUnits !== value ) {
this._useWorldUnits = value;
this.needsUpdate = true;
}
}
/**
* Whether the lines should be dashed or not.
*
* @type {boolean}
* @default false
*/
get dashed() {
return this._useDash;
}
set dashed( value ) {
if ( this._useDash !== value ) {
this._useDash = value;
this.needsUpdate = true;
}
}
/**
* Whether alpha to coverage should be used or not.
*
* @type {boolean}
* @default true
*/
get alphaToCoverage() {
return this._useAlphaToCoverage;
}
set alphaToCoverage( value ) {
if ( this._useAlphaToCoverage !== value ) {
this._useAlphaToCoverage = value;
this.needsUpdate = true;
}
}
}
const _defaultValues$a = /*@__PURE__*/ new MeshNormalMaterial();
/**
* Node material version of {@link MeshNormalMaterial}.
*
* @augments NodeMaterial
*/
class MeshNormalNodeMaterial extends NodeMaterial {
static get type() {
return 'MeshNormalNodeMaterial';
}
/**
* Constructs a new mesh normal node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMeshNormalNodeMaterial = true;
this.setDefaultValues( _defaultValues$a );
this.setValues( parameters );
}
/**
* Overwrites the default implementation by computing the diffuse color
* based on the normal data.
*/
setupDiffuseColor() {
const opacityNode = this.opacityNode ? float( this.opacityNode ) : materialOpacity;
// By convention, a normal packed to RGB is in sRGB color space. Convert it to working color space.
diffuseColor.assign( colorSpaceToWorking( vec4( packNormalToRGB( normalView ), opacityNode ), SRGBColorSpace ) );
}
}
const _cache = new WeakMap();
/**
* This node can be used to automatically convert environment maps in the
* equirectangular format into the cube map format.
*
* @augments Node
*/
class CubeMapNode extends Node {
static get type() {
return 'CubeMapNode';
}
/**
* Constructs a new cube map node.
*
* @param {Node} envNode - The node representing the environment map.
*/
constructor( envNode ) {
super( 'vec3' );
/**
* The node representing the environment map.
*
* @type {Node}
*/
this.envNode = envNode;
/**
* A reference to the internal cube texture.
*
* @private
* @type {?CubeTexture}
* @default null
*/
this._cubeTexture = null;
/**
* A reference to the internal cube texture node.
*
* @private
* @type {CubeTextureNode}
*/
this._cubeTextureNode = cubeTexture( null );
const defaultTexture = new CubeTexture();
defaultTexture.isRenderTargetTexture = true;
/**
* A default cube texture that acts as a placeholder.
* It is used when the conversion from equirectangular to cube
* map has not finished yet for a given texture.
*
* @private
* @type {CubeTexture}
*/
this._defaultTexture = defaultTexture;
/**
* The `updateBeforeType` is set to `NodeUpdateType.RENDER` since the node updates
* the texture once per render in its {@link CubeMapNode#updateBefore} method.
*
* @type {string}
* @default 'render'
*/
this.updateBeforeType = NodeUpdateType.RENDER;
}
updateBefore( frame ) {
const { renderer, material } = frame;
const envNode = this.envNode;
if ( envNode.isTextureNode || envNode.isMaterialReferenceNode ) {
const texture = ( envNode.isTextureNode ) ? envNode.value : material[ envNode.property ];
if ( texture && texture.isTexture ) {
const mapping = texture.mapping;
if ( mapping === EquirectangularReflectionMapping || mapping === EquirectangularRefractionMapping ) {
// check for converted cubemap map
if ( _cache.has( texture ) ) {
const cubeMap = _cache.get( texture );
mapTextureMapping( cubeMap, texture.mapping );
this._cubeTexture = cubeMap;
} else {
// create cube map from equirectangular map
const image = texture.image;
if ( isEquirectangularMapReady( image ) ) {
const renderTarget = new CubeRenderTarget( image.height );
renderTarget.fromEquirectangularTexture( renderer, texture );
mapTextureMapping( renderTarget.texture, texture.mapping );
this._cubeTexture = renderTarget.texture;
_cache.set( texture, renderTarget.texture );
texture.addEventListener( 'dispose', onTextureDispose );
} else {
// default cube texture as fallback when equirectangular texture is not yet loaded
this._cubeTexture = this._defaultTexture;
}
}
//
this._cubeTextureNode.value = this._cubeTexture;
} else {
// envNode already refers to a cube map
this._cubeTextureNode = this.envNode;
}
}
}
}
setup( builder ) {
this.updateBefore( builder );
return this._cubeTextureNode;
}
}
/**
* Returns true if the given equirectangular image has been fully loaded
* and is ready for further processing.
*
* @private
* @param {Image} image - The equirectangular image to check.
* @return {boolean} Whether the image is ready or not.
*/
function isEquirectangularMapReady( image ) {
if ( image === null || image === undefined ) return false;
return image.height > 0;
}
/**
* This function is executed when `dispose()` is called on the equirectangular
* texture. In this case, the generated cube map with its render target
* is deleted as well.
*
* @private
* @param {Object} event - The event object.
*/
function onTextureDispose( event ) {
const texture = event.target;
texture.removeEventListener( 'dispose', onTextureDispose );
const renderTarget = _cache.get( texture );
if ( renderTarget !== undefined ) {
_cache.delete( texture );
renderTarget.dispose();
}
}
/**
* This function makes sure the generated cube map uses the correct
* texture mapping that corresponds to the equirectangular original.
*
* @private
* @param {Texture} texture - The cube texture.
* @param {number} mapping - The original texture mapping.
*/
function mapTextureMapping( texture, mapping ) {
if ( mapping === EquirectangularReflectionMapping ) {
texture.mapping = CubeReflectionMapping;
} else if ( mapping === EquirectangularRefractionMapping ) {
texture.mapping = CubeRefractionMapping;
}
}
/**
* TSL function for creating a cube map node.
*
* @tsl
* @function
* @param {Node} envNode - The node representing the environment map.
* @returns {CubeMapNode}
*/
const cubeMapNode = /*@__PURE__*/ nodeProxy( CubeMapNode ).setParameterLength( 1 );
/**
* Represents a basic model for Image-based lighting (IBL). The environment
* is defined via environment maps in the equirectangular or cube map format.
* `BasicEnvironmentNode` is intended for non-PBR materials like {@link MeshBasicNodeMaterial}
* or {@link MeshPhongNodeMaterial}.
*
* @augments LightingNode
*/
class BasicEnvironmentNode extends LightingNode {
static get type() {
return 'BasicEnvironmentNode';
}
/**
* Constructs a new basic environment node.
*
* @param {Node} [envNode=null] - A node representing the environment.
*/
constructor( envNode = null ) {
super();
/**
* A node representing the environment.
*
* @type {Node}
* @default null
*/
this.envNode = envNode;
}
setup( builder ) {
// environment property is used in the finish() method of BasicLightingModel
builder.context.environment = cubeMapNode( this.envNode ).context( {
getUV: ( textureNode ) => materialEnvRotation.mul( textureNode.uvNode || textureNode.getDefaultUV() ),
forceUVContext: true
} );
}
}
/**
* A specific version of {@link IrradianceNode} that is only relevant
* for {@link MeshBasicNodeMaterial}. Since the material is unlit, it
* requires a special scaling factor for the light map.
*
* @augments LightingNode
*/
class BasicLightMapNode extends LightingNode {
static get type() {
return 'BasicLightMapNode';
}
/**
* Constructs a new basic light map node.
*
* @param {?Node<vec3>} [lightMapNode=null] - The light map node.
*/
constructor( lightMapNode = null ) {
super();
/**
* The light map node.
*
* @type {?Node<vec3>}
*/
this.lightMapNode = lightMapNode;
}
setup( builder ) {
// irradianceLightMap property is used in the indirectDiffuse() method of BasicLightingModel
const RECIPROCAL_PI = float( 1 / Math.PI );
builder.context.irradianceLightMap = this.lightMapNode.mul( RECIPROCAL_PI );
}
}
/**
* Abstract class for implementing lighting models. The module defines
* multiple methods that concrete lighting models can implement. These
* methods are executed at different points during the light evaluation
* process.
*/
class LightingModel {
/**
* This method is intended for setting up lighting model and context data
* which are later used in the evaluation process.
*
* @abstract
* @param {NodeBuilder} builder - The current node builder.
*/
start( builder ) {
// lights ( direct )
builder.lightsNode.setupLights( builder, builder.lightsNode.getLightNodes( builder ) );
// indirect
this.indirect( builder );
}
/**
* This method is intended for executing final tasks like final updates
* to the outgoing light.
*
* @abstract
* @param {NodeBuilder} builder - The current node builder.
*/
finish( /*builder*/ ) { }
/**
* This method is intended for implementing the direct light term and
* executed during the build process of directional, point and spot light nodes.
*
* @abstract
* @param {Object} lightData - The light data.
* @param {NodeBuilder} builder - The current node builder.
*/
direct( /*lightData, builder*/ ) { }
/**
* This method is intended for implementing the direct light term for
* rect area light nodes.
*
* @abstract
* @param {Object} lightData - The light data.
* @param {NodeBuilder} builder - The current node builder.
*/
directRectArea( /*lightData, builder*/ ) {}
/**
* This method is intended for implementing the indirect light term.
*
* @abstract
* @param {NodeBuilder} builder - The current node builder.
*/
indirect( /*builder*/ ) { }
/**
* This method is intended for implementing the ambient occlusion term.
* Unlike other methods, this method must be called manually by the lighting
* model in its indirect term.
*
* @abstract
* @param {NodeBuilder} builder - The current node builder.
*/
ambientOcclusion( /*input, stack, builder*/ ) { }
}
/**
* Represents the lighting model for unlit materials. The only light contribution
* is baked indirect lighting modulated with ambient occlusion and the material's
* diffuse color. Environment mapping is supported. Used in {@link MeshBasicNodeMaterial}.
*
* @augments LightingModel
*/
class BasicLightingModel extends LightingModel {
/**
* Constructs a new basic lighting model.
*/
constructor() {
super();
}
/**
* Implements the baked indirect lighting with its modulation.
*
* @param {NodeBuilder} builder - The current node builder.
*/
indirect( { context } ) {
const ambientOcclusion = context.ambientOcclusion;
const reflectedLight = context.reflectedLight;
const irradianceLightMap = context.irradianceLightMap;
reflectedLight.indirectDiffuse.assign( vec4( 0.0 ) );
// accumulation (baked indirect lighting only)
if ( irradianceLightMap ) {
reflectedLight.indirectDiffuse.addAssign( irradianceLightMap );
} else {
reflectedLight.indirectDiffuse.addAssign( vec4( 1.0, 1.0, 1.0, 0.0 ) );
}
// modulation
reflectedLight.indirectDiffuse.mulAssign( ambientOcclusion );
reflectedLight.indirectDiffuse.mulAssign( diffuseColor.rgb );
}
/**
* Implements the environment mapping.
*
* @param {NodeBuilder} builder - The current node builder.
*/
finish( builder ) {
const { material, context } = builder;
const outgoingLight = context.outgoingLight;
const envNode = builder.context.environment;
if ( envNode ) {
switch ( material.combine ) {
case MultiplyOperation:
outgoingLight.rgb.assign( mix( outgoingLight.rgb, outgoingLight.rgb.mul( envNode.rgb ), materialSpecularStrength.mul( materialReflectivity ) ) );
break;
case MixOperation:
outgoingLight.rgb.assign( mix( outgoingLight.rgb, envNode.rgb, materialSpecularStrength.mul( materialReflectivity ) ) );
break;
case AddOperation:
outgoingLight.rgb.addAssign( envNode.rgb.mul( materialSpecularStrength.mul( materialReflectivity ) ) );
break;
default:
warn( 'BasicLightingModel: Unsupported .combine value:', material.combine );
break;
}
}
}
}
const _defaultValues$9 = /*@__PURE__*/ new MeshBasicMaterial();
/**
* Node material version of {@link MeshBasicMaterial}.
*
* @augments NodeMaterial
*/
class MeshBasicNodeMaterial extends NodeMaterial {
static get type() {
return 'MeshBasicNodeMaterial';
}
/**
* Constructs a new mesh basic node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMeshBasicNodeMaterial = true;
/**
* Although the basic material is by definition unlit, we set
* this property to `true` since we use a lighting model to compute
* the outgoing light of the fragment shader.
*
* @type {boolean}
* @default true
*/
this.lights = true;
this.setDefaultValues( _defaultValues$9 );
this.setValues( parameters );
}
/**
* Basic materials are not affected by normal and bump maps so we
* return by default {@link normalViewGeometry}.
*
* @return {Node<vec3>} The normal node.
*/
setupNormal() {
return negateOnBackSide( normalViewGeometry ); // see #28839
}
/**
* Overwritten since this type of material uses {@link BasicEnvironmentNode}
* to implement the default environment mapping.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {?BasicEnvironmentNode<vec3>} The environment node.
*/
setupEnvironment( builder ) {
const envNode = super.setupEnvironment( builder );
return envNode ? new BasicEnvironmentNode( envNode ) : null;
}
/**
* This method must be overwritten since light maps are evaluated
* with a special scaling factor for basic materials.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {?BasicLightMapNode<vec3>} The light map node.
*/
setupLightMap( builder ) {
let node = null;
if ( builder.material.lightMap ) {
node = new BasicLightMapNode( materialLightMap );
}
return node;
}
/**
* The material overwrites this method because `lights` is set to `true` but
* we still want to return the diffuse color as the outgoing light.
*
* @return {Node<vec3>} The outgoing light node.
*/
setupOutgoingLight() {
return diffuseColor.rgb;
}
/**
* Setups the lighting model.
*
* @return {BasicLightingModel} The lighting model.
*/
setupLightingModel() {
return new BasicLightingModel();
}
}
const G_BlinnPhong_Implicit = () => float( 0.25 );
const D_BlinnPhong = /*@__PURE__*/ Fn( ( { dotNH } ) => {
return shininess.mul( float( 0.5 ) ).add( 1.0 ).mul( float( 1 / Math.PI ) ).mul( dotNH.pow( shininess ) );
} );
const BRDF_BlinnPhong = /*@__PURE__*/ Fn( ( { lightDirection } ) => {
const halfDir = lightDirection.add( positionViewDirection ).normalize();
const dotNH = normalView.dot( halfDir ).clamp();
const dotVH = positionViewDirection.dot( halfDir ).clamp();
const F = F_Schlick( { f0: specularColor, f90: 1.0, dotVH } );
const G = G_BlinnPhong_Implicit();
const D = D_BlinnPhong( { dotNH } );
return F.mul( G ).mul( D );
} );
/**
* Represents the lighting model for a phong material. Used in {@link MeshPhongNodeMaterial}.
*
* @augments BasicLightingModel
*/
class PhongLightingModel extends BasicLightingModel {
/**
* Constructs a new phong lighting model.
*
* @param {boolean} [specular=true] - Whether specular is supported or not.
*/
constructor( specular = true ) {
super();
/**
* Whether specular is supported or not. Set this to `false` if you are
* looking for a Lambert-like material meaning a material for non-shiny
* surfaces, without specular highlights.
*
* @type {boolean}
* @default true
*/
this.specular = specular;
}
/**
* Implements the direct lighting. The specular portion is optional and can be controlled
* with the {@link PhongLightingModel#specular} flag.
*
* @param {Object} lightData - The light data.
*/
direct( { lightDirection, lightColor, reflectedLight } ) {
const dotNL = normalView.dot( lightDirection ).clamp();
const irradiance = dotNL.mul( lightColor );
reflectedLight.directDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor: diffuseColor.rgb } ) ) );
if ( this.specular === true ) {
reflectedLight.directSpecular.addAssign( irradiance.mul( BRDF_BlinnPhong( { lightDirection } ) ).mul( materialSpecularStrength ) );
}
}
/**
* Implements the indirect lighting.
*
* @param {NodeBuilder} builder - The current node builder.
*/
indirect( builder ) {
const { ambientOcclusion, irradiance, reflectedLight } = builder.context;
reflectedLight.indirectDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor } ) ) );
reflectedLight.indirectDiffuse.mulAssign( ambientOcclusion );
}
}
const _defaultValues$8 = /*@__PURE__*/ new MeshLambertMaterial();
/**
* Node material version of {@link MeshLambertMaterial}.
*
* @augments NodeMaterial
*/
class MeshLambertNodeMaterial extends NodeMaterial {
static get type() {
return 'MeshLambertNodeMaterial';
}
/**
* Constructs a new mesh lambert node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMeshLambertNodeMaterial = true;
/**
* Set to `true` because lambert materials react on lights.
*
* @type {boolean}
* @default true
*/
this.lights = true;
/**
* The emissive color of lambert materials is by default inferred from the `emissive`,
* `emissiveIntensity` and `emissiveMap` properties. This node property allows to
* overwrite the default and define the emissive color with a node instead.
*
* If you don't want to overwrite the emissive color but modify the existing
* value instead, use {@link materialEmissive}.
*
* @type {?Node<vec3>}
* @default null
*/
this.emissiveNode = null;
this.setDefaultValues( _defaultValues$8 );
this.setValues( parameters );
}
/**
* Overwritten since this type of material uses {@link BasicEnvironmentNode}
* to implement the default environment mapping.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {?BasicEnvironmentNode<vec3>} The environment node.
*/
setupEnvironment( builder ) {
const envNode = super.setupEnvironment( builder );
return envNode ? new BasicEnvironmentNode( envNode ) : null;
}
/**
* Setups the lighting model.
*
* @return {PhongLightingModel} The lighting model.
*/
setupLightingModel( /*builder*/ ) {
return new PhongLightingModel( false ); // ( specular ) -> force lambert
}
}
const _defaultValues$7 = /*@__PURE__*/ new MeshPhongMaterial();
/**
* Node material version of {@link MeshPhongMaterial}.
*
* @augments NodeMaterial
*/
class MeshPhongNodeMaterial extends NodeMaterial {
static get type() {
return 'MeshPhongNodeMaterial';
}
/**
* Constructs a new mesh lambert node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMeshPhongNodeMaterial = true;
/**
* Set to `true` because phong materials react on lights.
*
* @type {boolean}
* @default true
*/
this.lights = true;
/**
* The emissive color of phong materials is by default inferred from the `emissive`,
* `emissiveIntensity` and `emissiveMap` properties. This node property allows to
* overwrite the default and define the emissive color with a node instead.
*
* If you don't want to overwrite the emissive color but modify the existing
* value instead, use {@link materialEmissive}.
*
* @type {?Node<vec3>}
* @default null
*/
this.emissiveNode = null;
/**
* The shininess of phong materials is by default inferred from the `shininess`
* property. This node property allows to overwrite the default
* and define the shininess with a node instead.
*
* If you don't want to overwrite the shininess but modify the existing
* value instead, use {@link materialShininess}.
*
* @type {?Node<float>}
* @default null
*/
this.shininessNode = null;
/**
* The specular color of phong materials is by default inferred from the
* `specular` property. This node property allows to overwrite the default
* and define the specular color with a node instead.
*
* If you don't want to overwrite the specular color but modify the existing
* value instead, use {@link materialSpecular}.
*
* @type {?Node<vec3>}
* @default null
*/
this.specularNode = null;
this.setDefaultValues( _defaultValues$7 );
this.setValues( parameters );
}
/**
* Overwritten since this type of material uses {@link BasicEnvironmentNode}
* to implement the default environment mapping.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {?BasicEnvironmentNode<vec3>} The environment node.
*/
setupEnvironment( builder ) {
const envNode = super.setupEnvironment( builder );
return envNode ? new BasicEnvironmentNode( envNode ) : null;
}
/**
* Setups the lighting model.
*
* @return {PhongLightingModel} The lighting model.
*/
setupLightingModel( /*builder*/ ) {
return new PhongLightingModel();
}
/**
* Setups the phong specific node variables.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setupVariants( /*builder*/ ) {
// SHININESS
const shininessNode = ( this.shininessNode ? float( this.shininessNode ) : materialShininess ).max( 1e-4 ); // to prevent pow( 0.0, 0.0 )
shininess.assign( shininessNode );
// SPECULAR COLOR
const specularNode = this.specularNode || materialSpecular;
specularColor.assign( specularNode );
}
}
//
// Transmission
//
const getVolumeTransmissionRay = /*@__PURE__*/ Fn( ( [ n, v, thickness, ior, modelMatrix ] ) => {
// Direction of refracted light.
const refractionVector = vec3( refract( v.negate(), normalize( n ), div( 1.0, ior ) ) );
// Compute rotation-independent scaling of the model matrix.
const modelScale = vec3(
length( modelMatrix[ 0 ].xyz ),
length( modelMatrix[ 1 ].xyz ),
length( modelMatrix[ 2 ].xyz )
);
// The thickness is specified in local space.
return normalize( refractionVector ).mul( thickness.mul( modelScale ) );
}, {
n: 'vec3',
v: 'vec3',
thickness: 'float',
ior: 'float',
modelMatrix: 'mat4',
return: 'vec3'
} );
const applyIorToRoughness = /*@__PURE__*/ Fn( ( [ roughness, ior ] ) => {
// Scale roughness with IOR so that an IOR of 1.0 results in no microfacet refraction and
// an IOR of 1.5 results in the default amount of microfacet refraction.
return roughness.mul( clamp( ior.mul( 2.0 ).sub( 2.0 ), 0.0, 1.0 ) );
}, { roughness: 'float', ior: 'float', return: 'float' } );
const viewportBackSideTexture = /*@__PURE__*/ viewportMipTexture();
const viewportFrontSideTexture = /*@__PURE__*/ viewportOpaqueMipTexture();
const getTransmissionSample = /*@__PURE__*/ Fn( ( [ fragCoord, roughness, ior ], { material } ) => {
const vTexture = material.side === BackSide ? viewportBackSideTexture : viewportFrontSideTexture;
const transmissionSample = vTexture.sample( fragCoord.mul( cameraViewport.zw ).add( cameraViewport.xy ).div( screenSize ) );
//const transmissionSample = viewportMipTexture( fragCoord );
const lod = log2( cameraViewport.z ).mul( applyIorToRoughness( roughness, ior ) );
return textureBicubicLevel( transmissionSample, lod );
} );
const volumeAttenuation = /*@__PURE__*/ Fn( ( [ transmissionDistance, attenuationColor, attenuationDistance ] ) => {
If( attenuationDistance.notEqual( 0 ), () => {
// Compute light attenuation using Beer's law.
const attenuationCoefficient = log( attenuationColor ).negate().div( attenuationDistance );
const transmittance = exp( attenuationCoefficient.negate().mul( transmissionDistance ) );
return transmittance;
} );
// Attenuation distance is +∞, i.e. the transmitted color is not attenuated at all.
return vec3( 1.0 );
}, { transmissionDistance: 'float', attenuationColor: 'vec3', attenuationDistance: 'float', return: 'vec3' } );
const getIBLVolumeRefraction = /*@__PURE__*/ Fn( ( [ n, v, roughness, diffuseColor, specularColor, specularF90, position, modelMatrix, viewMatrix, projMatrix, ior, thickness, attenuationColor, attenuationDistance, dispersion ] ) => {
let transmittedLight, transmittance;
if ( dispersion ) {
transmittedLight = vec4().toVar();
transmittance = vec3().toVar();
const halfSpread = ior.sub( 1.0 ).mul( dispersion.mul( 0.025 ) );
const iors = vec3( ior.sub( halfSpread ), ior, ior.add( halfSpread ) );
Loop( { start: 0, end: 3 }, ( { i } ) => {
const ior = iors.element( i );
const transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix );
const refractedRayExit = position.add( transmissionRay );
// Project refracted vector on the framebuffer, while mapping to normalized device coordinates.
const ndcPos = projMatrix.mul( viewMatrix.mul( vec4( refractedRayExit, 1.0 ) ) );
const refractionCoords = vec2( ndcPos.xy.div( ndcPos.w ) ).toVar();
refractionCoords.addAssign( 1.0 );
refractionCoords.divAssign( 2.0 );
refractionCoords.assign( vec2( refractionCoords.x, refractionCoords.y.oneMinus() ) ); // webgpu
// Sample framebuffer to get pixel the refracted ray hits.
const transmissionSample = getTransmissionSample( refractionCoords, roughness, ior );
transmittedLight.element( i ).assign( transmissionSample.element( i ) );
transmittedLight.a.addAssign( transmissionSample.a );
transmittance.element( i ).assign( diffuseColor.element( i ).mul( volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance ).element( i ) ) );
} );
transmittedLight.a.divAssign( 3.0 );
} else {
const transmissionRay = getVolumeTransmissionRay( n, v, thickness, ior, modelMatrix );
const refractedRayExit = position.add( transmissionRay );
// Project refracted vector on the framebuffer, while mapping to normalized device coordinates.
const ndcPos = projMatrix.mul( viewMatrix.mul( vec4( refractedRayExit, 1.0 ) ) );
const refractionCoords = vec2( ndcPos.xy.div( ndcPos.w ) ).toVar();
refractionCoords.addAssign( 1.0 );
refractionCoords.divAssign( 2.0 );
refractionCoords.assign( vec2( refractionCoords.x, refractionCoords.y.oneMinus() ) ); // webgpu
// Sample framebuffer to get pixel the refracted ray hits.
transmittedLight = getTransmissionSample( refractionCoords, roughness, ior );
transmittance = diffuseColor.mul( volumeAttenuation( length( transmissionRay ), attenuationColor, attenuationDistance ) );
}
const attenuatedColor = transmittance.rgb.mul( transmittedLight.rgb );
const dotNV = n.dot( v ).clamp();
// Get the specular component.
const F = vec3( EnvironmentBRDF( { // n, v, specularColor, specularF90, roughness
dotNV,
specularColor,
specularF90,
roughness
} ) );
// As less light is transmitted, the opacity should be increased. This simple approximation does a decent job
// of modulating a CSS background, and has no effect when the buffer is opaque, due to a solid object or clear color.
const transmittanceFactor = transmittance.r.add( transmittance.g, transmittance.b ).div( 3.0 );
return vec4( F.oneMinus().mul( attenuatedColor ), transmittedLight.a.oneMinus().mul( transmittanceFactor ).oneMinus() );
} );
//
// Iridescence
//
// XYZ to linear-sRGB color space
const XYZ_TO_REC709 = /*@__PURE__*/ mat3(
3.2404542, -1.5371385, -0.4985314,
-0.969266, 1.8760108, 0.0415560,
0.0556434, -0.2040259, 1.0572252
);
// Assume air interface for top
// Note: We don't handle the case fresnel0 == 1
const Fresnel0ToIor = ( fresnel0 ) => {
const sqrtF0 = fresnel0.sqrt();
return vec3( 1.0 ).add( sqrtF0 ).div( vec3( 1.0 ).sub( sqrtF0 ) );
};
// ior is a value between 1.0 and 3.0. 1.0 is air interface
const IorToFresnel0 = ( transmittedIor, incidentIor ) => {
return transmittedIor.sub( incidentIor ).div( transmittedIor.add( incidentIor ) ).pow2();
};
// Fresnel equations for dielectric/dielectric interfaces.
// Ref: https://belcour.github.io/blog/research/2017/05/01/brdf-thin-film.html
// Evaluation XYZ sensitivity curves in Fourier space
const evalSensitivity = ( OPD, shift ) => {
const phase = OPD.mul( 2.0 * Math.PI * 1.0e-9 );
const val = vec3( 5.4856e-13, 4.4201e-13, 5.2481e-13 );
const pos = vec3( 1.6810e+06, 1.7953e+06, 2.2084e+06 );
const VAR = vec3( 4.3278e+09, 9.3046e+09, 6.6121e+09 );
const x = float( 9.7470e-14 * Math.sqrt( 2.0 * Math.PI * 4.5282e+09 ) ).mul( phase.mul( 2.2399e+06 ).add( shift.x ).cos() ).mul( phase.pow2().mul( -45282e5 ).exp() );
let xyz = val.mul( VAR.mul( 2.0 * Math.PI ).sqrt() ).mul( pos.mul( phase ).add( shift ).cos() ).mul( phase.pow2().negate().mul( VAR ).exp() );
xyz = vec3( xyz.x.add( x ), xyz.y, xyz.z ).div( 1.0685e-7 );
const rgb = XYZ_TO_REC709.mul( xyz );
return rgb;
};
const evalIridescence = /*@__PURE__*/ Fn( ( { outsideIOR, eta2, cosTheta1, thinFilmThickness, baseF0 } ) => {
// Force iridescenceIOR -> outsideIOR when thinFilmThickness -> 0.0
const iridescenceIOR = mix( outsideIOR, eta2, smoothstep( 0.0, 0.03, thinFilmThickness ) );
// Evaluate the cosTheta on the base layer (Snell law)
const sinTheta2Sq = outsideIOR.div( iridescenceIOR ).pow2().mul( cosTheta1.pow2().oneMinus() );
// Handle TIR:
const cosTheta2Sq = sinTheta2Sq.oneMinus();
If( cosTheta2Sq.lessThan( 0 ), () => {
return vec3( 1.0 );
} );
const cosTheta2 = cosTheta2Sq.sqrt();
// First interface
const R0 = IorToFresnel0( iridescenceIOR, outsideIOR );
const R12 = F_Schlick( { f0: R0, f90: 1.0, dotVH: cosTheta1 } );
//const R21 = R12;
const T121 = R12.oneMinus();
const phi12 = iridescenceIOR.lessThan( outsideIOR ).select( Math.PI, 0.0 );
const phi21 = float( Math.PI ).sub( phi12 );
// Second interface
const baseIOR = Fresnel0ToIor( baseF0.clamp( 0.0, 0.9999 ) ); // guard against 1.0
const R1 = IorToFresnel0( baseIOR, iridescenceIOR.toVec3() );
const R23 = F_Schlick( { f0: R1, f90: 1.0, dotVH: cosTheta2 } );
const phi23 = vec3(
baseIOR.x.lessThan( iridescenceIOR ).select( Math.PI, 0.0 ),
baseIOR.y.lessThan( iridescenceIOR ).select( Math.PI, 0.0 ),
baseIOR.z.lessThan( iridescenceIOR ).select( Math.PI, 0.0 )
);
// Phase shift
const OPD = iridescenceIOR.mul( thinFilmThickness, cosTheta2, 2.0 );
const phi = vec3( phi21 ).add( phi23 );
// Compound terms
const R123 = R12.mul( R23 ).clamp( 1e-5, 0.9999 );
const r123 = R123.sqrt();
const Rs = T121.pow2().mul( R23 ).div( vec3( 1.0 ).sub( R123 ) );
// Reflectance term for m = 0 (DC term amplitude)
const C0 = R12.add( Rs );
const I = C0.toVar();
// Reflectance term for m > 0 (pairs of diracs)
const Cm = Rs.sub( T121 ).toVar();
Loop( { start: 1, end: 2, condition: '<=', name: 'm' }, ( { m } ) => {
Cm.mulAssign( r123 );
const Sm = evalSensitivity( float( m ).mul( OPD ), float( m ).mul( phi ) ).mul( 2.0 );
I.addAssign( Cm.mul( Sm ) );
} );
// Since out of gamut colors might be produced, negative color values are clamped to 0.
return I.max( vec3( 0.0 ) );
}, {
outsideIOR: 'float',
eta2: 'float',
cosTheta1: 'float',
thinFilmThickness: 'float',
baseF0: 'vec3',
return: 'vec3'
} );
//
// Sheen
//
// This is a curve-fit approximation to the "Charlie sheen" BRDF integrated over the hemisphere from
// Estevez and Kulla 2017, "Production Friendly Microfacet Sheen BRDF".
const IBLSheenBRDF = /*@__PURE__*/ Fn( ( { normal, viewDir, roughness } ) => {
const dotNV = normal.dot( viewDir ).saturate();
const r2 = roughness.mul( roughness );
const rInv = roughness.add( 0.1 ).reciprocal();
const a = float( -1.9362 ).add( roughness.mul( 1.0678 ) ).add( r2.mul( 0.4573 ) ).sub( rInv.mul( 0.8469 ) );
const b = float( -0.6014 ).add( roughness.mul( 0.5538 ) ).sub( r2.mul( 0.4670 ) ).sub( rInv.mul( 0.1255 ) );
const DG = a.mul( dotNV ).add( b ).exp();
return DG.saturate();
}, { normal: 'vec3', viewDir: 'vec3', roughness: 'float', return: 'float' } );
const clearcoatF0 = vec3( 0.04 );
const clearcoatF90 = float( 1 );
/**
* Represents the lighting model for a PBR material.
*
* @augments LightingModel
*/
class PhysicalLightingModel extends LightingModel {
/**
* Constructs a new physical lighting model.
*
* @param {boolean} [clearcoat=false] - Whether clearcoat is supported or not.
* @param {boolean} [sheen=false] - Whether sheen is supported or not.
* @param {boolean} [iridescence=false] - Whether iridescence is supported or not.
* @param {boolean} [anisotropy=false] - Whether anisotropy is supported or not.
* @param {boolean} [transmission=false] - Whether transmission is supported or not.
* @param {boolean} [dispersion=false] - Whether dispersion is supported or not.
* @param {boolean} [retroreflection=false] - Whether retroreflection is supported or not.
* @param {boolean} [diffuseRoughness=false] - Whether EON rough diffuse reflection is supported or not.
*/
constructor( clearcoat = false, sheen = false, iridescence = false, anisotropy = false, transmission = false, dispersion = false, retroreflection = false, diffuseRoughness = false ) {
super();
/**
* Whether clearcoat is supported or not.
*
* @type {boolean}
* @default false
*/
this.clearcoat = clearcoat;
/**
* Whether sheen is supported or not.
*
* @type {boolean}
* @default false
*/
this.sheen = sheen;
/**
* Whether iridescence is supported or not.
*
* @type {boolean}
* @default false
*/
this.iridescence = iridescence;
/**
* Whether anisotropy is supported or not.
*
* @type {boolean}
* @default false
*/
this.anisotropy = anisotropy;
/**
* Whether transmission is supported or not.
*
* @type {boolean}
* @default false
*/
this.transmission = transmission;
/**
* Whether dispersion is supported or not.
*
* @type {boolean}
* @default false
*/
this.dispersion = dispersion;
/**
* Whether retroreflection is supported or not.
*
* @type {boolean}
* @default false
*/
this.retroreflection = retroreflection;
/**
* Whether EON rough diffuse reflection is supported or not.
*
* @type {boolean}
* @default false
*/
this.diffuseRoughness = diffuseRoughness;
/**
* The clear coat radiance.
*
* @type {?Node}
* @default null
*/
this.clearcoatRadiance = null;
/**
* The clear coat specular direct.
*
* @type {?Node}
* @default null
*/
this.clearcoatSpecularDirect = null;
/**
* The clear coat specular indirect.
*
* @type {?Node}
* @default null
*/
this.clearcoatSpecularIndirect = null;
/**
* The sheen specular direct.
*
* @type {?Node}
* @default null
*/
this.sheenSpecularDirect = null;
/**
* The sheen specular indirect.
*
* @type {?Node}
* @default null
*/
this.sheenSpecularIndirect = null;
/**
* The iridescence Fresnel.
*
* @type {?Node}
* @default null
*/
this.iridescenceFresnel = null;
/**
* The iridescence F0 dielectric.
*
* @type {?Node}
* @default null
*/
this.iridescenceF0Dielectric = null;
/**
* The iridescence F0 metallic.
*
* @type {?Node}
* @default null
*/
this.iridescenceF0Metallic = null;
/**
* The sampled DFG LUT value, shared by the direct and indirect lighting paths.
*
* @type {?Node}
* @default null
*/
this.dfg = null;
/**
* The EON directional albedo, shared by the indirect lighting paths.
*
* @type {?Node}
* @default null
*/
this.eonDirectionalAlbedo = null;
/**
* The multi-scattering energy compensation for direct lighting.
*
* @type {?Node}
* @default null
*/
this.multiScatteringCompensation = null;
/**
* The dielectric single-scattering term, shared by the indirect lighting paths.
*
* @type {?Node}
* @default null
*/
this.singleScatteringDielectric = null;
/**
* The dielectric multi-scattering term, shared by the indirect lighting paths.
*
* @type {?Node}
* @default null
*/
this.multiScatteringDielectric = null;
}
/**
* Depending on what features are requested, the method prepares certain node variables
* which are later used for lighting computations.
*
* @param {NodeBuilder} builder - The current node builder.
*/
start( builder ) {
if ( this.clearcoat === true ) {
this.clearcoatRadiance = vec3().toVar( 'clearcoatRadiance' );
this.clearcoatSpecularDirect = vec3().toVar( 'clearcoatSpecularDirect' );
this.clearcoatSpecularIndirect = vec3().toVar( 'clearcoatSpecularIndirect' );
}
if ( this.sheen === true ) {
this.sheenSpecularDirect = vec3().toVar( 'sheenSpecularDirect' );
this.sheenSpecularIndirect = vec3().toVar( 'sheenSpecularIndirect' );
}
if ( this.iridescence === true ) {
const dotNVi = normalView.dot( positionViewDirection ).clamp();
const iridescenceFresnelDielectric = evalIridescence( {
outsideIOR: float( 1.0 ),
eta2: iridescenceIOR,
cosTheta1: dotNVi,
thinFilmThickness: iridescenceThickness,
baseF0: specularColor
} );
const iridescenceFresnelMetallic = evalIridescence( {
outsideIOR: float( 1.0 ),
eta2: iridescenceIOR,
cosTheta1: dotNVi,
thinFilmThickness: iridescenceThickness,
baseF0: diffuseColor.rgb
} );
this.iridescenceFresnel = mix( iridescenceFresnelDielectric, iridescenceFresnelMetallic, metalness );
this.iridescenceF0Dielectric = Schlick_to_F0( { f: iridescenceFresnelDielectric, f90: 1.0, dotVH: dotNVi } );
this.iridescenceF0Metallic = Schlick_to_F0( { f: iridescenceFresnelMetallic, f90: 1.0, dotVH: dotNVi } );
}
if ( this.transmission === true ) {
const position = positionWorld;
const v = cameraPosition.sub( positionWorld ).normalize(); // TODO: Create Node for this, same issue in MaterialX
const n = normalWorld;
const context = builder.context;
context.backdrop = getIBLVolumeRefraction(
n,
v,
roughness,
diffuseContribution,
specularColorBlended,
specularF90, // specularF90
position, // positionWorld
modelWorldMatrix, // modelMatrix
cameraViewMatrix, // viewMatrix
cameraProjectionMatrix, // projMatrix
ior,
thickness,
attenuationColor,
attenuationDistance,
this.dispersion ? dispersion : null
);
context.backdropAlpha = transmission;
diffuseColor.a.mulAssign( mix( 1, context.backdrop.a, transmission ) );
}
//
const dotNV = normalView.dot( positionViewDirection ).clamp();
this.dfg = DFGLUT( { roughness, dotNV } ).toConst( 'dfg' );
// Multi-scattering energy compensation for direct lighting
// Based on "Practical Multiple Scattering Compensation for Microfacet Models"
// https://blog.selfshadow.com/publications/turquin/ms_comp_final.pdf
// Energy of the single-scattering lobe in a white furnace ( F0 = F90 = 1 )
const Ess = this.dfg.x.add( this.dfg.y );
// Compensate for the energy lost to multiple scattering, tinting the added term by F0 ( equation 16 )
this.multiScatteringCompensation = specularColorBlended.mul( Ess.reciprocal().sub( 1.0 ) ).add( 1.0 ).toConst( 'multiScatteringCompensation' );
this.singleScatteringDielectric = vec3().toVar( 'singleScatteringDielectric' );
this.multiScatteringDielectric = vec3().toVar( 'multiScatteringDielectric' );
this.computeMultiscattering( this.singleScatteringDielectric, this.multiScatteringDielectric, specularF90, specularColor, this.iridescenceF0Dielectric );
if ( this.diffuseRoughness === true ) {
this.eonDirectionalAlbedo = EON_DirectionalAlbedo( { diffuseColor: diffuseColor.rgb, roughness: diffuseRoughness, dotNV: dotNV } );
}
super.start( builder );
}
// Fdez-Agüera's "Multiple-Scattering Microfacet Model for Real-Time Image Based Lighting"
// Approximates multi-scattering in order to preserve energy.
// http://www.jcgt.org/published/0008/01/03/
computeMultiscattering( singleScatter, multiScatter, specularF90, f0, iridescenceF0 = null ) {
const fab = this.dfg;
const Fr = iridescenceF0 ? iridescence.mix( f0, iridescenceF0 ) : f0;
const FssEss = Fr.mul( fab.x ).add( specularF90.mul( fab.y ) );
const Ess = fab.x.add( fab.y );
const Ems = Ess.oneMinus();
const Favg = Fr.add( Fr.oneMinus().mul( 0.047619 ) ); // 1/21
const Fms = FssEss.mul( Favg ).div( Ems.mul( Favg ).oneMinus() );
singleScatter.addAssign( FssEss );
multiScatter.addAssign( Fms.mul( Ems ) );
}
/**
* Implements the direct light.
*
* @param {Object} lightData - The light data.
* @param {NodeBuilder} builder - The current node builder.
*/
direct( { lightDirection, lightColor, reflectedLight }, /* builder */ ) {
const dotNL = normalView.dot( lightDirection ).clamp();
const irradiance = dotNL.mul( lightColor ).toVar();
if ( this.sheen === true ) {
this.sheenSpecularDirect.addAssign( irradiance.mul( BRDF_Sheen( { lightDirection } ) ) );
const sheenAlbedoV = IBLSheenBRDF( { normal: normalView, viewDir: positionViewDirection, roughness: sheenRoughness } );
const sheenAlbedoL = IBLSheenBRDF( { normal: normalView, viewDir: lightDirection, roughness: sheenRoughness } );
const sheenEnergyComp = sheen.r.max( sheen.g ).max( sheen.b ).mul( sheenAlbedoV.max( sheenAlbedoL ) ).oneMinus();
irradiance.mulAssign( sheenEnergyComp );
}
if ( this.clearcoat === true ) {
const dotNLcc = clearcoatNormalView.dot( lightDirection ).clamp();
const ccIrradiance = dotNLcc.mul( lightColor );
this.clearcoatSpecularDirect.addAssign( ccIrradiance.mul( BRDF_GGX( { lightDirection, f0: clearcoatF0, f90: clearcoatF90, roughness: clearcoatRoughness, normalView: clearcoatNormalView } ) ) );
}
// Light reflected by the specular interface is not available to the diffuse layer ( glTF fresnel_mix )
const halfDir = lightDirection.add( positionViewDirection ).normalize();
const dotVH = positionViewDirection.dot( halfDir ).clamp();
let F = F_Schlick( { f0: specularColor, f90: specularF90, dotVH } );
let specularBRDF = BRDF_GGX( { lightDirection, f0: specularColorBlended, f90: specularF90, roughness, f: this.iridescenceFresnel, USE_IRIDESCENCE: this.iridescence, USE_ANISOTROPY: this.anisotropy } );
if ( this.retroreflection === true ) {
// Minimal Retroreflective Microfacet Model:
// https://jcgt.org/published/0015/01/04/
const retroViewDirection = positionViewDirection.negate().reflect( normalView );
const retroHalfDir = lightDirection.add( retroViewDirection ).normalize();
const dotRetroVH = retroViewDirection.dot( retroHalfDir ).clamp();
const retroF = F_Schlick( { f0: specularColor, f90: specularF90, dotVH: dotRetroVH } );
const retroSpecularBRDF = BRDF_GGX( { lightDirection, viewDirection: retroViewDirection, f0: specularColorBlended, f90: specularF90, roughness, f: this.iridescenceFresnel, USE_IRIDESCENCE: this.iridescence, USE_ANISOTROPY: this.anisotropy } );
F = mix( F, retroF, retroreflectivity.clamp() );
specularBRDF = mix( specularBRDF, retroSpecularBRDF, retroreflectivity.clamp() );
}
const diffuseBRDF = this.diffuseRoughness
? BRDF_EON( { lightDirection, diffuseColor: diffuseColor.rgb, roughness: diffuseRoughness } ).mul( metalness.oneMinus() )
: BRDF_Lambert( { diffuseColor: diffuseContribution } );
reflectedLight.directDiffuse.addAssign( irradiance.mul( diffuseBRDF ).mul( F.oneMinus() ) );
reflectedLight.directSpecular.addAssign( irradiance.mul( specularBRDF ).mul( this.multiScatteringCompensation ) );
}
/**
* This method is intended for implementing the direct light term for
* rect area light nodes.
*
* @param {Object} input - The input data.
* @param {NodeBuilder} builder - The current node builder.
*/
directRectArea( { lightColor, lightPosition, halfWidth, halfHeight, reflectedLight, ltc_1, ltc_2 }, /* builder */ ) {
const p0 = lightPosition.add( halfWidth ).sub( halfHeight ); // counterclockwise; light shines in local neg z direction
const p1 = lightPosition.sub( halfWidth ).sub( halfHeight );
const p2 = lightPosition.sub( halfWidth ).add( halfHeight );
const p3 = lightPosition.add( halfWidth ).add( halfHeight );
const N = normalView;
const V = positionViewDirection;
const P = positionView.toVar();
const uv = LTC_Uv( { N, V, roughness } );
const t1 = ltc_1.sample( uv ).toVar();
const t2 = ltc_2.sample( uv ).toVar();
const mInv = mat3(
vec3( t1.x, 0, t1.y ),
vec3( 0, 1, 0 ),
vec3( t1.z, 0, t1.w )
).toVar();
// LTC Fresnel Approximation by Stephen Hill
// http://blog.selfshadow.com/publications/s2016-advances/s2016_ltc_fresnel.pdf
const fresnel = specularColorBlended.mul( t2.x ).add( specularF90.sub( specularColorBlended ).mul( t2.y ) ).toVar();
reflectedLight.directSpecular.addAssign( lightColor.mul( fresnel ).mul( LTC_Evaluate( { N, V, P, mInv, p0, p1, p2, p3 } ) ) );
reflectedLight.directDiffuse.addAssign( lightColor.mul( diffuseContribution ).mul( LTC_Evaluate( { N, V, P, mInv: mat3( 1, 0, 0, 0, 1, 0, 0, 0, 1 ), p0, p1, p2, p3 } ) ) );
if ( this.clearcoat === true ) {
const Ncc = clearcoatNormalView;
const uvClearcoat = LTC_Uv( { N: Ncc, V, roughness: clearcoatRoughness } );
const t1Clearcoat = ltc_1.sample( uvClearcoat );
const t2Clearcoat = ltc_2.sample( uvClearcoat );
const mInvClearcoat = mat3(
vec3( t1Clearcoat.x, 0, t1Clearcoat.y ),
vec3( 0, 1, 0 ),
vec3( t1Clearcoat.z, 0, t1Clearcoat.w )
);
// LTC Fresnel Approximation for clearcoat
const fresnelClearcoat = clearcoatF0.mul( t2Clearcoat.x ).add( clearcoatF90.sub( clearcoatF0 ).mul( t2Clearcoat.y ) );
this.clearcoatSpecularDirect.addAssign( lightColor.mul( fresnelClearcoat ).mul( LTC_Evaluate( { N: Ncc, V, P, mInv: mInvClearcoat, p0, p1, p2, p3 } ) ) );
}
}
/**
* Implements the indirect lighting.
*
* @param {NodeBuilder} builder - The current node builder.
*/
indirect( builder ) {
this.indirectDiffuse( builder );
this.indirectSpecular( builder );
this.ambientOcclusion( builder );
}
/**
* Implements the indirect diffuse term.
*
* @param {NodeBuilder} builder - The current node builder.
*/
indirectDiffuse( builder ) {
const { irradiance, reflectedLight } = builder.context;
// Energy reflected by the specular lobe is not available to the diffuse layer
const singleScattering = this.singleScatteringDielectric;
const multiScattering = this.multiScatteringDielectric;
const diffuseBRDF = this.diffuseRoughness
? this.eonDirectionalAlbedo.mul( metalness.oneMinus(), 1 / Math.PI )
: BRDF_Lambert( { diffuseColor: diffuseContribution } );
const diffuse = irradiance.mul( diffuseBRDF ).mul( singleScattering.add( multiScattering ).oneMinus() ).toVar();
if ( this.sheen === true ) {
const sheenAlbedo = IBLSheenBRDF( { normal: normalView, viewDir: positionViewDirection, roughness: sheenRoughness } );
this.sheenSpecularIndirect.addAssign( irradiance.mul( sheen, sheenAlbedo, 1 / Math.PI ) );
const sheenEnergyComp = sheen.r.max( sheen.g ).max( sheen.b ).mul( sheenAlbedo ).oneMinus();
diffuse.mulAssign( sheenEnergyComp );
}
reflectedLight.indirectDiffuse.addAssign( diffuse );
}
/**
* Implements the indirect specular term.
*
* @param {NodeBuilder} builder - The current node builder.
*/
indirectSpecular( builder ) {
const { radiance, iblIrradiance, reflectedLight } = builder.context;
if ( this.sheen === true ) {
this.sheenSpecularIndirect.addAssign( iblIrradiance.mul(
sheen,
IBLSheenBRDF( {
normal: normalView,
viewDir: positionViewDirection,
roughness: sheenRoughness
} ),
1 / Math.PI
) );
}
if ( this.clearcoat === true ) {
const dotNVcc = clearcoatNormalView.dot( positionViewDirection ).clamp();
const clearcoatEnv = EnvironmentBRDF( {
dotNV: dotNVcc,
specularColor: clearcoatF0,
specularF90: clearcoatF90,
roughness: clearcoatRoughness
} );
this.clearcoatSpecularIndirect.addAssign( this.clearcoatRadiance.mul( clearcoatEnv ) );
}
// Both indirect specular and indirect diffuse light accumulate here
// Compute multiscattering separately for dielectric and metallic, then mix
const singleScatteringDielectric = this.singleScatteringDielectric;
const multiScatteringDielectric = this.multiScatteringDielectric;
const singleScatteringMetallic = vec3().toVar( 'singleScatteringMetallic' );
const multiScatteringMetallic = vec3().toVar( 'multiScatteringMetallic' );
this.computeMultiscattering( singleScatteringMetallic, multiScatteringMetallic, specularF90, diffuseColor.rgb, this.iridescenceF0Metallic );
// Mix based on metalness
const singleScattering = mix( singleScatteringDielectric, singleScatteringMetallic, metalness );
const multiScattering = mix( multiScatteringDielectric, multiScatteringMetallic, metalness );
// Diffuse energy conservation uses dielectric path
const totalScatteringDielectric = singleScatteringDielectric.add( multiScatteringDielectric );
const diffuseAlbedo = this.diffuseRoughness
? this.eonDirectionalAlbedo.mul( metalness.oneMinus() )
: diffuseContribution;
const diffuse = diffuseAlbedo.mul( totalScatteringDielectric.oneMinus() );
const cosineWeightedIrradiance = iblIrradiance.mul( 1 / Math.PI );
const indirectSpecular = radiance.mul( singleScattering ).add( multiScattering.mul( cosineWeightedIrradiance ) ).toVar();
const indirectDiffuse = diffuse.mul( cosineWeightedIrradiance ).toVar();
if ( this.sheen === true ) {
const sheenAlbedo = IBLSheenBRDF( { normal: normalView, viewDir: positionViewDirection, roughness: sheenRoughness } );
const sheenEnergyComp = sheen.r.max( sheen.g ).max( sheen.b ).mul( sheenAlbedo ).oneMinus();
indirectSpecular.mulAssign( sheenEnergyComp );
indirectDiffuse.mulAssign( sheenEnergyComp );
}
reflectedLight.indirectSpecular.addAssign( indirectSpecular );
reflectedLight.indirectDiffuse.addAssign( indirectDiffuse );
}
/**
* Implements the ambient occlusion term.
*
* @param {NodeBuilder} builder - The current node builder.
*/
ambientOcclusion( builder ) {
const { ambientOcclusion, reflectedLight } = builder.context;
const dotNV = normalView.dot( positionViewDirection ).clamp(); // @ TODO: Move to core dotNV
const aoNV = dotNV.add( ambientOcclusion );
const aoExp = roughness.mul( -16 ).oneMinus().negate().exp2();
const aoNode = ambientOcclusion.sub( aoNV.pow( aoExp ).oneMinus() ).clamp();
if ( this.clearcoat === true ) {
this.clearcoatSpecularIndirect.mulAssign( ambientOcclusion );
}
if ( this.sheen === true ) {
this.sheenSpecularIndirect.mulAssign( ambientOcclusion );
}
reflectedLight.indirectDiffuse.mulAssign( ambientOcclusion );
reflectedLight.indirectSpecular.mulAssign( aoNode );
}
/**
* Used for final lighting accumulations depending on the requested features.
*
* @param {NodeBuilder} builder - The current node builder.
*/
finish( { context } ) {
const { outgoingLight } = context;
if ( this.clearcoat === true ) {
const dotNVcc = clearcoatNormalView.dot( positionViewDirection ).clamp();
const Fcc = F_Schlick( {
dotVH: dotNVcc,
f0: clearcoatF0,
f90: clearcoatF90
} );
const clearcoatLight = outgoingLight.mul( clearcoat.mul( Fcc ).oneMinus() ).add( this.clearcoatSpecularDirect.add( this.clearcoatSpecularIndirect ).mul( clearcoat ) );
outgoingLight.assign( clearcoatLight );
}
if ( this.sheen === true ) {
const sheenLight = outgoingLight.add( this.sheenSpecularDirect, this.sheenSpecularIndirect );
outgoingLight.assign( sheenLight );
}
}
}
const _rendererCache = new WeakMap();
/**
* Represents a physical model for Image-based lighting (IBL). The environment
* is defined by an equirectangular or cube texture.
* `EnvironmentNode` is intended for PBR materials like {@link MeshStandardNodeMaterial}.
*
* @augments LightingNode
*/
class EnvironmentNode extends LightingNode {
static get type() {
return 'EnvironmentNode';
}
/**
* Constructs a new environment node.
*
* @param {Node} [envNode=null] - A node representing the environment.
*/
constructor( envNode = null ) {
super();
/**
* A node representing the environment.
*
* @type {?Node}
* @default null
*/
this.envNode = envNode;
/**
* The PMREM node of a texture or material reference environment. Its texture is
* resolved before rendering since materials with different environments can share a shader.
*
* @private
* @type {?PMREMNode}
* @default null
*/
this._pmremNode = null;
}
setup( builder ) {
const { material } = builder;
let envNode = this.envNode;
if ( envNode.isTextureNode || envNode.isMaterialReferenceNode ) {
const cache = this._getPMREMNodeCache( builder.renderer );
let pmremNode = cache.get( envNode );
if ( pmremNode === undefined ) {
pmremNode = pmremTexture( this._getEnvTexture( builder ) );
cache.set( envNode, pmremNode );
}
this._pmremNode = pmremNode;
this.updateBeforeType = envNode.isMaterialReferenceNode ? NodeUpdateType.OBJECT : NodeUpdateType.RENDER;
envNode = pmremNode;
}
//
const useAnisotropy = material.useAnisotropy === true || material.anisotropy > 0;
const radianceNormalView = useAnisotropy ? bentNormalView : normalView;
let radiance = isolate( envNode.context( createRadianceContext( roughness, radianceNormalView ) ).mul( materialEnvIntensity ) );
if ( material.useRetroreflection === true || material.retroreflectivity > 0 ) {
const retroRadiance = isolate( envNode.context( createRetroRadianceContext( roughness, radianceNormalView ) ).mul( materialEnvIntensity ) );
radiance = retroreflectivity.clamp().mix( radiance, retroRadiance );
}
const irradiance = envNode.context( createIrradianceContext( normalWorld ) ).mul( Math.PI ).mul( materialEnvIntensity );
const isolateIrradiance = isolate( irradiance );
//
builder.context.radiance.addAssign( radiance );
builder.context.iblIrradiance.addAssign( isolateIrradiance );
//
const clearcoatRadiance = builder.context.lightingModel.clearcoatRadiance;
if ( clearcoatRadiance ) {
const clearcoatRadianceContext = envNode.context( createRadianceContext( clearcoatRoughness, clearcoatNormalView ) ).mul( materialEnvIntensity );
const isolateClearcoatRadiance = isolate( clearcoatRadianceContext );
clearcoatRadiance.addAssign( isolateClearcoatRadiance );
}
}
updateBefore( frame ) {
const texture = this._getEnvTexture( frame );
// the texture can be removed without a rebuild of the material
if ( texture && this._pmremNode.value !== texture ) {
this._pmremNode.value = texture;
this._pmremNode.updateBefore( frame );
}
}
/**
* Returns the current texture of the environment node.
*
* @private
* @param {(NodeFrame|NodeBuilder)} state - The current state.
* @return {Texture} The environment texture.
*/
_getEnvTexture( state ) {
const envNode = this.envNode;
if ( envNode.isMaterialReferenceNode ) {
envNode.updateReference( state );
return envNode.getValueFromReference();
}
return envNode.value;
}
/**
* Returns the PMREM node cache of the current renderer.
*
* @private
* @param {Renderer} renderer - The current renderer.
* @return {WeakMap} The node cache.
*/
_getPMREMNodeCache( renderer ) {
let pmremCache = _rendererCache.get( renderer );
if ( pmremCache === undefined ) {
pmremCache = new WeakMap();
_rendererCache.set( renderer, pmremCache );
}
return pmremCache;
}
}
const createRadianceContext = ( roughnessNode, normalViewNode ) => {
let reflectVec = null;
return {
getUV: () => {
if ( reflectVec === null ) {
reflectVec = positionViewDirection.negate().reflect( normalViewNode );
// Mixing the reflection with the normal is more accurate and keeps rough objects from gathering light from behind their tangent plane.
reflectVec = pow4( roughnessNode ).mix( reflectVec, normalViewNode ).normalize();
reflectVec = reflectVec.transformDirection( cameraWorldMatrix );
}
return reflectVec;
},
getTextureLevel: () => {
return roughnessNode;
}
};
};
const createRetroRadianceContext = ( roughnessNode, normalViewNode ) => {
let retroVec = null;
return {
getUV: () => {
if ( retroVec === null ) {
// The retroreflective lobe returns light toward its source, so the environment is sampled along the view direction
retroVec = pow4( roughnessNode ).mix( positionViewDirection, normalViewNode ).normalize();
retroVec = retroVec.transformDirection( cameraWorldMatrix );
}
return retroVec;
},
getTextureLevel: () => {
return roughnessNode;
}
};
};
const createIrradianceContext = ( normalWorldNode ) => {
return {
getUV: () => {
return normalWorldNode;
},
getTextureLevel: () => {
return float( 1.0 );
}
};
};
const _defaultValues$6 = /*@__PURE__*/ new MeshStandardMaterial();
/**
* Node material version of {@link MeshStandardMaterial}.
*
* @augments NodeMaterial
*/
class MeshStandardNodeMaterial extends NodeMaterial {
static get type() {
return 'MeshStandardNodeMaterial';
}
/**
* Constructs a new mesh standard node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMeshStandardNodeMaterial = true;
/**
* Set to `true` because standard materials react on lights.
*
* @type {boolean}
* @default true
*/
this.lights = true;
/**
* The emissive color of standard materials is by default inferred from the `emissive`,
* `emissiveIntensity` and `emissiveMap` properties. This node property allows to
* overwrite the default and define the emissive color with a node instead.
*
* If you don't want to overwrite the emissive color but modify the existing
* value instead, use {@link materialEmissive}.
*
* @type {?Node<vec3>}
* @default null
*/
this.emissiveNode = null;
/**
* The metalness of standard materials is by default inferred from the `metalness`,
* and `metalnessMap` properties. This node property allows to
* overwrite the default and define the metalness with a node instead.
*
* If you don't want to overwrite the metalness but modify the existing
* value instead, use {@link materialMetalness}.
*
* @type {?Node<float>}
* @default null
*/
this.metalnessNode = null;
/**
* The roughness of standard materials is by default inferred from the `roughness`,
* and `roughnessMap` properties. This node property allows to
* overwrite the default and define the roughness with a node instead.
*
* If you don't want to overwrite the roughness but modify the existing
* value instead, use {@link materialRoughness}.
*
* @type {?Node<float>}
* @default null
*/
this.roughnessNode = null;
this.setDefaultValues( _defaultValues$6 );
this.setValues( parameters );
}
/**
* Overwritten since this type of material uses {@link EnvironmentNode}
* to implement the PBR (PMREM based) environment mapping. Besides, the
* method honors `Scene.environment`.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {?EnvironmentNode<vec3>} The environment node.
*/
setupEnvironment( builder ) {
let envNode = super.setupEnvironment( builder );
if ( envNode === null && builder.environmentNode ) {
envNode = builder.environmentNode;
}
return envNode ? new EnvironmentNode( envNode ) : null;
}
/**
* Setups the lighting model.
*
* @return {PhysicalLightingModel} The lighting model.
*/
setupLightingModel( /*builder*/ ) {
return new PhysicalLightingModel();
}
/**
* Setups the specular related node variables.
*/
setupSpecular() {
const specularColorNode = mix( vec3( 0.04 ), diffuseColor.rgb, metalness );
specularColor.assign( vec3( 0.04 ) );
specularColorBlended.assign( specularColorNode );
specularF90.assign( 1.0 );
}
/**
* Setups the standard specific node variables.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setupVariants() {
// METALNESS
const metalnessNode = this.metalnessNode ? float( this.metalnessNode ) : materialMetalness;
metalness.assign( metalnessNode );
// ROUGHNESS
let roughnessNode = this.roughnessNode ? float( this.roughnessNode ) : materialRoughness;
roughnessNode = getRoughness( { roughness: roughnessNode } );
roughness.assign( roughnessNode );
// SPECULAR COLOR
this.setupSpecular();
// DIFFUSE COLOR
diffuseContribution.assign( diffuseColor.rgb.mul( metalnessNode.oneMinus() ) );
}
}
const _defaultValues$5 = /*@__PURE__*/ new MeshPhysicalMaterial();
/**
* Node material version of {@link MeshPhysicalMaterial}.
*
* @augments MeshStandardNodeMaterial
*/
class MeshPhysicalNodeMaterial extends MeshStandardNodeMaterial {
static get type() {
return 'MeshPhysicalNodeMaterial';
}
/**
* Constructs a new mesh physical node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMeshPhysicalNodeMaterial = true;
/**
* The clearcoat of physical materials is by default inferred from the `clearcoat`
* and `clearcoatMap` properties. This node property allows to overwrite the default
* and define the clearcoat with a node instead.
*
* If you don't want to overwrite the clearcoat but modify the existing
* value instead, use {@link materialClearcoat}.
*
* @type {?Node<float>}
* @default null
*/
this.clearcoatNode = null;
/**
* The clearcoat roughness of physical materials is by default inferred from the `clearcoatRoughness`
* and `clearcoatRoughnessMap` properties. This node property allows to overwrite the default
* and define the clearcoat roughness with a node instead.
*
* If you don't want to overwrite the clearcoat roughness but modify the existing
* value instead, use {@link materialClearcoatRoughness}.
*
* @type {?Node<float>}
* @default null
*/
this.clearcoatRoughnessNode = null;
/**
* The diffuse roughness of physical materials is by default inferred from the
* `diffuseRoughness` and `diffuseRoughnessMap` properties. This node property allows to overwrite the
* default and define the diffuse roughness with a node instead.
*
* If you don't want to overwrite the diffuse roughness but modify the existing
* value instead, use {@link materialDiffuseRoughness}.
*
* @type {?Node<float>}
* @default null
*/
this.diffuseRoughnessNode = null;
/**
* The clearcoat normal of physical materials is by default inferred from the `clearcoatNormalMap`
* property. This node property allows to overwrite the default
* and define the clearcoat normal with a node instead.
*
* If you don't want to overwrite the clearcoat normal but modify the existing
* value instead, use {@link materialClearcoatNormal}.
*
* @type {?Node<vec3>}
* @default null
*/
this.clearcoatNormalNode = null;
/**
* The sheen of physical materials is by default inferred from the `sheen`, `sheenColor`
* and `sheenColorMap` properties. This node property allows to overwrite the default
* and define the sheen with a node instead.
*
* If you don't want to overwrite the sheen but modify the existing
* value instead, use {@link materialSheen}.
*
* @type {?Node<vec3>}
* @default null
*/
this.sheenNode = null;
/**
* The sheen roughness of physical materials is by default inferred from the `sheenRoughness` and
* `sheenRoughnessMap` properties. This node property allows to overwrite the default
* and define the sheen roughness with a node instead.
*
* If you don't want to overwrite the sheen roughness but modify the existing
* value instead, use {@link materialSheenRoughness}.
*
* @type {?Node<float>}
* @default null
*/
this.sheenRoughnessNode = null;
/**
* The iridescence of physical materials is by default inferred from the `iridescence`
* property. This node property allows to overwrite the default
* and define the iridescence with a node instead.
*
* If you don't want to overwrite the iridescence but modify the existing
* value instead, use {@link materialIridescence}.
*
* @type {?Node<float>}
* @default null
*/
this.iridescenceNode = null;
/**
* The iridescence IOR of physical materials is by default inferred from the `iridescenceIOR`
* property. This node property allows to overwrite the default
* and define the iridescence IOR with a node instead.
*
* If you don't want to overwrite the iridescence IOR but modify the existing
* value instead, use {@link materialIridescenceIOR}.
*
* @type {?Node<float>}
* @default null
*/
this.iridescenceIORNode = null;
/**
* The iridescence thickness of physical materials is by default inferred from the `iridescenceThicknessMinimum`,
* `iridescenceThicknessMaximum` and `iridescenceThicknessMap` properties. This node property allows to overwrite the default
* and define the iridescence thickness with a node instead.
*
* If you don't want to overwrite the iridescence thickness but modify the existing
* value instead, use {@link materialIridescenceThickness}.
*
* @type {?Node<float>}
* @default null
*/
this.iridescenceThicknessNode = null;
/**
* The specular intensity of physical materials is by default inferred from the `specularIntensity`
* and `specularIntensityMap` properties. This node property allows to overwrite the default
* and define the specular intensity with a node instead.
*
* If you don't want to overwrite the specular intensity but modify the existing
* value instead, use {@link materialSpecularIntensity}.
*
* @type {?Node<float>}
* @default null
*/
this.specularIntensityNode = null;
/**
* The specular color of physical materials is by default inferred from the `specularColor`
* and `specularColorMap` properties. This node property allows to overwrite the default
* and define the specular color with a node instead.
*
* If you don't want to overwrite the specular color but modify the existing
* value instead, use {@link materialSpecularColor}.
*
* @type {?Node<vec3>}
* @default null
*/
this.specularColorNode = null;
/**
* The ior of physical materials is by default inferred from the `ior`
* property. This node property allows to overwrite the default
* and define the ior with a node instead.
*
* If you don't want to overwrite the ior but modify the existing
* value instead, use {@link materialIOR}.
*
* @type {?Node<float>}
* @default null
*/
this.iorNode = null;
/**
* The transmission of physical materials is by default inferred from the `transmission` and
* `transmissionMap` properties. This node property allows to overwrite the default
* and define the transmission with a node instead.
*
* If you don't want to overwrite the transmission but modify the existing
* value instead, use {@link materialTransmission}.
*
* @type {?Node<float>}
* @default null
*/
this.transmissionNode = null;
/**
* The thickness of physical materials is by default inferred from the `thickness` and
* `thicknessMap` properties. This node property allows to overwrite the default
* and define the thickness with a node instead.
*
* If you don't want to overwrite the thickness but modify the existing
* value instead, use {@link materialThickness}.
*
* @type {?Node<float>}
* @default null
*/
this.thicknessNode = null;
/**
* The attenuation distance of physical materials is by default inferred from the
* `attenuationDistance` property. This node property allows to overwrite the default
* and define the attenuation distance with a node instead.
*
* If you don't want to overwrite the attenuation distance but modify the existing
* value instead, use {@link materialAttenuationDistance}.
*
* @type {?Node<float>}
* @default null
*/
this.attenuationDistanceNode = null;
/**
* The attenuation color of physical materials is by default inferred from the
* `attenuationColor` property. This node property allows to overwrite the default
* and define the attenuation color with a node instead.
*
* If you don't want to overwrite the attenuation color but modify the existing
* value instead, use {@link materialAttenuationColor}.
*
* @type {?Node<vec3>}
* @default null
*/
this.attenuationColorNode = null;
/**
* The dispersion of physical materials is by default inferred from the
* `dispersion` property. This node property allows to overwrite the default
* and define the dispersion with a node instead.
*
* If you don't want to overwrite the dispersion but modify the existing
* value instead, use {@link materialDispersion}.
*
* @type {?Node<float>}
* @default null
*/
this.dispersionNode = null;
/**
* The retroreflective strength of physical materials is by default inferred from the
* `retroreflectivity` property. This node property allows to overwrite the default
* and define the retroreflective strength with a node instead.
*
* If you don't want to overwrite the retroreflective strength but modify the existing
* value instead, use {@link materialRetroreflectivity}.
*
* @type {?Node<float>}
* @default null
*/
this.retroreflectivityNode = null;
/**
* The anisotropy of physical materials is by default inferred from the
* `anisotropy` property. This node property allows to overwrite the default
* and define the anisotropy with a node instead.
*
* If you don't want to overwrite the anisotropy but modify the existing
* value instead, use {@link materialAnisotropy}.
*
* @type {?Node<float>}
* @default null
*/
this.anisotropyNode = null;
this.setDefaultValues( _defaultValues$5 );
this.setValues( parameters );
}
/**
* Whether the lighting model should use clearcoat or not.
*
* @type {boolean}
* @default true
*/
get useClearcoat() {
return this.clearcoat > 0 || this.clearcoatNode !== null;
}
/**
* Whether the lighting model should use EON rough diffuse reflection or not.
*
* @type {boolean}
* @default false
*/
get useDiffuseRoughness() {
return this.diffuseRoughness > 0 || this.diffuseRoughnessNode !== null;
}
/**
* Whether the lighting model should use iridescence or not.
*
* @type {boolean}
* @default true
*/
get useIridescence() {
return this.iridescence > 0 || this.iridescenceNode !== null;
}
/**
* Whether the lighting model should use sheen or not.
*
* @type {boolean}
* @default true
*/
get useSheen() {
return this.sheen > 0 || this.sheenNode !== null;
}
/**
* Whether the lighting model should use anisotropy or not.
*
* @type {boolean}
* @default true
*/
get useAnisotropy() {
return this.anisotropy > 0 || this.anisotropyNode !== null;
}
/**
* Whether the lighting model should use transmission or not.
*
* @type {boolean}
* @default true
*/
get useTransmission() {
return this.transmission > 0 || this.transmissionNode !== null;
}
/**
* Whether the lighting model should use dispersion or not.
*
* @type {boolean}
* @default true
*/
get useDispersion() {
return this.dispersion > 0 || this.dispersionNode !== null;
}
/**
* Whether the lighting model should use retroreflection or not.
*
* @type {boolean}
* @default true
*/
get useRetroreflection() {
return this.retroreflectivity > 0 || this.retroreflectivityNode !== null;
}
/**
* Setups the specular related node variables.
*/
setupSpecular() {
const iorNode = this.iorNode ? float( this.iorNode ) : materialIOR;
const specularColorNode = this.specularColorNode ? vec3( this.specularColorNode ) : materialSpecularColor;
const specularIntensityNode = this.specularIntensityNode ? float( this.specularIntensityNode ) : materialSpecularIntensity;
ior.assign( iorNode );
specularColor.assign( min( pow2( ior.sub( 1.0 ).div( ior.add( 1.0 ) ) ).mul( specularColorNode ), vec3( 1.0 ) ).mul( specularIntensityNode ) );
specularColorBlended.assign( mix( specularColor, diffuseColor.rgb, metalness ) );
specularF90.assign( mix( specularIntensityNode, 1.0, metalness ) );
}
/**
* Setups the lighting model.
*
* @return {PhysicalLightingModel} The lighting model.
*/
setupLightingModel( /*builder*/ ) {
return new PhysicalLightingModel( this.useClearcoat, this.useSheen, this.useIridescence, this.useAnisotropy, this.useTransmission, this.useDispersion, this.useRetroreflection, this.useDiffuseRoughness );
}
/**
* Setups the physical specific node variables.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setupVariants( builder ) {
super.setupVariants( builder );
// DIFFUSE ROUGHNESS
if ( this.useDiffuseRoughness ) {
const diffuseRoughnessNode = this.diffuseRoughnessNode ? float( this.diffuseRoughnessNode ) : materialDiffuseRoughness;
diffuseRoughness.assign( diffuseRoughnessNode.clamp() );
}
// CLEARCOAT
if ( this.useClearcoat ) {
const clearcoatNode = this.clearcoatNode ? float( this.clearcoatNode ) : materialClearcoat;
const clearcoatRoughnessNode = this.clearcoatRoughnessNode ? float( this.clearcoatRoughnessNode ) : materialClearcoatRoughness;
clearcoat.assign( clearcoatNode );
clearcoatRoughness.assign( getRoughness( { roughness: clearcoatRoughnessNode } ) );
}
// SHEEN
if ( this.useSheen ) {
const sheenNode = this.sheenNode ? vec3( this.sheenNode ) : materialSheen;
const sheenRoughnessNode = this.sheenRoughnessNode ? float( this.sheenRoughnessNode ) : materialSheenRoughness;
sheen.assign( sheenNode );
sheenRoughness.assign( sheenRoughnessNode );
}
// RETROREFLECTION
if ( this.useRetroreflection ) {
const retroreflectivityNode = this.retroreflectivityNode ? float( this.retroreflectivityNode ) : materialRetroreflectivity;
retroreflectivity.assign( retroreflectivityNode );
}
// IRIDESCENCE
if ( this.useIridescence ) {
const iridescenceNode = this.iridescenceNode ? float( this.iridescenceNode ) : materialIridescence;
const iridescenceIORNode = this.iridescenceIORNode ? float( this.iridescenceIORNode ) : materialIridescenceIOR;
const iridescenceThicknessNode = this.iridescenceThicknessNode ? float( this.iridescenceThicknessNode ) : materialIridescenceThickness;
iridescence.assign( iridescenceNode );
iridescenceIOR.assign( iridescenceIORNode );
iridescenceThickness.assign( iridescenceThicknessNode );
}
// ANISOTROPY
if ( this.useAnisotropy ) {
const anisotropyV = ( this.anisotropyNode ? vec2( this.anisotropyNode ) : materialAnisotropy ).toVar();
anisotropy.assign( anisotropyV.length() );
If( anisotropy.equal( 0.0 ), () => {
anisotropyV.assign( vec2( 1.0, 0.0 ) );
} ).Else( () => {
anisotropyV.divAssign( vec2( anisotropy ) );
anisotropy.assign( anisotropy.saturate() );
} );
// Roughness along the anisotropy bitangent is the material roughness, while the tangent roughness increases with anisotropy.
alphaT.assign( anisotropy.pow2().mix( roughness.pow2(), 1.0 ) );
anisotropyT.assign( TBNViewMatrix[ 0 ].mul( anisotropyV.x ).add( TBNViewMatrix[ 1 ].mul( anisotropyV.y ) ) );
anisotropyB.assign( TBNViewMatrix[ 1 ].mul( anisotropyV.x ).sub( TBNViewMatrix[ 0 ].mul( anisotropyV.y ) ) );
}
// TRANSMISSION
if ( this.useTransmission ) {
const transmissionNode = this.transmissionNode ? float( this.transmissionNode ) : materialTransmission;
const thicknessNode = this.thicknessNode ? float( this.thicknessNode ) : materialThickness;
const attenuationDistanceNode = this.attenuationDistanceNode ? float( this.attenuationDistanceNode ) : materialAttenuationDistance;
const attenuationColorNode = this.attenuationColorNode ? vec3( this.attenuationColorNode ) : materialAttenuationColor;
transmission.assign( transmissionNode );
thickness.assign( thicknessNode );
attenuationDistance.assign( attenuationDistanceNode );
attenuationColor.assign( attenuationColorNode );
if ( this.useDispersion ) {
const dispersionNode = this.dispersionNode ? float( this.dispersionNode ) : materialDispersion;
dispersion.assign( dispersionNode );
}
}
}
/**
* Setups the clearcoat normal node.
*
* @return {Node<vec3>} The clearcoat normal.
*/
setupClearcoatNormal() {
return this.clearcoatNormalNode ? vec3( this.clearcoatNormalNode ) : materialClearcoatNormal;
}
setup( builder ) {
builder.context.setupClearcoatNormal = () => subBuild( this.setupClearcoatNormal( builder ), 'NORMAL', 'vec3' );
super.setup( builder );
}
}
/**
* Represents the lighting model for {@link MeshSSSNodeMaterial}.
*
* @augments PhysicalLightingModel
*/
class SSSLightingModel extends PhysicalLightingModel {
/**
* Constructs a new physical lighting model.
*
* @param {boolean} [clearcoat=false] - Whether clearcoat is supported or not.
* @param {boolean} [sheen=false] - Whether sheen is supported or not.
* @param {boolean} [iridescence=false] - Whether iridescence is supported or not.
* @param {boolean} [anisotropy=false] - Whether anisotropy is supported or not.
* @param {boolean} [transmission=false] - Whether transmission is supported or not.
* @param {boolean} [dispersion=false] - Whether dispersion is supported or not.
* @param {boolean} [sss=false] - Whether SSS is supported or not.
*/
constructor( clearcoat = false, sheen = false, iridescence = false, anisotropy = false, transmission = false, dispersion = false, sss = false ) {
super( clearcoat, sheen, iridescence, anisotropy, transmission, dispersion );
/**
* Whether the lighting model should use SSS or not.
*
* @type {boolean}
* @default false
*/
this.useSSS = sss;
}
/**
* Extends the default implementation with a SSS term.
*
* Reference: [Approximating Translucency for a Fast, Cheap and Convincing Subsurface Scattering Look](https://colinbarrebrisebois.com/2011/03/07/gdc-2011-approximating-translucency-for-a-fast-cheap-and-convincing-subsurface-scattering-look/)
*
* @param {Object} input - The input data.
* @param {NodeBuilder} builder - The current node builder.
*/
direct( { lightDirection, lightColor, reflectedLight }, builder ) {
if ( this.useSSS === true ) {
const material = builder.material;
const { thicknessColorNode, thicknessDistortionNode, thicknessAmbientNode, thicknessAttenuationNode, thicknessPowerNode, thicknessScaleNode } = material;
const scatteringHalf = lightDirection.add( normalView.mul( thicknessDistortionNode ) ).normalize();
const scatteringDot = float( positionViewDirection.dot( scatteringHalf.negate() ).saturate().pow( thicknessPowerNode ).mul( thicknessScaleNode ) );
const scatteringIllu = vec3( scatteringDot.add( thicknessAmbientNode ).mul( thicknessColorNode ) );
reflectedLight.directDiffuse.addAssign( scatteringIllu.mul( thicknessAttenuationNode.mul( lightColor ) ) );
}
super.direct( { lightDirection, lightColor, reflectedLight }, builder );
}
}
/**
* This node material is an experimental extension of {@link MeshPhysicalNodeMaterial}
* that implements a Subsurface scattering (SSS) term.
*
* @augments MeshPhysicalNodeMaterial
*/
class MeshSSSNodeMaterial extends MeshPhysicalNodeMaterial {
static get type() {
return 'MeshSSSNodeMaterial';
}
/**
* Constructs a new mesh SSS node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super( parameters );
/**
* Represents the thickness color.
*
* @type {?Node<vec3>}
* @default null
*/
this.thicknessColorNode = null;
/**
* Represents the distortion factor.
*
* @type {?Node<float>}
*/
this.thicknessDistortionNode = float( 0.1 );
/**
* Represents the thickness ambient factor.
*
* @type {?Node<float>}
*/
this.thicknessAmbientNode = float( 0.0 );
/**
* Represents the thickness attenuation.
*
* @type {?Node<float>}
*/
this.thicknessAttenuationNode = float( .1 );
/**
* Represents the thickness power.
*
* @type {?Node<float>}
*/
this.thicknessPowerNode = float( 2.0 );
/**
* Represents the thickness scale.
*
* @type {?Node<float>}
*/
this.thicknessScaleNode = float( 10.0 );
}
/**
* Whether the lighting model should use SSS or not.
*
* @type {boolean}
* @default true
*/
get useSSS() {
return this.thicknessColorNode !== null;
}
/**
* Setups the lighting model.
*
* @return {SSSLightingModel} The lighting model.
*/
setupLightingModel( /*builder*/ ) {
return new SSSLightingModel( this.useClearcoat, this.useSheen, this.useIridescence, this.useAnisotropy, this.useTransmission, this.useDispersion, this.useSSS );
}
}
const getGradientIrradiance = /*@__PURE__*/ Fn( ( { normal, lightDirection, builder } ) => {
// dotNL will be from -1.0 to 1.0
const dotNL = normal.dot( lightDirection );
const coord = vec2( dotNL.mul( 0.5 ).add( 0.5 ), 0.0 );
if ( builder.material.gradientMap ) {
const gradientMap = materialReference( 'gradientMap', 'texture' ).context( { getUV: () => coord } );
return vec3( gradientMap.r );
} else {
const fw = coord.fwidth().mul( 0.5 );
return mix( vec3( 0.7 ), vec3( 1.0 ), smoothstep( float( 0.7 ).sub( fw.x ), float( 0.7 ).add( fw.x ), coord.x ) );
}
} );
/**
* Represents the lighting model for a toon material. Used in {@link MeshToonNodeMaterial}.
*
* @augments LightingModel
*/
class ToonLightingModel extends LightingModel {
/**
* Implements the direct lighting. Instead of using a conventional smooth irradiance, the irradiance is
* reduced to a small number of discrete shades to create a comic-like, flat look.
*
* @param {Object} lightData - The light data.
* @param {NodeBuilder} builder - The current node builder.
*/
direct( { lightDirection, lightColor, reflectedLight }, builder ) {
const irradiance = getGradientIrradiance( { normal: normalView, lightDirection, builder } ).mul( lightColor );
reflectedLight.directDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor: diffuseColor.rgb } ) ) );
}
/**
* Implements the indirect lighting.
*
* @param {NodeBuilder} builder - The current node builder.
*/
indirect( builder ) {
const { ambientOcclusion, irradiance, reflectedLight } = builder.context;
reflectedLight.indirectDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor } ) ) );
reflectedLight.indirectDiffuse.mulAssign( ambientOcclusion );
}
}
const _defaultValues$4 = /*@__PURE__*/ new MeshToonMaterial();
/**
* Node material version of {@link MeshToonMaterial}.
*
* @augments NodeMaterial
*/
class MeshToonNodeMaterial extends NodeMaterial {
static get type() {
return 'MeshToonNodeMaterial';
}
/**
* Constructs a new mesh toon node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMeshToonNodeMaterial = true;
/**
* Set to `true` because toon materials react on lights.
*
* @type {boolean}
* @default true
*/
this.lights = true;
/**
* The emissive color of toon materials is by default inferred from the `emissive`,
* `emissiveIntensity` and `emissiveMap` properties. This node property allows to
* overwrite the default and define the emissive color with a node instead.
*
* If you don't want to overwrite the emissive color but modify the existing
* value instead, use {@link materialEmissive}.
*
* @type {?Node<vec3>}
* @default null
*/
this.emissiveNode = null;
this.setDefaultValues( _defaultValues$4 );
this.setValues( parameters );
}
/**
* Setups the lighting model.
*
* @return {ToonLightingModel} The lighting model.
*/
setupLightingModel( /*builder*/ ) {
return new ToonLightingModel();
}
}
const _defaultValues$3 = /*@__PURE__*/ new MeshMatcapMaterial();
/**
* Node material version of {@link MeshMatcapMaterial}.
*
* @augments NodeMaterial
*/
class MeshMatcapNodeMaterial extends NodeMaterial {
static get type() {
return 'MeshMatcapNodeMaterial';
}
/**
* Constructs a new mesh normal node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMeshMatcapNodeMaterial = true;
this.setDefaultValues( _defaultValues$3 );
this.setValues( parameters );
}
/**
* Setups the matcap specific node variables.
*
* @param {NodeBuilder} builder - The current node builder.
*/
setupVariants( builder ) {
const uv = matcapUV;
let matcapColor;
if ( builder.material.matcap ) {
matcapColor = materialReference( 'matcap', 'texture' ).context( { getUV: () => uv } );
} else {
matcapColor = vec3( mix( 0.2, 0.8, uv.y ) ); // default if matcap is missing
}
diffuseColor.rgb.mulAssign( matcapColor.rgb );
}
}
const _defaultValues$2 = /*@__PURE__*/ new SpriteMaterial();
/**
* Node material version of {@link SpriteMaterial}.
*
* @augments NodeMaterial
*/
class SpriteNodeMaterial extends NodeMaterial {
static get type() {
return 'SpriteNodeMaterial';
}
/**
* Constructs a new sprite node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isSpriteNodeMaterial = true;
this._useSizeAttenuation = true;
/**
* This property makes it possible to define the position of the sprite with a
* node. That can be useful when the material is used with instanced rendering
* and node data are defined with an instanced attribute node:
* ```js
* const positionAttribute = new InstancedBufferAttribute( new Float32Array( positions ), 3 );
* material.positionNode = instancedBufferAttribute( positionAttribute );
* ```
* Another possibility is to compute the instanced data with a compute shader:
* ```js
* const positionBuffer = instancedArray( particleCount, 'vec3' );
* particleMaterial.positionNode = positionBuffer.toAttribute();
* ```
*
* @type {?Node<vec3>}
* @default null
*/
this.positionNode = null;
/**
* The rotation of sprite materials is by default inferred from the `rotation`,
* property. This node property allows to overwrite the default and define
* the rotation with a node instead.
*
* If you don't want to overwrite the rotation but modify the existing
* value instead, use {@link materialRotation}.
*
* @type {?Node<float>}
* @default null
*/
this.rotationNode = null;
/**
* This node property provides an additional way to scale sprites next to
* `Object3D.scale`. The scale transformation based in `Object3D.scale`
* is multiplied with the scale value of this node in the vertex shader.
*
* @type {?Node<vec2>}
* @default null
*/
this.scaleNode = null;
/**
* In Sprites, the transparent property is enabled by default.
*
* @type {boolean}
* @default true
*/
this.transparent = true;
this.setDefaultValues( _defaultValues$2 );
this.setValues( parameters );
}
/**
* Setups the position node in view space. This method implements
* the sprite specific vertex shader.
*
* @param {NodeBuilder} builder - The current node builder.
* @return {Node<vec3>} The position in view space.
*/
setupPositionView( builder ) {
const { object, camera } = builder;
const { positionNode, rotationNode, scaleNode, sizeAttenuation } = this;
const mvPosition = modelViewMatrix.mul( vec3( positionNode || 0 ) );
let scale = vec2( modelWorldMatrix[ 0 ].xyz.length(), modelWorldMatrix[ 1 ].xyz.length() );
if ( scaleNode !== null ) {
scale = scale.mul( vec2( scaleNode ) );
}
if ( camera.isOrthographicCamera !== true && sizeAttenuation === false ) {
scale = scale.mul( mvPosition.z.negate() );
}
let alignedPosition = positionGeometry.xy;
if ( object.center && object.center.isVector2 === true ) {
const center = reference( 'center', 'vec2' );
alignedPosition = alignedPosition.sub( center.sub( 0.5 ) );
}
alignedPosition = alignedPosition.mul( scale );
const rotation = float( rotationNode || materialRotation );
const rotatedPosition = rotate( alignedPosition, rotation );
return vec4( mvPosition.xy.add( rotatedPosition ), mvPosition.zw );
}
/**
* Whether to use size attenuation or not.
*
* @type {boolean}
* @default true
*/
get sizeAttenuation() {
return this._useSizeAttenuation;
}
set sizeAttenuation( value ) {
if ( this._useSizeAttenuation !== value ) {
this._useSizeAttenuation = value;
this.needsUpdate = true;
}
}
}
const _defaultValues$1 = /*@__PURE__*/ new PointsMaterial();
const _size$1 = /*@__PURE__*/ new Vector2();
/**
* Node material version of {@link PointsMaterial}.
*
* This material can be used in two ways:
*
* - By rendering point primitives with {@link Points}. Since WebGPU only supports point primitives
* with a pixel size of `1`, it's not possible to define a size.
*
* ```js
* const pointCloud = new THREE.Points( geometry, new THREE.PointsNodeMaterial() );
* ```
*
* - By rendering point primitives with {@link Sprites}. In this case, size is honored,
* see {@link PointsNodeMaterial#sizeNode}.
*
* ```js
* const instancedPoints = new THREE.Sprite( new THREE.PointsNodeMaterial( { positionNode: instancedBufferAttribute( positionAttribute ) } ) );
* ```
*
* @augments SpriteNodeMaterial
*/
class PointsNodeMaterial extends SpriteNodeMaterial {
static get type() {
return 'PointsNodeMaterial';
}
/**
* Constructs a new points node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This node property provides an additional way to set the point size.
*
* Note that WebGPU only supports point primitives with 1 pixel size. Consequently,
* this node has no effect when the material is used with {@link Points} and a WebGPU
* backend. If an application wants to render points with a size larger than 1 pixel,
* the material should be used with {@link Sprite} and instancing.
*
* @type {?Node<vec2>}
* @default null
*/
this.sizeNode = null;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isPointsNodeMaterial = true;
this.setDefaultValues( _defaultValues$1 );
this.setValues( parameters );
}
setupPositionView() {
const { positionNode } = this;
return modelViewMatrix.mul( vec3( positionNode || positionLocal ) ).xyz;
}
setupVertexSprite( builder ) {
const { material, camera } = builder;
const { rotationNode, scaleNode, sizeNode, sizeAttenuation } = this;
let mvp = super.setupVertex( builder );
// skip further processing if the material is not a node material
if ( material.isNodeMaterial !== true ) {
return mvp;
}
// point size
let pointSize = sizeNode !== null ? vec2( sizeNode ) : materialPointSize;
pointSize = pointSize.mul( screenDPR );
// size attenuation
if ( camera.isOrthographicCamera !== true && sizeAttenuation === true ) {
// follow WebGLRenderer's implementation, and scale by half the canvas height in logical units
pointSize = pointSize.mul( scale.div( positionView.z.negate() ) );
}
// scale
if ( scaleNode && scaleNode.isNode ) {
pointSize = pointSize.mul( vec2( scaleNode ) );
}
// compute offset
let offset = positionGeometry.xy;
// apply rotation
if ( rotationNode && rotationNode.isNode ) {
const rotation = float( rotationNode );
offset = rotate( offset, rotation );
}
// account for point size
offset = offset.mul( pointSize );
// scale by viewport size
offset = offset.div( viewportSize.div( 2 ) );
// compensate for the perspective divide
offset = offset.mul( mvp.w );
// add offset
mvp = mvp.add( vec4( offset, 0, 0 ) );
return mvp;
}
setupVertex( builder ) {
if ( builder.object.isPoints ) {
return super.setupVertex( builder );
} else {
return this.setupVertexSprite( builder );
}
}
/**
* Whether alpha to coverage should be used or not.
*
* @type {boolean}
* @default true
*/
get alphaToCoverage() {
return this._useAlphaToCoverage;
}
set alphaToCoverage( value ) {
if ( this._useAlphaToCoverage !== value ) {
this._useAlphaToCoverage = value;
this.needsUpdate = true;
}
}
}
const scale = /*@__PURE__*/ uniform( 1 ).setGroup( renderGroup ).onRenderUpdate( function ( { renderer } ) {
const size = renderer.getSize( _size$1 ); // logical units
this.value = 0.5 * size.y;
} );
/**
* Represents lighting model for a shadow material. Used in {@link ShadowNodeMaterial}.
*
* @augments LightingModel
*/
class ShadowMaskModel extends LightingModel {
/**
* Constructs a new shadow mask model.
*/
constructor() {
super();
/**
* The shadow mask node.
*
* @type {Node}
*/
this.shadowNode = float( 1 ).toVar( 'shadowMask' );
}
/**
* Only used to save the shadow mask.
*
* @param {Object} input - The input data.
*/
direct( { lightNode } ) {
if ( lightNode.shadowNode !== null ) {
this.shadowNode.mulAssign( lightNode.shadowNode );
}
}
/**
* Uses the shadow mask to produce the final color.
*
* @param {NodeBuilder} builder - The current node builder.
*/
finish( { context } ) {
diffuseColor.a.mulAssign( this.shadowNode.oneMinus() );
context.outgoingLight.rgb.assign( diffuseColor.rgb ); // TODO: Optimize LightsNode to avoid this assignment
}
}
const _defaultValues = /*@__PURE__*/ new ShadowMaterial();
/**
* Node material version of {@link ShadowMaterial}.
*
* @augments NodeMaterial
*/
class ShadowNodeMaterial extends NodeMaterial {
static get type() {
return 'ShadowNodeMaterial';
}
/**
* Constructs a new shadow node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isShadowNodeMaterial = true;
/**
* Set to `true` because so it's possible to implement
* the shadow mask effect.
*
* @type {boolean}
* @default true
*/
this.lights = true;
/**
* Overwritten since shadow materials are transparent
* by default.
*
* @type {boolean}
* @default true
*/
this.transparent = true;
this.setDefaultValues( _defaultValues );
this.setValues( parameters );
}
/**
* Setups the lighting model.
*
* @return {ShadowMaskModel} The lighting model.
*/
setupLightingModel( /*builder*/ ) {
return new ShadowMaskModel();
}
}
const scatteringDensity = property( 'vec3' );
const linearDepthRay = property( 'float' ); // always assigned a scalar (see `start()`) - not a per-channel value
const outgoingRayLight = property( 'vec3' );
/**
* VolumetricLightingModel class extends the LightingModel to implement volumetric lighting effects.
* This model calculates the scattering and transmittance of light through a volumetric medium.
* It dynamically adjusts the direction of the ray based on the camera and object positions.
* The model supports custom scattering and depth nodes to enhance the lighting effects.
*
* @augments LightingModel
*/
class VolumetricLightingModel extends LightingModel {
constructor() {
super();
}
start( builder ) {
const { material } = builder;
const startPos = property( 'vec3' );
const endPos = property( 'vec3' );
// This approach dynamically changes the direction of the ray,
// prioritizing the ray from the camera to the object if it is inside the mesh, and from the object to the camera if it is far away.
const isFrontToBack = property( 'bool' );
If( cameraPosition.sub( positionWorld ).length().greaterThan( modelRadius.mul( 2 ) ), () => {
startPos.assign( cameraPosition );
endPos.assign( positionWorld );
isFrontToBack.assign( true );
} ).Else( () => {
startPos.assign( positionWorld );
endPos.assign( cameraPosition );
isFrontToBack.assign( false );
} );
//
const viewVector = endPos.sub( startPos );
const steps = uniform( 'int' ).onRenderUpdate( ( { material } ) => material.steps );
const stepSize = viewVector.length().div( steps ).toVar();
const rayDir = viewVector.normalize().toVar(); // TODO: toVar() should be automatic here ( in loop )
const distTravelled = float( 0.0 ).toVar();
const transmittance = vec3( 1 ).toVar();
if ( material.offsetNode ) {
// reduce banding
distTravelled.addAssign( material.offsetNode.mul( stepSize ) );
}
Loop( steps, () => {
const positionRay = startPos.add( rayDir.mul( distTravelled ) );
const positionViewRay = cameraViewMatrix.mul( vec4( positionRay, 1 ) ).xyz;
if ( material.depthNode !== null ) {
linearDepthRay.assign( linearDepth( viewZToPerspectiveDepth( positionViewRay.z, cameraNear, cameraFar ) ) );
builder.context.sceneDepthNode = linearDepth( material.depthNode ).toVar();
}
builder.context.positionWorld = positionRay;
builder.context.shadowPositionWorld = positionRay;
builder.context.positionView = positionViewRay;
scatteringDensity.assign( 0 );
let scatteringNode;
let scatteringEmissiveNode;
if ( material.scatteringNode ) {
scatteringNode = material.scatteringNode( { positionRay } );
}
if ( material.scatteringEmissiveNode ) {
scatteringEmissiveNode = material.scatteringEmissiveNode( { positionRay } );
}
super.start( builder );
if ( scatteringNode ) {
scatteringDensity.mulAssign( scatteringNode );
}
const stepLight = scatteringDensity.mul( 0.01 ).toVar();
if ( scatteringEmissiveNode ) {
stepLight.addAssign( scatteringEmissiveNode.mul( 0.01 ) );
}
// beer's law
const falloff = scatteringDensity.mul( .01 ).negate().mul( stepSize ).exp();
If( isFrontToBack, () => {
outgoingRayLight.addAssign( stepLight.mul( transmittance ).mul( stepSize ) );
} ).Else( () => {
outgoingRayLight.assign( outgoingRayLight.mul( falloff ).add( stepLight.mul( stepSize ) ) );
} );
transmittance.mulAssign( falloff );
// move along the ray
distTravelled.addAssign( stepSize );
} );
}
scatteringLight( lightColor, builder ) {
const sceneDepthNode = builder.context.sceneDepthNode;
if ( sceneDepthNode ) {
If( sceneDepthNode.greaterThanEqual( linearDepthRay ), () => {
scatteringDensity.addAssign( lightColor );
} );
} else {
scatteringDensity.addAssign( lightColor );
}
}
direct( { lightNode, lightColor }, builder ) {
// Ignore non-analytical lights and lights with infinite distance
if ( lightNode.isAnalyticLightNode !== true || lightNode.light.distance === undefined ) return;
// TODO: We need a viewportOpaque*() ( output, depth ) to fit with modern rendering approaches
const directLight = lightColor.xyz.toVar();
if ( lightNode.shadowNode !== null ) {
directLight.mulAssign( lightNode.shadowNode ); // it no should be necessary if used in the same render pass
}
this.scatteringLight( directLight, builder );
}
directRectArea( { lightColor, lightPosition, halfWidth, halfHeight }, builder ) {
const p0 = lightPosition.add( halfWidth ).sub( halfHeight ); // counterclockwise; light shines in local neg z direction
const p1 = lightPosition.sub( halfWidth ).sub( halfHeight );
const p2 = lightPosition.sub( halfWidth ).add( halfHeight );
const p3 = lightPosition.add( halfWidth ).add( halfHeight );
const P = builder.context.positionView;
const directLight = lightColor.xyz.mul( LTC_Evaluate_Volume( { P, p0, p1, p2, p3 } ) ).pow( 1.5 );
this.scatteringLight( directLight, builder );
}
finish( builder ) {
builder.context.outgoingLight.assign( outgoingRayLight );
}
}
/**
* Volume node material.
*
* @augments NodeMaterial
*/
class VolumeNodeMaterial extends NodeMaterial {
static get type() {
return 'VolumeNodeMaterial';
}
/**
* Constructs a new volume node material.
*
* @param {Object} [parameters] - The configuration parameter.
*/
constructor( parameters ) {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isVolumeNodeMaterial = true;
/**
* Number of steps used for raymarching.
*
* @type {number}
* @default 25
*/
this.steps = 25;
/**
* Offsets the distance a ray has been traveled through a volume.
* Can be used to implement dithering to reduce banding.
*
* @type {Node<float>}
* @default null
*/
this.offsetNode = null;
/**
* Node used for scattering calculations.
*
* @type {Function|FunctionNode<vec4>}
* @default null
*/
this.scatteringNode = null;
this.lights = true;
this.transparent = true;
this.side = BackSide;
this.depthTest = false;
this.depthWrite = false;
this.setValues( parameters );
}
setupLightingModel() {
return new VolumetricLightingModel();
}
}
/**
* This module manages the internal animation loop of the renderer.
*
* @private
*/
class Animation {
/**
* Constructs a new animation loop management component.
*
* @param {Renderer} renderer - A reference to the main renderer.
* @param {NodeManager} nodes - Renderer component for managing nodes related logic.
* @param {Info} info - Renderer component for managing metrics and monitoring data.
*/
constructor( renderer, nodes, info ) {
/**
* A reference to the main renderer.
*
* @type {Renderer}
*/
this.renderer = renderer;
/**
* Renderer component for managing nodes related logic.
*
* @type {NodeManager}
*/
this.nodes = nodes;
/**
* Renderer component for managing metrics and monitoring data.
*
* @type {Info}
*/
this.info = info;
/**
* A reference to the context from `requestAnimationFrame()` can
* be called (usually `window`).
*
* @type {?(Window|XRSession)}
*/
this._context = typeof self !== 'undefined' ? self : null;
/**
* The user-defined animation loop.
*
* @type {?Function}
* @default null
*/
this._animationLoop = null;
/**
* The requestId which is returned from the `requestAnimationFrame()` call.
* Can be used to cancel the stop the animation loop.
*
* @type {?number}
* @default null
*/
this._requestId = null;
}
/**
* Starts the internal animation loop, if it is not running yet.
*/
start() {
if ( this._requestId !== null ) return;
const update = ( time, xrFrame ) => {
this._requestId = this._context.requestAnimationFrame( update );
if ( this.renderer._inspector.isRunning ) {
this.renderer._inspector.finish();
}
if ( this.info.autoReset === true ) this.info.reset();
this.nodes.nodeFrame.update();
this.info.frame = this.nodes.nodeFrame.frameId;
this.renderer._inspector.begin();
if ( this._animationLoop !== null ) {
this._animationLoop( time, xrFrame );
}
};
update();
}
/**
* Stops the internal animation loop.
*/
stop() {
if ( this.renderer._inspector.isRunning ) this.renderer._inspector.finish();
if ( this._context !== null ) this._context.cancelAnimationFrame( this._requestId );
this._requestId = null;
}
/**
* Returns the user-level animation loop.
*
* @return {?Function} The animation loop.
*/
getAnimationLoop() {
return this._animationLoop;
}
/**
* Defines the user-level animation loop.
*
* @param {?Function} callback - The animation loop.
*/
setAnimationLoop( callback ) {
this._animationLoop = callback;
}
/**
* Returns the animation context.
*
* @return {Window|XRSession} The animation context.
*/
getContext() {
return this._context;
}
/**
* Defines the context in which `requestAnimationFrame()` is executed.
*
* @param {Window|XRSession} context - The context to set.
*/
setContext( context ) {
this._context = context;
}
/**
* Frees all internal resources and stops the animation loop.
*/
dispose() {
this.stop();
this._animationLoop = null;
}
}
let _id$a = 0;
const _protoKeysCache = new WeakMap();
const _cacheKeyValues$1 = [ 0, 0, 0, 0, 0 ];
const _enumProperties = [ 'side', 'blending', 'combine', 'normalMapType' ];
function getKeys( obj ) {
const keys = Object.keys( obj );
let protoKeys = _protoKeysCache.get( obj.constructor );
if ( protoKeys === undefined ) {
protoKeys = [];
let proto = Object.getPrototypeOf( obj );
while ( proto ) {
const descriptors = Object.getOwnPropertyDescriptors( proto );
for ( const key in descriptors ) {
const descriptor = descriptors[ key ];
if ( descriptor && typeof descriptor.get === 'function' ) {
protoKeys.push( key );
}
}
proto = Object.getPrototypeOf( proto );
}
_protoKeysCache.set( obj.constructor, protoKeys );
}
for ( let i = 0; i < protoKeys.length; i ++ ) keys.push( protoKeys[ i ] );
return keys;
}
/**
* Returns a key for the kind of the given instance data: none, attribute or storage.
*
* @private
* @param {?BufferAttribute} attribute - The instance data.
* @return {string} The key.
*/
function getInstanceDataKey( attribute ) {
if ( attribute === null || attribute === undefined ) return '-';
return attribute.isStorageInstancedBufferAttribute === true ? 's' : 'a';
}
/**
* A render object is the renderer's representation of single entity that gets drawn
* with a draw command. There is no unique mapping of render objects to 3D objects in the
* scene since render objects also depend from the used material, the current render context
* and the current scene's lighting.
*
* In general, the basic process of the renderer is:
*
* - Analyze the 3D objects in the scene and generate render lists containing render items.
* - Process the render lists by calling one or more render commands for each render item.
* - For each render command, request a render object and perform the draw.
*
* The module provides an interface to get data required for the draw command like the actual
* draw parameters or vertex buffers. It also holds a series of caching related methods since
* creating render objects should only be done when necessary.
*
* @private
*/
class RenderObject {
/**
* Constructs a new render object.
*
* @param {NodeManager} nodes - Renderer component for managing nodes related logic.
* @param {Geometries} geometries - Renderer component for managing geometries.
* @param {Renderer} renderer - The renderer.
* @param {Object3D} object - The 3D object.
* @param {Material} material - The 3D object's material.
* @param {Scene} scene - The scene the 3D object belongs to.
* @param {Camera} camera - The camera the object should be rendered with.
* @param {LightsNode} lightsNode - The lights node.
* @param {RenderContext} renderContext - The render context.
* @param {ClippingContext} clippingContext - The clipping context.
*/
constructor( nodes, geometries, renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext ) {
this.id = _id$a ++;
/**
* Renderer component for managing nodes related logic.
*
* @type {NodeManager}
* @private
*/
this._nodes = nodes;
/**
* Renderer component for managing geometries.
*
* @type {Geometries}
* @private
*/
this._geometries = geometries;
/**
* The renderer.
*
* @type {Renderer}
*/
this.renderer = renderer;
/**
* The 3D object.
*
* @type {Object3D}
*/
this.object = object;
/**
* The 3D object's material.
*
* @type {Material}
*/
this.material = material;
/**
* The scene the 3D object belongs to.
*
* @type {Scene}
*/
this.scene = scene;
/**
* The camera the 3D object should be rendered with.
*
* @type {Camera}
*/
this.camera = camera;
/**
* The lights node.
*
* @type {LightsNode}
*/
this.lightsNode = lightsNode;
/**
* The render context.
*
* @type {RenderContext}
*/
this.context = renderContext;
/**
* The 3D object's geometry.
*
* @type {BufferGeometry}
*/
this.geometry = object.geometry;
/**
* The geometry's version. Incremented whenever the geometry is updated.
*
* @type {number}
* @default 0
*/
this.geometryVersion = 0;
/**
* The render object's version.
*
* @type {number}
*/
this.version = material.version;
/**
* The draw range of the geometry.
*
* @type {?Object}
* @default null
*/
this.drawRange = null;
/**
* An array holding the buffer attributes
* of the render object. This entails attribute
* definitions on geometry and node level.
*
* @type {?Array<BufferAttribute>}
* @default null
*/
this.attributes = null;
/**
* An object holding the version of the
* attributes. The keys are the attribute names
* and the values are the attribute versions.
*
* @type {?Object<string, number>}
* @default null
*/
this.attributesId = null;
/**
* A reference to a render pipeline the render
* object is processed with.
*
* @type {RenderPipeline}
* @default null
*/
this.pipeline = null;
/**
* Only relevant for objects using
* multiple materials. This represents a group entry
* from the respective `BufferGeometry`.
*
* @type {?{start: number, count: number}}
* @default null
*/
this.group = null;
/**
* An array holding the vertex buffers which can
* be buffer attributes but also interleaved buffers.
*
* @type {?Array<BufferAttribute|InterleavedBuffer>}
* @default null
*/
this.vertexBuffers = null;
/**
* The parameters for the draw command.
*
* @type {?Object}
* @default null
*/
this.drawParams = null;
/**
* If this render object is used inside a render bundle,
* this property points to the respective bundle group.
*
* @type {?BundleGroup}
* @default null
*/
this.bundle = null;
/**
* The clipping context.
*
* @type {ClippingContext}
*/
this.clippingContext = clippingContext;
/**
* The clipping context's cache key.
*
* @type {string}
*/
this.clippingContextCacheKey = clippingContext !== null ? clippingContext.cacheKey : '';
/**
* The initial node cache key.
*
* @type {number}
*/
this.initialNodesCacheKey = this.getDynamicCacheKey();
/**
* The initial cache key.
*
* @type {number}
*/
this.initialCacheKey = this.getCacheKey();
/**
* The initial geometry cache key.
*
* @type {string}
*/
this.initialGeometryCacheKey = this.getGeometryCacheKey();
/**
* The node builder state.
*
* @type {?NodeBuilderState}
* @private
* @default null
*/
this._nodeBuilderState = null;
/**
* An array of bindings.
*
* @type {?Array<BindGroup>}
* @private
* @default null
*/
this._bindings = null;
/**
* Reference to the node material observer.
*
* @type {?NodeMaterialObserver}
* @private
* @default null
*/
this._monitor = null;
/**
* The object's original material when this render object is drawn with an
* override material.
*
* @type {?Material}
* @private
* @default null
*/
this._sourceMaterial = renderer._currentSourceMaterial;
/**
* The version of the source material this render object was validated against.
*
* @type {number}
*/
this.sourceMaterialVersion = this._sourceMaterial !== null ? this._sourceMaterial.version : 0;
/**
* An event listener which is defined by `RenderObjects`. It performs
* clean up tasks when `dispose()` on this render object.
*
* @method
*/
this.onDispose = null;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isRenderObject = true;
/**
* An event listener which is executed when `dispose()` is called on
* the material of this render object.
*
* @method
*/
this.onMaterialDispose = () => {
this.dispose();
};
/**
* An event listener which is executed when `dispose()` is called on
* the geometry of this render object.
*
* @method
*/
this.onGeometryDispose = () => {
this._geometries.deleteNodeAttributes( this );
this._geometries.deleteVertexState( this );
// clear geometry cache attributes
this.attributes = null;
this.attributesId = null;
};
/**
* An event listener which is executed when `dispose()` is called on
* the 3D object of this render object.
*
* @method
*/
this.onObjectDispose = () => {
this.dispose();
};
/**
* An event listener which is executed when `dispose()` is called on
* the lights node of this render object, e.g. when the lighting system
* of the renderer is disposed.
*
* @method
*/
this.onLightsNodeDispose = () => {
this.dispose();
};
this.object.addEventListener( 'dispose', this.onObjectDispose );
this.material.addEventListener( 'dispose', this.onMaterialDispose );
this.geometry.addEventListener( 'dispose', this.onGeometryDispose );
this.lightsNode.addEventListener( 'dispose', this.onLightsNodeDispose );
if ( this._sourceMaterial !== null ) {
this._sourceMaterial.addEventListener( 'dispose', this.onMaterialDispose );
}
}
/**
* Updates the clipping context.
*
* @param {ClippingContext} context - The clipping context to set.
*/
updateClipping( context ) {
this.clippingContext = context;
}
/**
* Whether the clipping requires an update or not.
*
* @type {boolean}
* @readonly
*/
get clippingNeedsUpdate() {
if ( this.clippingContext === null || this.clippingContext.cacheKey === this.clippingContextCacheKey ) return false;
this.clippingContextCacheKey = this.clippingContext.cacheKey;
return true;
}
/**
* The number of clipping planes defined in context of hardware clipping.
*
* @type {number}
* @readonly
*/
get hardwareClippingPlanes() {
return this.getNodeBuilderState().hardwareClipping === true ? this.clippingContext.unionClippingCount : 0;
}
/**
* Returns the node builder state of this render object.
*
* @return {NodeBuilderState} The node builder state.
*/
getNodeBuilderState() {
return this._nodeBuilderState || ( this._nodeBuilderState = this._nodes.getForRender( this ) );
}
/**
* Returns the node material observer of this render object.
*
* @return {NodeMaterialObserver} The node material observer.
*/
getMonitor() {
return this._monitor || ( this._monitor = this.getNodeBuilderState().observer );
}
/**
* Returns an array of bind groups of this render object.
*
* @return {Array<BindGroup>} The bindings.
*/
getBindings() {
return this._bindings || ( this._bindings = this.getNodeBuilderState().createBindings( this.renderer._bindings, this.context ) );
}
/**
* Returns a binding group by group name of this render object.
*
* @param {string} name - The name of the binding group.
* @return {?BindGroup} The bindings.
*/
getBindingGroup( name ) {
for ( const bindingGroup of this.getBindings() ) {
if ( bindingGroup.name === name ) {
return bindingGroup;
}
}
}
/**
* Returns the index of the render object's geometry.
*
* @return {?BufferAttribute} The index. Returns `null` for non-indexed geometries.
*/
getIndex() {
return this._geometries.getIndex( this );
}
/**
* Returns the indirect buffer attribute.
*
* @return {?BufferAttribute} The indirect attribute. `null` if no indirect drawing is used.
*/
getIndirect() {
return this._geometries.getIndirect( this );
}
/**
* Returns the byte offset into the indirect attribute buffer.
*
* @return {number|Array<number>} The byte offset into the indirect attribute buffer.
*/
getIndirectOffset() {
return this._geometries.getIndirectOffset( this );
}
/**
* Returns an array that acts as a key for identifying the render object in a chain map.
*
* @return {Array<Object>} An array with object references.
*/
getChainArray() {
if ( this._sourceMaterial !== null ) {
return [ this.object, this.material, this.context, this.lightsNode, this._sourceMaterial ];
}
return [ this.object, this.material, this.context, this.lightsNode ];
}
/**
* This method is used when the geometry of a 3D object has been exchanged and the
* respective render object now requires an update.
*
* @param {BufferGeometry} geometry - The geometry to set.
*/
setGeometry( geometry ) {
// exchanging the geometry means we must move the dipose handler to the new geometry
this.geometry.removeEventListener( 'dispose', this.onGeometryDispose );
this.geometry = geometry;
this.geometry.addEventListener( 'dispose', this.onGeometryDispose );
this.attributes = null;
this.attributesId = null;
this.geometryVersion ++;
}
/**
* Returns the buffer attributes of the render object. The returned array holds
* attribute definitions on geometry and node level.
*
* @return {Array<BufferAttribute>} An array with buffer attributes.
*/
getAttributes() {
if ( this.attributes !== null ) return this.attributes;
const nodeAttributes = this.getNodeBuilderState().nodeAttributes;
const geometry = this.geometry;
const attributes = [];
const vertexBuffers = new Set();
const attributesId = {};
for ( const nodeAttribute of nodeAttributes ) {
let attribute;
if ( nodeAttribute.node && nodeAttribute.node.attribute ) {
// node attribute
attribute = nodeAttribute.node.getObjectAttribute( this.object );
} else {
// geometry attribute
attribute = geometry.getAttribute( nodeAttribute.name );
if ( attribute !== undefined ) {
if ( attribute.isInterleavedBufferAttribute ) {
attributesId[ nodeAttribute.name ] = attribute.data.uuid;
} else {
attributesId[ nodeAttribute.name ] = attribute.id;
}
}
}
if ( attribute === undefined ) continue;
attributes.push( attribute );
const bufferAttribute = attribute.isInterleavedBufferAttribute ? attribute.data : attribute;
vertexBuffers.add( bufferAttribute );
}
this.attributes = attributes;
this.attributesId = attributesId;
this.vertexBuffers = Array.from( vertexBuffers.values() );
return attributes;
}
/**
* Returns the vertex buffers of the render object.
*
* @return {Array<BufferAttribute|InterleavedBuffer>} An array with buffer attribute or interleaved buffers.
*/
getVertexBuffers() {
if ( this.vertexBuffers === null ) this.getAttributes();
return this.vertexBuffers;
}
/**
* Returns the draw parameters for the render object.
*
* @return {?{vertexCount: number, firstVertex: number, instanceCount: number, firstInstance: number}} The draw parameters.
*/
getDrawParameters() {
const { object, material, geometry, group, drawRange } = this;
const drawParams = this.drawParams || ( this.drawParams = {
vertexCount: 0,
firstVertex: 0,
instanceCount: 0,
firstInstance: 0
} );
const index = this.getIndex();
const hasIndex = ( index !== null );
let instanceCount = 1;
if ( geometry.isInstancedBufferGeometry === true ) {
instanceCount = geometry.instanceCount;
} else if ( object.count !== undefined ) {
instanceCount = Math.max( 0, object.count );
}
if ( instanceCount === 0 ) return null;
drawParams.instanceCount = instanceCount;
if ( object.isBatchedMesh === true ) return drawParams;
let rangeFactor = 1;
if ( material.wireframe === true && ! object.isPoints && ! object.isLineSegments && ! object.isLine && ! object.isLineLoop ) {
rangeFactor = 2;
}
let firstVertex = drawRange.start * rangeFactor;
let lastVertex = ( drawRange.start + drawRange.count ) * rangeFactor;
if ( group !== null ) {
firstVertex = Math.max( firstVertex, group.start * rangeFactor );
lastVertex = Math.min( lastVertex, ( group.start + group.count ) * rangeFactor );
}
const position = geometry.attributes.position;
let itemCount = Infinity;
if ( hasIndex ) {
itemCount = index.count;
} else if ( position !== undefined && position !== null ) {
itemCount = position.count;
}
firstVertex = Math.max( firstVertex, 0 );
lastVertex = Math.min( lastVertex, itemCount );
const count = lastVertex - firstVertex;
if ( count < 0 || count === Infinity ) return null;
drawParams.vertexCount = count;
drawParams.firstVertex = firstVertex;
return drawParams;
}
/**
* Returns the render object's geometry cache key.
*
* The geometry cache key is part of the material cache key.
*
* @return {string} The geometry cache key.
*/
getGeometryCacheKey() {
const { geometry } = this;
let cacheKey = '';
for ( const name of Object.keys( geometry.attributes ).sort() ) {
const attribute = geometry.attributes[ name ];
cacheKey += name + ',';
if ( attribute.itemSize ) cacheKey += attribute.itemSize + ',';
if ( attribute.normalized ) cacheKey += 'n,';
}
// structural equality isn't sufficient for morph targets since the
// data are maintained in textures. only if the targets are all equal
// the texture and thus the `morphReference` can be shared.
for ( const name of Object.keys( geometry.morphAttributes ).sort() ) {
const targets = geometry.morphAttributes[ name ];
cacheKey += 'morph-' + name + ',';
for ( let i = 0, l = targets.length; i < l; i ++ ) {
const attribute = targets[ i ];
cacheKey += attribute.id + ',';
}
}
return cacheKey;
}
/**
* Returns the render object's material cache key.
*
* The material cache key is part of the render object cache key.
*
* @return {number} The material cache key.
*/
getMaterialCacheKey() {
const { object, material, renderer, scene } = this;
let cacheKey = this._nodes.getCustomProgramCacheKey( material, scene );
for ( const property of getKeys( material ) ) {
if ( /^(is[A-Z]|_)|^(visible|version|uuid|name|opacity|userData|iridescenceThicknessRange)$/.test( property ) ) continue;
const value = material[ property ];
let valueKey;
if ( value !== null ) {
// some material values require a formatting
const type = typeof value;
if ( type === 'number' ) {
if ( _enumProperties.includes( property ) ) {
// retain enums since their exact value matters
valueKey = String( value );
} else {
// Other numbers are reduced to on/off
valueKey = value !== 0 ? '1' : '0';
}
} else if ( type === 'object' ) {
valueKey = '{';
if ( value.isTexture ) {
valueKey += value.mapping;
valueKey += value.channel;
// WebGPU must honor the sampler data because they are part of the bindings
if ( renderer.backend.isWebGPUBackend === true ) {
valueKey += value.magFilter;
valueKey += value.minFilter;
valueKey += value.wrapS;
valueKey += value.wrapT;
valueKey += value.wrapR;
}
}
valueKey += '}';
} else {
valueKey = String( value );
}
} else {
valueKey = String( value );
}
cacheKey += /*property + ':' +*/ valueKey + ',';
}
cacheKey += this.clippingContextCacheKey + ',';
if ( object.geometry ) {
cacheKey += this.getGeometryCacheKey();
}
if ( object.skeleton ) {
cacheKey += object.skeleton.bones.length + ',';
}
if ( object.isBatchedMesh ) {
cacheKey += object._matricesTexture.uuid + ',';
if ( object._colorsTexture !== null ) {
cacheKey += object._colorsTexture.uuid + ',';
}
}
if ( object.isInstancedMesh ) {
const { instanceMatrix, instanceColor, morphTexture } = object;
cacheKey += getInstanceDataKey( instanceMatrix ) + getInstanceDataKey( instanceColor ) + ( morphTexture !== null ? 'm' : '' ) + ',';
} else if ( object.count > 1 ) {
// TODO: https://github.com/mrdoob/three.js/pull/29066#issuecomment-2269400850
cacheKey += object.uuid + ',';
}
cacheKey += this.context.stateKey + ',';
cacheKey += object.receiveShadow + ',';
return hashString( cacheKey );
}
/**
* Whether the geometry requires an update or not.
*
* @type {boolean}
* @readonly
*/
get needsGeometryUpdate() {
if ( this.geometry.id !== this.object.geometry.id ) return true;
if ( this.attributes !== null ) {
const attributesId = this.attributesId;
for ( const name in attributesId ) {
const attribute = this.geometry.getAttribute( name );
if ( attribute === undefined ) return true;
const id = attribute.isInterleavedBufferAttribute ? attribute.data.uuid : attribute.id;
if ( attributesId[ name ] !== id ) {
return true;
}
}
for ( const { node } of this.getNodeBuilderState().nodeAttributes ) {
if ( node && node.attribute ) {
const attribute = node.getObjectAttribute( this.object );
if ( attribute && this.attributes.includes( attribute ) === false ) return true;
}
}
}
return false;
}
/**
* Whether the render object requires an update or not.
*
* Note: There are two distinct places where render objects are checked for an update.
*
* 1. In `RenderObjects.get()` which is executed when the render object is request. This
* method checks the `needsUpdate` flag and recreates the render object if necessary.
* 2. In `Renderer._renderObjectDirect()` right after getting the render object via
* `RenderObjects.get()`. The render object's NodeMaterialObserver is then used to detect
* a need for a refresh due to material, geometry or object related value changes.
*
* TODO: Investigate if it's possible to merge both steps so there is only a single place
* that performs the 'needsUpdate' check.
*
* @type {boolean}
* @readonly
*/
get needsUpdate() {
return /*this.object.static !== true &&*/ ( this.initialNodesCacheKey !== this.getDynamicCacheKey() || this.clippingNeedsUpdate );
}
/**
* Returns the dynamic cache key which represents a key that is computed per draw command.
*
* @return {number} The cache key.
*/
getDynamicCacheKey() {
let environmentKey = 0;
// `Nodes.getCacheKey()` returns an environment cache key which is not relevant when
// the renderer is inside a shadow pass.
if ( this.material.isShadowPassMaterial !== true ) {
environmentKey = this._nodes.getCacheKey( this.scene, this.lightsNode );
}
_cacheKeyValues$1[ 0 ] = environmentKey;
_cacheKeyValues$1[ 1 ] = this.camera.isArrayCamera ? this.camera.cameras.length : 0;
_cacheKeyValues$1[ 2 ] = this.object.receiveShadow ? 1 : 0;
_cacheKeyValues$1[ 3 ] = this.renderer.contextNode.id;
_cacheKeyValues$1[ 4 ] = this.renderer.contextNode.version;
return hashArray( _cacheKeyValues$1 );
}
/**
* Returns the render object's cache key.
*
* @return {number} The cache key.
*/
getCacheKey() {
return this.getMaterialCacheKey() + this.getDynamicCacheKey();
}
/**
* Frees internal resources.
*/
dispose() {
this.object.removeEventListener( 'dispose', this.onObjectDispose );
this.material.removeEventListener( 'dispose', this.onMaterialDispose );
this.geometry.removeEventListener( 'dispose', this.onGeometryDispose );
this.lightsNode.removeEventListener( 'dispose', this.onLightsNodeDispose );
if ( this._sourceMaterial !== null ) {
this._sourceMaterial.removeEventListener( 'dispose', this.onMaterialDispose );
}
this.onDispose();
}
}
const _chainKeys$3 = [ null, null, null, null ];
const _overrideChainKeys = [ null, null, null, null, null ];
/**
* This module manages the render objects of the renderer.
*
* @private
*/
class RenderObjects {
/**
* Constructs a new render object management component.
*
* @param {Renderer} renderer - The renderer.
* @param {NodeManager} nodes - Renderer component for managing nodes related logic.
* @param {Geometries} geometries - Renderer component for managing geometries.
* @param {Pipelines} pipelines - Renderer component for managing pipelines.
* @param {Bindings} bindings - Renderer component for managing bindings.
* @param {Info} info - Renderer component for managing metrics and monitoring data.
*/
constructor( renderer, nodes, geometries, pipelines, bindings, info ) {
/**
* The renderer.
*
* @type {Renderer}
*/
this.renderer = renderer;
/**
* Renderer component for managing nodes related logic.
*
* @type {NodeManager}
*/
this.nodes = nodes;
/**
* Renderer component for managing geometries.
*
* @type {Geometries}
*/
this.geometries = geometries;
/**
* Renderer component for managing pipelines.
*
* @type {Pipelines}
*/
this.pipelines = pipelines;
/**
* Renderer component for managing bindings.
*
* @type {Bindings}
*/
this.bindings = bindings;
/**
* Renderer component for managing metrics and monitoring data.
*
* @type {Info}
*/
this.info = info;
/**
* A dictionary that manages render contexts in chain maps
* for each pass ID.
*
* @private
* @type {Object<string,ChainMap>}
*/
this._chainMaps = {};
/**
* Stores all render objects created by this component.
*
* @private
* @type {Set<RenderObject>}
*/
this._renderObjects = new Set();
}
/**
* Returns a render object for the given object and state data.
*
* @param {Object3D} object - The 3D object.
* @param {Material} material - The 3D object's material.
* @param {Scene} scene - The scene the 3D object belongs to.
* @param {Camera} camera - The camera the 3D object should be rendered with.
* @param {LightsNode} lightsNode - The lights node.
* @param {RenderContext} renderContext - The render context.
* @param {ClippingContext} clippingContext - The clipping context.
* @param {string} [passId] - An optional ID for identifying the pass.
* @return {RenderObject} The render object.
*/
get( object, material, scene, camera, lightsNode, renderContext, clippingContext, passId ) {
const chainMap = this.getChainMap( passId );
const sourceMaterial = this.renderer._currentSourceMaterial;
const sourceMaterialVersion = sourceMaterial !== null ? sourceMaterial.version : 0;
// set chain keys
const chainKeys = sourceMaterial !== null ? _overrideChainKeys : _chainKeys$3;
chainKeys[ 0 ] = object;
chainKeys[ 1 ] = material;
chainKeys[ 2 ] = renderContext;
chainKeys[ 3 ] = lightsNode;
if ( sourceMaterial !== null ) chainKeys[ 4 ] = sourceMaterial;
//
let renderObject = chainMap.get( chainKeys );
if ( renderObject === undefined ) {
renderObject = this.createRenderObject( this.nodes, this.geometries, this.renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext, passId );
chainMap.set( chainKeys, renderObject );
} else {
let force = false;
// update references
renderObject.camera = camera;
// clipping and geometry updates
renderObject.updateClipping( clippingContext );
if ( renderObject.needsGeometryUpdate ) {
renderObject.setGeometry( object.geometry );
// structural change, force into rebuild
if ( renderObject.initialGeometryCacheKey !== renderObject.getGeometryCacheKey() ) {
force = true;
}
}
// check if a new render object (rebuild) is required
if ( renderObject.version !== material.version || renderObject.sourceMaterialVersion !== sourceMaterialVersion || renderObject.needsUpdate || force ) {
if ( renderObject.initialCacheKey !== renderObject.getCacheKey() ) {
renderObject.dispose();
renderObject = this.get( object, material, scene, camera, lightsNode, renderContext, clippingContext, passId );
} else {
renderObject.version = material.version;
renderObject.sourceMaterialVersion = sourceMaterialVersion;
}
}
}
// reset chain array
chainKeys[ 0 ] = null;
chainKeys[ 1 ] = null;
chainKeys[ 2 ] = null;
chainKeys[ 3 ] = null;
if ( sourceMaterial !== null ) chainKeys[ 4 ] = null;
//
return renderObject;
}
/**
* Returns a chain map for the given pass ID.
*
* @param {string} [passId='default'] - The pass ID.
* @return {ChainMap} The chain map.
*/
getChainMap( passId = 'default' ) {
return this._chainMaps[ passId ] || ( this._chainMaps[ passId ] = new ChainMap() );
}
/**
* Frees internal resources.
*/
dispose() {
for ( const renderObject of this._renderObjects ) {
renderObject.dispose();
}
this._renderObjects.clear();
this._chainMaps = {};
}
/**
* Factory method for creating render objects with the given list of parameters.
*
* @param {NodeManager} nodes - Renderer component for managing nodes related logic.
* @param {Geometries} geometries - Renderer component for managing geometries.
* @param {Renderer} renderer - The renderer.
* @param {Object3D} object - The 3D object.
* @param {Material} material - The object's material.
* @param {Scene} scene - The scene the 3D object belongs to.
* @param {Camera} camera - The camera the object should be rendered with.
* @param {LightsNode} lightsNode - The lights node.
* @param {RenderContext} renderContext - The render context.
* @param {ClippingContext} clippingContext - The clipping context.
* @param {string} [passId] - An optional ID for identifying the pass.
* @return {RenderObject} The render object.
*/
createRenderObject( nodes, geometries, renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext, passId ) {
const chainMap = this.getChainMap( passId );
const renderObject = new RenderObject( nodes, geometries, renderer, object, material, scene, camera, lightsNode, renderContext, clippingContext );
renderObject.onDispose = () => {
this.geometries.deleteVertexState( renderObject );
this.pipelines.delete( renderObject );
this.bindings.deleteForRender( renderObject );
this.nodes.delete( renderObject );
chainMap.delete( renderObject.getChainArray() );
this._renderObjects.delete( renderObject );
};
this._renderObjects.add( renderObject );
return renderObject;
}
}
/**
* Data structure for the renderer. It is intended to manage
* data of objects in dictionaries.
*
* @private
*/
class DataMap {
/**
* Constructs a new data map.
*/
constructor() {
/**
* `DataMap` internally uses a weak map
* to manage its data.
*
* @type {WeakMap<Object, Object>}
*/
this.data = new WeakMap();
}
/**
* Returns the dictionary for the given object.
*
* @param {Object} object - The object.
* @return {Object} The dictionary.
*/
get( object ) {
let map = this.data.get( object );
if ( map === undefined ) {
map = {};
this.data.set( object, map );
}
return map;
}
/**
* Deletes the dictionary for the given object.
*
* @param {Object} object - The object.
* @return {?Object} The deleted dictionary.
*/
delete( object ) {
let map = null;
if ( this.data.has( object ) ) {
map = this.data.get( object );
this.data.delete( object );
}
return map;
}
/**
* Returns `true` if the given object has a dictionary defined.
*
* @param {Object} object - The object to test.
* @return {boolean} Whether a dictionary is defined or not.
*/
has( object ) {
return this.data.has( object );
}
/**
* Frees internal resources.
*/
dispose() {
this.data = new WeakMap();
}
}
const AttributeType = {
VERTEX: 1,
INDEX: 2,
STORAGE: 3,
INDIRECT: 4
};
// size of a chunk in bytes (STD140 layout)
const GPU_CHUNK_BYTES = 16;
let _generation = 0;
/**
* This renderer module manages geometry attributes.
*
* @private
* @augments DataMap
*/
class Attributes extends DataMap {
/**
* Constructs a new attribute management component.
*
* @param {Backend} backend - The renderer's backend.
* @param {Info} info - Renderer component for managing metrics and monitoring data.
*/
constructor( backend, info ) {
super();
/**
* The renderer's backend.
*
* @type {Backend}
*/
this.backend = backend;
/**
* Renderer component for managing metrics and monitoring data.
*
* @type {Info}
*/
this.info = info;
/**
* Stores weak references to the buffer attributes with attached
* `dispose` event listeners.
*
* @private
* @type {Set<WeakRef<BufferAttribute>>}
*/
this._tracked = new Set();
/**
* Removes weak references from `_tracked` when their attribute
* has been garbage collected without an explicit `dispose()`.
*
* @private
* @type {FinalizationRegistry}
*/
this._registry = new FinalizationRegistry( ( ref ) => this._tracked.delete( ref ) );
/**
* Incremented whenever the buffer of an attribute is destroyed. Render objects
* only have to verify their attributes if this version has changed.
*
* @type {number}
* @default 0
*/
this.disposeVersion = 0;
}
/**
* Deletes the data for the given attribute.
*
* @param {BufferAttribute|InterleavedBuffer} attribute - The attribute.
* @return {?Object} The deleted attribute data.
*/
delete( attribute ) {
const attributeData = super.delete( attribute );
if ( attributeData !== null ) {
this.disposeVersion ++;
if ( attribute.isBufferAttribute === true ) {
attribute.removeEventListener( 'dispose', attributeData.onDispose );
this._tracked.delete( attributeData.ref );
this._registry.unregister( attributeData.ref );
}
this.backend.destroyAttribute( attribute );
this.info.destroyAttribute( attribute );
}
return attributeData;
}
/**
* Updates the given attribute. This method creates attribute buffers
* for new attributes and updates data for existing ones.
*
* @param {BufferAttribute|InterleavedBuffer} attribute - The attribute to update.
* @param {number} type - The attribute type.
*/
update( attribute, type ) {
const data = this.get( attribute );
if ( data.version === undefined ) {
if ( type === AttributeType.VERTEX ) {
this.backend.createAttribute( attribute );
this.info.createAttribute( attribute );
} else if ( type === AttributeType.INDEX ) {
this.backend.createIndexAttribute( attribute );
this.info.createIndexAttribute( attribute );
} else if ( type === AttributeType.STORAGE ) {
this.backend.createStorageAttribute( attribute );
this.info.createStorageAttribute( attribute );
} else if ( type === AttributeType.INDIRECT ) {
this.backend.createIndirectStorageAttribute( attribute );
this.info.createIndirectStorageAttribute( attribute );
}
data.version = attribute.version;
data.generation = _generation ++;
// interleaved buffer attributes are not event dispatchers
if ( attribute.isBufferAttribute === true ) {
data.onDispose = () => {
this.delete( attribute );
};
attribute.addEventListener( 'dispose', data.onDispose );
// see #31798 why tracking separate remove listeners is required right now
data.ref = new WeakRef( attribute );
this._tracked.add( data.ref );
this._registry.register( attribute, data.ref, data.ref );
}
} else {
if ( data.version < attribute.version || attribute.usage === DynamicDrawUsage ) {
this.backend.updateAttribute( attribute );
data.version = attribute.version;
}
}
}
dispose() {
for ( const ref of this._tracked ) {
const attribute = ref.deref();
if ( attribute === undefined || this.has( attribute ) === false ) continue;
this.delete( attribute );
}
this._tracked.clear();
super.dispose();
}
}
/**
* Returns the wireframe version for the given geometry.
*
* @private
* @function
* @param {BufferGeometry} geometry - The geometry.
* @return {number} The version.
*/
function getWireframeVersion( geometry ) {
return ( geometry.index !== null ) ? geometry.index.version : geometry.attributes.position.version;
}
/**
* Returns the wireframe ID for the given geometry.
*
* @private
* @function
* @param {BufferGeometry} geometry - The geometry.
* @return {number} The ID.
*/
function getWireframeId( geometry ) {
return ( geometry.index !== null ) ? geometry.index.id : geometry.attributes.position.id;
}
/**
* Returns a wireframe index attribute for the given geometry.
*
* @private
* @function
* @param {BufferGeometry} geometry - The geometry.
* @return {BufferAttribute} The wireframe index attribute.
*/
function getWireframeIndex( geometry ) {
const geometryIndex = geometry.index;
const geometryPosition = geometry.attributes.position;
const count = geometryIndex !== null ? geometryIndex.array.length : ( geometryPosition.array.length / 3 ) - 1;
const IndexBufferAttribute = geometryPosition.count >= 65535 ? Uint32BufferAttribute : Uint16BufferAttribute;
const attribute = new IndexBufferAttribute( Math.ceil( count / 3 ) * 6, 1 );
const indices = attribute.array;
if ( geometryIndex !== null ) {
const array = geometryIndex.array;
for ( let i = 0, j = 0; i < count; i += 3 ) {
const a = array[ i + 0 ];
const b = array[ i + 1 ];
const c = array[ i + 2 ];
indices[ j ++ ] = a; indices[ j ++ ] = b;
indices[ j ++ ] = b; indices[ j ++ ] = c;
indices[ j ++ ] = c; indices[ j ++ ] = a;
}
} else {
for ( let i = 0, j = 0; i < count; i += 3 ) {
const a = i + 0;
const b = i + 1;
const c = i + 2;
indices[ j ++ ] = a; indices[ j ++ ] = b;
indices[ j ++ ] = b; indices[ j ++ ] = c;
indices[ j ++ ] = c; indices[ j ++ ] = a;
}
}
attribute.version = getWireframeVersion( geometry );
attribute.__id = getWireframeId( geometry );
return attribute;
}
/**
* This renderer module manages geometries.
*
* @private
* @augments DataMap
*/
class Geometries extends DataMap {
/**
* Constructs a new geometry management component.
*
* @param {Backend} backend - The renderer's backend.
* @param {Attributes} attributes - Renderer component for managing attributes.
* @param {Info} info - Renderer component for managing metrics and monitoring data.
*/
constructor( backend, attributes, info ) {
super();
/**
* The renderer's backend.
*
* @type {Backend}
*/
this.backend = backend;
/**
* Renderer component for managing attributes.
*
* @type {Attributes}
*/
this.attributes = attributes;
/**
* Renderer component for managing metrics and monitoring data.
*
* @type {Info}
*/
this.info = info;
/**
* Weak Map for managing attributes for wireframe rendering.
*
* @type {WeakMap<BufferGeometry,BufferAttribute>}
*/
this.wireframes = new WeakMap();
/**
* This Weak Map is used to make sure buffer attributes are
* updated only once per render call.
*
* @type {WeakMap<BufferAttribute,number>}
*/
this.attributeCall = new WeakMap();
/**
* Stores weak references to the geometries with attached
* `dispose` event listeners.
*
* @private
* @type {Set<WeakRef<BufferGeometry>>}
*/
this._tracked = new Set();
/**
* Removes weak references from `_tracked` when their geometry
* has been garbage collected without an explicit `dispose()`.
*
* @private
* @type {FinalizationRegistry}
*/
this._registry = new FinalizationRegistry( ( ref ) => this._tracked.delete( ref ) );
}
/**
* Returns `true` if the given geometry is initialized.
*
* @param {BufferGeometry} geometry - The geometry.
* @return {boolean} Whether if the given geometry is initialized or not.
*/
has( geometry ) {
return super.has( geometry ) && this.get( geometry ).initialized === true;
}
/**
* Prepares the geometry of the given render object for rendering.
*
* @param {RenderObject} renderObject - The render object.
*/
updateForRender( renderObject ) {
const geometry = renderObject.geometry;
if ( this.has( geometry ) === false ) this.initGeometry( geometry );
this.updateAttributes( renderObject );
}
/**
* Initializes the given geometry.
*
* @param {BufferGeometry} geometry - The geometry.
*/
initGeometry( geometry ) {
const geometryData = this.get( geometry );
geometryData.initialized = true;
this.info.memory.geometries ++;
geometryData.onDispose = () => {
this.info.memory.geometries --;
// index
const index = geometry.index;
if ( index !== null ) {
this.attributes.delete( index );
}
// geometry attributes
for ( const attribute of Object.values( geometry.attributes ) ) {
this.attributes.delete( this.backend.getBufferAttribute( attribute ) );
}
// wireframe attributes
const wireframeAttribute = this.wireframes.get( geometry );
if ( wireframeAttribute !== undefined ) {
this.attributes.delete( wireframeAttribute );
}
//
geometry.removeEventListener( 'dispose', geometryData.onDispose );
this._tracked.delete( geometryData.ref );
this._registry.unregister( geometryData.ref );
this.delete( geometry );
};
geometry.addEventListener( 'dispose', geometryData.onDispose );
// see #31798 why tracking separate remove listeners is required right now
geometryData.ref = new WeakRef( geometry );
this._tracked.add( geometryData.ref );
this._registry.register( geometry, geometryData.ref, geometryData.ref );
}
/**
* Updates the geometry attributes of the given render object.
*
* @param {RenderObject} renderObject - The render object.
*/
updateAttributes( renderObject ) {
// attributes
const attributes = renderObject.getAttributes();
for ( const attribute of attributes ) {
if ( attribute.isStorageBufferAttribute || attribute.isStorageInstancedBufferAttribute ) {
this.updateAttribute( attribute, AttributeType.STORAGE );
} else {
this.updateAttribute( attribute, AttributeType.VERTEX );
}
}
// indexes
const index = this.getIndex( renderObject );
if ( index !== null ) {
this.updateAttribute( index, AttributeType.INDEX );
}
// indirect
const indirect = renderObject.geometry.indirect;
if ( indirect !== null ) {
this.updateAttribute( indirect, AttributeType.INDIRECT );
}
}
/**
* Updates the given attribute.
*
* @param {BufferAttribute} attribute - The attribute to update.
* @param {number} type - The attribute type.
*/
updateAttribute( attribute, type ) {
const callId = this.info.render.calls;
const bufferAttribute = this.backend.getBufferAttribute( attribute );
if ( this.attributeCall.get( bufferAttribute ) !== callId ) {
this.attributes.update( bufferAttribute, type );
this.attributeCall.set( bufferAttribute, callId );
}
}
/**
* Returns the indirect buffer attribute of the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @return {?BufferAttribute} The indirect attribute. `null` if no indirect drawing is used.
*/
getIndirect( renderObject ) {
return renderObject.geometry.indirect;
}
/**
* Returns the byte offset into the indirect attribute buffer of the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @return {number} The byte offset into the indirect attribute buffer.
*/
getIndirectOffset( renderObject ) {
return renderObject.geometry.indirectOffset;
}
/**
* Returns the index of the given render object's geometry. This is implemented
* in a method to return a wireframe index if necessary.
*
* @param {RenderObject} renderObject - The render object.
* @return {?BufferAttribute} The index. Returns `null` for non-indexed geometries.
*/
getIndex( renderObject ) {
const { geometry, material } = renderObject;
let index = geometry.index;
if ( material.wireframe === true ) {
const wireframes = this.wireframes;
let wireframeAttribute = wireframes.get( geometry );
if ( wireframeAttribute === undefined ) {
wireframeAttribute = getWireframeIndex( geometry );
wireframes.set( geometry, wireframeAttribute );
} else if ( wireframeAttribute.version !== getWireframeVersion( geometry ) || wireframeAttribute.__id !== getWireframeId( geometry ) ) {
this.attributes.delete( wireframeAttribute );
wireframeAttribute = getWireframeIndex( geometry );
wireframes.set( geometry, wireframeAttribute );
}
index = wireframeAttribute;
}
return index;
}
/**
* Deletes the attributes that are defined via nodes and not on geometry level.
*
* @param {RenderObject} renderObject - The render object.
*/
deleteNodeAttributes( renderObject ) {
const currentAttributes = new Set( Object.values( renderObject.geometry.attributes ) );
for ( const attribute of renderObject.getAttributes() ) {
if ( currentAttributes.has( attribute ) === false ) {
this.attributes.delete( this.backend.getBufferAttribute( attribute ) );
}
}
}
/**
* Deletes the vertex state for the given render object.
*
* @param {RenderObject} renderObject - The render object.
*/
deleteVertexState( renderObject ) {
this.backend.deleteVertexState( renderObject );
}
/**
* Frees internal resources.
*/
dispose() {
for ( const ref of this._tracked ) {
const geometry = ref.deref();
if ( geometry === undefined ) continue;
const geometryData = this.get( geometry );
geometryData.onDispose();
}
this._tracked.clear();
super.dispose();
}
}
/**
* This renderer module provides a series of statistical information
* about the GPU memory and the rendering process. Useful for debugging
* and monitoring.
*/
class Info {
/**
* Constructs a new info component.
*/
constructor() {
/**
* Whether frame related metrics should automatically
* be resetted or not. This property should be set to `false`
* by apps which manage their own animation loop. They must
* then call `renderer.info.reset()` once per frame manually.
*
* @type {boolean}
* @default true
*/
this.autoReset = true;
/**
* The current frame ID. This ID is managed
* by `NodeFrame`.
*
* @type {number}
* @readonly
* @default 0
*/
this.frame = 0;
/**
* The number of render calls since the
* app has been started.
*
* @type {number}
* @readonly
* @default 0
*/
this.calls = 0;
/**
* Render related metrics.
*
* @type {Object}
* @readonly
* @property {number} calls - The number of render calls since the app has been started.
* @property {number} frameCalls - The number of render calls of the current frame.
* @property {number} drawCalls - The number of draw calls of the current frame.
* @property {number} triangles - The number of rendered triangle primitives of the current frame.
* @property {number} points - The number of rendered point primitives of the current frame.
* @property {number} lines - The number of rendered line primitives of the current frame.
* @property {number} timestamp - The timestamp of the frame.
*/
this.render = {
calls: 0,
frameCalls: 0,
drawCalls: 0,
triangles: 0,
points: 0,
lines: 0,
timestamp: 0,
};
/**
* Compute related metrics.
*
* @type {Object}
* @readonly
* @property {number} calls - The number of compute calls since the app has been started.
* @property {number} frameCalls - The number of compute calls of the current frame.
* @property {number} timestamp - The timestamp of the frame when using `renderer.computeAsync()`.
*/
this.compute = {
calls: 0,
frameCalls: 0,
timestamp: 0
};
/**
* Memory related metrics.
*
* @type {Object}
* @readonly
* @property {number} attributes - The number of active attributes.
* @property {number} attributesSize - The memory size of active attributes in bytes.
* @property {number} geometries - The number of active geometries.
* @property {number} indexAttributes - The number of active index attributes.
* @property {number} indexAttributesSize - The memory size of active index attributes in bytes.
* @property {number} indirectStorageAttributes - The number of active indirect storage attributes.
* @property {number} indirectStorageAttributesSize - The memory size of active indirect storage attributes in bytes.
* @property {number} programs - The number of active programs.
* @property {number} programsSize - The memory size of active programs in bytes.
* @property {number} readbackBuffers - The number of active readback buffers.
* @property {number} readbackBuffersSize - The memory size of active readback buffers in bytes.
* @property {number} renderTargets - The number of active renderTargets.
* @property {number} storageAttributes - The number of active storage attributes.
* @property {number} storageAttributesSize - The memory size of active storage attributes in bytes.
* @property {number} textures - The number of active textures.
* @property {number} texturesSize - The memory size of active textures in bytes.
* @property {number} uniformBuffers - The number of active uniform buffers.
* @property {number} uniformBuffersSize - The memory size of active uniform buffers in bytes.
* @property {number} total - The total memory size in bytes.
*/
this.memory = {
attributes: 0,
attributesSize: 0,
geometries: 0,
indexAttributes: 0,
indexAttributesSize: 0,
indirectStorageAttributes: 0,
indirectStorageAttributesSize: 0,
programs: 0,
programsSize: 0,
readbackBuffers: 0,
readbackBuffersSize: 0,
renderTargets: 0,
storageAttributes: 0,
storageAttributesSize: 0,
textures: 0,
texturesSize: 0,
uniformBuffers: 0,
uniformBuffersSize: 0,
total: 0
};
/**
* Map for storing calculated byte sizes of tracked objects.
*
* @type {WeakMap<Object, number>}
* @private
*/
this.memoryMap = new WeakMap();
}
/**
* This method should be executed per draw call and updates the corresponding metrics.
*
* @param {Object3D} object - The 3D object that is going to be rendered.
* @param {number} count - The vertex or index count.
* @param {number} instanceCount - The instance count.
*/
update( object, count, instanceCount ) {
this.render.drawCalls ++;
if ( object.isMesh || object.isSprite ) {
this.render.triangles += instanceCount * ( count / 3 );
} else if ( object.isPoints ) {
this.render.points += instanceCount * count;
} else if ( object.isLineSegments ) {
this.render.lines += instanceCount * ( count / 2 );
} else if ( object.isLine ) {
this.render.lines += instanceCount * ( count - 1 );
} else {
error( 'WebGPUInfo: Unknown object type.' );
}
}
/**
* Resets frame related metrics.
*/
reset() {
this.render.drawCalls = 0;
this.render.frameCalls = 0;
this.compute.frameCalls = 0;
this.render.triangles = 0;
this.render.points = 0;
this.render.lines = 0;
}
/**
* Performs a complete reset of the object.
*/
dispose() {
this.reset();
this.calls = 0;
this.render.calls = 0;
this.compute.calls = 0;
this.render.timestamp = 0;
this.compute.timestamp = 0;
for ( const prop in this.memory ) {
this.memory[ prop ] = 0;
}
this.memoryMap = new WeakMap();
}
/**
* Tracks texture memory explicitly, updating counts and byte tracking.
*
* @param {Texture} texture
*/
createTexture( texture ) {
const size = this._getTextureMemorySize( texture );
this.memoryMap.set( texture, size );
this.memory.textures ++;
this.memory.total += size;
this.memory.texturesSize += size;
}
/**
* Tracks texture memory explicitly, updating counts and byte tracking.
*
* @param {Texture} texture
*/
destroyTexture( texture ) {
const size = this.memoryMap.get( texture ) || 0;
this.memoryMap.delete( texture );
this.memory.textures --;
this.memory.total -= size;
this.memory.texturesSize -= size;
}
/**
* Tracks attribute memory explicitly, updating counts and byte tracking.
*
* @param {BufferAttribute} attribute
* @param {string} type - type of attribute
* @private
*/
_createAttribute( attribute, type ) {
const size = this._getAttributeMemorySize( attribute );
this.memoryMap.set( attribute, { size, type } );
this.memory[ type ] ++;
this.memory.total += size;
this.memory[ type + 'Size' ] += size;
}
/**
* Tracks a regular attribute memory explicitly.
*
* @param {BufferAttribute} attribute - The attribute to track.
*/
createAttribute( attribute ) {
this._createAttribute( attribute, 'attributes' );
}
/**
* Tracks an index attribute memory explicitly.
*
* @param {BufferAttribute} attribute - The index attribute to track.
*/
createIndexAttribute( attribute ) {
this._createAttribute( attribute, 'indexAttributes' );
}
/**
* Tracks a storage attribute memory explicitly.
*
* @param {BufferAttribute} attribute - The storage attribute to track.
*/
createStorageAttribute( attribute ) {
this._createAttribute( attribute, 'storageAttributes' );
}
/**
* Tracks an indirect storage attribute memory explicitly.
*
* @param {BufferAttribute} attribute - The indirect storage attribute to track.
*/
createIndirectStorageAttribute( attribute ) {
this._createAttribute( attribute, 'indirectStorageAttributes' );
}
/**
* Tracks attribute memory explicitly, updating counts and byte tracking.
*
* @param {BufferAttribute} attribute
*/
destroyAttribute( attribute ) {
const data = this.memoryMap.get( attribute );
if ( data ) {
this.memoryMap.delete( attribute );
this.memory[ data.type ] --;
this.memory.total -= data.size;
this.memory[ data.type + 'Size' ] -= data.size;
}
}
/**
* Tracks a readback buffer memory explicitly.
*
* @param {ReadbackBuffer} readbackBuffer - The readback buffer to track.
*/
createReadbackBuffer( readbackBuffer ) {
const maxByteLength = readbackBuffer.maxByteLength;
this.memoryMap.set( readbackBuffer, { size: maxByteLength, type: 'readbackBuffers' } );
this.memory.readbackBuffers ++;
this.memory.total += maxByteLength;
this.memory.readbackBuffersSize += maxByteLength;
}
/**
* Tracks a readback buffer memory explicitly.
*
* @param {ReadbackBuffer} readbackBuffer - The readback buffer to track.
*/
destroyReadbackBuffer( readbackBuffer ) {
const { size } = this.memoryMap.get( readbackBuffer );
this.memoryMap.delete( readbackBuffer );
this.memory.readbackBuffers --;
this.memory.total -= size;
this.memory.readbackBuffersSize -= size;
}
/**
* Tracks a uniform buffer memory explicitly.
*
* @param {UniformBuffer} uniformBuffer - The uniform buffer to track.
*/
createUniformBuffer( uniformBuffer ) {
const size = uniformBuffer.byteLength;
this.memoryMap.set( uniformBuffer, { size, type: 'uniformBuffers' } );
this.memory.uniformBuffers ++;
this.memory.total += size;
this.memory.uniformBuffersSize += size;
}
/**
* Tracks a uniform buffer memory explicitly.
*
* @param {UniformBuffer} uniformBuffer - The uniform buffer to track.
*/
destroyUniformBuffer( uniformBuffer ) {
const data = this.memoryMap.get( uniformBuffer );
if ( data ) {
this.memoryMap.delete( uniformBuffer );
this.memory.uniformBuffers --;
this.memory.total -= data.size;
this.memory.uniformBuffersSize -= data.size;
}
}
/**
* Tracks program memory explicitly, updating counts and byte tracking.
*
* @param {ProgrammableStage} program - The program to track.
*/
createProgram( program ) {
const size = program.code.length; // Approx size
this.memoryMap.set( program, size );
this.memory.programs ++;
this.memory.total += size;
this.memory.programsSize += size;
}
/**
* Tracks program memory explicitly, updating counts and byte tracking.
*
* @param {Object} program - The program to track.
*/
destroyProgram( program ) {
const size = this.memoryMap.get( program ) || 0;
this.memoryMap.delete( program );
this.memory.programs --;
this.memory.total -= size;
this.memory.programsSize -= size;
}
/**
* Calculates the memory size of a texture in bytes.
*
* @param {Texture} texture - The texture to calculate the size for.
* @return {number} The calculated size in bytes.
* @private
*/
_getTextureMemorySize( texture ) {
if ( texture.isCompressedTexture ) {
return 1; // Fallback estimate since exact format decompressed isn't readily available without format maps
}
let bytesPerChannel = 1;
if ( texture.type === ByteType || texture.type === UnsignedByteType ) bytesPerChannel = 1;
else if ( texture.type === ShortType || texture.type === UnsignedShortType || texture.type === HalfFloatType ) bytesPerChannel = 2;
else if ( texture.type === IntType || texture.type === UnsignedIntType || texture.type === FloatType ) bytesPerChannel = 4;
let channels = 4; // RGBA default
if ( texture.format === AlphaFormat || texture.format === RedFormat || texture.format === RedIntegerFormat || texture.format === DepthFormat || texture.format === DepthStencilFormat ) channels = 1;
else if ( texture.format === RGFormat || texture.format === RGIntegerFormat ) channels = 2;
else if ( texture.format === RGBFormat || texture.format === RGBIntegerFormat ) channels = 3;
let bytesPerPixel = bytesPerChannel * channels;
// Packed overrides
if ( texture.type === UnsignedShort4444Type || texture.type === UnsignedShort5551Type ) bytesPerPixel = 2;
else if ( texture.type === UnsignedInt248Type || texture.type === UnsignedInt5999Type || texture.type === UnsignedInt101111Type ) bytesPerPixel = 4;
const width = texture.width || 1;
const height = texture.height || 1;
const depth = texture.isCubeTexture ? 6 : ( texture.depth || 1 );
let size = width * height * depth * bytesPerPixel;
const mipmaps = texture.mipmaps;
if ( mipmaps.length > 0 ) {
let mipmapSize = 0;
for ( let i = 0; i < mipmaps.length; i ++ ) {
const mipmap = mipmaps[ i ];
if ( mipmap.data ) {
mipmapSize += mipmap.data.byteLength;
} else {
const mipWidth = mipmap.width || Math.max( 1, width >> i );
const mipHeight = mipmap.height || Math.max( 1, height >> i );
mipmapSize += mipWidth * mipHeight * depth * bytesPerPixel;
}
}
size += mipmapSize;
} else if ( texture.generateMipmaps ) {
size = size * 1.333; // MiP chain approximation
}
return Math.round( size );
}
/**
* Calculates the memory size of an attribute in bytes.
*
* @param {BufferAttribute} attribute - The attribute to calculate the size for.
* @return {number} The calculated size in bytes.
* @private
*/
_getAttributeMemorySize( attribute ) {
if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;
if ( attribute.array ) {
return attribute.array.byteLength;
} else if ( attribute.count && attribute.itemSize ) {
return attribute.count * attribute.itemSize * 4; // Assume Float32
}
return 0;
}
}
/**
* Abstract class for representing pipelines.
*
* @private
* @abstract
*/
class Pipeline {
/**
* Constructs a new pipeline.
*
* @param {string} cacheKey - The pipeline's cache key.
*/
constructor( cacheKey ) {
/**
* The pipeline's cache key.
*
* @type {string}
*/
this.cacheKey = cacheKey;
/**
* How often the pipeline is currently in use.
*
* @type {number}
* @default 0
*/
this.usedTimes = 0;
}
}
/**
* Class for representing render pipelines.
*
* @private
* @augments Pipeline
*/
class RenderObjectPipeline extends Pipeline {
/**
* Constructs a new render object pipeline.
*
* @param {string} cacheKey - The pipeline's cache key.
* @param {ProgrammableStage} vertexProgram - The pipeline's vertex shader.
* @param {ProgrammableStage} fragmentProgram - The pipeline's fragment shader.
*/
constructor( cacheKey, vertexProgram, fragmentProgram ) {
super( cacheKey );
/**
* The pipeline's vertex shader.
*
* @type {ProgrammableStage}
*/
this.vertexProgram = vertexProgram;
/**
* The pipeline's fragment shader.
*
* @type {ProgrammableStage}
*/
this.fragmentProgram = fragmentProgram;
}
}
/**
* Class for representing compute pipelines.
*
* @private
* @augments Pipeline
*/
class ComputePipeline extends Pipeline {
/**
* Constructs a new compute pipeline.
*
* @param {string} cacheKey - The pipeline's cache key.
* @param {ProgrammableStage} computeProgram - The pipeline's compute shader.
* @param {?Array<Object>} [transforms=null] - The transforms (only relevant for WebGL 2 which uses Transform Feedback).
* @param {?Array<Object>} [attributes=null] - The attributes (only relevant for WebGL 2 which uses Transform Feedback).
*/
constructor( cacheKey, computeProgram, transforms = null, attributes = null ) {
super( cacheKey );
/**
* The pipeline's compute shader.
*
* @type {ProgrammableStage}
*/
this.computeProgram = computeProgram;
/**
* The transforms (only relevant for WebGL 2 which uses Transform Feedback).
*
* @type {?Array<Object>}
*/
this.transforms = transforms;
/**
* The attributes (only relevant for WebGL 2 which uses Transform Feedback).
*
* @type {?Array<Object>}
*/
this.attributes = attributes;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isComputePipeline = true;
}
}
let _id$9 = 0;
/**
* Class for representing programmable stages which are vertex,
* fragment or compute shaders. Unlike fixed-function states (like blending),
* they represent the programmable part of a pipeline.
*
* @private
*/
class ProgrammableStage {
/**
* Constructs a new programmable stage.
*
* @param {string} code - The shader code.
* @param {('vertex'|'fragment'|'compute')} stage - The type of stage.
* @param {string} name - The name of the shader.
*/
constructor( code, stage, name ) {
/**
* The id of the programmable stage.
*
* @type {number}
*/
this.id = _id$9 ++;
/**
* The shader code.
*
* @type {string}
*/
this.code = code;
/**
* The type of stage.
*
* @type {string}
*/
this.stage = stage;
/**
* The name of the stage.
* This is used for debugging purposes.
*
* @type {string}
*/
this.name = name;
/**
* How often the programmable stage is currently in use.
*
* @type {number}
* @default 0
*/
this.usedTimes = 0;
}
}
/**
* This renderer module manages the pipelines of the renderer.
*
* @private
* @augments DataMap
*/
class Pipelines extends DataMap {
/**
* Constructs a new pipeline management component.
*
* @param {Backend} backend - The renderer's backend.
* @param {NodeManager} nodes - Renderer component for managing nodes related logic.
* @param {Info} info - Renderer component for managing metrics and monitoring data.
*/
constructor( backend, nodes, info ) {
super();
/**
* The renderer's backend.
*
* @type {Backend}
*/
this.backend = backend;
/**
* Renderer component for managing nodes related logic.
*
* @type {NodeManager}
*/
this.nodes = nodes;
/**
* Renderer component for managing metrics and monitoring data.
*
* @type {Info}
*/
this.info = info;
/**
* A references to the bindings management component.
* This reference will be set inside the `Bindings`
* constructor.
*
* @type {?Bindings}
* @default null
*/
this.bindings = null;
/**
* Internal cache for maintaining pipelines.
* The key of the map is a cache key, the value the pipeline.
*
* @type {Map<string,Pipeline>}
*/
this.caches = new Map();
/**
* This dictionary maintains for each shader stage type (vertex,
* fragment and compute) the programmable stage objects which
* represent the actual shader code.
*
* @type {Object<string,Map<string, ProgrammableStage>>}
*/
this.programs = {
vertex: new Map(),
fragment: new Map(),
compute: new Map()
};
}
/**
* Returns a compute pipeline for the given compute node.
*
* @param {Node} computeNode - The compute node.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {?Array<Promise>} [promises=null] - An array of compilation promises which is only relevant in context of `Renderer.compileComputeAsync()`.
* @return {ComputePipeline} The compute pipeline.
*/
getForCompute( computeNode, bindings, promises = null ) {
const { backend } = this;
const data = this.get( computeNode );
if ( this._needsComputeUpdate( computeNode ) ) {
const previousPipeline = data.pipeline;
if ( previousPipeline ) {
previousPipeline.usedTimes --;
previousPipeline.computeProgram.usedTimes --;
}
// get shader
const nodeBuilderState = this.nodes.getForCompute( computeNode );
// programmable stage
let stageCompute = this.programs.compute.get( nodeBuilderState.computeShader );
if ( stageCompute === undefined ) {
stageCompute = new ProgrammableStage( nodeBuilderState.computeShader, 'compute', computeNode.name );
this.programs.compute.set( nodeBuilderState.computeShader, stageCompute );
backend.createProgram( stageCompute );
this.info.createProgram( stageCompute );
}
// determine compute pipeline
const cacheKey = this._getComputeCacheKey( computeNode, stageCompute );
let pipeline = this.caches.get( cacheKey );
if ( pipeline === undefined ) {
pipeline = this._getComputePipeline( computeNode, stageCompute, cacheKey, bindings, promises );
}
// keep track of all used times
pipeline.usedTimes ++;
stageCompute.usedTimes ++;
// release previous pipeline and program if they are not used anymore
if ( previousPipeline ) {
if ( previousPipeline.usedTimes === 0 ) this._releasePipeline( previousPipeline );
if ( previousPipeline.computeProgram.usedTimes === 0 ) this._releaseProgram( previousPipeline.computeProgram );
}
//
data.version = computeNode.version;
data.pipeline = pipeline;
}
return data.pipeline;
}
/**
* Returns a render pipeline for the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @param {?Array<Promise>} [promises=null] - An array of compilation promises which is only relevant in context of `Renderer.compileAsync()`.
* @return {RenderObjectPipeline} The render pipeline.
*/
getForRender( renderObject, promises = null ) {
const { backend } = this;
const data = this.get( renderObject );
if ( this._needsRenderUpdate( renderObject ) ) {
const previousPipeline = data.pipeline;
if ( previousPipeline ) {
previousPipeline.usedTimes --;
previousPipeline.vertexProgram.usedTimes --;
previousPipeline.fragmentProgram.usedTimes --;
}
// get shader
const nodeBuilderState = renderObject.getNodeBuilderState();
const name = renderObject.material ? renderObject.material.name : '';
// programmable stages
let stageVertex = this.programs.vertex.get( nodeBuilderState.vertexShader );
if ( stageVertex === undefined ) {
stageVertex = new ProgrammableStage( nodeBuilderState.vertexShader, 'vertex', name );
this.programs.vertex.set( nodeBuilderState.vertexShader, stageVertex );
backend.createProgram( stageVertex );
this.info.createProgram( stageVertex );
}
let stageFragment = this.programs.fragment.get( nodeBuilderState.fragmentShader );
if ( stageFragment === undefined ) {
stageFragment = new ProgrammableStage( nodeBuilderState.fragmentShader, 'fragment', name );
this.programs.fragment.set( nodeBuilderState.fragmentShader, stageFragment );
backend.createProgram( stageFragment );
this.info.createProgram( stageFragment );
}
// determine render pipeline
const cacheKey = this._getRenderCacheKey( renderObject, stageVertex, stageFragment );
let pipeline = this.caches.get( cacheKey );
if ( pipeline === undefined ) {
pipeline = this._getRenderPipeline( renderObject, stageVertex, stageFragment, cacheKey, promises );
} else {
renderObject.pipeline = pipeline;
}
// keep track of all used times
pipeline.usedTimes ++;
stageVertex.usedTimes ++;
stageFragment.usedTimes ++;
// release previous pipeline and programs if they are not used anymore
if ( previousPipeline ) {
if ( previousPipeline.usedTimes === 0 ) this._releasePipeline( previousPipeline );
if ( previousPipeline.vertexProgram.usedTimes === 0 ) this._releaseProgram( previousPipeline.vertexProgram );
if ( previousPipeline.fragmentProgram.usedTimes === 0 ) this._releaseProgram( previousPipeline.fragmentProgram );
}
//
data.pipeline = pipeline;
}
return data.pipeline;
}
/**
* Checks if the render pipeline for the given render object is ready for drawing.
* Returns false if the GPU pipeline is still being compiled asynchronously.
*
* @param {RenderObject} renderObject - The render object.
* @return {boolean} True if the pipeline is ready for drawing.
*/
isReady( renderObject ) {
const data = this.get( renderObject );
const pipeline = data.pipeline;
if ( pipeline === undefined ) return false;
const pipelineData = this.backend.get( pipeline );
return pipelineData.pipeline !== undefined && pipelineData.pipeline !== null;
}
/**
* Deletes the pipeline for the given render object.
*
* @param {RenderObject} object - The render object.
* @return {?Object} The deleted dictionary.
*/
delete( object ) {
const pipeline = this.get( object ).pipeline;
if ( pipeline ) {
// pipeline
pipeline.usedTimes --;
if ( pipeline.usedTimes === 0 ) this._releasePipeline( pipeline );
// programs
if ( pipeline.isComputePipeline ) {
pipeline.computeProgram.usedTimes --;
if ( pipeline.computeProgram.usedTimes === 0 ) this._releaseProgram( pipeline.computeProgram );
} else {
pipeline.fragmentProgram.usedTimes --;
pipeline.vertexProgram.usedTimes --;
if ( pipeline.vertexProgram.usedTimes === 0 ) this._releaseProgram( pipeline.vertexProgram );
if ( pipeline.fragmentProgram.usedTimes === 0 ) this._releaseProgram( pipeline.fragmentProgram );
}
}
return super.delete( object );
}
/**
* Frees internal resources.
*/
dispose() {
super.dispose();
this.caches = new Map();
this.programs = {
vertex: new Map(),
fragment: new Map(),
compute: new Map()
};
}
/**
* Updates the pipeline for the given render object.
*
* @param {RenderObject} renderObject - The render object.
*/
updateForRender( renderObject ) {
this.getForRender( renderObject );
}
/**
* Returns a compute pipeline for the given parameters.
*
* @private
* @param {Node} computeNode - The compute node.
* @param {ProgrammableStage} stageCompute - The programmable stage representing the compute shader.
* @param {string} cacheKey - The cache key.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {?Array<Promise>} promises - An array of compilation promises which is only relevant in context of `Renderer.compileComputeAsync()`.
* @return {ComputePipeline} The compute pipeline.
*/
_getComputePipeline( computeNode, stageCompute, cacheKey, bindings, promises ) {
// check for existing pipeline
cacheKey = cacheKey || this._getComputeCacheKey( computeNode, stageCompute );
let pipeline = this.caches.get( cacheKey );
if ( pipeline === undefined ) {
const { transforms, nodeAttributes } = this.nodes.getForCompute( computeNode );
pipeline = new ComputePipeline( cacheKey, stageCompute, transforms, nodeAttributes );
this.caches.set( cacheKey, pipeline );
this.backend.createComputePipeline( pipeline, bindings, promises );
}
return pipeline;
}
/**
* Returns a render pipeline for the given parameters.
*
* @private
* @param {RenderObject} renderObject - The render object.
* @param {ProgrammableStage} stageVertex - The programmable stage representing the vertex shader.
* @param {ProgrammableStage} stageFragment - The programmable stage representing the fragment shader.
* @param {string} cacheKey - The cache key.
* @param {?Array<Promise>} promises - An array of compilation promises which is only relevant in context of `Renderer.compileAsync()`.
* @return {RenderObjectPipeline} The render pipeline.
*/
_getRenderPipeline( renderObject, stageVertex, stageFragment, cacheKey, promises ) {
// check for existing pipeline
cacheKey = cacheKey || this._getRenderCacheKey( renderObject, stageVertex, stageFragment );
let pipeline = this.caches.get( cacheKey );
if ( pipeline === undefined ) {
pipeline = new RenderObjectPipeline( cacheKey, stageVertex, stageFragment );
this.caches.set( cacheKey, pipeline );
renderObject.pipeline = pipeline;
// The `promises` array is `null` by default and only set to an empty array when
// `Renderer.compileAsync()` is used. The next call actually fills the array with
// pending promises that resolve when the render pipelines are ready for rendering.
this.backend.createRenderPipeline( renderObject, promises );
}
return pipeline;
}
/**
* Computes a cache key representing a compute pipeline.
*
* @private
* @param {Node} computeNode - The compute node.
* @param {ProgrammableStage} stageCompute - The programmable stage representing the compute shader.
* @return {string} The cache key.
*/
_getComputeCacheKey( computeNode, stageCompute ) {
return computeNode.id + ',' + stageCompute.id;
}
/**
* Computes a cache key representing a render pipeline.
*
* @private
* @param {RenderObject} renderObject - The render object.
* @param {ProgrammableStage} stageVertex - The programmable stage representing the vertex shader.
* @param {ProgrammableStage} stageFragment - The programmable stage representing the fragment shader.
* @return {string} The cache key.
*/
_getRenderCacheKey( renderObject, stageVertex, stageFragment ) {
return stageVertex.id + ',' + stageFragment.id + ',' + this.backend.getRenderCacheKey( renderObject );
}
/**
* Releases the given pipeline.
*
* @private
* @param {Pipeline} pipeline - The pipeline to release.
*/
_releasePipeline( pipeline ) {
this.backend.destroyPipeline( pipeline );
this.caches.delete( pipeline.cacheKey );
}
/**
* Releases the shader program.
*
* @private
* @param {Object} program - The shader program to release.
*/
_releaseProgram( program ) {
const code = program.code;
const stage = program.stage;
this.programs[ stage ].delete( code );
this.backend.destroyProgram( program );
this.info.destroyProgram( program );
}
/**
* Returns `true` if the compute pipeline for the given compute node requires an update.
*
* @private
* @param {Node} computeNode - The compute node.
* @return {boolean} Whether the compute pipeline for the given compute node requires an update or not.
*/
_needsComputeUpdate( computeNode ) {
const data = this.get( computeNode );
return data.pipeline === undefined || data.version !== computeNode.version;
}
/**
* Returns `true` if the render pipeline for the given render object requires an update.
*
* @private
* @param {RenderObject} renderObject - The render object.
* @return {boolean} Whether the render object for the given render object requires an update or not.
*/
_needsRenderUpdate( renderObject ) {
const data = this.get( renderObject );
return data.pipeline === undefined || this.backend.needsRenderUpdate( renderObject );
}
}
const _cacheKeyIds = [];
/**
* This renderer module manages the bindings of the renderer.
*
* @private
* @augments DataMap
*/
class Bindings extends DataMap {
/**
* Constructs a new bindings management component.
*
* @param {Backend} backend - The renderer's backend.
* @param {NodeManager} nodes - Renderer component for managing nodes related logic.
* @param {Textures} textures - Renderer component for managing textures.
* @param {Attributes} attributes - Renderer component for managing attributes.
* @param {Pipelines} pipelines - Renderer component for managing pipelines.
* @param {Info} info - Renderer component for managing metrics and monitoring data.
*/
constructor( backend, nodes, textures, attributes, pipelines, info ) {
super();
/**
* The renderer's backend.
*
* @type {Backend}
*/
this.backend = backend;
/**
* Renderer component for managing textures.
*
* @type {Textures}
*/
this.textures = textures;
/**
* Renderer component for managing pipelines.
*
* @type {Pipelines}
*/
this.pipelines = pipelines;
/**
* Renderer component for managing attributes.
*
* @type {Attributes}
*/
this.attributes = attributes;
/**
* Renderer component for managing nodes related logic.
*
* @type {NodeManager}
*/
this.nodes = nodes;
/**
* Renderer component for managing metrics and monitoring data.
*
* @type {Info}
*/
this.info = info;
/**
* Shared bind groups per render context.
*
* @private
* @type {WeakMap<RenderContext|Renderer,Map<number,BindGroup>>}
*/
this._sharedBindGroups = new WeakMap();
this.pipelines.bindings = this; // assign bindings to pipelines
}
/**
* Returns the bind groups for the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @return {Array<BindGroup>} The bind groups.
*/
getForRender( renderObject ) {
const bindings = renderObject.getBindings();
const renderObjectData = this.get( renderObject );
if ( renderObjectData.initialized !== true ) {
// bind groups are created once per object
this._createBindings( bindings );
renderObjectData.initialized = true;
}
return bindings;
}
/**
* Returns the bind groups for the given compute node.
*
* @param {Node} computeNode - The compute node.
* @return {Array<BindGroup>} The bind groups.
*/
getForCompute( computeNode ) {
const bindings = this.nodes.getForCompute( computeNode ).bindings;
const computeNodeData = this.get( computeNode );
if ( computeNodeData.initialized !== true || computeNodeData.bindings !== bindings ) {
// bind groups are created once per compute node version
if ( computeNodeData.bindings !== undefined ) {
this._destroyBindings( computeNodeData.bindings );
}
this._createBindings( bindings );
computeNodeData.initialized = true;
computeNodeData.bindings = bindings;
}
return bindings;
}
/**
* Returns the shared bind group of the given render context that corresponds to the given bind group.
*
* @param {BindGroup} bindGroup - The bind group.
* @param {?RenderContext} [renderContext=null] - The render context. If not set, the current render context is used.
* @return {BindGroup} The shared bind group.
*/
getSharedBindGroup( bindGroup, renderContext = null ) {
if ( renderContext === null ) {
const renderer = this.backend.renderer;
renderContext = renderer._currentRenderContext || renderer; // use renderer as fallback until we have a compute context
}
// build cache key
let cacheKeyString = '';
for ( const binding of bindGroup.bindings ) {
if ( binding.isNodeUniformsGroup ) {
binding.uniforms.sort( ( a, b ) => a.nodeUniform.node.id - b.nodeUniform.node.id );
for ( const uniform of binding.uniforms ) {
cacheKeyString += uniform.nodeUniform.node.id + ',';
}
} else {
cacheKeyString += binding.nodeUniform.id + ',';
}
}
const cacheKey = hashString( cacheKeyString );
// lookup bind group cache
let bindGroupsCache = this._sharedBindGroups.get( renderContext );
if ( bindGroupsCache === undefined ) {
bindGroupsCache = new Map();
this._sharedBindGroups.set( renderContext, bindGroupsCache );
}
// lookup bind group
let sharedBindGroup = bindGroupsCache.get( cacheKey );
if ( sharedBindGroup === undefined ) {
// make sure shared bindings are cloned per render context
sharedBindGroup = this.has( bindGroup ) ? bindGroup.clone() : bindGroup;
bindGroupsCache.set( cacheKey, sharedBindGroup );
const groupData = this.get( sharedBindGroup );
groupData.renderContext = renderContext;
groupData.cacheKey = cacheKey;
}
return sharedBindGroup;
}
/**
* Updates the bindings for the given compute node.
*
* @param {Node} computeNode - The compute node.
*/
updateForCompute( computeNode ) {
this._updateBindings( this.getForCompute( computeNode ) );
}
/**
* Updates the bindings for the given render object.
*
* @param {RenderObject} renderObject - The render object.
*/
updateForRender( renderObject ) {
this._updateBindings( this.getForRender( renderObject ) );
}
/**
* Updates only the shared uniform buffers of the given render object.
*
* @param {RenderObject} renderObject - The render object.
*/
updateSharedForRender( renderObject ) {
const bindings = this.getForRender( renderObject );
for ( const bindGroup of bindings ) {
for ( const binding of bindGroup.bindings ) {
if ( ( binding.isNodeUniformsGroup === true || binding.isNodeUniformBuffer === true ) && binding.groupNode.shared === true ) {
const updatedGroup = this.nodes.updateGroup( binding );
if ( updatedGroup === false ) continue;
const updated = binding.update();
if ( updated ) {
this.backend.updateBinding( binding );
}
if ( binding.updateRanges.length > 0 ) binding.clearUpdateRanges();
}
}
}
}
/**
* Deletes the bindings for the given compute node.
*
* @param {Node} computeNode - The compute node.
*/
deleteForCompute( computeNode ) {
const computeNodeData = this.get( computeNode );
const bindings = computeNodeData.bindings || this.nodes.getForCompute( computeNode ).bindings;
this._destroyBindings( bindings );
this.delete( computeNode );
}
/**
* Deletes the bindings for the given renderObject node.
*
* @param {RenderObject} renderObject - The renderObject.
*/
deleteForRender( renderObject ) {
const bindings = renderObject.getBindings();
this._destroyBindings( bindings );
this.delete( renderObject );
}
/**
* Creates the bindings for the given array of bindings.
*
* @param {Array<BindGroup>} bindings - The bind groups.
*/
_createBindings( bindings ) {
for ( const bindGroup of bindings ) {
// binding group
const groupData = this.get( bindGroup );
if ( groupData.bindGroup === undefined ) {
// initialize
for ( const binding of bindGroup.bindings ) {
if ( binding.isUniformBuffer ) {
this.backend.createUniformBuffer( binding );
this.info.createUniformBuffer( binding );
} else if ( binding.isSampledTexture ) {
binding.generation = this.textures.updateTexture( binding.texture );
} else if ( binding.isSampler ) {
binding.samplerKey = this.textures.updateSampler( binding );
} else if ( binding.isStorageBuffer ) {
const attribute = binding.attribute;
const attributeType = attribute.isIndirectStorageBufferAttribute ? AttributeType.INDIRECT : AttributeType.STORAGE;
this.attributes.update( attribute, attributeType );
}
}
// each object defines an array of bindings (ubos, textures, samplers etc.)
this.backend.createBindings( bindGroup, bindings, '' );
groupData.bindGroup = bindGroup;
groupData.usedTimes = 1;
} else {
groupData.usedTimes ++;
}
}
}
/**
* Deletes the given array of bindings.
*
* @param {Array<BindGroup>} bindings - The bind groups.
*/
_destroyBindings( bindings ) {
for ( const bindGroup of bindings ) {
const groupData = this.get( bindGroup );
groupData.usedTimes --;
if ( groupData.usedTimes === 0 ) {
for ( const binding of bindGroup.bindings ) {
if ( binding.isUniformBuffer ) {
this.backend.destroyUniformBuffer( binding );
this.info.destroyUniformBuffer( binding );
// release arrays
binding.release();
} else if ( binding.isSampler ) {
if ( binding.isSampledTexture !== true ) {
this.backend.destroySampler( binding );
} else if ( binding.texture !== null ) {
// untrack destroyed bind group from its texture
const textureData = this.textures.get( binding.texture );
if ( textureData.bindGroups !== undefined ) textureData.bindGroups.delete( bindGroup );
}
binding.release();
}
}
if ( groupData.cacheKey !== undefined ) {
const bindGroupsCache = this._sharedBindGroups.get( groupData.renderContext );
bindGroupsCache.delete( groupData.cacheKey );
}
this.backend.deleteBindGroupData( bindGroup );
this.delete( bindGroup );
}
}
}
/**
* Updates the given array of bindings.
*
* @param {Array<BindGroup>} bindings - The bind groups.
*/
_updateBindings( bindings ) {
for ( const bindGroup of bindings ) {
this._update( bindGroup, bindings );
}
}
/**
* Updates the given bind group.
*
* @param {BindGroup} bindGroup - The bind group to update.
* @param {Array<BindGroup>} bindings - The bind groups.
*/
_update( bindGroup, bindings ) {
const { backend } = this;
let needsBindingsUpdate = false;
let cacheBindings = true;
let cacheKeyLength = 0;
let version = 0;
// iterate over all bindings and check if buffer updates or a new binding group is required
for ( const binding of bindGroup.bindings ) {
const updatedGroup = this.nodes.updateGroup( binding );
// every uniforms group is a uniform buffer. So if no update is required,
// we move one with the next binding. Otherwise the next if block will update the group.
if ( updatedGroup === false ) continue;
//
if ( binding.isStorageBuffer ) {
const attribute = binding.attribute;
const attributeType = attribute.isIndirectStorageBufferAttribute ? AttributeType.INDIRECT : AttributeType.STORAGE;
const bindingData = backend.get( binding );
this.attributes.update( attribute, attributeType );
// generation: a new buffer is created if the attribute has been disposed while in use
const generation = this.attributes.get( attribute ).generation;
if ( bindingData.attribute !== attribute || bindingData.generation !== generation ) {
bindingData.attribute = attribute;
bindingData.generation = generation;
needsBindingsUpdate = true;
}
// the generation is part of the version so a re-created buffer replaces the cached bind group
_cacheKeyIds[ cacheKeyLength ++ ] = attribute.id;
version += generation;
}
if ( binding.isUniformBuffer ) {
const updated = binding.update();
if ( updated ) {
backend.updateBinding( binding );
}
} else if ( binding.isSampledTexture ) {
const updated = binding.update();
// get the texture data after the update, to sync the texture reference from node
const texture = binding.texture;
const texturesTextureData = this.textures.get( texture );
if ( updated ) {
// version: update the texture data or create a new one
const generation = this.textures.updateTexture( texture );
// generation: update the bindings if the binding refers to a different texture object
if ( binding.generation !== generation ) {
binding.generation = generation;
needsBindingsUpdate = true;
}
// keep track which bind groups refer to the current texture (this is needed for dispose)
texturesTextureData.bindGroups.add( bindGroup );
}
const textureData = backend.get( texture );
if ( textureData.externalTexture !== undefined || texturesTextureData.isDefaultTexture ) {
cacheBindings = false;
} else {
_cacheKeyIds[ cacheKeyLength ++ ] = texture.id;
version += texture.version;
}
if ( texture.isStorageTexture === true && texture.mipmapsAutoUpdate === true ) {
const textureData = this.get( texture );
if ( binding.store === true ) {
textureData.needsMipmap = true;
} else if ( this.textures.needsMipmaps( texture ) && textureData.needsMipmap === true ) {
this.backend.generateMipmaps( texture );
textureData.needsMipmap = false;
}
}
} else if ( binding.isSampler ) {
const updated = binding.update();
if ( updated ) {
const samplerKey = this.textures.updateSampler( binding );
if ( binding.samplerKey !== samplerKey ) {
binding.samplerKey = samplerKey;
needsBindingsUpdate = true;
}
}
}
if ( binding.isBuffer && binding.updateRanges.length > 0 ) {
binding.clearUpdateRanges();
}
}
if ( needsBindingsUpdate === true ) {
_cacheKeyIds.length = cacheKeyLength;
const cacheKey = cacheBindings ? _cacheKeyIds.join( ',' ) : '';
this.backend.updateBindings( bindGroup, bindings, cacheKey, version );
}
}
}
const _emptyArray = /*@__PURE__*/ Object.freeze( [] );
/**
* Default sorting function for opaque render items.
*
* @private
* @function
* @param {Object} a - The first render item.
* @param {Object} b - The second render item.
* @return {number} A numeric value which defines the sort order.
*/
function painterSortStable( a, b ) {
if ( a.groupOrder !== b.groupOrder ) {
return a.groupOrder - b.groupOrder;
} else if ( a.renderOrder !== b.renderOrder ) {
return a.renderOrder - b.renderOrder;
} else if ( a.z !== b.z ) {
return a.z - b.z;
} else {
return a.id - b.id;
}
}
/**
* Default sorting function for transparent render items.
*
* @private
* @function
* @param {Object} a - The first render item.
* @param {Object} b - The second render item.
* @return {number} A numeric value which defines the sort order.
*/
function reversePainterSortStable( a, b ) {
if ( a.groupOrder !== b.groupOrder ) {
return a.groupOrder - b.groupOrder;
} else if ( a.renderOrder !== b.renderOrder ) {
return a.renderOrder - b.renderOrder;
} else if ( a.z !== b.z ) {
return b.z - a.z;
} else {
return a.id - b.id;
}
}
/**
* Returns `true` if the given transparent material requires a double pass.
*
* @private
* @function
* @param {Material} material - The transparent material.
* @return {boolean} Whether the given material requires a double pass or not.
*/
function needsDoublePass( material ) {
const hasTransmission = material.transmission > 0 || ( material.transmissionNode && material.transmissionNode.isNode );
return hasTransmission && material.side === DoubleSide && material.forceSinglePass === false;
}
/**
* When the renderer analyzes the scene at the beginning of a render call,
* it stores 3D object for further processing in render lists. Depending on the
* properties of a 3D objects (like their transformation or material state), the
* objects are maintained in ordered lists for the actual rendering.
*
* Render lists are unique per scene and camera combination.
*
* @private
* @augments Pipeline
*/
class RenderList {
/**
* Constructs a render list.
*
* @param {Lighting} lighting - The lighting management component.
* @param {Scene} scene - The scene.
* @param {Camera} camera - The camera the scene is rendered with.
*/
constructor( lighting, scene, camera ) {
/**
* 3D objects are transformed into render items and stored in this array.
*
* @type {Array<Object>}
*/
this.renderItems = [];
/**
* The current render items index.
*
* @type {number}
* @default 0
*/
this.renderItemsIndex = 0;
/**
* A list with opaque render items.
*
* @type {Array<Object>}
*/
this.opaque = [];
/**
* A list with transparent render items which require
* double pass rendering (e.g. transmissive objects).
*
* @type {Array<Object>}
*/
this.transparentDoublePass = [];
/**
* A list with transparent render items.
*
* @type {Array<Object>}
*/
this.transparent = [];
/**
* A list with transparent render bundle data.
*
* @type {Array<Object>}
*/
this.bundles = [];
/**
* The lighting management component.
*
* @type {Lighting}
*/
this.lighting = lighting;
/**
* The render list's lights node. This node is later
* relevant for the actual analytical light nodes which
* compute the scene's lighting in the shader.
*
* @type {LightsNode}
*/
this.lightsNode = lighting.getNode( scene );
/**
* The scene's lights stored in an array. This array
* is used to setup the lights node.
*
* @type {Array<Light>}
*/
this.lightsArray = [];
/**
* The scene.
*
* @type {Scene}
*/
this.scene = scene;
/**
* The camera the scene is rendered with.
*
* @type {Camera}
*/
this.camera = camera;
/**
* How many objects perform occlusion query tests.
*
* @type {number}
* @default 0
*/
this.occlusionQueryCount = 0;
/**
* The ID of the frame the render list was last used in.
*
* @type {number}
* @default -1
*/
this.frameId = -1;
/**
* The last object that was counted for occlusion query testing. Used to
* avoid counting an object more than once when it produces multiple render
* items (e.g. a mesh with multiple material groups), since such an object
* is covered by a single occlusion query.
*
* @private
* @type {?Object3D}
* @default null
*/
this._lastOcclusionObject = null;
}
/**
* This method is called right at the beginning of a render call
* before the scene is analyzed. It prepares the internal data
* structures for the upcoming render lists generation.
*
* @return {RenderList} A reference to this render list.
*/
begin() {
this.renderItemsIndex = 0;
this.opaque.length = 0;
this.transparentDoublePass.length = 0;
this.transparent.length = 0;
this.bundles.length = 0;
this.lightsArray.length = 0;
this.occlusionQueryCount = 0;
return this;
}
/**
* Returns a render item for the giving render item state. The state is defined
* by a series of object-related parameters.
*
* The method avoids object creation by holding render items and reusing them in
* subsequent render calls (just with different property values).
*
* @param {Object3D} object - The 3D object.
* @param {BufferGeometry} geometry - The 3D object's geometry.
* @param {Material} material - The 3D object's material.
* @param {number} groupOrder - The current group order.
* @param {number} z - Th 3D object's depth value (z value in clip space).
* @param {?number} group - {?Object} group - Only relevant for objects using multiple materials. This represents a group entry from the respective `BufferGeometry`.
* @param {ClippingContext} clippingContext - The current clipping context.
* @return {Object} The render item.
*/
getNextRenderItem( object, geometry, material, groupOrder, z, group, clippingContext ) {
let renderItem = this.renderItems[ this.renderItemsIndex ];
if ( renderItem === undefined ) {
renderItem = {
id: object.id,
object: object,
geometry: geometry,
material: material,
groupOrder: groupOrder,
renderOrder: object.renderOrder,
z: z,
group: group,
clippingContext: clippingContext
};
this.renderItems[ this.renderItemsIndex ] = renderItem;
} else {
renderItem.id = object.id;
renderItem.object = object;
renderItem.geometry = geometry;
renderItem.material = material;
renderItem.groupOrder = groupOrder;
renderItem.renderOrder = object.renderOrder;
renderItem.z = z;
renderItem.group = group;
renderItem.clippingContext = clippingContext;
}
this.renderItemsIndex ++;
return renderItem;
}
/**
* Pushes the given object as a render item to the internal render lists.
* The selected lists depend on the object properties.
*
* @param {Object3D} object - The 3D object.
* @param {BufferGeometry} geometry - The 3D object's geometry.
* @param {Material} material - The 3D object's material.
* @param {number} groupOrder - The current group order.
* @param {number} z - Th 3D object's depth value (z value in clip space).
* @param {?number} group - {?Object} group - Only relevant for objects using multiple materials. This represents a group entry from the respective `BufferGeometry`.
* @param {ClippingContext} clippingContext - The current clipping context.
*/
push( object, geometry, material, groupOrder, z, group, clippingContext ) {
// with a reversed depth buffer the projected z is inverted
if ( this.camera.reversedDepth === true ) z = - z;
const renderItem = this.getNextRenderItem( object, geometry, material, groupOrder, z, group, clippingContext );
if ( object.occlusionTest === true && this._lastOcclusionObject !== object ) {
this.occlusionQueryCount ++;
this._lastOcclusionObject = object;
}
if ( material.transparent === true || material.transmission > 0 ||
( material.transmissionNode && material.transmissionNode.isNode ) ||
( material.backdropNode && material.backdropNode.isNode ) ) {
if ( needsDoublePass( material ) ) this.transparentDoublePass.push( renderItem );
this.transparent.push( renderItem );
} else {
this.opaque.push( renderItem );
}
}
/**
* Inserts the given object as a render item at the start of the internal render lists.
* The selected lists depend on the object properties.
*
* @param {Object3D} object - The 3D object.
* @param {BufferGeometry} geometry - The 3D object's geometry.
* @param {Material} material - The 3D object's material.
* @param {number} groupOrder - The current group order.
* @param {number} z - Th 3D object's depth value (z value in clip space).
* @param {?number} group - {?Object} group - Only relevant for objects using multiple materials. This represents a group entry from the respective `BufferGeometry`.
* @param {ClippingContext} clippingContext - The current clipping context.
*/
unshift( object, geometry, material, groupOrder, z, group, clippingContext ) {
const renderItem = this.getNextRenderItem( object, geometry, material, groupOrder, z, group, clippingContext );
if ( material.transparent === true || material.transmission > 0 ||
( material.transmissionNode && material.transmissionNode.isNode ) ||
( material.backdropNode && material.backdropNode.isNode ) ) {
if ( needsDoublePass( material ) ) this.transparentDoublePass.unshift( renderItem );
this.transparent.unshift( renderItem );
} else {
this.opaque.unshift( renderItem );
}
}
/**
* Pushes render bundle group data into the render list.
*
* @param {Object} group - Bundle group data.
*/
pushBundle( group ) {
this.bundles.push( group );
}
/**
* Pushes a light into the render list.
*
* @param {Light} light - The light.
*/
pushLight( light ) {
this.lightsArray.push( light );
}
/**
* Sorts the internal render lists.
*
* @param {?function(any, any): number} customOpaqueSort - A custom sort function for opaque objects.
* @param {?function(any, any): number} customTransparentSort - A custom sort function for transparent objects.
*/
sort( customOpaqueSort, customTransparentSort ) {
if ( this.opaque.length > 1 ) this.opaque.sort( customOpaqueSort || painterSortStable );
if ( this.transparentDoublePass.length > 1 ) this.transparentDoublePass.sort( customTransparentSort || reversePainterSortStable );
if ( this.transparent.length > 1 ) this.transparent.sort( customTransparentSort || reversePainterSortStable );
}
/**
* This method performs finalizing tasks right after the render lists
* have been generated.
*/
finish() {
// update lights
this.lightsNode.setLights( this.lighting.enabled ? this.lightsArray : _emptyArray );
// Clear references from inactive renderItems in the list
for ( let i = this.renderItemsIndex, il = this.renderItems.length; i < il; i ++ ) {
const renderItem = this.renderItems[ i ];
if ( renderItem.id === null ) break;
resetRenderItem( renderItem );
}
this._lastOcclusionObject = null;
}
/**
* This method is called when the render list has become inactive.
*/
clear() {
// Clear all references from the render items so scene objects
// are not retained when the render list is not used anymore.
for ( let i = 0, il = this.renderItems.length; i < il; i ++ ) {
const renderItem = this.renderItems[ i ];
if ( renderItem.id === null ) break;
resetRenderItem( renderItem );
}
this.opaque.length = 0;
this.transparentDoublePass.length = 0;
this.transparent.length = 0;
this.bundles.length = 0;
}
}
function resetRenderItem( renderItem ) {
renderItem.id = null;
renderItem.object = null;
renderItem.geometry = null;
renderItem.material = null;
renderItem.groupOrder = null;
renderItem.renderOrder = null;
renderItem.z = null;
renderItem.group = null;
renderItem.clippingContext = null;
}
const _chainKeys$2 = [];
/**
* This renderer module manages the render lists which are unique
* per scene and camera combination.
*
* @private
*/
class RenderLists {
/**
* Constructs a render lists management component.
*/
constructor() {
/**
* The internal chain map which holds the render lists.
*
* @type {ChainMap}
*/
this.lists = new ChainMap();
/**
* The render lists which are currently in use. Lists are removed
* as soon as they become stale.
*
* @private
* @type {Set<RenderList>}
*/
this._activeLists = new Set();
/**
* The current frame ID.
*
* @private
* @type {number}
*/
this._frameId = -1;
}
/**
* Returns a render list for the given scene and camera.
*
* @param {Scene} scene - The scene.
* @param {Camera} camera - The camera.
* @param {Lighting} lighting - The lighting manager.
* @return {RenderList} The render list.
*/
get( scene, camera, lighting ) {
const lists = this.lists;
_chainKeys$2[ 0 ] = scene;
_chainKeys$2[ 1 ] = camera;
_chainKeys$2[ 2 ] = lighting;
let list = lists.get( _chainKeys$2 );
if ( list === undefined ) {
list = new RenderList( lighting, scene, camera );
lists.set( _chainKeys$2, list );
}
_chainKeys$2[ 0 ] = null;
_chainKeys$2[ 1 ] = null;
_chainKeys$2[ 2 ] = null;
//
list.frameId = this._frameId;
this._activeLists.add( list );
return list;
}
/**
* Must be called when a new frame begins.
*
* @param {number} frameId - The current frame ID.
*/
update( frameId ) {
if ( frameId === this._frameId ) return;
this._frameId = frameId;
for ( const list of this._activeLists ) {
// if a render list has not been used within 10 frames, consider
// it as inactive and clear it
if ( frameId - list.frameId > 10 ) {
list.clear();
this._activeLists.delete( list );
}
}
}
/**
* Frees all internal resources.
*/
dispose() {
this.lists = new ChainMap();
this._activeLists.clear();
this._frameId = -1;
}
}
let _id$8 = 0;
/**
* Any render or compute command is executed in a specific context that defines
* the state of the renderer and its backend. Typical examples for such context
* data are the current clear values or data from the active framebuffer. This
* module is used to represent these contexts as objects.
*
* @private
*/
class RenderContext {
/**
* Constructs a new render context.
*/
constructor() {
/**
* The context's ID.
*
* @type {number}
*/
this.id = _id$8 ++;
/**
* Identifies the attachment and MRT configuration of the context.
* Unlike the ID, it is the same for the contexts of all renderer
* call depths, so render objects use it in their cache key. That way,
* the shaders built by `compileAsync()` are also used by nested render
* calls like the scene pass of a render pipeline.
*
* @type {string}
* @default ''
*/
this.stateKey = '';
/**
* Cached sub-camera frustums shared by render-list and backend culling.
* Kept per context so nested renders do not overwrite the active frustums.
*
* @type {?FrustumArray}
* @default null
*/
this.frustumArray = null;
/**
* The MRT configuration.
*
* @type {?MRTNode}
* @default null
*/
this.mrt = null;
/**
* Whether the current active framebuffer has a color attachment.
*
* @type {boolean}
* @default true
*/
this.color = true;
/**
* Whether the color attachment should be cleared or not.
*
* @type {boolean}
* @default true
*/
this.clearColor = true;
/**
* The clear color value.
*
* @type {Object}
* @default true
*/
this.clearColorValue = { r: 0, g: 0, b: 0, a: 1 };
/**
* Whether the current active framebuffer has a depth attachment.
*
* @type {boolean}
* @default true
*/
this.depth = true;
/**
* Whether the depth attachment should be cleared or not.
*
* @type {boolean}
* @default true
*/
this.clearDepth = true;
/**
* The clear depth value.
*
* @type {number}
* @default 1
*/
this.clearDepthValue = 1;
/**
* Whether the current active framebuffer has a stencil attachment.
*
* @type {boolean}
* @default false
*/
this.stencil = false;
/**
* Whether the stencil attachment should be cleared or not.
*
* @type {boolean}
* @default true
*/
this.clearStencil = true;
/**
* The clear stencil value.
*
* @type {number}
* @default 1
*/
this.clearStencilValue = 1;
/**
* By default the viewport encloses the entire framebuffer If a smaller
* viewport is manually defined, this property is to `true` by the renderer.
*
* @type {boolean}
* @default false
*/
this.viewport = false;
/**
* The viewport value. This value is in physical pixels meaning it incorporates
* the renderer's pixel ratio. The viewport property of render targets or
* the renderer is in logical pixels.
*
* @type {Vector4}
*/
this.viewportValue = new Vector4();
/**
* When the scissor test is active and scissor rectangle smaller than the
* framebuffers dimensions, this property is to `true` by the renderer.
*
* @type {boolean}
* @default false
*/
this.scissor = false;
/**
* The scissor rectangle.
*
* @type {Vector4}
*/
this.scissorValue = new Vector4();
/**
* The active render target.
*
* @type {?RenderTarget}
* @default null
*/
this.renderTarget = null;
/**
* The textures of the active render target.
* `null` when no render target is set.
*
* @type {?Array<Texture>}
* @default null
*/
this.textures = null;
/**
* The depth texture of the active render target.
* `null` when no render target is set.
*
* @type {?DepthTexture}
* @default null
*/
this.depthTexture = null;
/**
* The active cube face.
*
* @type {number}
* @default 0
*/
this.activeCubeFace = 0;
/**
* The active mipmap level.
*
* @type {number}
* @default 0
*/
this.activeMipmapLevel = 0;
/**
* The number of MSAA samples. This value is always `1` when
* MSAA isn't used.
*
* @type {number}
* @default 1
*/
this.sampleCount = 1;
/**
* The active render target's width in physical pixels.
*
* @type {number}
* @default 0
*/
this.width = 0;
/**
* The active render target's height in physical pixels.
*
* @type {number}
* @default 0
*/
this.height = 0;
/**
* The occlusion query count.
*
* @type {number}
* @default 0
*/
this.occlusionQueryCount = 0;
/**
* The current clipping context.
*
* @type {?ClippingContext}
* @default null
*/
this.clippingContext = null;
/**
* The current camera.
*
* @type {?Camera}
* @default null
*/
this.camera = null;
/**
* Whether a fullscreen pass is rendered or not.
*
* @type {boolean}
* @default false
*/
this.fullscreenPass = false;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isRenderContext = true;
}
/**
* Returns the cache key of this render context.
*
* @return {number} The cache key.
*/
getCacheKey() {
return getCacheKey( this );
}
}
/**
* Computes a cache key for the given render context. This key
* should identify the render target state so it is possible to
* configure the correct attachments in the respective backend.
*
* @param {RenderContext} renderContext - The render context.
* @return {number} The cache key.
*/
function getCacheKey( renderContext ) {
const { textures, activeCubeFace, activeMipmapLevel } = renderContext;
const values = [ activeCubeFace, activeMipmapLevel ];
for ( const texture of textures ) {
values.push( texture.id );
}
return hashArray( values );
}
/**
* This module manages the render contexts of the renderer.
*
* @private
*/
class RenderContexts {
/**
* Constructs a new render context management component.
*
* @param {Renderer} renderer - The renderer.
*/
constructor( renderer ) {
/**
* The renderer.
*
* @type {Renderer}
*/
this.renderer = renderer;
/**
* A dictionary that manages render contexts.
*
* @type {Object<string,RenderContext>}
*/
this._renderContexts = {};
}
/**
* Returns a render context for the given scene, camera and render target.
*
* @param {?RenderTarget} [renderTarget=null] - The active render target.
* @param {?MRTNode} [mrt=null] - The MRT configuration
* @param {?number} [callDepth=0] - The call depth of the renderer.
* @param {?Camera} [camera=null] - The camera of the render call.
* @return {RenderContext} The render context.
*/
get( renderTarget = null, mrt = null, callDepth = 0, camera = null ) {
//
let attachmentState;
if ( renderTarget === null ) {
attachmentState = 'default';
} else {
const format = renderTarget.texture?.format;
const type = renderTarget.texture?.type;
const count = renderTarget.textures.length;
attachmentState = `${ count }:${ format }:${ type }:${ renderTarget.samples }:${ renderTarget.depthBuffer }:${ renderTarget.stencilBuffer }`;
}
//
const mrtState = ( mrt !== null ) ? mrt.id : 'default';
//
// materials check the camera type at build time, so camera types must not share render objects
const cameraState = camera?.isOrthographicCamera === true ? 'orthographic' : 'perspective';
//
const stateKey = attachmentState + '-' + mrtState + '-' + cameraState;
const renderStateKey = stateKey + '-' + callDepth; // one render context per call depth keeps the bindings of nested renders separate
let renderState = this._renderContexts[ renderStateKey ];
if ( renderState === undefined ) {
renderState = new RenderContext();
renderState.mrt = mrt;
renderState.stateKey = stateKey;
this._renderContexts[ renderStateKey ] = renderState;
}
if ( renderTarget !== null ) renderState.sampleCount = renderTarget.samples === 0 ? 1 : renderTarget.samples;
renderState.clearDepthValue = this.renderer.getClearDepth();
renderState.clearStencilValue = this.renderer.getClearStencil();
return renderState;
}
/**
* Frees internal resources.
*/
dispose() {
this._renderContexts = {};
}
}
const _size = /*@__PURE__*/ new Vector3();
/**
* Returns the mipmaps of the given texture. Compressed cube textures
* define them per face.
*
* @private
* @param {Texture} texture - The texture.
* @return {Array<Object>} The mipmaps.
*/
function getMipmaps( texture ) {
return texture.isCompressedCubeTexture === true ? texture.image[ 0 ].mipmaps : texture.mipmaps;
}
/**
* This module manages the textures of the renderer.
*
* @private
* @augments DataMap
*/
class Textures extends DataMap {
/**
* Constructs a new texture management component.
*
* @param {Renderer} renderer - The renderer.
* @param {Backend} backend - The renderer's backend.
* @param {Info} info - Renderer component for managing metrics and monitoring data.
*/
constructor( renderer, backend, info ) {
super();
/**
* The renderer.
*
* @type {Renderer}
*/
this.renderer = renderer;
/**
* The backend.
*
* @type {Backend}
*/
this.backend = backend;
/**
* Renderer component for managing metrics and monitoring data.
*
* @type {Info}
*/
this.info = info;
/**
* A set of HTMLTextures that need paint updates.
*
* @type {Set<HTMLTexture>}
*/
this._htmlTextures = new Set();
/**
* Stores weak references to the textures and render targets
* with attached `dispose` event listeners.
*
* @private
* @type {Set<WeakRef<(Texture|RenderTarget)>>}
*/
this._tracked = new Set();
/**
* Removes weak references from `_tracked` when their texture or
* render target has been garbage collected without an explicit
* `dispose()`.
*
* @private
* @type {FinalizationRegistry}
*/
this._registry = new FinalizationRegistry( ( ref ) => this._tracked.delete( ref ) );
}
/**
* Frees internal resources.
*/
dispose() {
for ( const ref of this._tracked ) {
const object = ref.deref();
if ( object === undefined || this.has( object ) === false ) continue;
if ( object.isRenderTarget === true ) {
this._destroyRenderTarget( object );
} else {
this._destroyTexture( object );
}
}
this._tracked.clear();
this._htmlTextures.clear();
super.dispose();
}
/**
* Updates the given render target. Based on the given render target configuration,
* it updates the texture states representing the attachments of the framebuffer.
*
* @param {RenderTarget} renderTarget - The render target to update.
* @param {number} [activeMipmapLevel=0] - The active mipmap level.
*/
updateRenderTarget( renderTarget, activeMipmapLevel = 0 ) {
const renderTargetData = this.get( renderTarget );
const sampleCount = renderTarget.samples === 0 ? 1 : renderTarget.samples;
const depthTextureMips = renderTargetData.depthTextureMips || ( renderTargetData.depthTextureMips = {} );
const textures = renderTarget.textures;
// Depth-only render targets take their size from the depth texture.
const size = this.getSize( textures.length > 0 ? textures[ 0 ] : renderTarget.depthTexture );
const mipWidth = size.width >> activeMipmapLevel;
const mipHeight = size.height >> activeMipmapLevel;
let depthTexture = renderTarget.depthTexture || depthTextureMips[ activeMipmapLevel ];
const useDepthTexture = renderTarget.depthBuffer === true || renderTarget.stencilBuffer === true;
let textureNeedsUpdate = false;
const hasArrayDepthTexture = depthTexture !== undefined && depthTexture.image !== undefined && depthTexture.image.depth > 1;
const useArrayDepth = size.depth > 1 && ( renderTarget.useArrayDepthTexture || renderTarget.multiview || hasArrayDepthTexture );
if ( depthTexture === undefined && useDepthTexture ) {
depthTexture = new DepthTexture();
depthTexture.format = renderTarget.stencilBuffer ? DepthStencilFormat : DepthFormat;
depthTexture.type = this.renderer.reversedDepthBuffer === true ? FloatType : ( renderTarget.stencilBuffer ? UnsignedInt248Type : UnsignedIntType );
depthTexture.image.width = mipWidth;
depthTexture.image.height = mipHeight;
depthTexture.image.depth = size.depth;
depthTexture.renderTarget = renderTarget;
depthTextureMips[ activeMipmapLevel ] = depthTexture;
}
if ( depthTexture ) {
depthTexture.isArrayTexture = useArrayDepth;
}
if ( renderTargetData.width !== size.width || size.height !== renderTargetData.height ) {
textureNeedsUpdate = true;
if ( depthTexture && depthTexture.renderTarget === renderTarget ) {
depthTexture.needsUpdate = true;
depthTexture.image.width = mipWidth;
depthTexture.image.height = mipHeight;
depthTexture.image.depth = useArrayDepth ? size.depth : 1;
}
}
renderTargetData.width = size.width;
renderTargetData.height = size.height;
renderTargetData.textures = textures;
renderTargetData.depthTexture = depthTexture || null;
renderTargetData.depth = renderTarget.depthBuffer;
renderTargetData.stencil = renderTarget.stencilBuffer;
renderTargetData.renderTarget = renderTarget;
if ( renderTargetData.sampleCount !== sampleCount ) {
textureNeedsUpdate = true;
if ( depthTexture && depthTexture.renderTarget === renderTarget ) {
depthTexture.needsUpdate = true;
}
renderTargetData.sampleCount = sampleCount;
}
// dispose handler
if ( renderTargetData.initialized !== true ) {
renderTargetData.initialized = true;
this.info.memory.renderTargets ++;
// dispose
renderTargetData.onDispose = () => {
this._destroyRenderTarget( renderTarget );
};
renderTarget.addEventListener( 'dispose', renderTargetData.onDispose );
// see #34368 why tracking separate remove listeners is required right now
renderTargetData.ref = new WeakRef( renderTarget );
this._tracked.add( renderTargetData.ref );
this._registry.register( renderTarget, renderTargetData.ref, renderTargetData.ref );
}
const options = { sampleCount };
// XR render targets require no texture updates
if ( renderTarget.isXRRenderTarget !== true ) {
for ( let i = 0; i < textures.length; i ++ ) {
const texture = textures[ i ];
if ( textureNeedsUpdate ) texture.needsUpdate = true;
this.updateTexture( texture, options );
}
if ( depthTexture ) {
this.updateTexture( depthTexture, options );
}
}
}
/**
* Updates the given texture. Depending on the texture state, this method
* triggers the upload of texture data to the GPU memory. If the texture data are
* not yet ready for the upload, it uses default texture data for as a placeholder.
*
* @param {Texture} texture - The texture to update.
* @param {Object} [options={}] - The options.
* @return {number} The current texture generation.
*/
updateTexture( texture, options = {} ) {
const textureData = this.get( texture );
if ( textureData.initialized === true && textureData.version === texture.version ) return textureData.generation;
// render target textures are created with their render target
const renderTarget = ( texture.isRenderTargetTexture || texture.isDepthTexture ) ? texture.renderTarget : null;
if ( renderTarget !== null && this.get( renderTarget ).initialized !== true ) {
this.updateRenderTarget( renderTarget );
return textureData.generation;
}
const isRenderTarget = texture.isRenderTargetTexture || texture.isDepthTexture || texture.isFramebufferTexture;
const backend = this.backend;
if ( isRenderTarget && textureData.initialized === true ) {
// it's an update
backend.destroyTexture( texture );
}
//
if ( texture.isFramebufferTexture ) {
const renderTarget = this.renderer.getRenderTarget();
if ( renderTarget ) {
texture.type = renderTarget.texture.type;
} else {
texture.type = UnsignedByteType;
}
}
// Ensure HTMLTexture elements are in the canvas before measuring size.
if ( texture.isHTMLTexture && texture.image ) {
const canvas = this.renderer.domElement;
if ( 'requestPaint' in canvas ) {
if ( ! canvas.hasAttribute( 'layoutsubtree' ) ) {
canvas.setAttribute( 'layoutsubtree', 'true' );
}
if ( texture.image.parentNode !== canvas ) {
canvas.appendChild( texture.image );
}
// Set up shared paint callback for all HTMLTextures.
if ( this._htmlTextures.size === 0 ) {
const htmlTextures = this._htmlTextures;
canvas.onpaint = ( event ) => {
const changed = event && event.changedElements;
for ( const t of htmlTextures ) {
if ( ! changed || changed.includes( t.image ) ) {
t.needsUpdate = true;
}
}
};
}
this._htmlTextures.add( texture );
}
}
//
const { width, height, depth } = this.getSize( texture );
options.width = width;
options.height = height;
options.depth = depth;
options.needsMipmaps = this.needsMipmaps( texture );
options.levels = options.needsMipmaps ? this.getMipLevels( texture, width, height ) : 1;
// TODO: Uniformly handle mipmap definitions
// Normal textures, compressed cube textures and render targets define base level + mips with their mipmap array
// Uncompressed cube textures use their mipmap array only for mips (no base level)
if ( texture.isCubeTexture && texture.mipmaps.length > 0 && ! isRenderTarget ) options.levels ++;
//
if ( isRenderTarget || texture.isStorageTexture === true || texture.isExternalTexture === true ) {
backend.createTexture( texture, options );
textureData.generation = texture.version;
} else {
if ( texture.version > 0 ) {
const image = texture.image;
if ( image === undefined ) {
warn( 'Renderer: Texture marked for update but image is undefined.' );
} else if ( image.complete === false ) {
warn( 'Renderer: Texture marked for update but image is incomplete.' );
} else {
if ( texture.images ) {
const images = [];
for ( const image of texture.images ) {
images.push( image );
}
options.images = images;
} else {
options.image = image;
}
if ( textureData.isDefaultTexture === undefined || textureData.isDefaultTexture === true ) {
backend.createTexture( texture, options );
textureData.isDefaultTexture = false;
textureData.generation = texture.version;
}
if ( texture.source.dataReady === true ) {
backend.updateTexture( texture, options );
texture.clearUpdateRanges();
}
if ( options.needsMipmaps && getMipmaps( texture ).length === 0 && texture.mipmapsAutoUpdate === true ) {
backend.generateMipmaps( texture );
}
if ( texture.onUpdate ) texture.onUpdate( texture );
}
} else {
// async update
backend.createDefaultTexture( texture );
textureData.isDefaultTexture = true;
textureData.generation = texture.version;
}
}
// dispose handler
if ( textureData.initialized !== true ) {
textureData.initialized = true;
textureData.generation = texture.version;
textureData.bindGroups = new Set();
//
this.info.createTexture( texture );
//
if ( texture.isVideoTexture && ColorManagement.enabled === true && ColorManagement.getTransfer( texture.colorSpace ) !== SRGBTransfer ) {
warn( 'WebGPURenderer: Video textures must use a color space with a sRGB transfer function, e.g. SRGBColorSpace.' );
}
// dispose
if ( texture.isRenderTargetTexture !== true ) {
textureData.onDispose = () => {
this._destroyTexture( texture );
};
texture.addEventListener( 'dispose', textureData.onDispose );
}
// see #34368 why tracking separate remove listeners is required right now
textureData.ref = new WeakRef( texture );
this._tracked.add( textureData.ref );
this._registry.register( texture, textureData.ref, textureData.ref );
}
//
textureData.version = texture.version;
return textureData.generation;
}
/**
* Updates the sampler for the given texture. This method has no effect
* for the WebGL backend since it has no concept of samplers. Texture
* parameters are configured with the `texParameter()` command for each
* texture.
*
* In WebGPU, samplers are objects like textures and it's possible to share
* them when the texture parameters match.
*
* @param {Sampler} binding - The sampler binding to update.
* @return {string} The current sampler key.
*/
updateSampler( binding ) {
return this.backend.updateSampler( binding );
}
/**
* Computes the size of the given texture and writes the result
* into the target vector. This vector is also returned by the
* method.
*
* If no texture data are available for the compute yet, the method
* returns default size values.
*
* @param {Texture} texture - The texture to compute the size for.
* @param {Vector3} target - The target vector.
* @return {Vector3} The target vector.
*/
getSize( texture, target = _size ) {
let image = texture.images ? texture.images[ 0 ] : texture.image;
if ( texture.isCompressedCubeTexture === true ) image = image[ 0 ];
if ( image ) {
if ( image.image !== undefined ) image = image.image;
if ( texture.isHTMLTexture ) {
target.width = image.offsetWidth || 1;
target.height = image.offsetHeight || 1;
target.depth = 1;
} else if ( ( typeof HTMLVideoElement !== 'undefined' ) && ( image instanceof HTMLVideoElement ) ) {
target.width = image.videoWidth || 1;
target.height = image.videoHeight || 1;
target.depth = 1;
} else if ( ( typeof VideoFrame !== 'undefined' ) && ( image instanceof VideoFrame ) ) {
target.width = image.displayWidth || 1;
target.height = image.displayHeight || 1;
target.depth = 1;
} else {
target.width = image.width || 1;
target.height = image.height || 1;
target.depth = texture.isCubeTexture ? 6 : ( image.depth || 1 );
}
} else {
target.width = target.height = target.depth = 1;
}
return target;
}
/**
* Computes the number of mipmap levels for the given texture.
*
* @param {Texture} texture - The texture.
* @param {number} width - The texture's width.
* @param {number} height - The texture's height.
* @return {number} The number of mipmap levels.
*/
getMipLevels( texture, width, height ) {
let mipLevelCount;
const mipmaps = getMipmaps( texture );
if ( mipmaps.length > 0 ) {
mipLevelCount = mipmaps.length;
} else {
if ( texture.isCompressedTexture === true ) {
// it is not possible to compute mipmaps for compressed textures. So
// when no mipmaps are defined in "texture.mipmaps", force a texture
// level of 1
mipLevelCount = 1;
} else {
mipLevelCount = Math.floor( Math.log2( Math.max( width, height ) ) ) + 1;
}
}
return mipLevelCount;
}
/**
* Returns `true` if the given texture makes use of mipmapping.
*
* @param {Texture} texture - The texture.
* @return {boolean} Whether mipmaps are required or not.
*/
needsMipmaps( texture ) {
return texture.generateMipmaps === true || getMipmaps( texture ).length > 0;
}
/**
* Frees internal resources when the given render target isn't
* required anymore.
*
* @param {RenderTarget} renderTarget - The render target to destroy.
*/
_destroyRenderTarget( renderTarget ) {
if ( this.has( renderTarget ) === true ) {
const renderTargetData = this.get( renderTarget );
const textures = renderTargetData.textures;
const depthTexture = renderTargetData.depthTexture;
//
renderTarget.removeEventListener( 'dispose', renderTargetData.onDispose );
this._tracked.delete( renderTargetData.ref );
this._registry.unregister( renderTargetData.ref );
//
for ( let i = 0; i < textures.length; i ++ ) {
this._destroyTexture( textures[ i ] );
}
if ( depthTexture && depthTexture.renderTarget === renderTarget ) {
this._destroyTexture( depthTexture );
}
this.delete( renderTarget );
this.backend.delete( renderTarget );
this.info.memory.renderTargets --;
}
}
/**
* Frees internal resource when the given texture isn't
* required anymore.
*
* @param {Texture} texture - The texture to destroy.
*/
_destroyTexture( texture ) {
if ( this.has( texture ) === true ) {
const textureData = this.get( texture );
//
texture.removeEventListener( 'dispose', textureData.onDispose );
this._tracked.delete( textureData.ref );
this._registry.unregister( textureData.ref );
// if a texture is not ready for use, it falls back to a default texture so it's possible
// to use it for rendering. If a texture in this state is disposed, it's important to
// not destroy/delete the underlying GPU texture object since it is cached and shared with
// other textures.
const isDefaultTexture = textureData.isDefaultTexture;
this.backend.destroyTexture( texture, isDefaultTexture );
// delete cached bind groups so they don't point to destroyed textures
if ( textureData.bindGroups ) {
for ( const bindGroup of textureData.bindGroups ) {
const bindingsData = this.backend.get( bindGroup );
bindingsData.groups = undefined;
bindingsData.versions = undefined;
// go through all bindings and if one points to the destroyed texture, trigger dispose as well
for ( const binding of bindGroup.bindings ) {
if ( binding.isSampler && binding.texture === texture ) {
if ( binding.isSampledTexture !== true ) {
this.backend.destroySampler( binding );
}
binding.reset();
binding.release();
}
}
}
}
this._htmlTextures.delete( texture );
this.delete( texture );
this.info.destroyTexture( texture );
}
}
}
const _clearColor = /*@__PURE__*/ new Color4();
/**
* This renderer module manages the background.
*
* @private
* @augments DataMap
*/
class Background extends DataMap {
/**
* Constructs a new background management component.
*
* @param {Renderer} renderer - The renderer.
* @param {NodeManager} nodes - Renderer component for managing nodes related logic.
*/
constructor( renderer, nodes ) {
super();
/**
* The renderer.
*
* @type {Renderer}
*/
this.renderer = renderer;
/**
* Renderer component for managing nodes related logic.
*
* @type {NodeManager}
*/
this.nodes = nodes;
}
/**
* Updates the background for the given scene. Depending on how `Scene.background`
* or `Scene.backgroundNode` are configured, this method might configure a simple clear
* or add a mesh to the render list for rendering the background as a textured plane
* or skybox.
*
* @param {Scene} scene - The scene.
* @param {RenderList} renderList - The current render list.
* @param {RenderContext} renderContext - The current render context.
*/
update( scene, renderList, renderContext ) {
const renderer = this.renderer;
const background = this.nodes.getBackgroundNode( scene ) || scene.background;
let forceClear = false;
if ( background === null ) {
// no background settings, use clear color configuration from the renderer
renderer._clearColor.getRGB( _clearColor );
_clearColor.a = renderer._clearColor.a;
} else if ( background.isColor === true ) {
// background is an opaque color
background.getRGB( _clearColor );
_clearColor.a = 1;
forceClear = true;
} else if ( background.isNode === true ) {
const sceneData = this.get( scene );
const backgroundNode = background;
_clearColor.copy( renderer._clearColor );
let backgroundMesh = sceneData.backgroundMesh;
if ( backgroundMesh === undefined ) {
const backgroundMeshNode = vec4( backgroundNode ).mul( backgroundIntensity ).context( {
// @TODO: Add Texture2D support using node context
getUV: () => backgroundRotation.mul( normalWorldGeometry ),
getTextureLevel: () => backgroundBlurriness
} );
// when using orthographic cameras, we must scale the skybox sphere
// up to exceed the dimensions of the camera's viewing box.
const isOrtho = cameraProjectionMatrix.element( 3 ).element( 3 ).equal( 1.0 );
// calculate the orthographic scale
// projectionMatrix[1][1] is (1 / top). Invert it to get the height and multiply by 3.0
// (an arbitrary safety factor) to ensure the skybox is large enough to cover the corners
// of the rectangular screen
const orthoScale = div( 1.0, cameraProjectionMatrix.element( 1 ).element( 1 ) ).mul( 3.0 );
// compute vertex position
const modifiedPosition = isOrtho.select( positionLocal.mul( orthoScale ), positionLocal );
// by using a w component of 0, the skybox will not translate when the camera moves through the scene
const viewPosition = modelViewMatrix.mul( vec4( modifiedPosition, 0.0 ) );
// we force w=1.0 here to prevent the w_clip=0 divide-by-zero error for ortho cameras.
let viewProj = cameraProjectionMatrix.mul( vec4( viewPosition.xyz, 1.0 ) );
// force background to far plane so it does not occlude objects
viewProj = viewProj.setZ( viewProj.w );
const nodeMaterial = new NodeMaterial();
nodeMaterial.name = 'Background.material';
nodeMaterial.side = BackSide;
nodeMaterial.depthTest = false;
nodeMaterial.depthWrite = false;
nodeMaterial.allowOverride = false;
nodeMaterial.fog = false;
nodeMaterial.lights = false;
nodeMaterial.vertexNode = viewProj;
nodeMaterial.colorNode = backgroundMeshNode;
sceneData.backgroundMeshNode = backgroundMeshNode;
sceneData.backgroundMesh = backgroundMesh = new Mesh( new SphereGeometry( 1, 32, 32 ), nodeMaterial );
backgroundMesh.frustumCulled = false;
backgroundMesh.name = 'Background.mesh';
function onBackgroundDispose() {
background.removeEventListener( 'dispose', onBackgroundDispose );
backgroundMesh.material.dispose();
backgroundMesh.geometry.dispose();
}
background.addEventListener( 'dispose', onBackgroundDispose );
}
const backgroundCacheKey = backgroundNode.getCacheKey();
if ( sceneData.backgroundCacheKey !== backgroundCacheKey ) {
sceneData.backgroundMeshNode.node = vec4( backgroundNode ).mul( backgroundIntensity );
sceneData.backgroundMeshNode.needsUpdate = true;
backgroundMesh.material.needsUpdate = true;
sceneData.backgroundCacheKey = backgroundCacheKey;
}
renderList.unshift( backgroundMesh, backgroundMesh.geometry, backgroundMesh.material, 0, 0, null, null );
} else {
error( 'Renderer: Unsupported background configuration.', background );
}
//
const environmentBlendMode = renderer.xr.getEnvironmentBlendMode();
if ( environmentBlendMode === 'additive' ) {
_clearColor.set( 0, 0, 0, 1 );
} else if ( environmentBlendMode === 'alpha-blend' ) {
_clearColor.set( 0, 0, 0, 0 );
}
//
if ( renderer.autoClear === true || forceClear === true ) {
const clearColorValue = renderContext.clearColorValue;
clearColorValue.r = _clearColor.r;
clearColorValue.g = _clearColor.g;
clearColorValue.b = _clearColor.b;
clearColorValue.a = _clearColor.a;
// premultiply alpha
if ( renderer.backend.isWebGLBackend === true || renderer.alpha === true ) {
clearColorValue.r *= clearColorValue.a;
clearColorValue.g *= clearColorValue.a;
clearColorValue.b *= clearColorValue.a;
}
//
renderContext.depthClearValue = renderer.getClearDepth();
renderContext.stencilClearValue = renderer.getClearStencil();
renderContext.clearColor = renderer.autoClearColor === true;
renderContext.clearDepth = renderer.autoClearDepth === true;
renderContext.clearStencil = renderer.autoClearStencil === true;
} else {
renderContext.clearColor = false;
renderContext.clearDepth = false;
renderContext.clearStencil = false;
}
}
}
let _id$7 = 0;
/**
* A bind group represents a collection of bindings and thus a collection
* or resources. Bind groups are assigned to pipelines to provide them
* with the required resources (like uniform buffers or textures).
*
* @private
*/
class BindGroup {
/**
* Constructs a new bind group.
*
* @param {string} name - The bind group's name.
* @param {Array<Binding>} bindings - An array of bindings.
*/
constructor( name = '', bindings = [] ) {
/**
* The bind group's name.
*
* @type {string}
*/
this.name = name;
/**
* An array of bindings.
*
* @type {Array<Binding>}
*/
this.bindings = bindings;
/**
* The group's ID.
*
* @type {number}
*/
this.id = _id$7 ++;
}
/**
* Returns a new bind group with the same name and clones of the bindings.
*
* @return {BindGroup} A clone of this bind group.
*/
clone() {
return new BindGroup( this.name, this.bindings.map( ( binding ) => binding.clone() ) );
}
}
/**
* This module represents the state of a node builder after it was
* used to build the nodes for a render object. The state holds the
* results of the build for further processing in the renderer.
*
* Render objects with identical cache keys share the same node builder state.
*
* @private
*/
class NodeBuilderState {
/**
* Constructs a new node builder state.
*
* @param {string} vertexShader - The native vertex shader code.
* @param {string} fragmentShader - The native fragment shader code.
* @param {string} computeShader - The native compute shader code.
* @param {Array<NodeAttribute>} nodeAttributes - An array of node attributes.
* @param {Array<BindGroup>} bindings - An array of bind groups.
* @param {Array<Node>} updateNodes - An array of nodes that implement their `update()` method.
* @param {Array<Node>} updateBeforeNodes - An array of nodes that implement their `updateBefore()` method.
* @param {Array<Node>} updateAfterNodes - An array of nodes that implement their `updateAfter()` method.
* @param {NodeMaterialObserver} observer - A node material observer.
* @param {boolean} hardwareClipping - Whether the built material uses hardware clipping or not.
* @param {Array<Object>} transforms - An array with transform attribute objects. Only relevant when using compute shaders with WebGL 2.
*/
constructor( vertexShader, fragmentShader, computeShader, nodeAttributes, bindings, updateNodes, updateBeforeNodes, updateAfterNodes, observer, hardwareClipping, transforms = [] ) {
/**
* The native vertex shader code.
*
* @type {string}
*/
this.vertexShader = vertexShader;
/**
* The native fragment shader code.
*
* @type {string}
*/
this.fragmentShader = fragmentShader;
/**
* The native compute shader code.
*
* @type {string}
*/
this.computeShader = computeShader;
/**
* An array with transform attribute objects.
* Only relevant when using compute shaders with WebGL 2.
*
* @type {Array<Object>}
*/
this.transforms = transforms;
/**
* An array of node attributes representing
* the attributes of the shaders.
*
* @type {Array<NodeAttribute>}
*/
this.nodeAttributes = nodeAttributes;
/**
* An array of bind groups representing the uniform or storage
* buffers, texture or samplers of the shader.
*
* @type {Array<BindGroup>}
*/
this.bindings = bindings;
/**
* An array of nodes that implement their `update()` method.
*
* @type {Array<Node>}
*/
this.updateNodes = updateNodes;
/**
* An array of nodes that implement their `updateBefore()` method.
*
* @type {Array<Node>}
*/
this.updateBeforeNodes = updateBeforeNodes;
/**
* An array of nodes that implement their `updateAfter()` method.
*
* @type {Array<Node>}
*/
this.updateAfterNodes = updateAfterNodes;
/**
* A node material observer.
*
* @type {NodeMaterialObserver}
*/
this.observer = observer;
/**
* Whether the built material uses hardware clipping or not.
*
* @type {boolean}
*/
this.hardwareClipping = hardwareClipping;
/**
* How often this state is used by render objects.
*
* @type {number}
*/
this.usedTimes = 0;
}
/**
* This method is used to create a array of bind groups based
* on the existing bind groups of this state. Shared groups are
* not cloned.
*
* @param {Bindings} bindings - Renderer component for managing bindings.
* @param {RenderContext} renderContext - The render context.
* @return {Array<BindGroup>} A array of bind groups.
*/
createBindings( bindings, renderContext ) {
const bindGroups = [];
for ( const instanceGroup of this.bindings ) {
const shared = instanceGroup.bindings[ 0 ].groupNode.shared; // All bindings in the group must have the same groupNode.
if ( shared !== true ) {
const bindingsGroup = new BindGroup( instanceGroup.name, [] );
bindGroups.push( bindingsGroup );
for ( const instanceBinding of instanceGroup.bindings ) {
bindingsGroup.bindings.push( instanceBinding.clone() );
}
} else {
bindGroups.push( bindings.getSharedBindGroup( instanceGroup, renderContext ) );
}
}
return bindGroups;
}
}
/**
* {@link NodeBuilder} is going to create instances of this class during the build process
* of nodes. They represent the final shader attributes that are going to be generated
* by the builder. Arrays of node attributes is maintained in {@link NodeBuilder#attributes}
* and {@link NodeBuilder#bufferAttributes} for this purpose.
*/
class NodeAttribute {
/**
* Constructs a new node attribute.
*
* @param {string} name - The name of the attribute.
* @param {string} type - The type of the attribute.
* @param {?Node} node - An optional reference to the node.
*/
constructor( name, type, node = null ) {
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isNodeAttribute = true;
/**
* The name of the attribute.
*
* @type {string}
*/
this.name = name;
/**
* The type of the attribute.
*
* @type {string}
*/
this.type = type;
/**
* An optional reference to the node.
*
* @type {?Node}
* @default null
*/
this.node = node;
}
}
/**
* {@link NodeBuilder} is going to create instances of this class during the build process
* of nodes. They represent the final shader uniforms that are going to be generated
* by the builder. A dictionary of node uniforms is maintained in {@link NodeBuilder#uniforms}
* for this purpose.
*/
class NodeUniform {
/**
* Constructs a new node uniform.
*
* @param {string} name - The name of the uniform.
* @param {string} type - The type of the uniform.
* @param {UniformNode} node - An reference to the node.
*/
constructor( name, type, node ) {
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isNodeUniform = true;
/**
* The name of the uniform.
*
* @type {string}
*/
this.name = name;
/**
* The type of the uniform.
*
* @type {string}
*/
this.type = type;
/**
* An reference to the node.
*
* @type {UniformNode}
*/
this.node = node;
}
/**
* The value of the uniform node.
*
* @type {any}
*/
get value() {
return this.node.value;
}
set value( val ) {
this.node.value = val;
}
/**
* The id of the uniform node.
*
* @type {number}
*/
get id() {
return this.node.id;
}
/**
* The uniform node's group.
*
* @type {UniformGroupNode}
*/
get groupNode() {
return this.node.groupNode;
}
}
/**
* {@link NodeBuilder} is going to create instances of this class during the build process
* of nodes. They represent the final shader variables that are going to be generated
* by the builder. A dictionary of node variables is maintained in {@link NodeBuilder#vars} for
* this purpose.
*/
class NodeVar {
/**
* Constructs a new node variable.
*
* @param {string} name - The name of the variable.
* @param {string} type - The type of the variable.
* @param {boolean} [readOnly=false] - The read-only flag.
* @param {?number} [count=null] - The size.
* @param {boolean} [local=false] - Whether the variable is declared locally in the flow.
*/
constructor( name, type, readOnly = false, count = null, local = false ) {
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isNodeVar = true;
/**
* The name of the variable.
*
* @type {string}
*/
this.name = name;
/**
* The type of the variable.
*
* @type {string}
*/
this.type = type;
/**
* The read-only flag.
*
* @type {boolean}
*/
this.readOnly = readOnly;
/**
* Whether the variable is declared locally in the flow.
*
* @type {boolean}
* @default false
*/
this.local = local;
/**
* The size.
*
* @type {?number}
*/
this.count = count;
}
}
/**
* {@link NodeBuilder} is going to create instances of this class during the build process
* of nodes. They represent the final shader varyings that are going to be generated
* by the builder. An array of node varyings is maintained in {@link NodeBuilder#varyings} for
* this purpose.
*
* @augments NodeVar
*/
class NodeVarying extends NodeVar {
/**
* Constructs a new node varying.
*
* @param {string} name - The name of the varying.
* @param {string} type - The type of the varying.
* @param {?string} interpolationType - The interpolation type of the varying.
* @param {?string} interpolationSampling - The interpolation sampling type of the varying.
*/
constructor( name, type, interpolationType = null, interpolationSampling = null ) {
super( name, type );
/**
* Whether this varying requires interpolation or not. This property can be used
* to check if the varying can be optimized for a variable.
*
* @type {boolean}
* @default false
*/
this.needsInterpolation = false;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isNodeVarying = true;
/**
* The interpolation type of the varying data.
*
* @type {?string}
* @default null
*/
this.interpolationType = interpolationType;
/**
* The interpolation sampling type of varying data.
*
* @type {?string}
* @default null
*/
this.interpolationSampling = interpolationSampling;
}
}
/**
* {@link NodeBuilder} is going to create instances of this class during the build process
* of nodes. They represent user-defined, native shader code portions that are going to be
* injected by the builder. A dictionary of node codes is maintained in {@link NodeBuilder#codes}
* for this purpose.
*/
class NodeCode {
/**
* Constructs a new code node.
*
* @param {string} name - The name of the code.
* @param {string} type - The node type.
* @param {string} [code=''] - The native shader code.
*/
constructor( name, type, code = '' ) {
/**
* The name of the code.
*
* @type {string}
*/
this.name = name;
/**
* The node type.
*
* @type {string}
*/
this.type = type;
/**
* The native shader code.
*
* @type {string}
* @default ''
*/
this.code = code;
Object.defineProperty( this, 'isNodeCode', { value: true } );
}
}
let _id$6 = 0;
/**
* This utility class is used in {@link NodeBuilder} as an internal
* cache data structure for node data.
*/
class NodeCache {
/**
* Constructs a new node cache.
*
* @param {?NodeCache} parent - A reference to a parent cache.
*/
constructor( parent = null ) {
/**
* The id of the cache.
*
* @type {number}
* @readonly
*/
this.id = _id$6 ++;
/**
* A weak map for managing node data.
*
* @type {WeakMap<Node, Object>}
*/
this.nodesData = new WeakMap();
/**
* Reference to a parent node cache.
*
* @type {?NodeCache}
* @default null
*/
this.parent = parent;
}
/**
* Returns the data for the given node.
*
* @param {Node} node - The node.
* @return {?Object} The data for the node.
*/
getData( node ) {
let data = this.nodesData.get( node );
if ( data === undefined && this.parent !== null ) {
data = this.parent.getData( node );
}
return data;
}
/**
* Sets the data for a given node.
*
* @param {Node} node - The node.
* @param {Object} data - The data that should be cached.
*/
setData( node, data ) {
this.nodesData.set( node, data );
}
}
class StructType {
constructor( name, members ) {
this.name = name;
this.members = members;
this.output = false;
}
}
/**
* Abstract base class for uniforms.
*
* @abstract
* @private
*/
class Uniform {
/**
* Constructs a new uniform.
*
* @param {string} name - The uniform's name.
* @param {any} value - The uniform's value.
*/
constructor( name, value ) {
/**
* The uniform's name.
*
* @type {string}
*/
this.name = name;
/**
* The uniform's value.
*
* @type {any}
*/
this.value = value;
/**
* Used to build the uniform buffer according to the STD140 layout.
* Derived uniforms will set this property to a data type specific
* value.
*
* @type {number}
*/
this.boundary = 0;
/**
* The item size. Derived uniforms will set this property to a data
* type specific value.
*
* @type {number}
*/
this.itemSize = 0;
/**
* This property is set by {@link UniformsGroup} and marks
* the start position in the uniform buffer.
*
* @type {number}
*/
this.offset = 0;
/**
* This property is set by {@link UniformsGroup} and marks
* the index position in the uniform array.
*
* @type {number}
*/
this.index = -1;
}
/**
* Sets the uniform's value.
*
* @param {any} value - The value to set.
*/
setValue( value ) {
this.value = value;
}
/**
* Returns the uniform's value.
*
* @return {any} The value.
*/
getValue() {
return this.value;
}
}
/**
* Represents a Number uniform.
*
* @private
* @augments Uniform
*/
class NumberUniform extends Uniform {
/**
* Constructs a new Number uniform.
*
* @param {string} name - The uniform's name.
* @param {number} value - The uniform's value.
*/
constructor( name, value = 0 ) {
super( name, value );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isNumberUniform = true;
this.boundary = 4;
this.itemSize = 1;
}
}
/**
* Represents a Vector2 uniform.
*
* @private
* @augments Uniform
*/
class Vector2Uniform extends Uniform {
/**
* Constructs a new Number uniform.
*
* @param {string} name - The uniform's name.
* @param {Vector2} value - The uniform's value.
*/
constructor( name, value = new Vector2() ) {
super( name, value );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isVector2Uniform = true;
this.boundary = 8;
this.itemSize = 2;
}
}
/**
* Represents a Vector3 uniform.
*
* @private
* @augments Uniform
*/
class Vector3Uniform extends Uniform {
/**
* Constructs a new Number uniform.
*
* @param {string} name - The uniform's name.
* @param {Vector3} value - The uniform's value.
*/
constructor( name, value = new Vector3() ) {
super( name, value );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isVector3Uniform = true;
this.boundary = 16;
this.itemSize = 3;
}
}
/**
* Represents a Vector4 uniform.
*
* @private
* @augments Uniform
*/
class Vector4Uniform extends Uniform {
/**
* Constructs a new Number uniform.
*
* @param {string} name - The uniform's name.
* @param {Vector4} value - The uniform's value.
*/
constructor( name, value = new Vector4() ) {
super( name, value );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isVector4Uniform = true;
this.boundary = 16;
this.itemSize = 4;
}
}
/**
* Represents a Color uniform.
*
* @private
* @augments Uniform
*/
class ColorUniform extends Uniform {
/**
* Constructs a new Number uniform.
*
* @param {string} name - The uniform's name.
* @param {Color} value - The uniform's value.
*/
constructor( name, value = new Color() ) {
super( name, value );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isColorUniform = true;
this.boundary = 16;
this.itemSize = 3;
}
}
/**
* Represents a Matrix2 uniform.
*
* @private
* @augments Uniform
*/
class Matrix2Uniform extends Uniform {
/**
* Constructs a new Number uniform.
*
* @param {string} name - The uniform's name.
* @param {Matrix2} value - The uniform's value.
*/
constructor( name, value = new Matrix2() ) {
super( name, value );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMatrix2Uniform = true;
this.boundary = 8;
this.itemSize = 4;
}
}
/**
* Represents a Matrix3 uniform.
*
* @private
* @augments Uniform
*/
class Matrix3Uniform extends Uniform {
/**
* Constructs a new Number uniform.
*
* @param {string} name - The uniform's name.
* @param {Matrix3} value - The uniform's value.
*/
constructor( name, value = new Matrix3() ) {
super( name, value );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMatrix3Uniform = true;
this.boundary = 48;
this.itemSize = 12;
}
}
/**
* Represents a Matrix4 uniform.
*
* @private
* @augments Uniform
*/
class Matrix4Uniform extends Uniform {
/**
* Constructs a new Number uniform.
*
* @param {string} name - The uniform's name.
* @param {Matrix4} value - The uniform's value.
*/
constructor( name, value = new Matrix4() ) {
super( name, value );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isMatrix4Uniform = true;
this.boundary = 64;
this.itemSize = 16;
}
}
/**
* A special form of Number uniform binding type.
* It's value is managed by a node object.
*
* @private
* @augments NumberUniform
*/
class NumberNodeUniform extends NumberUniform {
/**
* Constructs a new node-based Number uniform.
*
* @param {NodeUniform} nodeUniform - The node uniform.
*/
constructor( nodeUniform ) {
super( nodeUniform.name, nodeUniform.value );
/**
* The node uniform.
*
* @type {NodeUniform}
*/
this.nodeUniform = nodeUniform;
}
/**
* Overwritten to return the value of the node uniform.
*
* @return {number} The value.
*/
getValue() {
return this.nodeUniform.value;
}
/**
* Returns the node uniform data type.
*
* @return {string} The data type.
*/
getType() {
return this.nodeUniform.type;
}
}
/**
* A special form of Vector2 uniform binding type.
* It's value is managed by a node object.
*
* @private
* @augments Vector2Uniform
*/
class Vector2NodeUniform extends Vector2Uniform {
/**
* Constructs a new node-based Vector2 uniform.
*
* @param {NodeUniform} nodeUniform - The node uniform.
*/
constructor( nodeUniform ) {
super( nodeUniform.name, nodeUniform.value );
/**
* The node uniform.
*
* @type {NodeUniform}
*/
this.nodeUniform = nodeUniform;
}
/**
* Overwritten to return the value of the node uniform.
*
* @return {Vector2} The value.
*/
getValue() {
return this.nodeUniform.value;
}
/**
* Returns the node uniform data type.
*
* @return {string} The data type.
*/
getType() {
return this.nodeUniform.type;
}
}
/**
* A special form of Vector3 uniform binding type.
* It's value is managed by a node object.
*
* @private
* @augments Vector3Uniform
*/
class Vector3NodeUniform extends Vector3Uniform {
/**
* Constructs a new node-based Vector3 uniform.
*
* @param {NodeUniform} nodeUniform - The node uniform.
*/
constructor( nodeUniform ) {
super( nodeUniform.name, nodeUniform.value );
/**
* The node uniform.
*
* @type {NodeUniform}
*/
this.nodeUniform = nodeUniform;
}
/**
* Overwritten to return the value of the node uniform.
*
* @return {Vector3} The value.
*/
getValue() {
return this.nodeUniform.value;
}
/**
* Returns the node uniform data type.
*
* @return {string} The data type.
*/
getType() {
return this.nodeUniform.type;
}
}
/**
* A special form of Vector4 uniform binding type.
* It's value is managed by a node object.
*
* @private
* @augments Vector4Uniform
*/
class Vector4NodeUniform extends Vector4Uniform {
/**
* Constructs a new node-based Vector4 uniform.
*
* @param {NodeUniform} nodeUniform - The node uniform.
*/
constructor( nodeUniform ) {
super( nodeUniform.name, nodeUniform.value );
/**
* The node uniform.
*
* @type {NodeUniform}
*/
this.nodeUniform = nodeUniform;
}
/**
* Overwritten to return the value of the node uniform.
*
* @return {Vector4} The value.
*/
getValue() {
return this.nodeUniform.value;
}
/**
* Returns the node uniform data type.
*
* @return {string} The data type.
*/
getType() {
return this.nodeUniform.type;
}
}
/**
* A special form of Color uniform binding type.
* It's value is managed by a node object.
*
* @private
* @augments ColorUniform
*/
class ColorNodeUniform extends ColorUniform {
/**
* Constructs a new node-based Color uniform.
*
* @param {NodeUniform} nodeUniform - The node uniform.
*/
constructor( nodeUniform ) {
super( nodeUniform.name, nodeUniform.value );
/**
* The node uniform.
*
* @type {NodeUniform}
*/
this.nodeUniform = nodeUniform;
}
/**
* Overwritten to return the value of the node uniform.
*
* @return {Color} The value.
*/
getValue() {
return this.nodeUniform.value;
}
/**
* Returns the node uniform data type.
*
* @return {string} The data type.
*/
getType() {
return this.nodeUniform.type;
}
}
/**
* A special form of Matrix2 uniform binding type.
* It's value is managed by a node object.
*
* @private
* @augments Matrix2Uniform
*/
class Matrix2NodeUniform extends Matrix2Uniform {
/**
* Constructs a new node-based Matrix2 uniform.
*
* @param {NodeUniform} nodeUniform - The node uniform.
*/
constructor( nodeUniform ) {
super( nodeUniform.name, nodeUniform.value );
/**
* The node uniform.
*
* @type {NodeUniform}
*/
this.nodeUniform = nodeUniform;
}
/**
* Overwritten to return the value of the node uniform.
*
* @return {Matrix2} The value.
*/
getValue() {
return this.nodeUniform.value;
}
/**
* Returns the node uniform data type.
*
* @return {string} The data type.
*/
getType() {
return this.nodeUniform.type;
}
}
/**
* A special form of Matrix3 uniform binding type.
* It's value is managed by a node object.
*
* @private
* @augments Matrix3Uniform
*/
class Matrix3NodeUniform extends Matrix3Uniform {
/**
* Constructs a new node-based Matrix3 uniform.
*
* @param {NodeUniform} nodeUniform - The node uniform.
*/
constructor( nodeUniform ) {
super( nodeUniform.name, nodeUniform.value );
/**
* The node uniform.
*
* @type {NodeUniform}
*/
this.nodeUniform = nodeUniform;
}
/**
* Overwritten to return the value of the node uniform.
*
* @return {Matrix3} The value.
*/
getValue() {
return this.nodeUniform.value;
}
/**
* Returns the node uniform data type.
*
* @return {string} The data type.
*/
getType() {
return this.nodeUniform.type;
}
}
/**
* A special form of Matrix4 uniform binding type.
* It's value is managed by a node object.
*
* @private
* @augments Matrix4Uniform
*/
class Matrix4NodeUniform extends Matrix4Uniform {
/**
* Constructs a new node-based Matrix4 uniform.
*
* @param {NodeUniform} nodeUniform - The node uniform.
*/
constructor( nodeUniform ) {
super( nodeUniform.name, nodeUniform.value );
/**
* The node uniform.
*
* @type {NodeUniform}
*/
this.nodeUniform = nodeUniform;
}
/**
* Overwritten to return the value of the node uniform.
*
* @return {Matrix4} The value.
*/
getValue() {
return this.nodeUniform.value;
}
/**
* Returns the node uniform data type.
*
* @return {string} The data type.
*/
getType() {
return this.nodeUniform.type;
}
}
let _id$5 = 0;
const _functionNodeCache = new WeakMap();
const _layoutDefaultsCache = new WeakMap();
// Inputs of a layout function call. They can be destructured as an array or as an object.
const _createLayoutInputs = ( layout, values ) => {
const inputs = {
[ Symbol.iterator ]() {
let index = 0;
const values = Object.values( this );
return {
next: () => ( {
value: values[ index ],
done: index ++ >= values.length
} )
};
}
};
layout.inputs.forEach( ( input, i ) => {
inputs[ input.name ] = values[ i ];
} );
return inputs;
};
const typeFromArray = new Map( [
[ Int8Array, 'int' ],
[ Int16Array, 'int' ],
[ Int32Array, 'int' ],
[ Uint8Array, 'uint' ],
[ Uint16Array, 'uint' ],
[ Uint32Array, 'uint' ],
[ Float32Array, 'float' ]
] );
const _toFloat = ( value ) => {
if ( /e/g.test( value ) ) {
return String( value ).replace( /\+/g, '' );
} else {
value = Number( value );
return value + ( value % 1 ? '' : '.0' );
}
};
const _componentTypeRanks = { bool: 0, uint: 1, int: 2, float: 3 };
/**
* Data of a node in a shader stage. All common properties are declared upfront,
* so the objects share a single shape.
*
* @private
*/
class NodeData {
constructor() {
this.properties = undefined;
this.type = undefined;
this.typeFromOutput = undefined;
this.buildStages = undefined;
this.usageCount = undefined;
this.readUsageCount = undefined;
this.writeUsageCount = undefined;
this.stack = undefined;
this.stages = undefined;
this.snippet = undefined;
this.propertyName = undefined;
this.generated = undefined;
this.flowBlock = undefined;
this.assign = undefined;
this.forceDeclaration = undefined;
this.variable = undefined;
this.cacheVariable = undefined;
this.uniform = undefined;
this.subBuilds = undefined;
this.subBuildsCache = undefined;
}
}
const _componentTypeCache = new Map();
const _typeLengthCache = new Map();
const _isVectorCache = new Map();
const _isMatrixCache = new Map();
const _isIntegerCache = new Map();
const _checkWriteUsage = ( data ) => {
if ( data.writeUsageCount > 0 ) return true;
if ( data.subBuildsCache !== undefined ) {
for ( const subBuild in data.subBuildsCache ) {
if ( _checkWriteUsage( data.subBuildsCache[ subBuild ] ) ) {
return true;
}
}
}
return false;
};
const _hasContextOverrides = ( context, baseContext ) => {
for ( const key in context ) {
if ( key !== 'nodeBlock' && key !== 'nodeLoop' && context[ key ] !== baseContext[ key ] ) return true;
}
return false;
};
/**
* Base class for builders which generate a shader program based
* on a 3D object and its node material definition.
*/
class NodeBuilder {
/**
* Constructs a new node builder.
*
* @param {Object3D} object - The 3D object.
* @param {Renderer} renderer - The current renderer.
* @param {NodeParser} parser - A reference to a node parser.
*/
constructor( object, renderer, parser ) {
/**
* The 3D object.
*
* @type {Object3D}
*/
this.object = object;
/**
* The material of the 3D object.
*
* @type {?Material}
*/
this.material = ( object && object.material ) || null;
/**
* The geometry of the 3D object.
*
* @type {?BufferGeometry}
*/
this.geometry = ( object && object.geometry ) || null;
/**
* The compute node, if building for compute.
*
* @type {?ComputeNode}
* @default null
*/
this.compute = null;
/**
* The current renderer.
*
* @type {Renderer}
*/
this.renderer = renderer;
/**
* A reference to a node parser.
*
* @type {NodeParser}
*/
this.parser = parser;
/**
* The scene the 3D object belongs to.
*
* @type {?Scene}
* @default null
*/
this.scene = null;
/**
* The camera the 3D object is rendered with.
*
* @type {?Camera}
* @default null
*/
this.camera = null;
/**
* A list of all nodes the builder is processing
* for this 3D object.
*
* @type {Set<Node>}
*/
this.nodes = new Set();
/**
* A list of all nodes the builder is processing in sequential order.
*
* This is used to determine the update order of nodes, which is important for
* {@link NodeUpdateType#UPDATE_BEFORE} and {@link NodeUpdateType#UPDATE_AFTER}.
*
* @type {Set<Node>}
*/
this.sequentialNodes = new Set();
/**
* A list of all nodes which {@link Node#update} method should be executed.
*
* @type {Array<Node>}
*/
this.updateNodes = [];
/**
* A list of all nodes which {@link Node#updateBefore} method should be executed.
*
* @type {Array<Node>}
*/
this.updateBeforeNodes = [];
/**
* A list of all nodes which {@link Node#updateAfter} method should be executed.
*
* @type {Array<Node>}
*/
this.updateAfterNodes = [];
/**
* A dictionary that assigns each node to a unique hash.
*
* @type {Object<number,Node>}
*/
this.hashNodes = {};
/**
* A reference to a node material observer.
*
* @type {?NodeMaterialObserver}
* @default null
*/
this.observer = null;
/**
* A reference to the current lights node.
*
* @type {?LightsNode}
* @default null
*/
this.lightsNode = null;
/**
* A reference to the current environment node.
*
* @type {?Node}
* @default null
*/
this.environmentNode = null;
/**
* A reference to the current fog node.
*
* @type {?Node}
* @default null
*/
this.fogNode = null;
/**
* The current clipping context.
*
* @type {?ClippingContext}
*/
this.clippingContext = null;
/**
* Whether the built material uses hardware clipping or not.
*
* @type {boolean}
* @default false
*/
this.hardwareClipping = false;
/**
* Whether the build embeds data of the 3D object it is built for, like its buffers,
* instead of binding the data of the rendered object. Such builds can't be shared
* with other objects, so the renderer doesn't cache them.
*
* @type {boolean}
* @default false
*/
this.objectSpecific = false;
/**
* The generated vertex shader.
*
* @type {?string}
*/
this.vertexShader = null;
/**
* The generated fragment shader.
*
* @type {?string}
*/
this.fragmentShader = null;
/**
* The generated compute shader.
*
* @type {?string}
*/
this.computeShader = null;
/**
* Nodes used in the primary flow of code generation.
*
* @type {Object<string,Array<Node>>}
*/
this.flowNodes = { vertex: [], fragment: [], compute: [] };
/**
* Nodes code from `.flowNodes`.
*
* @type {Object<string,string>}
*/
this.flowCode = { vertex: '', fragment: '', compute: '' };
/**
* This dictionary holds the node uniforms of the builder.
* The uniforms are maintained in an array for each shader stage.
*
* @type {Object}
*/
this.uniforms = { vertex: [], fragment: [], compute: [], index: 0 };
/**
* This dictionary holds the output structs of the builder.
* The structs are maintained in an array for each shader stage.
*
* @type {Object}
*/
this.structs = { vertex: [], fragment: [], compute: [], index: 0 };
/**
* This dictionary holds the types of the builder.
*
* @type {Object}
*/
this.types = { vertex: [], fragment: [], compute: [], index: 0 };
/**
* This dictionary holds the bindings for each shader stage.
*
* @type {Object}
*/
this.bindings = { vertex: {}, fragment: {}, compute: {} };
/**
* This dictionary maintains the binding indices per bind group.
*
* @type {Object}
*/
this.bindingsIndexes = {};
/**
* Reference to the array of bind groups.
*
* @type {?Array<BindGroup>}
*/
this.bindGroups = null;
/**
* This array holds the node attributes of this builder
* created via {@link AttributeNode}.
*
* @type {Array<NodeAttribute>}
*/
this.attributes = [];
/**
* This array holds the node attributes of this builder
* created via {@link BufferAttributeNode}.
*
* @type {Array<NodeAttribute>}
*/
this.bufferAttributes = [];
/**
* This array holds the node varyings of this builder.
*
* @type {Array<NodeVarying>}
*/
this.varyings = [];
/**
* This dictionary holds the (native) node codes of this builder.
* The codes are maintained in an array for each shader stage.
*
* @type {Object<string,Array<NodeCode>>}
*/
this.codes = {};
/**
* This dictionary holds the node variables of this builder.
* The variables are maintained in an array for each shader stage.
* This dictionary is also used to count the number of variables
* according to their type (const, vars).
*
* @type {Object<string,Array<NodeVar>|number>}
*/
this.vars = {};
/**
* This dictionary holds the declarations for each shader stage.
*
* @type {Object}
*/
this.declarations = {};
/**
* Current code flow.
* All code generated in this stack will be stored in `.flow`.
*
* @type {{code: string}}
*/
this.flow = { code: '' };
/**
* A chain of nodes.
* Used to check recursive calls in node-graph.
*
* @type {Array<Node>}
*/
this.chaining = [];
/**
* The current stack.
* This reflects the current process in the code block hierarchy,
* it is useful to know if the current process is inside a conditional for example.
*
* @type {StackNode}
*/
this.stack = stack();
/**
* List of stack nodes.
* The current stack hierarchy is stored in an array.
*
* @type {Array<StackNode>}
*/
this.stacks = [];
/**
* A tab value. Used for shader string generation.
*
* @type {string}
* @default '\t'
*/
this.tab = '\t';
/**
* Reference to the current function node.
*
* @type {?FunctionNode}
* @default null
*/
this.currentFunctionNode = null;
/**
* The builder's context.
*
* @type {Object}
*/
this.context = {
material: this.material
};
/**
* The builder's cache.
*
* @type {NodeCache}
*/
this.cache = new NodeCache();
/**
* Since the {@link NodeBuilder#cache} might be temporarily
* overwritten by other caches, this member retains the reference
* to the builder's own cache.
*
* @type {NodeCache}
* @default this.cache
*/
this.globalCache = this.cache;
this.flowsData = new WeakMap();
/**
* The current shader stage.
*
* @type {?('vertex'|'fragment'|'compute'|'any')}
*/
this.shaderStage = null;
/**
* The current build stage.
*
* @type {?('setup'|'analyze'|'generate')}
*/
this.buildStage = null;
/**
* The sub-build layers.
*
* @type {Array<SubBuildNode>}
* @default []
*/
this.subBuildLayers = [];
/**
* The active stack nodes.
*
* @type {Array<StackNode>}
*/
this.activeStacks = [];
/**
* The current sub-build TSL function(Fn).
*
* @type {?string}
* @default null
*/
this.subBuildFn = null;
/**
* The current TSL function(Fn) call node.
*
* @type {?Node}
* @default null
*/
this.fnCall = null;
/**
* The block of generated code the builder is in, e.g. a loop body or a conditional branch.
* Every generated block is a new object linked to its parent, `null` outside of any block.
*
* @type {?{parent: ?Object}}
* @default null
*/
this.flowBlock = null;
Object.defineProperty( this, 'id', { value: _id$5 ++ } );
}
/**
* Whether the material is using flat shading or not.
*
* @returns {boolean} Whether the material is using flat shading or not.
*/
isFlatShading() {
return this.material.flatShading === true || this.geometry.hasAttribute( 'normal' ) === false;
}
/**
* Whether the material is opaque or not.
*
* @return {boolean} Whether the material is opaque or not.
*/
isOpaque() {
const material = this.material;
return material.transparent === false && material.blending === NormalBlending && material.alphaToCoverage === false;
}
/**
* Factory method for creating an instance of {@link RenderTarget} with the given
* dimensions and options.
*
* @param {number} width - The width of the render target.
* @param {number} height - The height of the render target.
* @param {Object} options - The options of the render target.
* @return {RenderTarget} The render target.
*/
createRenderTarget( width, height, options ) {
return new RenderTarget( width, height, options );
}
/**
* Factory method for creating an instance of {@link CubeRenderTarget} with the given
* dimensions and options.
*
* @param {number} size - The size of the cube render target.
* @param {Object} options - The options of the cube render target.
* @return {CubeRenderTarget} The cube render target.
*/
createCubeRenderTarget( size, options ) {
return new CubeRenderTarget( size, options );
}
/**
* Whether the given node is included in the internal array of nodes or not.
*
* @param {Node} node - The node to test.
* @return {boolean} Whether the given node is included in the internal array of nodes or not.
*/
includes( node ) {
return this.nodes.has( node );
}
/**
* Returns the type of the color output based on the renderer's render target.
*
* @param {number} [index=0] - The index of the render target texture.
* @return {string} The type.
*/
getOutputType( index = 0 ) {
const renderTarget = this.renderer.getRenderTarget();
if ( renderTarget !== null && renderTarget.textures[ index ] !== undefined ) {
return getTextureType( renderTarget.textures[ index ] );
}
return 'vec4';
}
/**
* Returns the output struct name which is required by
* {@link OutputStructNode}.
*
* @abstract
* @return {string} The name of the output struct.
*/
getOutputStructName() {}
/**
* Returns a bind group for the given group name and binding.
*
* @private
* @param {string} groupName - The group name.
* @param {Array<NodeUniformsGroup>} bindings - List of bindings.
* @return {BindGroup} The bind group
*/
_getBindGroup( groupName, bindings ) {
const groupNode = bindings[ 0 ].groupNode;
let sharedGroup = groupNode.shared;
if ( sharedGroup ) {
for ( let i = 1; i < bindings.length; i ++ ) {
if ( groupNode !== bindings[ i ].groupNode ) {
sharedGroup = false;
}
}
}
//
let bindGroup = new BindGroup( groupName, bindings );
if ( sharedGroup && this.renderer._bindings !== undefined ) {
bindGroup = this.renderer._bindings.getSharedBindGroup( bindGroup );
}
return bindGroup;
}
/**
* Returns an array of node uniform groups for the given group name and shader stage.
*
* @param {string} groupName - The group name.
* @param {('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage.
* @return {Array<NodeUniformsGroup>} The array of node uniform groups.
*/
getBindGroupArray( groupName, shaderStage ) {
const bindings = this.bindings[ shaderStage ];
let bindGroup = bindings[ groupName ];
if ( bindGroup === undefined ) {
if ( this.bindingsIndexes[ groupName ] === undefined ) {
this.bindingsIndexes[ groupName ] = { binding: 0, group: Object.keys( this.bindingsIndexes ).length };
}
bindings[ groupName ] = bindGroup = [];
}
return bindGroup;
}
/**
* Returns a list bindings of all shader stages separated by groups.
*
* @return {Array<BindGroup>} The list of bindings.
*/
getBindings() {
let bindingsGroups = this.bindGroups;
if ( bindingsGroups === null ) {
const groups = {};
const bindings = this.bindings;
for ( const shaderStage of shaderStages ) {
for ( const groupName in bindings[ shaderStage ] ) {
const uniforms = bindings[ shaderStage ][ groupName ];
const groupUniforms = groups[ groupName ] || ( groups[ groupName ] = [] );
for ( const uniform of uniforms ) {
if ( groupUniforms.includes( uniform ) === false ) {
groupUniforms.push( uniform );
}
}
}
}
bindingsGroups = [];
for ( const groupName in groups ) {
const group = groups[ groupName ];
const bindingsGroup = this._getBindGroup( groupName, group );
bindingsGroups.push( bindingsGroup );
}
this.bindGroups = bindingsGroups;
}
return bindingsGroups;
}
/**
* Sorts the bind groups and updates {@link NodeBuilder#bindingsIndexes}.
*/
sortBindingGroups() {
const bindingsGroups = this.getBindings();
bindingsGroups.sort( ( a, b ) => ( a.bindings[ 0 ].groupNode.order - b.bindings[ 0 ].groupNode.order ) );
for ( let i = 0; i < bindingsGroups.length; i ++ ) {
const bindingGroup = bindingsGroups[ i ];
this.bindingsIndexes[ bindingGroup.name ].group = i;
}
}
/**
* The builder maintains each node in a hash-based dictionary.
* This method sets the given node (value) with the given hash (key) into this dictionary.
*
* @param {Node} node - The node to add.
* @param {number} hash - The hash of the node.
*/
setHashNode( node, hash ) {
this.hashNodes[ hash ] = node;
}
/**
* Adds a node to this builder.
*
* @param {Node} node - The node to add.
*/
addNode( node ) {
if ( this.nodes.has( node ) === false ) {
this.nodes.add( node );
this.setHashNode( node, node.getHash( this ) );
}
}
/**
* It is used to add Nodes that will be used as FRAME and RENDER events,
* and need to follow a certain sequence in the calls to work correctly.
* This function should be called after 'setup()' in the 'build()' process to ensure that the child nodes are processed first.
*
* @param {Node} node - The node to add.
*/
addSequentialNode( node ) {
const updateBeforeType = node.updateBeforeType;
const updateAfterType = node.updateAfterType;
if ( updateBeforeType !== NodeUpdateType.NONE || updateAfterType !== NodeUpdateType.NONE ) {
this.sequentialNodes.add( node );
}
}
/**
* Checks the update types of nodes
*/
buildUpdateNodes() {
for ( const node of this.nodes ) {
const updateType = node.updateType;
if ( updateType !== NodeUpdateType.NONE ) {
this.updateNodes.push( node );
}
}
for ( const node of this.sequentialNodes ) {
const updateBeforeType = node.updateBeforeType;
const updateAfterType = node.updateAfterType;
if ( updateBeforeType !== NodeUpdateType.NONE ) {
this.updateBeforeNodes.push( node );
}
if ( updateAfterType !== NodeUpdateType.NONE ) {
this.updateAfterNodes.push( node );
}
}
}
/**
* A reference the current node which is the
* last node in the chain of nodes.
*
* @type {Node}
*/
get currentNode() {
return this.chaining[ this.chaining.length - 1 ];
}
/**
* A reference to the render pipeline.
*
* @type {RenderPipeline}
*/
get renderPipeline() {
return this.context.renderPipeline;
}
/**
* Whether the given texture is filtered or not.
*
* @param {Texture} texture - The texture to check.
* @return {boolean} Whether the given texture is filtered or not.
*/
isFilteredTexture( texture ) {
return ( texture.magFilter === LinearFilter || texture.magFilter === LinearMipmapNearestFilter || texture.magFilter === NearestMipmapLinearFilter || texture.magFilter === LinearMipmapLinearFilter ||
texture.minFilter === LinearFilter || texture.minFilter === LinearMipmapNearestFilter || texture.minFilter === NearestMipmapLinearFilter || texture.minFilter === LinearMipmapLinearFilter );
}
/**
* Returns the maximum number of bytes available for uniform buffers.
*
* @return {number} The maximum number of bytes available for uniform buffers.
*/
getUniformBufferLimit() {
return this.renderer.backend.capabilities.getUniformBufferLimit();
}
/**
* Adds the given node to the internal node chain.
* This is used to check recursive calls in node-graph.
*
* @param {Node} node - The node to add.
*/
addChain( node ) {
/*
if ( this.chaining.indexOf( node ) !== - 1 ) {
warn( 'Recursive node: ', node );
}
*/
this.chaining.push( node );
}
/**
* Removes the given node from the internal node chain.
*
* @param {Node} node - The node to remove.
*/
removeChain( node ) {
const lastChain = this.chaining.pop();
if ( lastChain !== node ) {
throw new Error( 'THREE.NodeBuilder: Invalid node chaining!' );
}
}
/**
* Returns the native shader method name for a given generic name. E.g.
* the method name `textureDimensions` matches the WGSL name but must be
* resolved to `textureSize` in GLSL.
*
* @abstract
* @param {string} method - The method name to resolve.
* @return {string} The resolved method name.
*/
getMethod( method ) {
return method;
}
/**
* Returns the native snippet for a ternary operation. E.g. GLSL would output
* a ternary op as `cond ? x : y` whereas WGSL would output it as `select(y, x, cond)`
*
* @abstract
* @param {string} condSnippet - The condition determining which expression gets resolved.
* @param {string} ifSnippet - The expression to resolve to if the condition is true.
* @param {string} elseSnippet - The expression to resolve to if the condition is false.
* @return {string} The resolved method name.
*/
getTernary( /* condSnippet, ifSnippet, elseSnippet*/ ) {
return null;
}
/**
* Returns the native snippet for a per-component vector select. The default
* implementation uses {@link NodeBuilder#getTernary}; renderers can
* override this when their ternary operation does not accept vectors.
*
* @param {string} condSnippet - The per-component boolean (`bvecN`) condition.
* @param {string} ifSnippet - The vector expression selected where `condSnippet` is `true`.
* @param {string} elseSnippet - The vector expression selected where `condSnippet` is `false`.
* @param {string} type - The (vector) type of `ifSnippet`/`elseSnippet`.
* @return {string} The resolved method name.
*/
getVectorSelect( condSnippet, ifSnippet, elseSnippet /*, type*/ ) {
return this.getTernary( condSnippet, ifSnippet, elseSnippet );
}
/**
* Returns a node for the given hash, see {@link NodeBuilder#setHashNode}.
*
* @param {number} hash - The hash of the node.
* @return {Node} The found node.
*/
getNodeFromHash( hash ) {
return this.hashNodes[ hash ];
}
/**
* Adds the Node to a target flow so that it can generate code in the 'generate' process.
*
* @param {('vertex'|'fragment'|'compute')} shaderStage - The shader stage.
* @param {Node} node - The node to add.
* @return {Node} The node.
*/
addFlow( shaderStage, node ) {
this.flowNodes[ shaderStage ].push( node );
return node;
}
/**
* Sets builder's context.
*
* @param {Object} context - The context to set.
*/
setContext( context ) {
this.context = context;
}
/**
* Returns the builder's current context.
*
* @return {Object} The builder's current context.
*/
getContext() {
return this.context;
}
/**
* Adds context data to the builder's current context.
*
* @param {Object} context - The context to add.
* @return {Object} The previous context.
*/
addContext( context ) {
const previousContext = this.getContext();
this.setContext( mergeContext( this.context, context ) );
return previousContext;
}
/**
* Gets a context used in shader construction that can be shared across different materials.
* This is necessary since the renderer cache can reuse shaders generated in one material and use them in another.
*
* @return {Object} The builder's current context without material.
*/
getSharedContext() {
const context = { ...this.context };
delete context.material;
delete context.getUV;
delete context.getOutput;
delete context.getTextureLevel;
delete context.getAO;
delete context.getGI;
delete context.getShadow;
delete context.nodeLoop;
delete context.nodeBlock;
return context;
}
/**
* Sets builder's cache.
*
* @param {NodeCache} cache - The cache to set.
*/
setCache( cache ) {
this.cache = cache;
}
/**
* Returns the builder's current cache.
*
* @return {NodeCache} The builder's current cache.
*/
getCache() {
return this.cache;
}
/**
* Returns a cache for the given node.
*
* @param {Node} node - The node.
* @param {boolean} [parent=true] - Whether this node refers to a shared parent cache or not.
* @return {NodeCache} The cache.
*/
getCacheFromNode( node, parent = true ) {
const data = this.getDataFromNode( node );
if ( data.cache === undefined ) data.cache = new NodeCache( parent ? this.getCache() : null );
return data.cache;
}
/**
* Whether the requested feature is available or not.
*
* @abstract
* @param {string} name - The requested feature.
* @return {boolean} Whether the requested feature is supported or not.
*/
isAvailable( /*name*/ ) {
return false;
}
/**
* Returns the vertexIndex input variable as a native shader string.
*
* @abstract
* @return {string} The instanceIndex shader string.
*/
getVertexIndex() {
warn( 'Abstract function.' );
}
/**
* Contextually returns either the vertex stage instance index builtin
* or the linearized index of an compute invocation within a grid of workgroups.
*
* @abstract
* @return {string} The instanceIndex shader string.
*/
getInstanceIndex() {
warn( 'Abstract function.' );
}
/**
* Returns the drawIndex input variable as a native shader string.
* Only relevant for WebGL and its `WEBGL_multi_draw` extension.
*
* @abstract
* @return {?string} The drawIndex shader string.
*/
getDrawIndex() {
warn( 'Abstract function.' );
}
/**
* Returns the frontFacing input variable as a native shader string.
*
* @abstract
* @return {string} The frontFacing shader string.
*/
getFrontFacing() {
warn( 'Abstract function.' );
}
/**
* Returns the fragCoord input variable as a native shader string.
*
* @abstract
* @return {string} The fragCoord shader string.
*/
getFragCoord() {
warn( 'Abstract function.' );
}
/**
* Returns a builtin representing the size of a subgroup within the current shader.
*
* @abstract
* @return {string} The subgroup size shader string.
*/
getSubgroupSize() {
warn( 'Abstract function.' );
}
/**
* Returns a builtin representing the index of an invocation within its subgroup.
*
* @abstract
* @return {string} The invocation subgroup index shader string.
*/
getInvocationSubgroupIndex() {
warn( 'Abstract function.' );
}
/**
* Returns a builtin representing the index of the current invocation's subgroup within its workgroup.
*
* @abstract
* @return {string} The subgroup index shader string.
*/
getSubgroupIndex() {
warn( 'Abstract function.' );
}
/**
* Enables subgroups.
*
* @abstract
*/
enableSubGroups() {
warn( 'Abstract function.' );
}
/**
* Enables 16 bit floats.
*
* @abstract
*/
enableShaderF16() {
warn( 'Abstract function.' );
}
/**
* Enables dual source blending.
*
* @abstract
*/
enableDualSourceBlending() {
warn( 'Abstract function.' );
}
/**
* Whether to flip texture data along its vertical axis or not. WebGL needs
* this method evaluate to `true`, WebGPU to `false`.
*
* @abstract
* @return {boolean} Whether to flip texture data along its vertical axis or not.
*/
isFlipY() {
return false;
}
/**
* Returns whether the builder is currently in an assignment context.
*
* @return {boolean} Whether the builder is in an assignment context.
*/
isContextAssign() {
return this.context.assign === true;
}
/**
* Calling this method increases the usage count for the given node by one.
*
* @param {Node} node - The node to increase the usage count for.
* @return {number} The updated usage count.
*/
increaseUsage( node ) {
const nodeData = this.getDataFromNode( node );
nodeData.usageCount = nodeData.usageCount === undefined ? 1 : nodeData.usageCount + 1;
if ( this.isContextAssign() ) {
nodeData.writeUsageCount = nodeData.writeUsageCount === undefined ? 1 : nodeData.writeUsageCount + 1;
} else {
nodeData.readUsageCount = nodeData.readUsageCount === undefined ? 1 : nodeData.readUsageCount + 1;
}
return nodeData.usageCount;
}
/**
* Returns whether the given node has been written to in any shader stage.
*
* @param {Node} node - The node to check.
* @return {boolean} Whether the node has been written to.
*/
hasWriteUsage( node ) {
const refNode = node.getShared( this );
const cache = refNode.isGlobal( this ) ? this.globalCache : this.cache;
const nodeData = cache.getData( refNode );
if ( nodeData !== undefined ) {
for ( const shaderStage in nodeData ) {
if ( _checkWriteUsage( nodeData[ shaderStage ] ) ) {
return true;
}
}
}
return false;
}
/**
* Generates a texture sample shader string for the given texture data.
*
* @abstract
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The texture property name.
* @param {string} uvSnippet - Snippet defining the texture coordinates.
* @return {string} The generated shader string.
*/
generateTexture( /* texture, textureProperty, uvSnippet */ ) {
warn( 'Abstract function.' );
}
/**
* Generates a texture LOD shader string for the given texture data.
*
* @abstract
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The texture property name.
* @param {string} uvSnippet - Snippet defining the texture coordinates.
* @param {?string} depthSnippet - Snippet defining the 0-based texture array index to sample.
* @param {string} levelSnippet - Snippet defining the mip level.
* @return {string} The generated shader string.
*/
generateTextureLod( /* texture, textureProperty, uvSnippet, depthSnippet, levelSnippet */ ) {
warn( 'Abstract function.' );
}
/**
* Generates a texture size shader string for the given texture data.
*
* @abstract
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The texture property name.
* @param {string} levelSnippet - Snippet defining the mip level.
* @return {string} The generated shader string.
*/
generateTextureSize( /* texture, textureProperty, levelSnippet */ ) {
warn( 'Abstract function.' );
}
/**
* Generates the array declaration string.
*
* @param {string} type - The type.
* @param {?number} [count] - The count.
* @return {string} The generated value as a shader string.
*/
generateArrayDeclaration( type, count ) {
return this.getType( type ) + '[ ' + count + ' ]';
}
/**
* Generates the array shader string for the given type and value.
*
* @param {string} type - The type.
* @param {?number} [count] - The count.
* @param {?Array<Node>} [values=null] - The default values.
* @return {string} The generated value as a shader string.
*/
generateArray( type, count, values = null ) {
let snippet = this.generateArrayDeclaration( type, count ) + '( ';
for ( let i = 0; i < count; i ++ ) {
const value = values ? values[ i ] : null;
if ( value !== null ) {
snippet += value.build( this, type );
} else {
snippet += this.generateConst( type );
}
if ( i < count - 1 ) snippet += ', ';
}
snippet += ' )';
return snippet;
}
/**
* Generates the struct shader string.
*
* @param {string} type - The type.
* @param {Array<Object>} [membersLayout] - The count.
* @param {?Array<Node>} [values=null] - The default values.
* @return {string} The generated value as a shader string.
*/
generateStruct( type, membersLayout, values = null ) {
const snippets = [];
for ( const member of membersLayout ) {
const { name, type } = member;
if ( values && values[ name ] && values[ name ].isNode ) {
snippets.push( values[ name ].build( this, type ) );
} else {
snippets.push( this.generateConst( type ) );
}
}
return type + '( ' + snippets.join( ', ' ) + ' )';
}
/**
* Generates the shader string for the given type and value.
*
* @param {string} type - The type.
* @param {?any} [value=null] - The value.
* @return {string} The generated value as a shader string.
*/
generateConst( type, value = null ) {
if ( value === null ) {
if ( type === 'float' || type === 'int' || type === 'uint' ) value = 0;
else if ( type === 'bool' ) value = false;
else if ( type === 'color' ) value = new Color();
else if ( type === 'vec2' || type === 'uvec2' || type === 'ivec2' || type === 'bvec2' ) value = new Vector2();
else if ( type === 'vec3' || type === 'uvec3' || type === 'ivec3' || type === 'bvec3' ) value = new Vector3();
else if ( type === 'vec4' || type === 'uvec4' || type === 'ivec4' || type === 'bvec4' ) value = new Vector4();
}
if ( type === 'float' ) return _toFloat( value );
if ( type === 'int' ) return `${ Math.trunc( value ) }`;
if ( type === 'uint' ) return value >= 0 ? `${ Math.trunc( value ) }u` : '0u';
if ( type === 'bool' ) return value ? 'true' : 'false';
if ( type === 'color' ) return `${ this.getType( 'vec3' ) }( ${ _toFloat( value.r ) }, ${ _toFloat( value.g ) }, ${ _toFloat( value.b ) } )`;
const typeLength = this.getTypeLength( type );
const componentType = this.getComponentType( type );
const generateConst = value => this.generateConst( componentType, value );
if ( typeLength === 2 ) {
return `${ this.getType( type ) }( ${ generateConst( value.x ) }, ${ generateConst( value.y ) } )`;
} else if ( typeLength === 3 ) {
return `${ this.getType( type ) }( ${ generateConst( value.x ) }, ${ generateConst( value.y ) }, ${ generateConst( value.z ) } )`;
} else if ( typeLength === 4 && type !== 'mat2' ) {
return `${ this.getType( type ) }( ${ generateConst( value.x ) }, ${ generateConst( value.y ) }, ${ generateConst( value.z ) }, ${ generateConst( value.w ) } )`;
} else if ( typeLength >= 4 && value && ( value.isMatrix2 || value.isMatrix3 || value.isMatrix4 ) ) {
return `${ this.getType( type ) }( ${ value.elements.map( generateConst ).join( ', ' ) } )`;
} else if ( typeLength > 4 ) {
return `${ this.getType( type ) }()`;
}
throw new Error( `THREE.NodeBuilder: Type '${type}' not found in generate constant attempt.` );
}
/**
* It might be necessary to convert certain data types to different ones
* so this method can be used to hide the conversion.
*
* @param {string} type - The type.
* @return {string} The updated type.
*/
getType( type ) {
if ( type === 'color' ) return 'vec3';
return type;
}
/**
* Whether the given attribute name is defined in the geometry or not.
*
* @param {string} name - The attribute name.
* @return {boolean} Whether the given attribute name is defined in the geometry.
*/
hasGeometryAttribute( name ) {
return this.geometry && this.geometry.getAttribute( name ) !== undefined;
}
/**
* Returns a node attribute for the given name and type.
*
* @param {string} name - The attribute's name.
* @param {string} type - The attribute's type.
* @return {NodeAttribute} The node attribute.
*/
getAttribute( name, type ) {
const attributes = this.attributes;
// find attribute
for ( const attribute of attributes ) {
if ( attribute.name === name ) {
return attribute;
}
}
// create a new if no exist
const attribute = new NodeAttribute( name, type );
this.registerDeclaration( attribute );
attributes.push( attribute );
return attribute;
}
/**
* Returns for the given node and shader stage the property name for the shader.
*
* @param {Node} node - The node.
* @param {('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage.
* @return {string} The property name.
*/
getPropertyName( node/*, shaderStage*/ ) {
return node.name;
}
/**
* Whether the given type is a scalar type or not.
*
* @param {string} type - The type to check.
* @return {boolean} Whether the given type is a scalar type or not.
*/
isScalar( type ) {
return type === 'float' || type === 'bool' || type === 'int' || type === 'uint';
}
/**
* Returns whether the given name is a reserved keyword of the backend's
* shading language. Backends override this method to provide their
* language-specific keywords.
*
* @param {string} name - The name to test.
* @return {boolean} Whether the name is a reserved keyword or not.
*/
isReservedKeyword( /* name */ ) {
return false;
}
/**
* Whether the given type is a vector type or not.
*
* @param {string} type - The type to check.
* @return {boolean} Whether the given type is a vector type or not.
*/
isVector( type ) {
let result = _isVectorCache.get( type );
if ( result === undefined ) _isVectorCache.set( type, result = /vec\d/.test( type ) );
return result;
}
/**
* Whether the given type is a matrix type or not.
*
* @param {string} type - The type to check.
* @return {boolean} Whether the given type is a matrix type or not.
*/
isMatrix( type ) {
let result = _isMatrixCache.get( type );
if ( result === undefined ) _isMatrixCache.set( type, result = /mat\d/.test( type ) );
return result;
}
/**
* Whether the given type is a reference type or not.
*
* @param {string} type - The type to check.
* @return {boolean} Whether the given type is a reference type or not.
*/
isReference( type ) {
return type === 'void' || type === 'property' || type === 'sampler' || type === 'samplerComparison' || type === 'texture' || type === 'cubeTexture' || type === 'storageTexture' || type === 'depthTexture' || type === 'texture3D';
}
/**
* Checks if the given texture requires a manual conversion to the working color space.
*
* @abstract
* @param {Texture} texture - The texture to check.
* @return {boolean} Whether the given texture requires a conversion to working color space or not.
*/
needsToWorkingColorSpace( /*texture*/ ) {
return false;
}
/**
* Returns the component type of a given texture.
*
* @param {Texture} texture - The texture.
* @return {string} The component type.
*/
getComponentTypeFromTexture( texture ) {
const type = texture.type;
if ( texture.isDepthTexture === true ) return 'float';
if ( type === IntType ) return 'int';
if ( type === UnsignedIntType ) return 'uint';
return 'float';
}
/**
* Returns the element type for a given type.
*
* @param {string} type - The type.
* @return {string} The element type.
*/
getElementType( type ) {
if ( type === 'mat2' ) return 'vec2';
if ( type === 'mat3' ) return 'vec3';
if ( type === 'mat4' ) return 'vec4';
return this.getComponentType( type );
}
/**
* Returns the component type for a given type.
*
* @param {string} type - The type.
* @return {string} The component type.
*/
getComponentType( type ) {
type = this.getVectorType( type );
if ( type === 'float' || type === 'bool' || type === 'int' || type === 'uint' ) return type;
let result = _componentTypeCache.get( type );
if ( result === undefined ) {
const componentType = /(b|i|u|)(vec|mat)([2-4])/.exec( type );
if ( componentType === null ) result = null;
else if ( componentType[ 1 ] === 'b' ) result = 'bool';
else if ( componentType[ 1 ] === 'i' ) result = 'int';
else if ( componentType[ 1 ] === 'u' ) result = 'uint';
else result = 'float';
_componentTypeCache.set( type, result );
}
return result;
}
/**
* Returns the vector type for a given type.
*
* @param {string} type - The type.
* @return {string} The vector type.
*/
getVectorType( type ) {
if ( type === 'color' ) return 'vec3';
if ( type === 'texture' || type === 'cubeTexture' || type === 'storageTexture' || type === 'texture3D' ) return 'vec4';
return type;
}
/**
* Returns the data type for the given the length and component type.
*
* @param {number} length - The length.
* @param {string} [componentType='float'] - The component type.
* @return {string} The type.
*/
getTypeFromLength( length, componentType = 'float' ) {
if ( length === 1 ) return componentType;
let baseType = getTypeFromLength( length );
const prefix = componentType === 'float' ? '' : componentType[ 0 ];
// fix edge case for mat2x2 being same size as vec4
if ( /mat2/.test( componentType ) === true ) {
baseType = baseType.replace( 'vec', 'mat' );
}
return prefix + baseType;
}
/**
* Returns the type for a given typed array.
*
* @param {TypedArray} array - The typed array.
* @return {string} The type.
*/
getTypeFromArray( array ) {
return typeFromArray.get( array.constructor );
}
/**
* Returns the type is an integer type.
*
* @param {string} type - The type.
* @return {boolean} Whether the type is an integer type or not.
*/
isInteger( type ) {
let result = _isIntegerCache.get( type );
if ( result === undefined ) _isIntegerCache.set( type, result = /int|uint|(i|u)vec/.test( type ) );
return result;
}
/**
* Returns the type for a given buffer attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
* @return {string} The type.
*/
getTypeFromAttribute( attribute ) {
let dataAttribute = attribute;
if ( attribute.isInterleavedBufferAttribute ) dataAttribute = attribute.data;
const array = dataAttribute.array;
const itemSize = attribute.itemSize;
const normalized = attribute.normalized;
let arrayType;
if ( ! ( attribute instanceof Float16BufferAttribute ) && normalized !== true ) {
arrayType = this.getTypeFromArray( array );
}
return this.getTypeFromLength( itemSize, arrayType );
}
/**
* Returns the length for the given data type.
*
* @param {string} type - The data type.
* @return {number} The length.
*/
getTypeLength( type ) {
const vecType = this.getVectorType( type );
let length = _typeLengthCache.get( vecType );
if ( length === undefined ) {
length = this._getTypeLength( vecType );
_typeLengthCache.set( vecType, length );
}
return length;
}
_getTypeLength( type ) {
const vecType = type;
const vecNum = /vec([2-4])/.exec( vecType );
if ( vecNum !== null ) return Number( vecNum[ 1 ] );
if ( vecType === 'float' || vecType === 'bool' || vecType === 'int' || vecType === 'uint' ) return 1;
if ( /mat2/.test( type ) === true ) return 4;
if ( /mat3/.test( type ) === true ) return 9;
if ( /mat4/.test( type ) === true ) return 16;
return 0;
}
/**
* Returns the vector type for a given matrix type.
*
* @param {string} type - The matrix type.
* @return {string} The vector type.
*/
getVectorFromMatrix( type ) {
return type.replace( 'mat', 'vec' );
}
/**
* For a given type this method changes the component type to the
* given value. E.g. `vec4` should be changed to the new component type
* `uint` which results in `uvec4`.
*
* @param {string} type - The type.
* @param {string} newComponentType - The new component type.
* @return {string} The new type.
*/
changeComponentType( type, newComponentType ) {
return this.getTypeFromLength( this.getTypeLength( type ), newComponentType );
}
/**
* Returns the common component type of the input nodes. Explicit types are
* promoted first, then weak constants adopt that type, being truncated like other
* integer constants. Only weak constants out of the integer range promote it.
* Inputs consisting only of weak constants default to float.
*
* @param {...?Node} nodes - The input nodes.
* @return {string} The promoted component type.
*/
getPromotedComponentType( ...nodes ) {
let type = null;
let hasWeak = false;
let fitsUint = true;
let fitsInt = true;
for ( const node of nodes ) {
if ( node === null ) continue;
if ( node.isWeak === true ) {
const value = Math.trunc( node.value );
hasWeak = true;
fitsUint = fitsUint && value >= 0 && value <= 0xffffffff;
fitsInt = fitsInt && value >= -2147483648 && value <= 0x7fffffff;
} else {
const componentType = this.getComponentType( node.getNodeType( this ) );
if ( type === null || _componentTypeRanks[ componentType ] > _componentTypeRanks[ type ] ) type = componentType;
}
}
if ( type === null || ( type === 'bool' && hasWeak ) ) return 'float';
if ( type === 'uint' && fitsUint === false ) return fitsInt ? 'int' : 'float';
if ( type === 'int' && fitsInt === false ) return 'float';
return type;
}
/**
* Returns the integer type pendant for the given type.
*
* @param {string} type - The type.
* @return {string} The integer type.
*/
getIntegerType( type ) {
const componentType = this.getComponentType( type );
if ( componentType === 'int' || componentType === 'uint' ) return type;
return this.changeComponentType( type, 'int' );
}
/**
* Adds an active stack to the internal stack.
*
* @param {StackNode} stack - The stack node to add.
*/
setActiveStack( stack ) {
this.activeStacks.push( stack );
}
/**
* Removes the active stack from the internal stack.
*
* @param {StackNode} stack - The stack node to remove.
*/
removeActiveStack( stack ) {
if ( this.activeStacks[ this.activeStacks.length - 1 ] === stack ) {
this.activeStacks.pop();
} else {
throw new Error( 'THREE.NodeBuilder: Invalid active stack removal.' );
}
}
/**
* Returns the active stack.
*
* @return {StackNode} The active stack.
*/
getActiveStack() {
return this.activeStacks[ this.activeStacks.length - 1 ];
}
/**
* Returns the base stack.
*
* @return {StackNode} The base stack.
*/
getBaseStack() {
return this.activeStacks[ 0 ];
}
/**
* Creates a block that repeats its code, e.g. a loop body, used in the current context.
* The block keeps its parent block, the context and its position in the active stack.
*
* @return {Object} The code block.
*/
createBlock() {
const stack = this.getActiveStack();
return {
parent: this.context.nodeBlock || null,
context: this.context,
stack,
node: stack !== undefined ? stack._currentNode : undefined
};
}
/**
* Declares a node outside of the stacks of this build, e.g. created outside of a function, before the outermost
* block that would repeat it, e.g. a loop body. Blocks set up in another build or with context overrides of the
* current one are kept, since the node could have another value in them.
*
* @param {Node} node - The node to declare.
* @return {StackNode|undefined} The stack where the node was declared.
*/
declareBeforeBlock( node ) {
if ( this.flowBlock !== null ) return;
let block;
for ( let nodeBlock = this.context.nodeBlock; nodeBlock; nodeBlock = nodeBlock.parent ) {
if ( this.activeStacks.includes( nodeBlock.stack ) === false || _hasContextOverrides( this.context, nodeBlock.context ) ) break;
block = nodeBlock;
}
if ( block === undefined || this.activeStacks.some( activeStack => activeStack.nodes.includes( node ) ) ) return;
block.stack.addToStackBefore( node, block.node );
return block.stack;
}
/**
* Adds a stack node to the internal stack.
*
* @return {StackNode} The added stack node.
*/
addStack() {
this.stack = stack( this.stack );
const previousStack = getCurrentStack();
this.stacks.push( previousStack );
setCurrentStack( this.stack );
return this.stack;
}
/**
* Removes the last stack node from the internal stack.
*
* @return {StackNode} The removed stack node.
*/
removeStack() {
const lastStack = this.stack;
for ( const node of lastStack.nodes ) {
const nodeData = this.getDataFromNode( node );
nodeData.stack = lastStack;
}
this.stack = lastStack.parent;
setCurrentStack( this.stacks.pop() );
return lastStack;
}
/**
* The builder maintains (cached) data for each node during the building process. This method
* can be used to get these data for a specific shader stage and cache.
*
* @param {Node} node - The node to get the data for.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage=this.shaderStage] - The shader stage.
* @param {?NodeCache} cache - An optional cache.
* @return {Object} The node data.
*/
getDataFromNode( node, shaderStage = this.shaderStage, cache = null ) {
cache = cache === null ? ( node.isGlobal( this ) ? this.globalCache : this.cache ) : cache;
let nodeData = cache.getData( node );
if ( nodeData === undefined ) {
nodeData = {};
cache.setData( node, nodeData );
}
if ( nodeData[ shaderStage ] === undefined ) nodeData[ shaderStage ] = new NodeData();
//
let data = nodeData[ shaderStage ];
if ( this.subBuildLayers.length === 0 ) return data;
const subBuilds = nodeData.any ? nodeData.any.subBuilds : null;
const subBuild = this.getClosestSubBuild( subBuilds );
if ( subBuild ) {
if ( data.subBuildsCache === undefined ) data.subBuildsCache = {};
data = data.subBuildsCache[ subBuild ] || ( data.subBuildsCache[ subBuild ] = new NodeData() );
data.subBuilds = subBuilds;
}
return data;
}
/**
* Returns the properties for the given node and shader stage.
*
* Properties are typically used within a build stage to reference a node's
* child node or nodes manually assigned to the properties in a separate build stage.
* A typical usage pattern for defining nodes manually would be assigning dependency nodes
* to the current node's properties in the setup stage and building those properties in the generate stage.
*
* @param {Node} node - The node to get the properties for.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage='any'] - The shader stage.
* @return {Object} The node properties.
*/
getNodeProperties( node, shaderStage = 'any' ) {
const nodeData = this.getDataFromNode( node, shaderStage );
return nodeData.properties || ( nodeData.properties = { outputNode: null } );
}
/**
* Returns an instance of {@link NodeAttribute} for the given buffer attribute node.
*
* @param {BufferAttributeNode} node - The buffer attribute node.
* @param {string} type - The node type.
* @param {?string} [name=null] - The name of the buffer attribute.
* @return {NodeAttribute} The node attribute.
*/
getBufferAttributeFromNode( node, type, name = null ) {
const nodeData = this.getDataFromNode( node, 'vertex' );
let bufferAttribute = nodeData.bufferAttribute;
if ( bufferAttribute === undefined ) {
const index = this.uniforms.index ++;
if ( name === null ) {
name = 'nodeAttribute' + index;
}
bufferAttribute = new NodeAttribute( name, type, node );
this.bufferAttributes.push( bufferAttribute );
nodeData.bufferAttribute = bufferAttribute;
}
return bufferAttribute;
}
/**
* Returns an instance of {@link StructType} for the given struct name and shader stage
* or null if not found.
*
* @param {string} name - The name of the struct.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage=this.shaderStage] - The shader stage.
* @return {?StructType} The struct type or null if not found.
*/
getStructTypeNode( name, shaderStage = this.shaderStage ) {
return this.types[ shaderStage ][ name ] || null;
}
/**
* Returns an instance of {@link StructType} for the given output struct node.
*
* @param {OutputStructNode} node - The output struct node.
* @param {Array<Object>} membersLayout - The output struct types.
* @param {?string} [name=null] - The name of the struct.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage=this.shaderStage] - The shader stage.
* @return {StructType} The struct type attribute.
*/
getStructTypeFromNode( node, membersLayout, name = null, shaderStage = this.shaderStage ) {
const nodeData = this.getDataFromNode( node, shaderStage, this.globalCache );
let structType = nodeData.structType;
if ( structType === undefined ) {
const index = this.structs.index ++;
if ( name === null ) name = 'StructType' + index;
structType = new StructType( name, membersLayout );
this.structs[ shaderStage ].push( structType );
this.types[ shaderStage ][ name ] = node;
nodeData.structType = structType;
}
return structType;
}
/**
* Returns an instance of {@link StructType} for the given output struct node.
*
* @param {OutputStructNode} node - The output struct node.
* @param {Array<Object>} membersLayout - The output struct types.
* @return {StructType} The struct type attribute.
*/
getOutputStructTypeFromNode( node, membersLayout ) {
const structType = this.getStructTypeFromNode( node, membersLayout, 'OutputType', 'fragment' );
structType.output = true;
return structType;
}
/**
* Returns an instance of {@link NodeUniform} for the given uniform node.
*
* @param {UniformNode} node - The uniform node.
* @param {string} type - The uniform type.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage=this.shaderStage] - The shader stage.
* @param {?string} name - The name of the uniform.
* @return {NodeUniform} The node uniform.
*/
getUniformFromNode( node, type, shaderStage = this.shaderStage, name = null ) {
const nodeData = this.getDataFromNode( node, shaderStage, this.globalCache );
let nodeUniform = nodeData.uniform;
if ( nodeUniform === undefined ) {
const index = this.uniforms.index ++;
nodeUniform = new NodeUniform( name || ( 'nodeUniform' + index ), type, node );
this.uniforms[ shaderStage ].push( nodeUniform );
this.registerDeclaration( nodeUniform );
nodeData.uniform = nodeUniform;
}
return nodeUniform;
}
/**
* Returns an instance of {@link NodeVar} for the given variable node.
*
* @param {VarNode} node - The variable node.
* @param {?string} name - The variable's name.
* @param {string} [type=node.getNodeType( this )] - The variable's type.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage=this.shaderStage] - The shader stage.
* @param {boolean} [readOnly=false] - Whether the variable is read-only or not.
* @param {boolean} [local=false] - Whether the variable is declared locally in the flow instead of the variable section.
* @param {string} [property='variable'] - The node data property that holds the variable. Allows a node to own more than one variable.
*
* @return {NodeVar} The node variable.
*/
getVarFromNode( node, name = null, type = node.getNodeType( this ), shaderStage = this.shaderStage, readOnly = false, local = false, property = 'variable' ) {
const nodeData = this.getDataFromNode( node, shaderStage );
const subBuildVariable = this.getSubBuildProperty( property, nodeData.subBuilds );
let nodeVar = nodeData[ subBuildVariable ];
if ( nodeVar === undefined ) {
const idNS = readOnly ? '_const' : '_var';
const vars = this.vars[ shaderStage ] || ( this.vars[ shaderStage ] = [] );
const id = this.vars[ idNS ] || ( this.vars[ idNS ] = 0 );
if ( name === null ) {
name = ( readOnly ? 'nodeConst' : 'nodeVar' ) + id;
this.vars[ idNS ] ++;
}
//
if ( subBuildVariable !== property ) {
name = this.getSubBuildProperty( name, nodeData.subBuilds );
}
//
const count = node.getArrayCount( this );
nodeVar = new NodeVar( name, type, readOnly, count, local );
if ( ! readOnly && ! local ) {
vars.push( nodeVar );
}
this.registerDeclaration( nodeVar );
nodeData[ subBuildVariable ] = nodeVar;
}
return nodeVar;
}
/**
* Returns whether a Node or its flow is deterministic, useful for use in `const`.
*
* @param {Node} node - The varying node.
* @return {boolean} Returns true if deterministic.
*/
isDeterministic( node ) {
if ( node.isVarNode && node.assignable ) {
node = node.node;
}
if ( node.isMathNode ) {
return this.isDeterministic( node.aNode ) &&
( node.bNode ? this.isDeterministic( node.bNode ) : true ) &&
( node.cNode ? this.isDeterministic( node.cNode ) : true );
} else if ( node.isOperatorNode ) {
return this.isDeterministic( node.aNode ) &&
( node.bNode ? this.isDeterministic( node.bNode ) : true );
} else if ( node.isArrayNode ) {
if ( node.values !== null ) {
for ( const n of node.values ) {
if ( ! this.isDeterministic( n ) ) {
return false;
}
}
}
return true;
} else if ( node.isConstNode ) {
return true;
}
return false;
}
/**
* Returns an instance of {@link NodeVarying} for the given varying node.
*
* @param {(VaryingNode|PropertyNode)} node - The varying node.
* @param {?string} name - The varying's name.
* @param {string} [type=node.getNodeType( this )] - The varying's type.
* @param {?string} interpolationType - The interpolation type of the varying.
* @param {?string} interpolationSampling - The interpolation sampling type of the varying.
* @return {NodeVar} The node varying.
*/
getVaryingFromNode( node, name = null, type = node.getNodeType( this ), interpolationType = null, interpolationSampling = null ) {
const nodeData = this.getDataFromNode( node, 'any' );
const subBuildVarying = this.getSubBuildProperty( 'varying', nodeData.subBuilds );
let nodeVarying = nodeData[ subBuildVarying ];
if ( nodeVarying === undefined ) {
const varyings = this.varyings;
const index = varyings.length;
if ( name === null ) name = 'nodeVarying' + index;
//
if ( subBuildVarying !== 'varying' ) {
name = this.getSubBuildProperty( name, nodeData.subBuilds );
}
//
nodeVarying = new NodeVarying( name, type, interpolationType, interpolationSampling );
varyings.push( nodeVarying );
this.registerDeclaration( nodeVarying );
nodeData[ subBuildVarying ] = nodeVarying;
}
return nodeVarying;
}
/**
* Registers a node declaration in the current shader stage.
*
* @param {Object} node - The node to be registered.
*/
registerDeclaration( node ) {
const shaderStage = this.shaderStage;
const declarations = this.declarations[ shaderStage ] || ( this.declarations[ shaderStage ] = {} );
const checkKeywords = this.renderer.debug.diagnostics.keywords;
const baseName = node.name;
let name = baseName;
let declarationName = this.getDeclarationName( node );
let index = 1;
// Automatically renames the property if the name is already in use or reserved.
while ( ( checkKeywords && this.isReservedKeyword( name ) ) || declarations[ declarationName ] !== undefined ) {
name = baseName + '_' + index ++;
node.name = name;
declarationName = this.getDeclarationName( node );
}
if ( name !== baseName ) {
warn( `TSL: Declaration name '${ baseName }' of '${ node.type }' is a reserved keyword or already in use. Renamed to '${ name }'.` );
}
declarations[ declarationName ] = node;
}
/**
* Returns the identifier the given node declares in the shader. It is used
* by {@link NodeBuilder#registerDeclaration} to detect name collisions.
*
* @param {Object} node - The node.
* @return {string} The declaration name.
*/
getDeclarationName( node ) {
return this.getPropertyName( node );
}
/**
* Returns an instance of {@link NodeCode} for the given code node.
*
* @param {CodeNode} node - The code node.
* @param {string} type - The node type.
* @param {('vertex'|'fragment'|'compute'|'any')} [shaderStage=this.shaderStage] - The shader stage.
* @return {NodeCode} The node code.
*/
getCodeFromNode( node, type, shaderStage = this.shaderStage ) {
const nodeData = this.getDataFromNode( node );
let nodeCode = nodeData.code;
if ( nodeCode === undefined ) {
const codes = this.codes[ shaderStage ] || ( this.codes[ shaderStage ] = [] );
const index = codes.length;
nodeCode = new NodeCode( 'nodeCode' + index, type );
codes.push( nodeCode );
nodeData.code = nodeCode;
}
return nodeCode;
}
/**
* Add a inline-code to the current flow.
*
* @param {string} code - The code to add.
* @param {?Node} [node= null] - The node that generated the code.
* @return {NodeBuilder} A reference to this node builder.
*/
addLineFlowCode( code /*, node = null */ ) {
if ( code === '' ) return this;
code = this.tab + code;
if ( code.trimEnd().endsWith( ';' ) === false ) {
code = code + ';\n';
}
this.flow.code += code;
return this;
}
/**
* Adds a code to the current code flow.
*
* @param {string} code - Shader code.
* @return {NodeBuilder} A reference to this node builder.
*/
addFlowCode( code ) {
this.flow.code += code;
return this;
}
/**
* Add tab in the code that will be generated so that other snippets respect the current tabulation.
* Typically used in codes with If,Else.
*
* @return {NodeBuilder} A reference to this node builder.
*/
addFlowTab() {
this.tab += '\t';
return this;
}
/**
* Removes a tab.
*
* @return {NodeBuilder} A reference to this node builder.
*/
removeFlowTab() {
this.tab = this.tab.slice( 0, -1 );
return this;
}
/**
* Gets the current flow data based on a Node.
*
* @param {Node} node - Node that the flow was started.
* @param {('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage.
* @return {Object} The flow data.
*/
getFlowData( node/*, shaderStage*/ ) {
return this.flowsData.get( node );
}
/**
* Executes the node flow based on a root node to generate the final shader code.
*
* @param {Node} node - The node to execute.
* @return {Object} The code flow.
*/
flowNode( node ) {
const output = node.getNodeType( this );
const flowData = this.flowChildNode( node, output );
this.flowsData.set( node, flowData );
return flowData;
}
/**
* Includes a node in the current function node.
*
* @param {Node} node - The node to include.
* @returns {void}
*/
addInclude( node ) {
if ( this.currentFunctionNode !== null && this.currentFunctionNode.includes.includes( node ) === false ) {
this.currentFunctionNode.includes.push( node );
}
}
/**
* Returns the native shader operator name for a given generic name.
* It is a similar type of method like {@link NodeBuilder#getMethod}.
*
* @param {ShaderNodeInternal} shaderNode - The shader node to build the function node with.
* @return {FunctionNode} The build function node.
*/
buildFunctionNode( shaderNode ) {
const backend = this.renderer.backend;
let cache = _functionNodeCache.get( backend );
if ( cache === undefined ) {
cache = new WeakMap();
_functionNodeCache.set( backend, cache );
}
let fn = cache.get( shaderNode );
if ( fn === undefined ) {
fn = new FunctionNode();
const previous = this.currentFunctionNode;
this.currentFunctionNode = fn;
fn.code = this.buildFunctionCode( shaderNode );
this.currentFunctionNode = previous;
cache.set( shaderNode, fn );
}
return fn;
}
/**
* Generates a code flow based on a TSL function: Fn().
*
* @param {ShaderNodeInternal} shaderNode - A function code will be generated based on the input.
* @return {Object}
*/
flowShaderNode( shaderNode ) {
const layout = shaderNode.layout;
const inputs = _createLayoutInputs( layout, layout.inputs.map( input => new ParameterNode( input.type, input.name ) ) );
//
shaderNode.layout = null;
const callNode = shaderNode.call( inputs );
const flowData = this.flowStagesNode( callNode, layout.type );
shaderNode.layout = layout;
return flowData;
}
/**
* Returns the inputs of a call to a TSL function with layout. Missing inputs are completed
* with the default values of the function parameters, found by running the function once
* with all inputs and once without the missing ones, and comparing both node graphs.
*
* @private
* @param {ShaderNodeInternal} shaderNode - The shader node of the function.
* @param {?Array} rawInputs - The inputs of the call.
* @return {?Array} The inputs of the call, or `null` if the defaults can't be resolved and the function must be expanded inline.
*/
_getLayoutInputs( shaderNode, rawInputs ) {
const inputs = shaderNode.layout.inputs;
const firstInput = rawInputs !== null && rawInputs.length > 0 ? rawInputs[ 0 ] : null;
const object = firstInput !== null && firstInput !== undefined && isArrayAsParameter( rawInputs ) === false ? firstInput : null;
const provided = inputs.map( ( input, i ) => object !== null ? object[ input.name ] !== undefined : rawInputs !== null && rawInputs[ i ] !== undefined );
if ( provided.every( Boolean ) ) return rawInputs !== null ? rawInputs : [];
if ( shaderNode.once ) return null;
// The defaults only depend on which inputs are missing, so they are shared by all builders.
let cache = _layoutDefaultsCache.get( shaderNode );
if ( cache === undefined ) {
cache = new Map();
_layoutDefaultsCache.set( shaderNode, cache );
}
const key = provided.map( Number ).join( '' );
let defaults = cache.get( key );
if ( defaults === undefined ) {
defaults = this._resolveLayoutDefaults( shaderNode, provided );
cache.set( key, defaults );
}
if ( defaults === null ) return null;
if ( object !== null ) {
const completed = { ...object };
inputs.forEach( ( input, i ) => {
if ( provided[ i ] === false ) completed[ input.name ] = defaults[ i ];
} );
return [ completed ];
}
return provided.map( ( isProvided, i ) => isProvided ? rawInputs[ i ] : defaults[ i ] );
}
/**
* Resolves the default values of the missing inputs of a TSL function with layout.
*
* @private
* @param {ShaderNodeInternal} shaderNode - The shader node of the function.
* @param {Array<boolean>} provided - Whether each input of the layout was provided.
* @return {?Array<Node>} The default value of each missing input, or `null` if they could not be resolved.
*/
_resolveLayoutDefaults( shaderNode, provided ) {
const layout = shaderNode.layout;
const parameters = layout.inputs.map( input => new ParameterNode( input.type, input.name ) );
const fullGraph = this._getLayoutCallGraph( shaderNode, _createLayoutInputs( layout, parameters ) );
const partialGraph = this._getLayoutCallGraph( shaderNode, _createLayoutInputs( layout, parameters.map( ( parameter, i ) => provided[ i ] ? parameter : undefined ) ) );
// Walk both graphs together: where the full graph uses the parameter of a missing input,
// the partial graph uses its default value.
const missing = new Map();
parameters.forEach( ( parameter, i ) => {
if ( provided[ i ] === false ) missing.set( parameter, i );
} );
const defaults = [];
const visited = new Map();
const walk = ( a, b ) => {
if ( a === b ) return true;
if ( ! a || ! b || a.isNode !== true || b.isNode !== true ) return false;
const index = missing.get( a );
if ( index !== undefined ) {
if ( defaults[ index ] === undefined ) defaults[ index ] = b;
return defaults[ index ] === b;
}
if ( visited.has( a ) ) return visited.get( a ) === b;
if ( a.constructor !== b.constructor ) return false;
visited.set( a, b );
const childrenA = [ ...a.getChildren() ];
const childrenB = [ ...b.getChildren() ];
if ( childrenA.length !== childrenB.length ) return false;
for ( let i = 0; i < childrenA.length; i ++ ) {
if ( walk( childrenA[ i ], childrenB[ i ] ) === false ) return false;
}
return true;
};
// The default values are evaluated when the parameters are destructured, before the body,
// so the nodes they add to the stack come first. Align the body statements from the end.
const extra = partialGraph.nodes.length - fullGraph.nodes.length;
if ( extra < 0 ) return null;
visited.set( fullGraph, partialGraph );
if ( walk( fullGraph.outputNode, partialGraph.outputNode ) === false ) return null;
for ( let i = 0; i < fullGraph.nodes.length; i ++ ) {
if ( walk( fullGraph.nodes[ i ], partialGraph.nodes[ extra + i ] ) === false ) return null;
}
// Every missing input must have a default that does not depend on other parameters.
const parameterSet = new Set( parameters );
for ( const index of missing.values() ) {
const value = defaults[ index ];
if ( value === undefined ) return null;
const stack = [ value ];
const seen = new Set();
while ( stack.length > 0 ) {
const node = stack.pop();
if ( parameterSet.has( node ) ) return null;
if ( seen.has( node ) ) continue;
seen.add( node );
for ( const child of node.getChildren() ) stack.push( child );
}
}
return defaults;
}
/**
* Runs a TSL function with layout as an inline call and returns the resulting stack.
*
* @private
* @param {ShaderNodeInternal} shaderNode - The shader node of the function.
* @param {Object} inputs - The inputs of the call.
* @return {StackNode} The stack with the nodes of the call.
*/
_getLayoutCallGraph( shaderNode, inputs ) {
const layout = shaderNode.layout;
let graph;
shaderNode.layout = null;
this.addStack();
try {
this.stack.outputNode = shaderNode.call( inputs ).call( this );
} finally {
graph = this.removeStack();
shaderNode.layout = layout;
}
return graph;
}
/**
* Executes the node in a specific build stage.
*
* This function can be used to arbitrarily execute the specified build stage
* outside of the standard build process. For instance, if a node's type depends
* on properties created by the 'setup' stage, then flowBuildStage(node, 'setup')
* can be used to execute the setup build stage and access its generated nodes
* before the standard build process begins.
*
* @param {Node} node - The node to execute.
* @param {string} buildStage - The build stage to execute the node in.
* @param {?(Node|string)} [output=null] - Expected output type. For example 'vec3'.
* @return {?(Node|string)} The result of the node build.
*/
flowBuildStage( node, buildStage, output = null ) {
const previousBuildStage = this.getBuildStage();
this.setBuildStage( buildStage );
const result = node.build( this, output );
this.setBuildStage( previousBuildStage );
return result;
}
/**
* Runs the node flow through all the steps of creation, 'setup', 'analyze', 'generate'.
*
* @param {Node} node - The node to execute.
* @param {?string} output - Expected output type. For example 'vec3'.
* @return {Object}
*/
flowStagesNode( node, output = null ) {
const previousFlow = this.flow;
const previousVars = this.vars;
const previousDeclarations = this.declarations;
const previousCache = this.cache;
const previousBuildStage = this.buildStage;
const previousStack = this.stack;
const previousFlowBlock = this.flowBlock;
const flow = {
code: ''
};
this.flow = flow;
this.vars = {};
this.declarations = {};
this.cache = new NodeCache();
this.stack = stack();
this.flowBlock = null;
for ( const buildStage of defaultBuildStages ) {
this.setBuildStage( buildStage );
flow.result = node.build( this, output );
}
flow.vars = this.getVars( this.shaderStage );
this.flow = previousFlow;
this.vars = previousVars;
this.declarations = previousDeclarations;
this.cache = previousCache;
this.stack = previousStack;
this.flowBlock = previousFlowBlock;
this.setBuildStage( previousBuildStage );
return flow;
}
/**
* Returns the native shader operator name for a given generic name.
* It is a similar type of method like {@link NodeBuilder#getMethod}.
*
* @abstract
* @param {string} op - The operator name to resolve.
* @return {?string} The resolved operator name.
*/
getFunctionOperator( /* op */ ) {
return null;
}
/**
* Builds the given shader node.
*
* @abstract
* @param {ShaderNodeInternal} shaderNode - The shader node.
* @return {string} The function code.
*/
buildFunctionCode( /* shaderNode */ ) {
warn( 'Abstract function.' );
}
/**
* Generates a code flow based on a child Node.
*
* @param {Node} node - The node to execute.
* @param {?string} output - Expected output type. For example 'vec3'.
* @return {Object} The code flow.
*/
flowChildNode( node, output = null ) {
const previousFlow = this.flow;
const flow = {
code: ''
};
this.flow = flow;
flow.result = node.build( this, output );
this.flow = previousFlow;
return flow;
}
/**
* Executes a flow of code in a different stage.
*
* Some nodes like `varying()` have the ability to compute code in vertex-stage and
* return the value in fragment-stage even if it is being executed in an input fragment.
*
* @param {('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage.
* @param {Node} node - The node to execute.
* @param {?string} output - Expected output type. For example 'vec3'.
* @param {?string} propertyName - The property name to assign the result.
* @return {?(Object|Node)} The code flow or node.build() result.
*/
flowNodeFromShaderStage( shaderStage, node, output = null, propertyName = null ) {
const previousTab = this.tab;
const previousCache = this.cache;
const previousShaderStage = this.shaderStage;
const previousContext = this.context;
const previousFlowBlock = this.flowBlock;
this.setShaderStage( shaderStage );
const context = { ...this.context };
delete context.nodeBlock;
this.flowBlock = null;
this.cache = this.globalCache;
this.tab = '\t';
this.context = context;
let result = null;
if ( this.buildStage === 'generate' ) {
const flowData = this.flowChildNode( node, output );
if ( propertyName !== null ) {
flowData.code += `${ this.tab + propertyName } = ${ flowData.result };\n`;
}
this.flowCode[ shaderStage ] = this.flowCode[ shaderStage ] + flowData.code;
result = flowData;
} else {
result = node.build( this );
}
this.setShaderStage( previousShaderStage );
this.cache = previousCache;
this.tab = previousTab;
this.context = previousContext;
this.flowBlock = previousFlowBlock;
return result;
}
/**
* Returns an array holding all node attributes of this node builder.
*
* @return {Array<NodeAttribute>} The node attributes of this builder.
*/
getAttributesArray() {
return this.attributes.concat( this.bufferAttributes );
}
/**
* Returns the attribute definitions as a shader string for the given shader stage.
*
* @abstract
* @param {('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage.
* @return {string} The attribute code section.
*/
getAttributes( /*shaderStage*/ ) {
warn( 'Abstract function.' );
}
/**
* Returns the varying definitions as a shader string for the given shader stage.
*
* @abstract
* @param {('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage.
* @return {string} The varying code section.
*/
getVaryings( /*shaderStage*/ ) {
warn( 'Abstract function.' );
}
/**
* Returns a single variable definition as a shader string for the given variable type and name.
*
* @param {string} type - The variable's type.
* @param {string} name - The variable's name.
* @param {?number} [count=null] - The array length.
* @return {string} The shader string.
*/
getVar( type, name, count = null ) {
return `${ count !== null ? this.generateArrayDeclaration( type, count ) : this.getType( type ) } ${ name }`;
}
/**
* Returns a single const variable statement as a shader string for the given variable type and name.
*
* @param {string} type - The variable's type.
* @param {string} name - The variable's name.
* @param {?number} [count=null] - The array length.
* @return {string} The shader string.
*/
generateConstStatement( type, name, count = null ) {
return `const ${ this.getVar( type, name, count ) }`;
}
/**
* Returns a single variable statement as a shader string for the given variable type and name.
*
* @param {string} type - The variable's type.
* @param {string} name - The variable's name.
* @param {?number} [count=null] - The array length.
* @return {string} The shader string.
*/
generateVarStatement( type, name, count = null ) {
return this.getVar( type, name, count );
}
/**
* Returns a runtime read-only variable statement as a shader string.
* Backends without a let declaration use a regular variable declaration.
*
* @param {string} type - The variable's type.
* @param {string} name - The variable's name.
* @param {?number} [count=null] - The array length.
* @return {string} The shader string.
*/
generateLetStatement( type, name, count = null ) {
return this.generateVarStatement( type, name, count );
}
/**
* Returns the variable definitions as a shader string for the given shader stage.
*
* @param {('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage.
* @param {boolean} [global=false] - Whether the variables are global.
* @return {string} The variable code section.
*/
getVars( shaderStage, global = false ) {
const snippets = [];
const vars = this.vars[ shaderStage ];
if ( vars !== undefined ) {
for ( const variable of vars ) {
snippets.push( `${ this.getVar( variable.type, variable.name, variable.count ) };` );
}
}
return snippets.join( global ? '\n' : '\n\t' );
}
/**
* Returns the uniform definitions as a shader string for the given shader stage.
*
* @abstract
* @param {('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage.
* @return {string} The uniform code section.
*/
getUniforms( /*shaderStage*/ ) {
warn( 'Abstract function.' );
}
/**
* Returns the native code definitions as a shader string for the given shader stage.
*
* @param {('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage.
* @return {string} The native code section.
*/
getCodes( shaderStage ) {
const codes = this.codes[ shaderStage ];
let code = '';
if ( codes !== undefined ) {
for ( const nodeCode of codes ) {
code += nodeCode.code + '\n';
}
}
return code;
}
/**
* Returns the hash of this node builder.
*
* @return {string} The hash.
*/
getHash() {
return this.vertexShader + this.fragmentShader + this.computeShader;
}
/**
* Sets the current shader stage.
*
* @param {?('vertex'|'fragment'|'compute'|'any')} shaderStage - The shader stage to set.
*/
setShaderStage( shaderStage ) {
this.shaderStage = shaderStage;
}
/**
* Returns the current shader stage.
*
* @return {?('vertex'|'fragment'|'compute'|'any')} The current shader stage.
*/
getShaderStage() {
return this.shaderStage;
}
/**
* Sets the current build stage.
*
* @param {?('setup'|'analyze'|'generate')} buildStage - The build stage to set.
*/
setBuildStage( buildStage ) {
this.buildStage = buildStage;
}
/**
* Returns the current build stage.
*
* @return {?('setup'|'analyze'|'generate')} The current build stage.
*/
getBuildStage() {
return this.buildStage;
}
/**
* Controls the code build of the shader stages.
*
* @abstract
*/
buildCode() {
warn( 'Abstract function.' );
}
/**
* Returns the current sub-build layer.
*
* @return {SubBuildNode} The current sub-build layers.
*/
get subBuild() {
return this.subBuildLayers[ this.subBuildLayers.length - 1 ] || null;
}
/**
* Adds a sub-build layer to the node builder.
*
* @param {SubBuildNode} subBuild - The sub-build layer to add.
*/
addSubBuild( subBuild ) {
this.subBuildLayers.push( subBuild );
}
/**
* Removes the last sub-build layer from the node builder.
*
* @return {SubBuildNode} The removed sub-build layer.
*/
removeSubBuild() {
return this.subBuildLayers.pop();
}
/**
* Returns the closest sub-build layer for the given data.
*
* @param {Node|Set<string>|Array<string>} data - The data to get the closest sub-build layer from.
* @return {?string} The closest sub-build name or null if none found.
*/
getClosestSubBuild( data ) {
let subBuilds;
if ( data && data.isNode ) {
if ( data.isShaderCallNodeInternal ) {
subBuilds = data.shaderNode.subBuilds;
} else if ( data.isStackNode ) {
subBuilds = [ data.subBuild ];
} else {
subBuilds = this.getDataFromNode( data, 'any' ).subBuilds;
}
} else if ( data instanceof Set ) {
subBuilds = [ ...data ];
} else {
subBuilds = data;
}
if ( ! subBuilds ) return null;
const subBuildLayers = this.subBuildLayers;
for ( let i = subBuilds.length - 1; i >= 0; i -- ) {
const subBuild = subBuilds[ i ];
if ( subBuildLayers.includes( subBuild ) ) {
return subBuild;
}
}
return null;
}
/**
* Returns the output node of a sub-build layer.
*
* @param {Node} node - The node to get the output from.
* @return {string} The output node name.
*/
getSubBuildOutput( node ) {
return this.getSubBuildProperty( 'outputNode', node );
}
/**
* Returns the sub-build property name for the given property and node.
*
* @param {string} [property=''] - The property name.
* @param {?Node} [node=null] - The node to get the sub-build from.
* @return {string} The sub-build property name.
*/
getSubBuildProperty( property = '', node = null ) {
let subBuild;
if ( node !== null ) {
subBuild = this.getClosestSubBuild( node );
} else {
subBuild = this.subBuildFn;
}
let result;
if ( subBuild ) {
result = property ? ( subBuild + '_' + property ) : subBuild;
} else {
result = property;
}
return result;
}
/**
* Prebuild the node builder.
*/
prebuild() {
const { renderer, material } = this;
// < renderer.contextNode >
if ( renderer.contextNode.isContextNode === true ) {
this.addContext( renderer.contextNode.getFlowContextData() );
} else {
error( 'NodeBuilder: "renderer.contextNode" must be an instance of `context()`.' );
}
// < material.contextNode >
if ( material && material.contextNode ) {
if ( material.contextNode.isContextNode === true ) {
this.addContext( material.contextNode.getFlowContextData() );
} else {
error( 'NodeBuilder: "material.contextNode" must be an instance of `context()`.' );
}
}
// < nodeMaterial >
if ( material !== null ) {
let nodeMaterial = renderer.library.fromMaterial( material );
if ( nodeMaterial === null ) {
error( `NodeBuilder: Material "${ material.type }" is not compatible.` );
nodeMaterial = new NodeMaterial();
}
nodeMaterial.build( this );
} else {
this.addFlow( 'compute', this.compute );
}
}
/**
* Central build method which controls the build for the given object.
*
* @return {NodeBuilder} A reference to this node builder.
*/
build() {
this.prebuild();
// setup() -> stage 1: create possible new nodes and/or return an output reference node
// analyze() -> stage 2: analyze nodes to possible optimization and validation
// generate() -> stage 3: generate shader
for ( const buildStage of defaultBuildStages ) {
this.setBuildStage( buildStage );
if ( this.context.position && this.context.position.isNode ) {
this.flowNodeFromShaderStage( 'vertex', this.context.position );
}
for ( const shaderStage of shaderStages ) {
this.setShaderStage( shaderStage );
const flowNodes = this.flowNodes[ shaderStage ];
for ( const node of flowNodes ) {
if ( buildStage === 'generate' ) {
this.flowNode( node );
} else {
node.build( this );
}
}
}
}
this.setBuildStage( null );
this.setShaderStage( null );
// stage 4: build code for a specific output
this.buildCode();
this.buildUpdateNodes();
return this;
}
/**
* Async version of build() that yields to main thread between shader stages.
* Use this in compileAsync() to prevent blocking the main thread.
*
* @param {Function} [yieldFn=yieldToMain] - The function used to yield to the main thread.
* @return {Promise<NodeBuilder>} A promise that resolves to this node builder.
*/
async buildAsync( yieldFn = yieldToMain ) {
this.prebuild();
// setup() -> stage 1: create possible new nodes and/or return an output reference node
// analyze() -> stage 2: analyze nodes to possible optimization and validation
// generate() -> stage 3: generate shader
for ( const buildStage of defaultBuildStages ) {
this.setBuildStage( buildStage );
if ( this.context.position && this.context.position.isNode ) {
this.flowNodeFromShaderStage( 'vertex', this.context.position );
}
for ( const shaderStage of shaderStages ) {
this.setShaderStage( shaderStage );
const flowNodes = this.flowNodes[ shaderStage ];
for ( const node of flowNodes ) {
if ( buildStage === 'generate' ) {
this.flowNode( node );
} else {
node.build( this );
}
}
// Yield to main thread after each shader stage to prevent blocking
await yieldFn();
}
}
this.setBuildStage( null );
this.setShaderStage( null );
// stage 4: build code for a specific output
this.buildCode();
this.buildUpdateNodes();
return this;
}
/**
* Returns a uniform representation which is later used for UBO generation and rendering.
*
* @param {NodeUniform} uniformNode - The uniform node.
* @param {string} type - The requested type.
* @return {Uniform} The uniform.
*/
getNodeUniform( uniformNode, type ) {
if ( type === 'float' || type === 'int' || type === 'uint' ) return new NumberNodeUniform( uniformNode );
if ( type === 'vec2' || type === 'ivec2' || type === 'uvec2' ) return new Vector2NodeUniform( uniformNode );
if ( type === 'vec3' || type === 'ivec3' || type === 'uvec3' ) return new Vector3NodeUniform( uniformNode );
if ( type === 'vec4' || type === 'ivec4' || type === 'uvec4' ) return new Vector4NodeUniform( uniformNode );
if ( type === 'color' ) return new ColorNodeUniform( uniformNode );
if ( type === 'mat2' ) return new Matrix2NodeUniform( uniformNode );
if ( type === 'mat3' ) return new Matrix3NodeUniform( uniformNode );
if ( type === 'mat4' ) return new Matrix4NodeUniform( uniformNode );
throw new Error( `THREE.NodeBuilder: Uniform "${ type }" not implemented.` );
}
/**
* Formats the given shader snippet from a given type into another one. E.g.
* this method might be used to convert a simple float string `"1.0"` into a
* `vec3` representation: `"vec3<f32>( 1.0 )"`.
*
* @param {string} snippet - The shader snippet.
* @param {string} fromType - The source type.
* @param {string} toType - The target type.
* @return {string} The updated shader string.
*/
format( snippet, fromType, toType ) {
fromType = this.getVectorType( fromType );
toType = this.getVectorType( toType );
if ( fromType === toType || toType === null || this.isReference( toType ) ) {
return snippet;
}
const fromTypeLength = this.getTypeLength( fromType );
const toTypeLength = this.getTypeLength( toType );
if ( fromTypeLength === 16 && toTypeLength === 9 ) {
return `${ this.getType( toType ) }( ${ snippet }[ 0 ].xyz, ${ snippet }[ 1 ].xyz, ${ snippet }[ 2 ].xyz )`;
}
if ( fromTypeLength === 9 && toTypeLength === 4 ) {
return `${ this.getType( toType ) }( ${ snippet }[ 0 ].xy, ${ snippet }[ 1 ].xy )`;
}
if ( fromTypeLength > 4 ) { // fromType is matrix-like
// @TODO: ignore for now
return snippet;
}
if ( toTypeLength > 4 || toTypeLength === 0 ) { // toType is matrix-like or unknown
// @TODO: ignore for now
return snippet;
}
if ( fromTypeLength === toTypeLength ) {
return `${ this.getType( toType ) }( ${ snippet } )`;
}
if ( fromTypeLength > toTypeLength ) {
snippet = toType === 'bool' ? `all( ${ snippet } )` : `${ snippet }.${ 'xyz'.slice( 0, toTypeLength ) }`;
return this.format( snippet, this.getTypeFromLength( toTypeLength, this.getComponentType( fromType ) ), toType );
}
if ( toTypeLength === 4 && fromTypeLength > 1 ) { // toType is vec4-like
const componentType = this.getComponentType( toType );
const vectorType = this.getTypeFromLength( 3, componentType );
return `${ this.getType( toType ) }( ${ this.format( snippet, fromType, vectorType ) }, ${ this.generateConst( componentType, componentType === 'bool' ? true : 1 ) } )`;
}
if ( fromTypeLength === 2 ) { // fromType is vec2-like and toType is vec3-like
const componentType = this.getComponentType( toType );
const vectorType = this.getTypeFromLength( 2, componentType );
return `${ this.getType( toType ) }( ${ this.format( snippet, fromType, vectorType ) }, ${ this.generateConst( componentType, componentType === 'bool' ? false : 0 ) } )`;
}
if ( fromTypeLength === 1 && toTypeLength > 1 && fromType !== this.getComponentType( toType ) ) { // fromType is float-like
// convert a number value to vector type, e.g:
// vec3( 1u ) -> vec3( float( 1u ) )
snippet = `${ this.getType( this.getComponentType( toType ) ) }( ${ snippet } )`;
}
return `${ this.getType( toType ) }( ${ snippet } )`; // fromType is float-like
}
/**
* Returns a signature with the engine's current revision.
*
* @return {string} The signature.
*/
getSignature() {
return `// Three.js r${ REVISION } - Node System\n`;
}
/**
* Returns `true` if data from the previous frame are required. Relevant
* when computing motion vectors with {@link VelocityNode}.
*
* @return {boolean} Whether data from the previous frame are required or not.
*/
needsPreviousData() {
const mrt = this.renderer.getMRT();
return ( mrt !== null && mrt.has( 'velocity' ) );
}
}
/**
* Management class for updating nodes. The module tracks metrics like
* the elapsed time, delta time, the render and frame ID to correctly
* call the node update methods {@link Node#updateBefore}, {@link Node#update}
* and {@link Node#updateAfter} depending on the node's configuration.
*/
class NodeFrame {
/**
* Constructs a new node fame.
*/
constructor() {
/**
* The elapsed time in seconds.
*
* @type {number}
* @default 0
*/
this.time = 0;
/**
* The delta time in seconds.
*
* @type {number}
* @default 0
*/
this.deltaTime = 0;
/**
* The frame ID.
*
* @type {number}
* @default 0
*/
this.frameId = 0;
/**
* The render ID.
*
* @type {number}
* @default 0
*/
this.renderId = 0;
/**
* Used to control the {@link Node#update} call.
*
* @type {WeakMap<Node, Object>}
*/
this.updateMap = new WeakMap();
/**
* Used to control the {@link Node#updateBefore} call.
*
* @type {WeakMap<Node, Object>}
*/
this.updateBeforeMap = new WeakMap();
/**
* Used to control the {@link Node#updateAfter} call.
*
* @type {WeakMap<Node, Object>}
*/
this.updateAfterMap = new WeakMap();
/**
* A reference to the current renderer.
*
* @type {?Renderer}
* @default null
*/
this.renderer = null;
/**
* A reference to the current material.
*
* @type {?Material}
* @default null
*/
this.material = null;
/**
* A reference to the current camera.
*
* @type {?Camera}
* @default null
*/
this.camera = null;
/**
* A reference to the current 3D object.
*
* @type {?Object3D}
* @default null
*/
this.object = null;
/**
* A reference to the current scene.
*
* @type {?Scene}
* @default null
*/
this.scene = null;
/**
* A reference to the current compute node.
*
* @type {?ComputeNode}
* @default null
*/
this.compute = null;
}
/**
* Returns a dictionary for a given node and update map which
* is used to correctly call node update methods per frame or render.
*
* @private
* @param {WeakMap<Node, Object>} referenceMap - The reference weak map.
* @param {Node} nodeRef - The reference to the current node.
* @return {Object<string,WeakMap<Object, number>>} The dictionary.
*/
_getMaps( referenceMap, nodeRef ) {
let maps = referenceMap.get( nodeRef );
if ( maps === undefined ) {
maps = {
renderId: 0,
frameId: 0,
};
referenceMap.set( nodeRef, maps );
}
return maps;
}
/**
* This method executes the {@link Node#updateBefore} for the given node.
* It makes sure {@link Node#updateBeforeType} is honored meaning the update
* is only executed once per frame, render or object depending on the update
* type.
*
* @param {Node} node - The node that should be updated.
*/
updateBeforeNode( node ) {
const updateType = node.getUpdateBeforeType( this );
const reference = node.updateReference( this );
if ( updateType === NodeUpdateType.FRAME ) {
const nodeUpdateBeforeMap = this._getMaps( this.updateBeforeMap, reference );
if ( nodeUpdateBeforeMap.frameId !== this.frameId ) {
const previousFrameId = nodeUpdateBeforeMap.frameId;
nodeUpdateBeforeMap.frameId = this.frameId;
if ( node.updateBefore( this ) === false ) {
nodeUpdateBeforeMap.frameId = previousFrameId;
}
}
} else if ( updateType === NodeUpdateType.RENDER ) {
const nodeUpdateBeforeMap = this._getMaps( this.updateBeforeMap, reference );
if ( nodeUpdateBeforeMap.renderId !== this.renderId ) {
const previousRenderId = nodeUpdateBeforeMap.renderId;
nodeUpdateBeforeMap.renderId = this.renderId;
if ( node.updateBefore( this ) === false ) {
nodeUpdateBeforeMap.renderId = previousRenderId;
}
}
} else if ( updateType === NodeUpdateType.OBJECT ) {
node.updateBefore( this );
}
}
/**
* This method executes the {@link Node#updateAfter} for the given node.
* It makes sure {@link Node#updateAfterType} is honored meaning the update
* is only executed once per frame, render or object depending on the update
* type.
*
* @param {Node} node - The node that should be updated.
*/
updateAfterNode( node ) {
const updateType = node.getUpdateAfterType( this );
const reference = node.updateReference( this );
if ( updateType === NodeUpdateType.FRAME ) {
const nodeUpdateAfterMap = this._getMaps( this.updateAfterMap, reference );
if ( nodeUpdateAfterMap.frameId !== this.frameId ) {
if ( node.updateAfter( this ) !== false ) {
nodeUpdateAfterMap.frameId = this.frameId;
}
}
} else if ( updateType === NodeUpdateType.RENDER ) {
const nodeUpdateAfterMap = this._getMaps( this.updateAfterMap, reference );
if ( nodeUpdateAfterMap.renderId !== this.renderId ) {
if ( node.updateAfter( this ) !== false ) {
nodeUpdateAfterMap.renderId = this.renderId;
}
}
} else if ( updateType === NodeUpdateType.OBJECT ) {
node.updateAfter( this );
}
}
/**
* This method executes the {@link Node#update} for the given node.
* It makes sure {@link Node#updateType} is honored meaning the update
* is only executed once per frame, render or object depending on the update
* type.
*
* @param {Node} node - The node that should be updated.
*/
updateNode( node ) {
const updateType = node.getUpdateType( this );
const reference = node.updateReference( this );
if ( updateType === NodeUpdateType.FRAME ) {
const nodeUpdateMap = this._getMaps( this.updateMap, reference );
if ( nodeUpdateMap.frameId !== this.frameId ) {
if ( node.update( this ) !== false ) {
nodeUpdateMap.frameId = this.frameId;
}
}
} else if ( updateType === NodeUpdateType.RENDER ) {
const nodeUpdateMap = this._getMaps( this.updateMap, reference );
if ( nodeUpdateMap.renderId !== this.renderId ) {
if ( node.update( this ) !== false ) {
nodeUpdateMap.renderId = this.renderId;
}
}
} else if ( updateType === NodeUpdateType.OBJECT ) {
node.update( this );
}
}
/**
* Updates the internal state of the node frame. This method is
* called by the renderer in its internal animation loop.
*/
update() {
this.frameId ++;
if ( this.lastTime === undefined ) this.lastTime = performance.now();
this.deltaTime = ( performance.now() - this.lastTime ) / 1000;
this.lastTime = performance.now();
this.time += this.deltaTime;
}
}
/**
* Describes the input of a {@link NodeFunction}.
*/
class NodeFunctionInput {
/**
* Constructs a new node function input.
*
* @param {string} type - The input type.
* @param {string} name - The input name.
* @param {?number} [count=null] - If the input is an Array, count will be the length.
* @param {('in'|'out'|'inout')} [qualifier=''] - The parameter qualifier (only relevant for GLSL).
* @param {boolean} [isConst=false] - Whether the input uses a const qualifier or not (only relevant for GLSL).
*/
constructor( type, name, count = null, qualifier = '', isConst = false ) {
/**
* The input type.
*
* @type {string}
*/
this.type = type;
/**
* The input name.
*
* @type {string}
*/
this.name = name;
/**
* If the input is an Array, count will be the length.
*
* @type {?number}
* @default null
*/
this.count = count;
/**
*The parameter qualifier (only relevant for GLSL).
*
* @type {('in'|'out'|'inout')}
* @default ''
*/
this.qualifier = qualifier;
/**
* Whether the input uses a const qualifier or not (only relevant for GLSL).
*
* @type {boolean}
* @default false
*/
this.isConst = isConst;
}
}
NodeFunctionInput.isNodeFunctionInput = true;
/**
* This module uses cache management to create temporary variables
* if the node is used more than once to prevent duplicate calculations.
*
* The class acts as a base class for many other nodes types.
*
* @deprecated Extend Node instead.
* @augments Node
*/
class TempNode extends Node {
static get type() {
return 'TempNode';
}
/**
* Constructs a temp node.
*
* @param {?string} nodeType - The node type.
*/
constructor( nodeType = null ) {
super( nodeType );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isTempNode = true;
warn( 'TempNode: This module has been deprecated. Extend Node instead.' );
}
isCacheable( builder ) {
return this.hasDependencies( builder );
}
/**
* Whether this node is used more than once in context of other nodes.
*
* @param {NodeBuilder} builder - The node builder.
* @return {boolean} A flag that indicates if there is more than one dependency to other nodes.
*/
hasDependencies( builder ) {
return builder.getDataFromNode( this ).usageCount > 1;
}
}
/**
* Module for representing ambient lights as nodes.
*
* @augments AnalyticLightNode
*/
class AmbientLightNode extends AnalyticLightNode {
static get type() {
return 'AmbientLightNode';
}
/**
* Constructs a new ambient light node.
*
* @param {?AmbientLight} [light=null] - The ambient light source.
*/
constructor( light = null ) {
super( light );
}
setup( { context } ) {
context.irradiance.addAssign( this.colorNode );
}
}
/**
* Module for representing directional lights as nodes.
*
* @augments AnalyticLightNode
*/
class DirectionalLightNode extends AnalyticLightNode {
static get type() {
return 'DirectionalLightNode';
}
/**
* Constructs a new directional light node.
*
* @param {?DirectionalLight} [light=null] - The directional light source.
*/
constructor( light = null ) {
super( light );
}
setupDirect() {
const lightColor = this.colorNode;
const lightDirection = lightTargetDirection( this.light );
return { lightDirection, lightColor };
}
}
/**
* Module for representing hemisphere lights as nodes.
*
* @augments AnalyticLightNode
*/
class HemisphereLightNode extends AnalyticLightNode {
static get type() {
return 'HemisphereLightNode';
}
/**
* Constructs a new hemisphere light node.
*
* @param {?HemisphereLight} [light=null] - The hemisphere light source.
*/
constructor( light = null ) {
super( light );
/**
* Uniform node representing the light's position.
*
* @type {UniformNode<vec3>}
*/
this.lightPositionNode = lightPosition( light );
/**
* A node representing the light's direction.
*
* @type {Node<vec3>}
*/
this.lightDirectionNode = this.lightPositionNode.normalize();
/**
* Uniform node representing the light's ground color.
*
* @type {UniformNode<vec3>}
*/
this.groundColorNode = uniform( new Color() ).setGroup( renderGroup );
}
/**
* Overwritten to updated hemisphere light specific uniforms.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
update( frame ) {
const { light } = this;
super.update( frame );
this.lightPositionNode.object3d = light;
this.groundColorNode.value.copy( light.groundColor ).multiplyScalar( light.intensity );
}
setup( builder ) {
const { colorNode, groundColorNode, lightDirectionNode } = this;
const dotNL = normalWorld.dot( lightDirectionNode );
const hemiDiffuseWeight = dotNL.mul( 0.5 ).add( 0.5 );
const irradiance = mix( groundColorNode, colorNode, hemiDiffuseWeight );
builder.context.irradiance.addAssign( irradiance );
}
}
/**
* Module for representing spot lights as nodes.
*
* @augments AnalyticLightNode
*/
class SpotLightNode extends AnalyticLightNode {
static get type() {
return 'SpotLightNode';
}
/**
* Constructs a new spot light node.
*
* @param {?SpotLight} [light=null] - The spot light source.
*/
constructor( light = null ) {
super( light );
/**
* Uniform node representing the cone cosine.
*
* @type {UniformNode<float>}
*/
this.coneCosNode = uniform( 0 ).setGroup( renderGroup );
/**
* Uniform node representing the penumbra cosine.
*
* @type {UniformNode<float>}
*/
this.penumbraCosNode = uniform( 0 ).setGroup( renderGroup );
/**
* Uniform node representing the cutoff distance.
*
* @type {UniformNode<float>}
*/
this.cutoffDistanceNode = uniform( 0 ).setGroup( renderGroup );
/**
* Uniform node representing the decay exponent.
*
* @type {UniformNode<float>}
*/
this.decayExponentNode = uniform( 0 ).setGroup( renderGroup );
/**
* Uniform node representing the light color.
*
* @type {UniformNode<Color>}
*/
this.colorNode = uniform( this.color ).setGroup( renderGroup );
}
/**
* Overwritten to updated spot light specific uniforms.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
update( frame ) {
super.update( frame );
const { light } = this;
this.coneCosNode.value = Math.cos( light.angle );
this.penumbraCosNode.value = Math.cos( light.angle * ( 1 - light.penumbra ) );
this.cutoffDistanceNode.value = light.distance;
this.decayExponentNode.value = light.decay;
}
/**
* Computes the spot attenuation for the given angle.
*
* @param {NodeBuilder} builder - The node builder.
* @param {Node<float>} angleCosine - The angle to compute the spot attenuation for.
* @return {Node<float>} The spot attenuation.
*/
getSpotAttenuation( builder, angleCosine ) {
const { coneCosNode, penumbraCosNode } = this;
return smoothstep( coneCosNode, penumbraCosNode, angleCosine );
}
getLightCoord( builder ) {
const properties = builder.getNodeProperties( this );
let projectionUV = properties.projectionUV;
if ( projectionUV === undefined ) {
projectionUV = lightProjectionUV( this.light, builder.context.positionWorld );
properties.projectionUV = projectionUV;
}
return projectionUV;
}
setupDirect( builder ) {
const { colorNode, cutoffDistanceNode, decayExponentNode, light } = this;
const lightVector = this.getLightVector( builder );
const lightDirection = lightVector.normalize();
const angleCos = lightDirection.dot( lightTargetDirection( light ) );
const spotAttenuation = this.getSpotAttenuation( builder, angleCos );
const lightDistance = lightVector.length();
const lightAttenuation = getDistanceAttenuation( {
lightDistance,
cutoffDistance: cutoffDistanceNode,
decayExponent: decayExponentNode
} );
let lightColor = colorNode.mul( spotAttenuation ).mul( lightAttenuation );
let projected, lightCoord;
if ( light.colorNode ) {
lightCoord = this.getLightCoord( builder );
projected = light.colorNode( lightCoord );
} else if ( light.map ) {
lightCoord = this.getLightCoord( builder );
projected = texture( light.map, lightCoord.xy ).onRenderUpdate( () => light.map );
}
if ( projected ) {
const inSpotLightMap = lightCoord.mul( 2. ).sub( 1. ).abs().lessThan( 1. ).all();
lightColor = inSpotLightMap.select( lightColor.mul( projected ), lightColor );
}
return { lightColor, lightDirection };
}
}
/**
* An IES version of the default spot light node.
*
* @augments SpotLightNode
*/
class IESSpotLightNode extends SpotLightNode {
static get type() {
return 'IESSpotLightNode';
}
/**
* Constructs a new IES spot light node.
*
* @param {?SpotLight} [light=null] - The spot light source.
*/
constructor( light = null ) {
super( light );
/**
* The texture node representing the IES texture.
*
* @type {?TextureNode}
* @default null
*/
this._iesTextureNode = null;
}
/**
* Overwrites the default implementation to compute an IES conform spot attenuation.
*
* @param {NodeBuilder} builder - The node builder.
* @param {Node<float>} angleCosine - The angle to compute the spot attenuation for.
* @return {Node<float>} The spot attenuation.
*/
getSpotAttenuation( builder, angleCosine ) {
const iesMap = this.light.iesMap;
let spotAttenuation = null;
if ( iesMap && iesMap.isTexture === true ) {
// the light space coordinate used for projected maps, centered on the forward axis twist calculation
const lightCoord = this.getLightCoord( builder ).sub( 0.5 );
this._iesTextureNode = texture( iesMap );
// get the width of half a texel in uv
const texelInset = float( 0.5 ).div( vec2( textureSize( this._iesTextureNode ) ) );
// the twist angle around the light's forward axis, mapping from [0, 359]deg texels
// offset by half a texel so we start at the center of the first texel
const twistAngle = remap( atan( lightCoord.y, lightCoord.x ), - Math.PI, Math.PI ).add( texelInset.y );
// the tilt angle off the forward axis spanning from [0, 180]deg
// inset by half a texel on each side so we're clamping to the center of the extreme texels
const tiltAngle = remap( angleCosine.acos(), 0, Math.PI, texelInset.x, float( 1 ).sub( texelInset.x ) );
spotAttenuation = this._iesTextureNode.sample( vec2( tiltAngle, twistAngle ) ).level( 0 ).r;
} else {
spotAttenuation = super.getSpotAttenuation( builder, angleCosine );
}
return spotAttenuation;
}
/**
* Overwritten to update the IES spot light texture.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
update( frame ) {
super.update( frame );
if ( this._iesTextureNode !== null && this.light.iesMap ) {
this._iesTextureNode.value = this.light.iesMap;
}
}
}
/**
* Module for representing light probes as nodes.
*
* @augments AnalyticLightNode
*/
class LightProbeNode extends AnalyticLightNode {
static get type() {
return 'LightProbeNode';
}
/**
* Constructs a new light probe node.
*
* @param {?LightProbe} [light=null] - The light probe.
*/
constructor( light = null ) {
super( light );
const array = [];
for ( let i = 0; i < 9; i ++ ) array.push( new Vector3() );
/**
* Light probe represented as a uniform of spherical harmonics.
*
* @type {UniformArrayNode}
*/
this.lightProbe = uniformArray( array ).setGroup( renderGroup );
}
/**
* Overwritten to updated light probe specific uniforms.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
update( frame ) {
const { light } = this;
super.update( frame );
//
for ( let i = 0; i < 9; i ++ ) {
this.lightProbe.array[ i ].copy( light.sh.coefficients[ i ] ).multiplyScalar( light.intensity );
}
}
setup( builder ) {
const irradiance = getShIrradianceAt( normalWorld, this.lightProbe );
builder.context.irradiance.addAssign( irradiance );
}
}
const sdBox = /*@__PURE__*/ Fn( ( [ p, b ] ) => {
const d = p.abs().sub( b );
return length( max( d, 0.0 ) ).add( min( max( d.x, d.y ), 0.0 ) );
}, { p: 'vec2', b: 'vec2', return: 'float' } );
/**
* An implementation of a projector light node.
*
* @augments SpotLightNode
*/
class ProjectorLightNode extends SpotLightNode {
static get type() {
return 'ProjectorLightNode';
}
update( frame ) {
super.update( frame );
const light = this.light;
this.penumbraCosNode.value = Math.min( Math.cos( light.angle * ( 1 - light.penumbra ) ), .99999 );
if ( light.aspect === null ) {
let aspect = 1;
if ( light.map !== null ) {
aspect = light.map.width / light.map.height;
}
light.shadow.aspect = aspect;
} else {
light.shadow.aspect = light.aspect;
}
}
/**
* Overwrites the default implementation to compute projection attenuation.
*
* @param {NodeBuilder} builder - The node builder.
* @return {Node<float>} The spot attenuation.
*/
getSpotAttenuation( builder ) {
const attenuation = float( 0 );
const penumbraCos = this.penumbraCosNode;
// compute the fragment's position in the light's clip space
const spotLightCoord = lightShadowMatrix( this.light ).mul( builder.context.positionWorld || positionWorld );
// the sign of w determines whether the current fragment is in front or behind the light.
// to avoid a back-projection, it's important to only compute an attenuation if w is positive
If( spotLightCoord.w.greaterThan( 0 ), () => {
const projectionUV = spotLightCoord.xyz.div( spotLightCoord.w );
const boxDist = sdBox( projectionUV.xy.sub( vec2( 0.5 ) ), vec2( 0.5 ) );
const angleFactor = div( -1, sub( 1.0, acos( penumbraCos ) ).sub( 1.0 ) );
attenuation.assign( saturate( boxDist.mul( -2 ).mul( angleFactor ) ) );
} );
return attenuation;
}
}
const _matrix4 = /*@__PURE__*/ new Matrix4();
let _ltcLib = null;
/**
* Module for representing rect area lights as nodes.
*
* @augments AnalyticLightNode
*/
class RectAreaLightNode extends AnalyticLightNode {
static get type() {
return 'RectAreaLightNode';
}
/**
* Constructs a new rect area light node.
*
* @param {?RectAreaLight} [light=null] - The rect area light source.
*/
constructor( light = null ) {
super( light );
/**
* Uniform node representing the half height of the are light.
*
* @type {UniformNode<vec3>}
*/
this.halfHeight = uniform( new Vector3() ).setGroup( renderGroup );
/**
* Uniform node representing the half width of the are light.
*
* @type {UniformNode<vec3>}
*/
this.halfWidth = uniform( new Vector3() ).setGroup( renderGroup );
/**
* The `updateType` is set to `NodeUpdateType.RENDER` since the light's
* world matrix might vary per render call.
*
* @type {string}
* @default 'render'
*/
this.updateType = NodeUpdateType.RENDER;
}
/**
* Overwritten to updated rect area light specific uniforms.
*
* @param {NodeFrame} frame - A reference to the current node frame.
*/
update( frame ) {
super.update( frame );
const { light } = this;
_matrix4.extractRotation( light.matrixWorld );
this.halfWidth.value.set( light.width * 0.5, 0.0, 0.0 );
this.halfHeight.value.set( 0.0, light.height * 0.5, 0.0 );
this.halfWidth.value.applyMatrix4( _matrix4 );
this.halfHeight.value.applyMatrix4( _matrix4 );
}
setupDirectRectArea( builder ) {
let ltc_1, ltc_2;
if ( builder.isAvailable( 'float32Filterable' ) ) {
ltc_1 = texture( _ltcLib.LTC_FLOAT_1 );
ltc_2 = texture( _ltcLib.LTC_FLOAT_2 );
} else {
ltc_1 = texture( _ltcLib.LTC_HALF_1 );
ltc_2 = texture( _ltcLib.LTC_HALF_2 );
}
const { colorNode, light } = this;
const lightPosition = lightViewPosition( light );
const halfWidth = cameraViewMatrix.mul( vec4( this.halfWidth, 0 ) ).xyz;
const halfHeight = cameraViewMatrix.mul( vec4( this.halfHeight, 0 ) ).xyz;
return {
lightColor: colorNode,
lightPosition,
halfWidth,
halfHeight,
ltc_1,
ltc_2
};
}
/**
* Used to configure the internal BRDF approximation texture data.
*
* @param {RectAreaLightTexturesLib} ltc - The BRDF approximation texture data.
*/
static setLTC( ltc ) {
_ltcLib = ltc;
}
}
/**
* Base class for node parsers. A derived parser must be implemented
* for each supported native shader language.
*/
class NodeParser {
/**
* The method parses the given native code an returns a node function.
*
* @abstract
* @param {string} source - The native shader code.
* @return {NodeFunction} A node function.
*/
parseFunction( /*source*/ ) {
warn( 'Abstract function.' );
}
}
/**
* Base class for node functions. A derived module must be implemented
* for each supported native shader language. Similar to other `Node*` modules,
* this class is only relevant during the building process and not used
* in user-level code.
*/
class NodeFunction {
/**
* Constructs a new node function.
*
* @param {string} type - The node type. This type is the return type of the node function.
* @param {Array<NodeFunctionInput>} inputs - The function's inputs.
* @param {string} [name=''] - The function's name.
* @param {string} [precision=''] - The precision qualifier.
*/
constructor( type, inputs, name = '', precision = '' ) {
/**
* The node type. This type is the return type of the node function.
*
* @type {string}
*/
this.type = type;
/**
* The function's inputs.
*
* @type {Array<NodeFunctionInput>}
*/
this.inputs = inputs;
/**
* The name of the uniform.
*
* @type {string}
* @default ''
*/
this.name = name;
/**
* The precision qualifier.
*
* @type {string}
* @default ''
*/
this.precision = precision;
}
/**
* This method returns the native code of the node function.
*
* @abstract
* @param {string} name - The function's name.
* @return {string} A shader code.
*/
getCode( /*name = this.name*/ ) {
warn( 'Abstract function.' );
}
}
NodeFunction.isNodeFunction = true;
const declarationRegexp$1 = /^\s*(highp|mediump|lowp)?\s*([a-z_0-9]+)\s*([a-z_0-9]+)?\s*\(([\s\S]*?)\)/i;
const propertiesRegexp$1 = /[a-z_0-9]+/ig;
const pragmaMain = '#pragma main';
const parse$1 = ( source ) => {
source = source.trim();
const pragmaMainIndex = source.indexOf( pragmaMain );
const mainCode = ( pragmaMainIndex !== -1 ? source.slice( pragmaMainIndex + pragmaMain.length ) : source ).replace( /^(?:\s*\/\/[^\r\n]*|\s*\/\*[\s\S]*?\*\/|\s*)+/, '' );
const declaration = mainCode.match( declarationRegexp$1 );
if ( declaration !== null && declaration.length === 5 ) {
// tokenizer
const inputsCode = declaration[ 4 ];
const propsMatches = [];
let nameMatch = null;
while ( ( nameMatch = propertiesRegexp$1.exec( inputsCode ) ) !== null ) {
propsMatches.push( nameMatch );
}
// parser
const inputs = [];
let i = 0;
while ( i < propsMatches.length ) {
const isConst = propsMatches[ i ][ 0 ] === 'const';
if ( isConst === true ) {
i ++;
}
let qualifier = propsMatches[ i ][ 0 ];
if ( qualifier === 'in' || qualifier === 'out' || qualifier === 'inout' ) {
i ++;
} else {
qualifier = '';
}
const type = propsMatches[ i ++ ][ 0 ];
let count = Number.parseInt( propsMatches[ i ][ 0 ] );
if ( Number.isNaN( count ) === false ) i ++;
else count = null;
const name = propsMatches[ i ++ ][ 0 ];
inputs.push( new NodeFunctionInput( type, name, count, qualifier, isConst ) );
}
//
const blockCode = mainCode.substring( declaration[ 0 ].length );
const name = declaration[ 3 ] !== undefined ? declaration[ 3 ] : '';
const type = declaration[ 2 ];
const precision = declaration[ 1 ] !== undefined ? declaration[ 1 ] : '';
const headerCode = pragmaMainIndex !== -1 ? source.slice( 0, pragmaMainIndex ) : '';
return {
type,
inputs,
name,
precision,
inputsCode,
blockCode,
headerCode
};
} else {
throw new Error( 'THREE.FunctionNode: Function is not a GLSL code.' );
}
};
/**
* This class represents a GLSL node function.
*
* @augments NodeFunction
*/
class GLSLNodeFunction extends NodeFunction {
/**
* Constructs a new GLSL node function.
*
* @param {string} source - The GLSL source.
*/
constructor( source ) {
const { type, inputs, name, precision, inputsCode, blockCode, headerCode } = parse$1( source );
super( type, inputs, name, precision );
this.inputsCode = inputsCode;
this.blockCode = blockCode;
this.headerCode = headerCode;
}
/**
* This method returns the GLSL code of the node function.
*
* @param {string} [name=this.name] - The function's name.
* @return {string} The shader code.
*/
getCode( name = this.name ) {
let code;
const blockCode = this.blockCode;
if ( blockCode !== '' ) {
const { type, inputsCode, headerCode, precision } = this;
let declarationCode = `${ type } ${ name } ( ${ inputsCode.trim() } )`;
if ( precision !== '' ) {
declarationCode = `${ precision } ${ declarationCode }`;
}
code = headerCode + declarationCode + blockCode;
} else {
// interface function
code = '';
}
return code;
}
}
/**
* A GLSL node parser.
*
* @augments NodeParser
*/
class GLSLNodeParser extends NodeParser {
/**
* The method parses the given GLSL code an returns a node function.
*
* @param {string} source - The GLSL code.
* @return {GLSLNodeFunction} A node function.
*/
parseFunction( source ) {
return new GLSLNodeFunction( source );
}
}
const _chainKeys$1 = [];
const _cacheKeyValues = [];
// Dedicated uniform for output pass array layer selection
// This is separate from cameraIndex to avoid the sharedUniformGroup complexity
const _outputLayerIndex = /*@__PURE__*/ uniform( 0, 'int' ).setGroup( renderGroup );
/**
* This renderer module manages node-related objects and is the
* primary interface between the renderer and the node system.
*
* @private
* @augments DataMap
*/
class NodeManager extends DataMap {
/**
* Constructs a new nodes management component.
*
* @param {Renderer} renderer - The renderer.
* @param {Backend} backend - The renderer's backend.
*/
constructor( renderer, backend ) {
super();
/**
* The renderer.
*
* @type {Renderer}
*/
this.renderer = renderer;
/**
* The renderer's backend.
*
* @type {Backend}
*/
this.backend = backend;
/**
* The node frame.
*
* @type {Renderer}
*/
this.nodeFrame = new NodeFrame();
/**
* A cache for managing node builder states.
*
* @type {Map<number,NodeBuilderState>}
*/
this.nodeBuilderCache = new Map();
/**
* A cache for managing data cache key data.
*
* @type {ChainMap}
*/
this.callHashCache = new ChainMap();
/**
* A cache for managing node uniforms group data.
*
* @type {ChainMap}
*/
this.groupsData = new ChainMap();
/**
* Queue for pending async builds to limit concurrent compilation.
*
* @private
* @type {Array<Function>}
*/
this._buildQueue = [];
/**
* Whether an async build is currently in progress.
*
* @private
* @type {boolean}
*/
this._buildInProgress = false;
/**
* A cache for managing node objects of
* scene properties like fog or environments.
*
* @type {Object<string,WeakMap>}
*/
this.cacheLib = {};
}
/**
* Returns `true` if the given node uniforms group must be updated or not.
*
* @param {NodeUniformsGroup} nodeUniformsGroup - The node uniforms group.
* @return {boolean} Whether the node uniforms group requires an update or not.
*/
updateGroup( nodeUniformsGroup ) {
const groupNode = nodeUniformsGroup.groupNode;
// groups that are updated per object always require an update so no further checks are needed
if ( groupNode.updateType === NodeUpdateType.OBJECT ) return true;
// check for update
_chainKeys$1[ 0 ] = groupNode;
_chainKeys$1[ 1 ] = nodeUniformsGroup;
let groupData = this.groupsData.get( _chainKeys$1 );
if ( groupData === undefined ) this.groupsData.set( _chainKeys$1, groupData = {} );
_chainKeys$1[ 0 ] = null;
_chainKeys$1[ 1 ] = null;
if ( groupData.version !== groupNode.version ) {
groupData.version = groupNode.version;
return true;
}
return false;
}
/**
* Returns the cache key for the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @return {number} The cache key.
*/
getForRenderCacheKey( renderObject ) {
return renderObject.initialCacheKey;
}
/**
* Creates a node builder configured for the given render object and material.
*
* @private
* @param {RenderObject} renderObject - The render object.
* @param {Material} material - The material to use.
* @return {NodeBuilder} The configured node builder.
*/
_createNodeBuilder( renderObject, material ) {
const nodeBuilder = this.backend.createNodeBuilder( renderObject.object, this.renderer );
const onNodeBuilderCreated = this.renderer.debug.onNodeBuilderCreated;
if ( onNodeBuilderCreated !== null ) onNodeBuilderCreated( nodeBuilder, renderObject );
nodeBuilder.scene = renderObject.scene;
nodeBuilder.material = material;
nodeBuilder.camera = renderObject.camera;
nodeBuilder.context.material = material;
nodeBuilder.lightsNode = renderObject.lightsNode;
nodeBuilder.environmentNode = this.getEnvironmentNode( renderObject.scene );
nodeBuilder.fogNode = this.getFogNode( renderObject.scene );
nodeBuilder.clippingContext = renderObject.clippingContext;
if ( this.renderer.getOutputRenderTarget() ? this.renderer.getOutputRenderTarget().multiview : false ) {
nodeBuilder.enableMultiview();
}
return nodeBuilder;
}
/**
* Logs an error thrown while building a node material.
*
* @private
* @param {Error} e - The build error.
*/
_reportBuildError( e ) {
let stackTrace = e.stackTrace;
if ( ! stackTrace && e.stack ) {
// Capture stack trace for JavaScript errors
stackTrace = new StackTrace( e.stack );
}
error( 'TSL: ' + e, stackTrace );
}
/**
* Returns a node builder state for the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @param {?Function} [yieldFn=null] - If set, the build is async and yields to the main thread via this function.
* @return {NodeBuilderState|Promise<NodeBuilderState>} The node builder state (or Promise if async).
*/
getForRender( renderObject, yieldFn = null ) {
const renderObjectData = this.get( renderObject );
let nodeBuilderState = renderObjectData.nodeBuilderState;
if ( nodeBuilderState === undefined ) {
const { nodeBuilderCache } = this;
const cacheKey = this.getForRenderCacheKey( renderObject );
nodeBuilderState = nodeBuilderCache.get( cacheKey );
if ( nodeBuilderState === undefined ) {
const buildNodeBuilder = async () => {
let nodeBuilder = this._createNodeBuilder( renderObject, renderObject.material );
try {
if ( yieldFn !== null ) {
await nodeBuilder.buildAsync( yieldFn );
} else {
nodeBuilder.build();
}
} catch ( e ) {
nodeBuilder = this._createNodeBuilder( renderObject, new NodeMaterial() );
if ( yieldFn !== null ) {
await nodeBuilder.buildAsync( yieldFn );
} else {
nodeBuilder.build();
}
this._reportBuildError( e );
}
return nodeBuilder;
};
if ( yieldFn !== null ) {
return buildNodeBuilder().then( ( nodeBuilder ) => {
nodeBuilderState = this._createNodeBuilderState( nodeBuilder );
if ( nodeBuilder.objectSpecific === false ) nodeBuilderCache.set( cacheKey, nodeBuilderState );
nodeBuilderState.usedTimes ++;
renderObjectData.nodeBuilderState = nodeBuilderState;
return nodeBuilderState;
} );
} else {
// Synchronous path - call buildNodeBuilder but don't await
let nodeBuilder = this._createNodeBuilder( renderObject, renderObject.material );
try {
nodeBuilder.build();
} catch ( e ) {
nodeBuilder = this._createNodeBuilder( renderObject, new NodeMaterial() );
nodeBuilder.build();
this._reportBuildError( e );
}
nodeBuilderState = this._createNodeBuilderState( nodeBuilder );
if ( nodeBuilder.objectSpecific === false ) nodeBuilderCache.set( cacheKey, nodeBuilderState );
}
}
nodeBuilderState.usedTimes ++;
renderObjectData.nodeBuilderState = nodeBuilderState;
}
return nodeBuilderState;
}
/**
* Async version of getForRender() that yields to main thread during build.
* Use this in compileAsync() to prevent blocking the main thread.
*
* @param {RenderObject} renderObject - The render object.
* @param {Function} [yieldFn=yieldToMain] - The function used to yield to the main thread.
* @return {Promise<NodeBuilderState>} A promise that resolves to the node builder state.
*/
getForRenderAsync( renderObject, yieldFn = yieldToMain ) {
const result = this.getForRender( renderObject, yieldFn );
// Ensure we always return a Promise (cache hit returns nodeBuilderState directly)
if ( result.then ) {
return result;
}
return Promise.resolve( result );
}
/**
* Returns nodeBuilderState if ready, null if pending async build.
* Queues async build on first call for cache miss.
* Use this in render() path to enable non-blocking compilation.
*
* @param {RenderObject} renderObject - The render object.
* @return {?NodeBuilderState} The node builder state, or null if still building.
*/
getForRenderDeferred( renderObject ) {
const renderObjectData = this.get( renderObject );
// Already built for this renderObject
if ( renderObjectData.nodeBuilderState !== undefined ) {
return renderObjectData.nodeBuilderState;
}
// Check cache with stable key
const cacheKey = this.getForRenderCacheKey( renderObject );
const nodeBuilderState = this.nodeBuilderCache.get( cacheKey );
if ( nodeBuilderState !== undefined ) {
// Cache hit - use it
nodeBuilderState.usedTimes ++;
renderObjectData.nodeBuilderState = nodeBuilderState;
return nodeBuilderState;
}
// Cache miss - check if async build already queued
if ( renderObjectData.pendingBuild !== true ) {
// Mark as pending and add to build queue
renderObjectData.pendingBuild = true;
this._buildQueue.push( () => {
return this.getForRenderAsync( renderObject ).then( () => {
renderObjectData.pendingBuild = false;
} );
} );
// Start processing queue if not already running
this._processBuildQueue();
}
return null; // Not ready
}
/**
* Processes the build queue one item at a time.
* This ensures builds don't all run simultaneously and freeze the main thread.
*
* @private
*/
_processBuildQueue() {
if ( this._buildInProgress || this._buildQueue.length === 0 ) {
return;
}
this._buildInProgress = true;
const buildFn = this._buildQueue.shift();
buildFn().then( () => {
this._buildInProgress = false;
// Process next item in queue
this._processBuildQueue();
} );
}
/**
* Deletes the given object from the internal data map
*
* @param {any} object - The object to delete.
* @return {?Object} The deleted dictionary.
*/
delete( object ) {
if ( object.isRenderObject ) {
const nodeBuilderState = this.get( object ).nodeBuilderState;
if ( nodeBuilderState !== undefined ) {
nodeBuilderState.usedTimes --;
if ( nodeBuilderState.usedTimes === 0 ) {
this.nodeBuilderCache.delete( this.getForRenderCacheKey( object ) );
}
}
}
return super.delete( object );
}
/**
* Returns a node builder state for the given compute node.
*
* @param {Node} computeNode - The compute node.
* @param {boolean} [useAsync=false] - Whether to use async build with yielding.
* @param {Function} [yieldFn=yieldToMain] - The function used to yield to the main thread in an async build.
* @return {NodeBuilderState|Promise<NodeBuilderState>} The node builder state (or Promise if async).
*/
getForCompute( computeNode, useAsync = false, yieldFn = yieldToMain ) {
const computeData = this.get( computeNode );
let nodeBuilderState = computeData.nodeBuilderState;
if ( nodeBuilderState === undefined || computeData.version !== computeNode.version ) {
const nodeBuilder = this.backend.createNodeBuilder( null, this.renderer );
nodeBuilder.compute = computeNode;
const onNodeBuilderCreated = this.renderer.debug.onNodeBuilderCreated;
if ( onNodeBuilderCreated !== null ) onNodeBuilderCreated( nodeBuilder, computeNode );
if ( useAsync ) {
return nodeBuilder.buildAsync( yieldFn ).then( () => {
nodeBuilderState = this._createNodeBuilderState( nodeBuilder );
computeData.nodeBuilderState = nodeBuilderState;
computeData.version = computeNode.version;
return nodeBuilderState;
} );
}
nodeBuilder.build();
nodeBuilderState = this._createNodeBuilderState( nodeBuilder );
computeData.nodeBuilderState = nodeBuilderState;
computeData.version = computeNode.version;
}
return nodeBuilderState;
}
/**
* Async version of getForCompute() that yields to main thread during build.
* Use this in compileComputeAsync() to prevent blocking the main thread.
*
* @param {Node} computeNode - The compute node.
* @param {Function} [yieldFn=yieldToMain] - The function used to yield to the main thread.
* @return {Promise<NodeBuilderState>} A promise that resolves to the node builder state.
*/
getForComputeAsync( computeNode, yieldFn = yieldToMain ) {
const result = this.getForCompute( computeNode, true, yieldFn );
if ( result.then ) {
return result;
}
return Promise.resolve( result );
}
/**
* Creates a node builder state for the given node builder.
*
* @private
* @param {NodeBuilder} nodeBuilder - The node builder.
* @return {NodeBuilderState} The node builder state.
*/
_createNodeBuilderState( nodeBuilder ) {
return new NodeBuilderState(
nodeBuilder.vertexShader,
nodeBuilder.fragmentShader,
nodeBuilder.computeShader,
nodeBuilder.getAttributesArray(),
nodeBuilder.getBindings(),
nodeBuilder.updateNodes,
nodeBuilder.updateBeforeNodes,
nodeBuilder.updateAfterNodes,
nodeBuilder.observer,
nodeBuilder.hardwareClipping,
nodeBuilder.transforms
);
}
/**
* Returns an environment node for the current configured
* scene environment.
*
* @param {Scene} scene - The scene.
* @return {Node} A node representing the current scene environment.
*/
getEnvironmentNode( scene ) {
if ( this.renderer.lighting.enabled === false ) return null;
this.updateEnvironment( scene );
let environmentNode = null;
if ( scene.environmentNode && scene.environmentNode.isNode ) {
environmentNode = scene.environmentNode;
} else {
const sceneData = this.get( scene );
if ( sceneData.environmentNode ) {
environmentNode = sceneData.environmentNode;
}
}
return environmentNode;
}
/**
* Returns a background node for the current configured
* scene background.
*
* @param {Scene} scene - The scene.
* @return {Node} A node representing the current scene background.
*/
getBackgroundNode( scene ) {
this.updateBackground( scene );
let backgroundNode = null;
if ( scene.backgroundNode && scene.backgroundNode.isNode ) {
backgroundNode = scene.backgroundNode;
} else {
const sceneData = this.get( scene );
if ( sceneData.backgroundNode ) {
backgroundNode = sceneData.backgroundNode;
}
}
return backgroundNode;
}
/**
* Returns a fog node for the current configured scene fog.
*
* @param {Scene} scene - The scene.
* @return {Node} A node representing the current scene fog.
*/
getFogNode( scene ) {
this.updateFog( scene );
return scene.fogNode || this.get( scene ).fogNode || null;
}
/**
* Returns the custom program cache key of the given material. Since
* computing the key traverses the material's node graph, the result
* is cached per material version if possible.
*
* @param {Material} material - The material.
* @param {Scene} scene - The scene.
* @return {string} The custom program cache key.
*/
getCustomProgramCacheKey( material, scene ) {
if ( material === scene.overrideMaterial ) {
// override materials can't be cached since they dynamically change node properties
return material.customProgramCacheKey();
} else {
const materialData = this.get( material );
if ( materialData.cacheKeyVersion !== material.version ) {
materialData.cacheKeyVersion = material.version;
materialData.cacheKey = material.customProgramCacheKey();
}
return materialData.cacheKey;
}
}
/**
* Returns a cache key for the given scene and lights node.
* This key is used by `RenderObject` as a part of the dynamic
* cache key (a key that must be checked every time the render
* objects is drawn).
*
* @param {Scene} scene - The scene.
* @param {LightsNode} lightsNode - The lights node.
* @return {number} The cache key.
*/
getCacheKey( scene, lightsNode ) {
_chainKeys$1[ 0 ] = scene;
_chainKeys$1[ 1 ] = lightsNode;
const callId = this.renderer.info.calls;
const cacheKeyData = this.callHashCache.get( _chainKeys$1 ) || {};
if ( cacheKeyData.callId !== callId ) {
_cacheKeyValues.push( this.renderer.getOutputRenderTarget() && this.renderer.getOutputRenderTarget().multiview ? 1 : 0 );
_cacheKeyValues.push( this.renderer.lighting.enabled ? 1 : 0 );
if ( this.renderer.lighting.enabled ) {
_cacheKeyValues.push( lightsNode.getCacheKey( true ) );
_cacheKeyValues.push( this.renderer.shadowMap.enabled ? 1 : 0 );
_cacheKeyValues.push( this.renderer.shadowMap.type );
const environmentNode = this.getEnvironmentNode( scene );
if ( environmentNode ) _cacheKeyValues.push( environmentNode.getCacheKey() );
}
const fogNode = this.getFogNode( scene );
if ( fogNode ) _cacheKeyValues.push( fogNode.getCacheKey() );
cacheKeyData.callId = callId;
cacheKeyData.cacheKey = hashArray( _cacheKeyValues );
this.callHashCache.set( _chainKeys$1, cacheKeyData );
_cacheKeyValues.length = 0;
}
_chainKeys$1[ 0 ] = null;
_chainKeys$1[ 1 ] = null;
return cacheKeyData.cacheKey;
}
/**
* A boolean that indicates whether tone mapping should be enabled
* or not.
*
* @type {boolean}
*/
get isToneMappingState() {
return this.renderer.getRenderTarget() ? false : true;
}
/**
* If a scene background is configured, this method makes sure to
* represent the background with a corresponding node-based implementation.
*
* @param {Scene} scene - The scene.
*/
updateBackground( scene ) {
const sceneData = this.get( scene );
const background = scene.background;
if ( background ) {
const forceUpdate = ( scene.backgroundBlurriness === 0 && sceneData.backgroundBlurriness > 0 ) || ( scene.backgroundBlurriness > 0 && sceneData.backgroundBlurriness === 0 );
if ( sceneData.background !== background || forceUpdate ) {
const backgroundNode = this.getCacheNode( 'background', background, () => {
if ( background.isCubeTexture === true || background.mapping === EquirectangularReflectionMapping || background.mapping === EquirectangularRefractionMapping ) {
if ( scene.backgroundBlurriness > 0 || background.isPMREMTexture === true ) {
return pmremTexture( background );
} else {
let envMap;
if ( background.isCubeTexture === true ) {
envMap = cubeTexture( background );
} else {
envMap = texture( background );
}
return cubeMapNode( envMap );
}
} else if ( background.isTexture === true ) {
return texture( background, screenUV.flipY() ).setUpdateMatrix( true );
} else if ( background.isColor !== true ) {
error( 'WebGPUNodes: Unsupported background configuration.', background );
}
}, forceUpdate );
sceneData.backgroundNode = backgroundNode;
sceneData.background = background;
sceneData.backgroundBlurriness = scene.backgroundBlurriness;
}
} else if ( sceneData.backgroundNode ) {
delete sceneData.backgroundNode;
delete sceneData.background;
}
}
/**
* This method is part of the caching of nodes which are used to represents the
* scene's background, fog or environment.
*
* @param {string} type - The type of object to cache.
* @param {Object} object - The object.
* @param {Function} callback - A callback that produces a node representation for the given object.
* @param {boolean} [forceUpdate=false] - Whether an update should be enforced or not.
* @return {Node} The node representation.
*/
getCacheNode( type, object, callback, forceUpdate = false ) {
const nodeCache = this.cacheLib[ type ] || ( this.cacheLib[ type ] = new WeakMap() );
let node = nodeCache.get( object );
if ( node === undefined || forceUpdate ) {
if ( node === undefined && object.isTexture === true ) {
const onTextureDispose = () => {
object.removeEventListener( 'dispose', onTextureDispose );
const node = nodeCache.get( object );
if ( node !== undefined ) {
nodeCache.delete( object );
node.dispose();
}
};
object.addEventListener( 'dispose', onTextureDispose );
}
node = callback();
nodeCache.set( object, node );
}
return node;
}
/**
* If a scene fog is configured, this method makes sure to
* represent the fog with a corresponding node-based implementation.
*
* @param {Scene} scene - The scene.
*/
updateFog( scene ) {
const sceneData = this.get( scene );
const sceneFog = scene.fog;
if ( sceneFog ) {
if ( sceneData.fog !== sceneFog ) {
const fogNode = this.getCacheNode( 'fog', sceneFog, () => {
if ( sceneFog.isFogExp2 ) {
const color = reference$1( 'color', 'color', sceneFog ).setGroup( renderGroup );
const density = reference$1( 'density', 'float', sceneFog ).setGroup( renderGroup );
return fog( color, densityFogFactor( density ) );
} else if ( sceneFog.isFog ) {
const color = reference$1( 'color', 'color', sceneFog ).setGroup( renderGroup );
const near = reference$1( 'near', 'float', sceneFog ).setGroup( renderGroup );
const far = reference$1( 'far', 'float', sceneFog ).setGroup( renderGroup );
return fog( color, rangeFogFactor( near, far ) );
} else {
error( 'Renderer: Unsupported fog configuration.', sceneFog );
}
} );
sceneData.fogNode = fogNode;
sceneData.fog = sceneFog;
}
} else {
delete sceneData.fogNode;
delete sceneData.fog;
}
}
/**
* If a scene environment is configured, this method makes sure to
* represent the environment with a corresponding node-based implementation.
*
* @param {Scene} scene - The scene.
*/
updateEnvironment( scene ) {
const sceneData = this.get( scene );
const environment = scene.environment;
if ( environment ) {
if ( sceneData.environment !== environment ) {
const environmentNode = this.getCacheNode( 'environment', environment, () => {
if ( environment.isCubeTexture === true ) {
return cubeTexture( environment );
} else if ( environment.isTexture === true ) {
return texture( environment );
} else {
error( 'Nodes: Unsupported environment configuration.', environment );
}
} );
sceneData.environmentNode = environmentNode;
sceneData.environment = environment;
}
} else if ( sceneData.environmentNode ) {
delete sceneData.environmentNode;
delete sceneData.environment;
}
}
/**
* Returns a node frame configured with the given parameters.
*
* @param {Renderer} [renderer=this.renderer] - The renderer.
* @param {?Scene} [scene=null] - The scene.
* @param {?Object3D} [object=null] - The object.
* @param {?Camera} [camera=null] - The camera.
* @param {?Material} [material=null] - The material.
* @param {?Node} [compute=null] - The compute node.
* @return {NodeFrame} The node frame.
*/
getNodeFrame( renderer = this.renderer, scene = null, object = null, camera = null, material = null, compute = null ) {
const nodeFrame = this.nodeFrame;
nodeFrame.renderer = renderer;
nodeFrame.scene = scene;
nodeFrame.object = object;
nodeFrame.camera = camera;
nodeFrame.material = material;
nodeFrame.compute = compute;
return nodeFrame;
}
/**
* Returns a node frame configured for the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @return {NodeFrame} The node frame.
*/
getNodeFrameForRender( renderObject ) {
return this.getNodeFrame( renderObject.renderer, renderObject.scene, renderObject.object, renderObject.camera, renderObject.material );
}
/**
* Returns a node frame configured for the given compute node.
*
* @param {Node} computeNode - The compute node.
* @return {NodeFrame} The node frame.
*/
getNodeFrameForCompute( computeNode ) {
return this.getNodeFrame( this.renderer, null, null, null, null, computeNode );
}
/**
* Returns the current output cache key.
*
* @return {string} The output cache key.
*/
getOutputCacheKey() {
const renderer = this.renderer;
return renderer.toneMapping + ',' + renderer.currentColorSpace + ',' + renderer.xr.isPresenting;
}
/**
* Returns a node that represents the output configuration (tone mapping and
* color space) for the current target.
*
* @param {Texture} outputTarget - The output target.
* @return {Node} The output node.
*/
getOutputNode( outputTarget ) {
const renderer = this.renderer;
let output;
if ( outputTarget.isArrayTexture ) {
if ( this.backend.isWebGLBackend ) {
output = texture( outputTarget, screenUV ).depth( builtin( 'gl_ViewID_OVR' ) ).renderOutput( renderer.toneMapping, renderer.currentColorSpace );
} else {
output = texture( outputTarget, screenUV ).depth( _outputLayerIndex ).renderOutput( renderer.toneMapping, renderer.currentColorSpace );
}
} else {
output = texture( outputTarget, screenUV ).renderOutput( renderer.toneMapping, renderer.currentColorSpace );
}
return output;
}
/**
* Sets the output layer index for array texture output pass.
* This should be called before each layer render during the output pass.
*
* @param {number} index - The layer index.
*/
setOutputLayerIndex( index ) {
_outputLayerIndex.value = index;
}
/**
* Triggers the call of `updateBefore()` methods
* for all nodes of the given render object.
*
* @param {RenderObject} renderObject - The render object.
*/
updateBefore( renderObject ) {
const nodeBuilder = renderObject.getNodeBuilderState();
for ( const node of nodeBuilder.updateBeforeNodes ) {
// update frame state for each node
this.getNodeFrameForRender( renderObject ).updateBeforeNode( node );
}
}
/**
* Triggers the call of `updateAfter()` methods
* for all nodes of the given render object.
*
* @param {RenderObject} renderObject - The render object.
*/
updateAfter( renderObject ) {
const nodeBuilder = renderObject.getNodeBuilderState();
for ( const node of nodeBuilder.updateAfterNodes ) {
// update frame state for each node
this.getNodeFrameForRender( renderObject ).updateAfterNode( node );
}
}
/**
* Triggers the call of `updateBefore()` methods
* for all nodes of the given compute node.
*
* @param {Node} computeNode - The compute node.
*/
updateBeforeForCompute( computeNode ) {
const nodeBuilder = this.getForCompute( computeNode );
for ( const node of nodeBuilder.updateBeforeNodes ) {
// update frame state for each node
this.getNodeFrameForCompute( computeNode ).updateBeforeNode( node );
}
}
/**
* Triggers the call of `updateAfter()` methods
* for all nodes of the given compute node.
*
* @param {Node} computeNode - The compute node.
*/
updateAfterForCompute( computeNode ) {
const nodeBuilder = this.getForCompute( computeNode );
for ( const node of nodeBuilder.updateAfterNodes ) {
// update frame state for each node
this.getNodeFrameForCompute( computeNode ).updateAfterNode( node );
}
}
/**
* Triggers the call of `update()` methods
* for all nodes of the given compute node.
*
* @param {Node} computeNode - The compute node.
*/
updateForCompute( computeNode ) {
const nodeFrame = this.getNodeFrame();
const nodeBuilder = this.getForCompute( computeNode );
for ( const node of nodeBuilder.updateNodes ) {
nodeFrame.updateNode( node );
}
}
/**
* Triggers the call of `update()` methods
* for all nodes of the given render object.
*
* @param {RenderObject} renderObject - The render object.
*/
updateForRender( renderObject ) {
const nodeFrame = this.getNodeFrameForRender( renderObject );
const nodeBuilder = renderObject.getNodeBuilderState();
for ( const node of nodeBuilder.updateNodes ) {
nodeFrame.updateNode( node );
}
}
/**
* Returns the refresh type of the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @return {number} The refresh type, see `RenderObjectRefreshType`.
*/
needsRefresh( renderObject ) {
const nodeFrame = this.getNodeFrameForRender( renderObject );
const monitor = renderObject.getMonitor();
return monitor.needsRefresh( renderObject, nodeFrame );
}
/**
* Frees the internal resources.
*/
dispose() {
super.dispose();
this.nodeFrame = new NodeFrame();
this.nodeBuilderCache = new Map();
this.cacheLib = {};
}
}
const _plane = /*@__PURE__*/ new Plane();
let _clippingContextId = 0;
/**
* Represents the state that is used to perform clipping via clipping planes.
* There is a default clipping context for each render context. When the
* scene holds instances of `ClippingGroup`, there will be a context for each
* group.
*
* @private
*/
class ClippingContext {
/**
* Constructs a new clipping context.
*
* @param {?ClippingContext} [parentContext=null] - A reference to the parent clipping context.
*/
constructor( parentContext = null ) {
/**
* The id of the clipping context.
*
* @type {number}
* @readonly
*/
this.id = _clippingContextId ++;
/**
* The clipping context's version.
*
* @type {number}
* @readonly
*/
this.version = 0;
/**
* Whether the intersection of the clipping planes is used to clip objects, rather than their union.
*
* @type {?boolean}
* @default null
*/
this.clipIntersection = null;
/**
* The clipping context's cache key.
*
* @type {string}
*/
this.cacheKey = '';
/**
* Whether the shadow pass is active or not.
*
* @type {boolean}
* @default false
*/
this.shadowPass = false;
/**
* The view matrix.
*
* @type {Matrix4}
*/
this.viewMatrix = new Matrix4();
/**
* The view normal matrix.
*
* @type {Matrix3}
*/
this.viewNormalMatrix = new Matrix3();
/**
* Internal cache for maintaining clipping contexts.
*
* @type {WeakMap<ClippingGroup,ClippingContext>}
*/
this.clippingGroupContexts = new WeakMap();
/**
* The intersection planes.
*
* @type {Array<Vector4>}
*/
this.intersectionPlanes = [];
/**
* The intersection planes.
*
* @type {Array<Vector4>}
*/
this.unionPlanes = [];
/**
* The version of the clipping context's parent context.
*
* @type {?number}
* @readonly
*/
this.parentVersion = null;
if ( parentContext !== null ) {
this.viewMatrix = parentContext.viewMatrix;
this.viewNormalMatrix = parentContext.viewNormalMatrix;
this.clippingGroupContexts = parentContext.clippingGroupContexts;
this.shadowPass = parentContext.shadowPass;
}
}
/**
* Projects the given source clipping planes and writes the result into the
* destination array.
*
* @param {Array<Plane>} source - The source clipping planes.
* @param {Array<Vector4>} destination - The destination.
* @param {number} offset - The offset.
*/
projectPlanes( source, destination, offset ) {
const l = source.length;
for ( let i = 0; i < l; i ++ ) {
_plane.copy( source[ i ] ).applyMatrix4( this.viewMatrix, this.viewNormalMatrix );
const v = destination[ offset + i ];
const normal = _plane.normal;
v.x = - normal.x;
v.y = - normal.y;
v.z = - normal.z;
v.w = _plane.constant;
}
}
/**
* Updates the root clipping context of a scene.
*
* @param {Scene} scene - The scene.
* @param {Camera} camera - The camera that is used to render the scene.
*/
updateGlobal( scene, camera ) {
this.shadowPass = ( scene.overrideMaterial !== null && scene.overrideMaterial.isShadowPassMaterial );
if ( camera.isArrayCamera === true ) {
// ArrayCamera sub-cameras share the planes, so keep them in world space.
this.viewMatrix.identity();
} else {
this.viewMatrix.copy( camera.matrixWorldInverse );
}
this.viewNormalMatrix.getNormalMatrix( this.viewMatrix );
}
/**
* Updates the clipping context.
*
* @param {ClippingContext} parentContext - The parent context.
* @param {ClippingGroup} clippingGroup - The clipping group this context belongs to.
*/
update( parentContext, clippingGroup ) {
let update = false;
if ( parentContext.version !== this.parentVersion ) {
this.intersectionPlanes = Array.from( parentContext.intersectionPlanes );
this.unionPlanes = Array.from( parentContext.unionPlanes );
this.parentVersion = parentContext.version;
}
if ( this.clipIntersection !== clippingGroup.clipIntersection ) {
this.clipIntersection = clippingGroup.clipIntersection;
if ( this.clipIntersection ) {
this.unionPlanes.length = parentContext.unionPlanes.length;
} else {
this.intersectionPlanes.length = parentContext.intersectionPlanes.length;
}
}
const srcClippingPlanes = clippingGroup.clippingPlanes;
const l = srcClippingPlanes.length;
let dstClippingPlanes;
let offset;
if ( this.clipIntersection ) {
dstClippingPlanes = this.intersectionPlanes;
offset = parentContext.intersectionPlanes.length;
} else {
dstClippingPlanes = this.unionPlanes;
offset = parentContext.unionPlanes.length;
}
if ( dstClippingPlanes.length !== offset + l ) {
dstClippingPlanes.length = offset + l;
for ( let i = 0; i < l; i ++ ) {
dstClippingPlanes[ offset + i ] = new Vector4();
}
update = true;
}
this.projectPlanes( srcClippingPlanes, dstClippingPlanes, offset );
if ( update ) {
this.version ++;
this.cacheKey = `${ this.id }:${ this.intersectionPlanes.length }:${ this.unionPlanes.length }`;
}
}
/**
* Returns a clipping context for the given clipping group.
*
* @param {ClippingGroup} clippingGroup - The clipping group.
* @return {ClippingContext} The clipping context.
*/
getGroupContext( clippingGroup ) {
if ( this.shadowPass && ! clippingGroup.clipShadows ) return this;
let context = this.clippingGroupContexts.get( clippingGroup );
if ( context === undefined ) {
context = new ClippingContext( this );
this.clippingGroupContexts.set( clippingGroup, context );
}
context.update( this, clippingGroup );
return context;
}
/**
* The count of union clipping planes.
*
* @type {number}
* @readonly
*/
get unionClippingCount() {
return this.unionPlanes.length;
}
}
/**
* This module is used to represent render bundles inside the renderer
* for further processing.
*
* @private
*/
class RenderBundle {
/**
* Constructs a new bundle group.
*
* @param {BundleGroup} bundleGroup - The bundle group.
* @param {Camera} camera - The camera the bundle group is rendered with.
* @param {RenderContext} renderContext - The render context the bundle is rendered with.
*/
constructor( bundleGroup, camera, renderContext ) {
this.bundleGroup = bundleGroup;
this.camera = camera;
this.renderContext = renderContext;
}
}
const _chainKeys = [];
/**
* This renderer module manages render bundles.
*
* @private
*/
class RenderBundles {
/**
* Constructs a new render bundle management component.
*/
constructor() {
/**
* A chain map for maintaining the render bundles.
*
* @type {ChainMap}
*/
this.bundles = new ChainMap();
}
/**
* Returns a render bundle for the given bundle group and camera.
*
* @param {BundleGroup} bundleGroup - The bundle group.
* @param {Camera} camera - The camera the bundle group is rendered with.
* @param {RenderContext} renderContext - The render context the bundle is rendered with.
* @return {RenderBundle} The render bundle.
*/
get( bundleGroup, camera, renderContext ) {
const bundles = this.bundles;
_chainKeys[ 0 ] = bundleGroup;
_chainKeys[ 1 ] = camera;
_chainKeys[ 2 ] = renderContext;
let bundle = bundles.get( _chainKeys );
if ( bundle === undefined ) {
bundle = new RenderBundle( bundleGroup, camera, renderContext );
bundles.set( _chainKeys, bundle );
}
_chainKeys[ 0 ] = null;
_chainKeys[ 1 ] = null;
_chainKeys[ 2 ] = null;
return bundle;
}
/**
* Frees all internal resources.
*/
dispose() {
this.bundles = new ChainMap();
}
}
/**
* The purpose of a node library is to assign node implementations
* to existing library features. In `WebGPURenderer` lights, materials
* which are not based on `NodeMaterial` as well as tone mapping techniques
* are implemented with node-based modules.
*
* @private
*/
class NodeLibrary {
/**
* Constructs a new node library.
*/
constructor() {
/**
* A map that maps materials to node materials.
*
* @type {Map<string,NodeMaterial.constructor>}
*/
this.materialNodes = new Map();
/**
* A map that maps tone mapping techniques (constants)
* to tone mapping node functions.
*
* @type {Map<number,Function>}
*/
this.toneMappingNodes = new Map();
}
/**
* Returns a matching node material instance for the given material object.
*
* This method also assigns/copies the properties of the given material object
* to the node material. This is done to make sure the current material
* configuration carries over to the node version.
*
* @param {Material} material - A material.
* @return {NodeMaterial} The corresponding node material.
*/
fromMaterial( material ) {
if ( material.isNodeMaterial ) return material;
let nodeMaterial = null;
const nodeMaterialClass = this.getMaterialNodeClass( material.type );
if ( nodeMaterialClass !== null ) {
nodeMaterial = new nodeMaterialClass();
for ( const key in material ) {
nodeMaterial[ key ] = material[ key ];
}
}
return nodeMaterial;
}
/**
* Adds a tone mapping node function for a tone mapping technique (constant).
*
* @param {Function} toneMappingNode - The tone mapping node function.
* @param {number} toneMapping - The tone mapping.
*/
addToneMapping( toneMappingNode, toneMapping ) {
this.addType( toneMappingNode, toneMapping, this.toneMappingNodes );
}
/**
* Returns a tone mapping node function for a tone mapping technique (constant).
*
* @param {number} toneMapping - The tone mapping.
* @return {?Function} The tone mapping node function. Returns `null` if no node function is found.
*/
getToneMappingFunction( toneMapping ) {
return this.toneMappingNodes.get( toneMapping ) || null;
}
/**
* Returns a node material class definition for a material type.
*
* @param {string} materialType - The material type.
* @return {?NodeMaterial.constructor} The node material class definition. Returns `null` if no node material is found.
*/
getMaterialNodeClass( materialType ) {
return this.materialNodes.get( materialType ) || null;
}
/**
* Adds a node material class definition for a given material type.
*
* @param {NodeMaterial.constructor} materialNodeClass - The node material class definition.
* @param {string} materialClassType - The material type.
*/
addMaterial( materialNodeClass, materialClassType ) {
this.addType( materialNodeClass, materialClassType, this.materialNodes );
}
/**
* Returns a light node class definition for a light class definition.
*
* @deprecated since r187. Use `Light.registerNode()` to assign light node classes instead.
* @param {Light.constructor} lightClass - The light class definition.
* @return {?AnalyticLightNode.constructor} The light node class definition. Returns `null` if no light node is found.
*/
getLightNodeClass( lightClass ) {
warnOnce( 'NodeLibrary: "getLightNodeClass()" has been deprecated. Use "Light.registerNode()" to assign light node classes instead.' ); // @deprecated r187
return lightClass.prototype._lightNode || null;
}
/**
* Adds a light node class definition for a given light class definition.
*
* @deprecated since r187. Use `Light.registerNode()` instead.
* @param {AnalyticLightNode.constructor} lightNodeClass - The light node class definition.
* @param {Light.constructor} lightClass - The light class definition.
*/
addLight( lightNodeClass, lightClass ) {
warnOnce( 'NodeLibrary: "addLight()" has been deprecated. Use "Light.registerNode()" instead.' ); // @deprecated r187
lightClass.registerNode( lightNodeClass );
}
/**
* Adds a node class definition for the given type to the provided type library.
*
* @param {Node.constructor} nodeClass - The node class definition.
* @param {number|string} type - The object type.
* @param {Map<number|string,Node.constructor>} library - The type library.
*/
addType( nodeClass, type, library ) {
if ( library.has( type ) ) {
warn( `Redefinition of node ${ type }` );
return;
}
if ( typeof nodeClass !== 'function' ) throw new Error( `THREE.NodeLibrary: Node class ${ nodeClass.name } is not a class.` );
if ( typeof type === 'function' || typeof type === 'object' ) throw new Error( `THREE.NodeLibrary: Base class ${ type } is not a class.` );
library.set( type, nodeClass );
}
}
const _defaultLights = /*@__PURE__*/ new LightsNode();
/**
* This renderer module manages the lights nodes which are unique
* per scene + camera + lighting combination.
*
* The lights node itself is later configured in the render list
* with the actual lights from the scene.
*
* @private
*/
class Lighting {
/**
* Constructs a new lighting manager.
*/
constructor() {
/**
* Whether this lighting manager is enabled or not.
*
* @type {boolean}
* @default true
*/
this.enabled = true;
/**
* A stack of light arrays saved per render via {@link Lighting#beginRender}.
*
* @private
* @type {Array<Array<Light>>}
*/
this._cache = [];
/**
* A map of lights nodes per scene.
*
* @private
* @type {WeakMap<Scene, LightsNode>}
*/
this._lightsNodeMap = new WeakMap();
/**
* Weak references to the lights nodes created by this lighting manager, so they
* can be disposed with {@link Lighting#dispose} without keeping scenes alive.
*
* @private
* @type {Set<WeakRef<LightsNode>>}
*/
this._tracked = new Set();
/**
* Removes weak references from `_tracked` when their lights node has been
* garbage collected without an explicit `dispose()`.
*
* @private
* @type {FinalizationRegistry}
*/
this._registry = new FinalizationRegistry( ( ref ) => this._tracked.delete( ref ) );
}
/**
* Creates a new lights node for the given array of lights.
*
* @param {Array<Light>} lights - The render object.
* @return {LightsNode} The lights node.
*/
createNode( lights = [] ) {
return new LightsNode().setLights( lights );
}
/**
* Returns a lights node for the given scene.
*
* @param {Scene} scene - The scene.
* @return {LightsNode} The lights node.
*/
getNode( scene ) {
// Ignore renderable objects, e.g: Mesh, Sprite, etc.
if ( scene.isScene !== true && scene.isGroup !== true ) return _defaultLights;
let node = this._lightsNodeMap.get( scene );
if ( node === undefined ) {
node = this.createNode();
this._lightsNodeMap.set( scene, node );
this._track( node );
}
return node;
}
/**
* Tracks the given lights node so it is disposed with {@link Lighting#dispose}.
* Subclasses creating additional lights nodes should track them as well.
*
* @private
* @param {LightsNode} node - The lights node.
*/
_track( node ) {
const ref = new WeakRef( node );
this._tracked.add( ref );
this._registry.register( node, ref, ref );
}
/**
* Saves the current lights of the scene's lights node so they can be restored
* in {@link Lighting#finishRender}. Must be paired with a `finishRender()` call
* to avoid memory leaks.
*
* Nested render calls might mutate the lights array so a save/restore is required
* for each render call.
*
* @param {Scene} scene - The scene.
*/
beginRender( scene ) {
this._cache.push( this.getNode( scene ).getLights() );
}
/**
* Restores the lights saved by the matching {@link Lighting#beginRender} call.
*
* @param {Scene} scene - The scene.
*/
finishRender( scene ) {
this.getNode( scene ).setLights( this._cache.pop() );
}
/**
* Frees the resources of the lights nodes created by this lighting manager. Called by
* the renderer when it is disposed, or by the app after replacing the lighting system.
* The lighting manager must not be used for rendering anymore after this call.
*/
dispose() {
for ( const ref of this._tracked ) {
const node = ref.deref();
if ( node !== undefined ) {
this._registry.unregister( ref );
node.dispose();
}
}
this._tracked.clear();
}
}
/**
* A special type of render target that is used when rendering
* with the WebXR Device API.
*
* @private
* @augments RenderTarget
*/
class XRRenderTarget extends RenderTarget {
/**
* Constructs a new XR render target.
*
* @param {number} [width=1] - The width of the render target.
* @param {number} [height=1] - The height of the render target.
* @param {Object} [options={}] - The configuration options.
*/
constructor( width = 1, height = 1, options = {} ) {
super( width, height, options );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isXRRenderTarget = true;
/**
* Whether the attachments of the render target
* are defined by external textures. This flag is
* set to `true` when using the WebXR Layers API.
*
* @private
* @type {boolean}
* @default false
*/
this._hasExternalTextures = false;
/**
* Whether a depth buffer should automatically be allocated
* for this XR render target or not.
*
* Allocating a depth buffer is the default behavior of XR render
* targets. However, when using the WebXR Layers API, this flag
* must be set to `false` when the `ignoreDepthValues` property of
* the projection layers evaluates to `false`.
*
* Reference: {@link https://www.w3.org/TR/webxrlayers-1/#dom-xrprojectionlayer-ignoredepthvalues}.
*
* @private
* @type {boolean}
* @default true
*/
this._autoAllocateDepthBuffer = true;
/**
* Whether this render target is associated with a XRWebGLLayer.
*
* A XRWebGLLayer points to an opaque framebuffer. Basically,
* this means that you don't have access to its bound color,
* stencil and depth buffers. We need to handle this framebuffer
* differently since its textures are always bound.
*
* @private
* @type {boolean}
* @default false
* */
this._isOpaqueFramebuffer = false;
}
copy( source ) {
super.copy( source );
this._hasExternalTextures = source._hasExternalTextures;
this._autoAllocateDepthBuffer = source._autoAllocateDepthBuffer;
this._isOpaqueFramebuffer = source._isOpaqueFramebuffer;
return this;
}
}
const _contextNodeLib = /*@__PURE__*/ new WeakMap();
/**
* The XR manager is built on top of the WebXR Device API to
* manage XR sessions with renderer backends.
*
* @augments EventDispatcher
*/
class XRManager extends EventDispatcher {
/**
* Constructs a new XR manager.
*
* @param {Renderer} renderer - The renderer.
* @param {boolean} [multiview=false] - Enables multiview if the device supports it.
*/
constructor( renderer, multiview = false ) {
super();
/**
* This flag globally enables XR rendering.
*
* @type {boolean}
* @default false
*/
this.enabled = false;
/**
* Whether the XR device is currently presenting or not.
*
* @type {boolean}
* @default false
* @readonly
*/
this.isPresenting = false;
/**
* Whether the XR camera should automatically be updated or not.
*
* @type {boolean}
* @default true
*/
this.cameraAutoUpdate = true;
/**
* The renderer.
*
* @private
* @type {Renderer}
*/
this._renderer = renderer;
// camera
/**
* Represents the camera for the left eye.
*
* @private
* @type {PerspectiveCamera}
*/
this._cameraL = new PerspectiveCamera();
this._cameraL.viewport = new Vector4();
this._cameraL.matrixWorldAutoUpdate = false;
/**
* Represents the camera for the right eye.
*
* @private
* @type {PerspectiveCamera}
*/
this._cameraR = new PerspectiveCamera();
this._cameraR.viewport = new Vector4();
this._cameraR.matrixWorldAutoUpdate = false;
/**
* A list of cameras used for rendering the XR views.
*
* @private
* @type {Array<Camera>}
*/
this._cameras = [ this._cameraL, this._cameraR ];
/**
* The main XR camera.
*
* @private
* @type {ArrayCamera}
*/
this._cameraXR = new ArrayCamera();
/**
* The current near value of the XR camera.
*
* @private
* @type {?number}
* @default null
*/
this._currentDepthNear = null;
/**
* The current far value of the XR camera.
*
* @private
* @type {?number}
* @default null
*/
this._currentDepthFar = null;
/**
* A list of WebXR controllers requested by the application.
*
* @private
* @type {Array<WebXRController>}
*/
this._controllers = [];
/**
* A list of XR input source. Each input source belongs to
* an instance of WebXRController.
*
* @private
* @type {Array<XRInputSource?>}
*/
this._controllerInputSources = [];
/**
* The XR render target that represents the rendering destination
* during an active XR session.
*
* @private
* @type {?RenderTarget}
* @default null
*/
this._xrRenderTarget = null;
/**
* An array holding all the non-projection layers
*
* @private
* @type {Array<Object>}
* @default []
*/
this._layers = [];
/**
* Whether the XR session uses layers.
*
* @private
* @type {boolean}
* @default false
*/
this._sessionUsesLayers = false;
/**
* Whether the device supports binding gl objects.
*
* @private
* @type {boolean}
* @readonly
*/
this._supportsGlBinding = typeof XRWebGLBinding !== 'undefined';
this._supportsWebGPUBinding = typeof globalThis.XRGPUBinding !== 'undefined';
/**
* Helper function to create native WebXR Layer.
*
* @private
* @type {Function}
*/
this._createXRLayer = createXRLayer.bind( this );
/**
* The current WebGL context.
*
* @private
* @type {?WebGL2RenderingContext}
* @default null
*/
this._gl = null;
/**
* The current animation context.
*
* @private
* @type {?Window}
* @default null
*/
this._currentAnimationContext = null;
/**
* The current animation loop.
*
* @private
* @type {?Function}
* @default null
*/
this._currentAnimationLoop = null;
/**
* The current pixel ratio.
*
* @private
* @type {?number}
* @default null
*/
this._currentPixelRatio = null;
/**
* The current size of the renderer's canvas
* in logical pixel unit.
*
* @private
* @type {Vector2}
*/
this._currentSize = new Vector2();
/**
* Holds a reference to the user camera and its current settings.
*
* @private
* @type {?Object}
* @default null
*/
this._currentCameraSettings = null;
/**
* The default event listener for handling events inside a XR session.
*
* @private
* @type {Function}
*/
this._onSessionEvent = onSessionEvent.bind( this );
/**
* The event listener for handling the end of a XR session.
*
* @private
* @type {Function}
*/
this._onSessionEnd = onSessionEnd.bind( this );
/**
* The event listener for handling the `inputsourceschange` event.
*
* @private
* @type {Function}
*/
this._onInputSourcesChange = onInputSourcesChange.bind( this );
/**
* The animation loop which is used as a replacement for the default
* animation loop of the application. It is only used when a XR session
* is active.
*
* @private
* @type {Function}
*/
this._onAnimationFrame = onAnimationFrame.bind( this );
/**
* The current XR reference space.
*
* @private
* @type {?XRReferenceSpace}
* @default null
*/
this._referenceSpace = null;
/**
* The current XR reference space type.
*
* @private
* @type {XRReferenceSpaceType}
* @default 'local-floor'
*/
this._referenceSpaceType = 'local-floor';
/**
* A custom reference space defined by the application.
*
* @private
* @type {?XRReferenceSpace}
* @default null
*/
this._customReferenceSpace = null;
/**
* The framebuffer scale factor.
*
* @private
* @type {number}
* @default 1
*/
this._framebufferScaleFactor = 1;
/**
* The foveation factor.
*
* @private
* @type {number}
* @default 1
*/
this._foveation = 1.0;
/**
* A reference to the current XR session.
*
* @private
* @type {?XRSession}
* @default null
*/
this._session = null;
/**
* A reference to the current XR base layer.
*
* @private
* @type {?XRWebGLLayer}
* @default null
*/
this._glBaseLayer = null;
/**
* A reference to the current XR binding.
*
* @private
* @type {?XRWebGLBinding}
* @default null
*/
this._glBinding = null;
/**
* A reference to the current XR WebGPU binding.
*
* @private
* @type {?XRGPUBinding}
* @default null
*/
this._webgpuBinding = null;
/**
* A reference to the current XR projection layer.
*
* @private
* @type {?XRProjectionLayer}
* @default null
*/
this._glProjLayer = null;
/**
* A reference to the current XR frame.
*
* @private
* @type {?XRFrame}
* @default null
*/
this._xrFrame = null;
/**
* Whether the browser supports the APIs necessary to use XRProjectionLayers.
*
* Note: this does not represent XRSession explicitly requesting
* `'layers'` as a feature - see `_sessionUsesLayers` and #30112
*
* @private
* @type {boolean}
* @readonly
*/
this._supportsLayers = ( this._supportsGlBinding && 'createProjectionLayer' in XRWebGLBinding.prototype ); // eslint-disable-line compat/compat
/**
* Whether the usage of multiview has been requested by the application or not.
*
* @private
* @type {boolean}
* @default false
* @readonly
*/
this._useMultiviewIfPossible = multiview;
/**
* Whether the usage of multiview is actually enabled. This flag only evaluates to `true`
* if multiview has been requested by the application and the `OVR_multiview2` is available.
*
* @private
* @type {boolean}
* @readonly
*/
this._useMultiview = false;
}
/**
* Returns an instance of `THREE.Group` that represents the transformation
* of a XR controller in target ray space. The requested controller is defined
* by the given index.
*
* @param {number} index - The index of the XR controller.
* @return {Group} A group that represents the controller's transformation.
*/
getController( index ) {
const controller = this._getController( index );
return controller.getTargetRaySpace();
}
/**
* Returns an instance of `THREE.Group` that represents the transformation
* of a XR controller in grip space. The requested controller is defined
* by the given index.
*
* @param {number} index - The index of the XR controller.
* @return {Group} A group that represents the controller's transformation.
*/
getControllerGrip( index ) {
const controller = this._getController( index );
return controller.getGripSpace();
}
/**
* Returns an instance of `THREE.Group` that represents the transformation
* of a XR controller in hand space. The requested controller is defined
* by the given index.
*
* @param {number} index - The index of the XR controller.
* @return {Group} A group that represents the controller's transformation.
*/
getHand( index ) {
const controller = this._getController( index );
return controller.getHandSpace();
}
/**
* Returns the foveation value.
*
* @return {number|undefined} The foveation value.
*/
getFoveation() {
return this._foveation;
}
/**
* Sets the foveation value.
*
* @param {number} foveation - A number in the range `[0,1]` where `0` means no foveation (full resolution)
* and `1` means maximum foveation (the edges render at lower resolution).
*/
setFoveation( foveation ) {
this._foveation = foveation;
if ( this._glProjLayer !== null ) {
this._glProjLayer.fixedFoveation = foveation;
}
if ( this._glBaseLayer !== null && this._glBaseLayer.fixedFoveation !== undefined ) {
this._glBaseLayer.fixedFoveation = foveation;
}
}
/**
* Returns the framebuffer scale factor.
*
* @return {number} The framebuffer scale factor.
*/
getFramebufferScaleFactor() {
return this._framebufferScaleFactor;
}
/**
* Sets the framebuffer scale factor.
*
* This method can not be used during a XR session.
*
* @param {number} factor - The framebuffer scale factor.
*/
setFramebufferScaleFactor( factor ) {
this._framebufferScaleFactor = factor;
if ( this.isPresenting === true ) {
warn( 'XRManager: Cannot change framebuffer scale while presenting.' );
}
}
/**
* Returns the reference space type.
*
* @return {XRReferenceSpaceType} The reference space type.
*/
getReferenceSpaceType() {
return this._referenceSpaceType;
}
/**
* Sets the reference space type.
*
* This method can not be used during a XR session.
*
* @param {XRReferenceSpaceType} type - The reference space type.
*/
setReferenceSpaceType( type ) {
this._referenceSpaceType = type;
if ( this.isPresenting === true ) {
warn( 'XRManager: Cannot change reference space type while presenting.' );
}
}
/**
* Returns the XR reference space.
*
* @return {XRReferenceSpace} The XR reference space.
*/
getReferenceSpace() {
return this._customReferenceSpace || this._referenceSpace;
}
/**
* Sets a custom XR reference space.
*
* @param {XRReferenceSpace} space - The XR reference space.
*/
setReferenceSpace( space ) {
this._customReferenceSpace = space;
}
/**
* Returns the XR camera.
*
* The camera's transformation and projection matrix are derived from the first
* sub camera (e.g. the left eye in a stereo setup).
*
* @return {ArrayCamera} The XR camera.
*/
getCamera() {
return this._cameraXR;
}
/**
* Returns the environment blend mode from the current XR session.
*
* @return {'opaque'|'additive'|'alpha-blend'|undefined} The environment blend mode. Returns `undefined` when used outside of a XR session.
*/
getEnvironmentBlendMode() {
if ( this._session !== null ) {
return this._session.environmentBlendMode;
}
}
/**
* Returns the current base layer.
*
* This is an `XRProjectionLayer` when the targeted XR device supports the
* WebXR Layers API, or an `XRWebGLLayer` otherwise.
*
* @return {?(XRWebGLLayer|XRProjectionLayer)} The XR base layer.
*/
getBaseLayer() {
return this._glProjLayer !== null ? this._glProjLayer : this._glBaseLayer;
}
/**
* Returns the current XR binding.
*
* Creates a new binding if needed and the browser is
* capable of doing so.
*
* @return {?XRWebGLBinding} The XR binding. Returns `null` if one cannot be created.
*/
getBinding() {
if ( this._glBinding === null && this._supportsGlBinding ) {
this._glBinding = new XRWebGLBinding( this._session, this._gl );
}
return this._glBinding;
}
/**
* Applies WebXR fixed foveation to the internal post-processing render target
* used by the first XR render pass before compositing into a projection layer.
*
* Browser-side `XRWebGLBinding.foveateBoundTexture()` failures are treated as
* non-fatal so they do not interrupt rendering.
*
* @param {?RenderTarget} renderTarget - The internal render target.
*/
foveateBoundTexture( renderTarget ) {
if ( renderTarget === null || renderTarget.isPostProcessingRenderTarget !== true ) return;
if ( this.isPresenting !== true ) return;
if ( this._glProjLayer === null ) return;
const backend = this._renderer.backend;
if ( backend === undefined || backend.isWebGLBackend !== true ) return;
if ( backend.state === null ) return;
const outputRenderTarget = this._renderer.getOutputRenderTarget();
if ( outputRenderTarget === null || outputRenderTarget.isXRRenderTarget !== true ) return;
const glBinding = this.getBinding();
if ( glBinding === null || typeof glBinding.foveateBoundTexture !== 'function' ) return;
this._renderer._textures.updateRenderTarget( renderTarget );
const { textureGPU, glTextureType } = backend.get( renderTarget.texture );
if ( textureGPU === undefined || glTextureType === undefined ) return;
if ( renderTarget._xrFoveationTextureGPU === textureGPU ) return;
renderTarget._xrFoveationTextureGPU = textureGPU;
backend.state.bindTexture( glTextureType, textureGPU );
try {
glBinding.foveateBoundTexture( glTextureType, this.getFoveation() );
} catch ( error ) {
warnOnce( `XRManager: Unable to foveate bound XR post-processing texture. ${error.name}: ${error.message}` );
} finally {
backend.state.unbindTexture();
}
}
/**
* Returns the current XR WebGPU binding.
*
* Creates a new binding if needed and the browser is
* capable of doing so.
*
* @return {?XRGPUBinding} The XR WebGPU binding. Returns `null` if one cannot be created.
*/
getWebGPUBinding() {
if ( this._webgpuBinding === null && this._supportsWebGPUBinding ) {
this._webgpuBinding = new globalThis.XRGPUBinding( this._session, this._renderer.backend.device );
}
return this._webgpuBinding;
}
/**
* Returns whether the current XR session is using WebGPU.
*
* @private
* @return {boolean} Whether the current session uses the WebGPU backend and the `webgpu` session feature.
*/
_isWebGPUSession() {
return this._renderer.backend.isWebGPUBackend === true &&
this._session !== null &&
this._session.enabledFeatures.includes( 'webgpu' );
}
/**
* Validates the current WebGPU XR session requirements.
*
* @private
*/
_validateWebGPUSession() {
if ( this._renderer.backend.isWebGPUBackend !== true ) return;
if ( this._session.enabledFeatures.includes( 'webgpu' ) === false ) {
throw new Error( 'THREE.XRManager: WebGPU XR sessions require the "webgpu" session feature. Use VRButtonGPU/XRButton with "webgpu" enabled or use a WebGL backend.' );
}
}
/**
* Initializes the WebGPU XR projection layer and render target.
*
* @private
* @async
* @param {XRSession} session - The XR session.
* @return {Promise<void>}
*/
async _initWebGPUSession( session ) {
const webgpuBinding = this.getWebGPUBinding();
const glProjLayer = webgpuBinding.createProjectionLayer( {
colorFormat: webgpuBinding.getPreferredColorFormat()
} );
this._glProjLayer = glProjLayer;
session.updateRenderState( { layers: [ glProjLayer ] } );
this._referenceSpace = await session.requestReferenceSpace( this.getReferenceSpaceType() );
this._xrRenderTarget = new RenderTarget( glProjLayer.textureWidth, glProjLayer.textureHeight, {
depth: 2,
minFilter: LinearFilter,
magFilter: LinearFilter,
depthBuffer: true,
multiview: false,
useArrayDepthTexture: true,
storeMultisampledColorBuffer: false,
storeMultisampledDepthBuffer: false,
storeMultisampledStencilBuffer: false,
samples: this._renderer.samples
} );
this._xrRenderTarget.texture.isArrayTexture = true;
if ( this._useMultiviewIfPossible === true ) {
warnOnce( 'THREE.XRManager: WebGPU XR does not support multiview yet. Disabling multiview for this XR session.' );
}
this._useMultiview = false;
}
/**
* Releases WebGPU XR resources associated with the current session.
*
* @private
*/
_disposeWebGPUSession() {
const renderer = this._renderer;
const backend = renderer.backend;
const xrRenderTarget = this._xrRenderTarget;
if ( xrRenderTarget === null || backend.isWebGPUBackend !== true ) return;
if ( renderer._renderContexts && renderer._renderContexts.dispose ) {
renderer._renderContexts.dispose();
}
xrRenderTarget.dispose();
// The external texture can be registered before the render target is initialized.
for ( const texture of xrRenderTarget.textures ) {
if ( backend.has( texture ) ) backend.destroyTexture( texture );
}
backend.delete( xrRenderTarget );
}
/**
* Collects WebGPU XR sub-image data for the current frame.
*
* @private
* @param {Array<XRView>} views - The XR views for the current pose.
* @return {{colorTexture:?GPUTexture, viewDescriptors:Array<Object>, viewports:Array<XRViewport>}} The WebGPU XR view data.
*/
_getWebGPUViewData( views ) {
const webgpuBinding = this.getWebGPUBinding();
const viewData = {
colorTexture: null,
viewDescriptors: [],
viewports: []
};
for ( let i = 0; i < views.length; i ++ ) {
const gpuSubImage = webgpuBinding.getViewSubImage( this._glProjLayer, views[ i ] );
if ( viewData.colorTexture === null ) {
viewData.colorTexture = gpuSubImage.colorTexture;
}
viewData.viewports.push( gpuSubImage.viewport );
if ( gpuSubImage.getViewDescriptor ) {
viewData.viewDescriptors.push( gpuSubImage.getViewDescriptor() );
}
}
return viewData;
}
/**
* Returns the current XR frame.
*
* @return {?XRFrame} The XR frame. Returns `null` when used outside a XR session.
*/
getFrame() {
return this._xrFrame;
}
/**
* Returns `true` if the engine renders to a multiview target.
*
* @return {boolean} Whether the engine renders to a multiview render target or not.
*/
useMultiview() {
return this._useMultiview;
}
/**
* This method can be used in XR applications to create a quadratic layer that presents a separate
* rendered scene.
*
* @param {number} width - The width of the layer plane in world units.
* @param {number} height - The height of the layer plane in world units.
* @param {Vector3} translation - The position/translation of the layer plane in world units.
* @param {Quaternion} quaternion - The orientation of the layer plane expressed as a quaternion.
* @param {number} pixelwidth - The width of the layer's render target in pixels.
* @param {number} pixelheight - The height of the layer's render target in pixels.
* @param {Function} rendercall - A callback function that renders the layer. Similar to code in
* the default animation loop, this method can be used to update/transform 3D object in the layer's scene.
* @param {Object} [attributes={}] - Allows to configure the layer's render target.
* @param {number} [attributes.samples] - The scene MSAA sample count. Defaults to the renderer's sample count.
* @return {Mesh} A mesh representing the quadratic XR layer. This mesh should be added to the XR scene.
*/
createQuadLayer( width, height, translation, quaternion, pixelwidth, pixelheight, rendercall, attributes = {} ) {
const geometry = new PlaneGeometry( width, height );
const renderTarget = new XRRenderTarget(
pixelwidth,
pixelheight,
{
format: RGBAFormat,
type: UnsignedByteType,
depthTexture: new DepthTexture(
pixelwidth,
pixelheight,
attributes.stencil ? UnsignedInt248Type : UnsignedIntType,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
attributes.stencil ? DepthStencilFormat : DepthFormat
),
stencilBuffer: attributes.stencil,
samples: attributes.samples ?? this._renderer.samples,
resolveDepthBuffer: false,
resolveStencilBuffer: false,
storeMultisampledColorBuffer: false,
storeMultisampledDepthBuffer: false,
storeMultisampledStencilBuffer: false
} );
const material = new MeshBasicMaterial( { color: 0xffffff, side: FrontSide } );
material.map = renderTarget.texture;
material.map.offset.y = 1;
material.map.repeat.y = -1;
const plane = new Mesh( geometry, material );
plane.position.copy( translation );
plane.quaternion.copy( quaternion );
const layer = {
type: 'quad',
width: width,
height: height,
translation: translation,
quaternion: quaternion,
pixelwidth: pixelwidth,
pixelheight: pixelheight,
samples: renderTarget.samples,
plane: plane,
material: material,
rendercall: rendercall,
renderTarget: renderTarget };
this._layers.push( layer );
if ( this._session !== null ) {
layer.plane.material = new MeshBasicMaterial( { color: 0xffffff, side: FrontSide } );
layer.plane.material.blending = CustomBlending;
layer.plane.material.blendEquation = AddEquation;
layer.plane.material.blendSrc = ZeroFactor;
layer.plane.material.blendDst = ZeroFactor;
layer.xrlayer = this._createXRLayer( layer );
const xrlayers = this._session.renderState.layers;
xrlayers.unshift( layer.xrlayer );
this._session.updateRenderState( { layers: xrlayers } );
} else {
renderTarget.isXRRenderTarget = false;
}
return plane;
}
/**
* This method can be used in XR applications to create a cylindrical layer that presents a separate
* rendered scene.
*
* @param {number} radius - The radius of the cylinder in world units.
* @param {number} centralAngle - The central angle of the cylinder in radians.
* @param {number} aspectratio - The aspect ratio.
* @param {Vector3} translation - The position/translation of the layer plane in world units.
* @param {Quaternion} quaternion - The orientation of the layer plane expressed as a quaternion.
* @param {number} pixelwidth - The width of the layer's render target in pixels.
* @param {number} pixelheight - The height of the layer's render target in pixels.
* @param {Function} rendercall - A callback function that renders the layer. Similar to code in
* the default animation loop, this method can be used to update/transform 3D object in the layer's scene.
* @param {Object} [attributes={}] - Allows to configure the layer's render target.
* @param {number} [attributes.samples] - The scene MSAA sample count. Defaults to the renderer's sample count.
* @return {Mesh} A mesh representing the cylindrical XR layer. This mesh should be added to the XR scene.
*/
createCylinderLayer( radius, centralAngle, aspectratio, translation, quaternion, pixelwidth, pixelheight, rendercall, attributes = {} ) {
const geometry = new CylinderGeometry( radius, radius, radius * centralAngle / aspectratio, 64, 64, true, Math.PI - centralAngle / 2, centralAngle );
const renderTarget = new XRRenderTarget(
pixelwidth,
pixelheight,
{
format: RGBAFormat,
type: UnsignedByteType,
depthTexture: new DepthTexture(
pixelwidth,
pixelheight,
attributes.stencil ? UnsignedInt248Type : UnsignedIntType,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
attributes.stencil ? DepthStencilFormat : DepthFormat
),
stencilBuffer: attributes.stencil,
samples: attributes.samples ?? this._renderer.samples,
resolveDepthBuffer: false,
resolveStencilBuffer: false,
storeMultisampledColorBuffer: false,
storeMultisampledDepthBuffer: false,
storeMultisampledStencilBuffer: false
} );
const material = new MeshBasicMaterial( { color: 0xffffff, side: BackSide } );
material.map = renderTarget.texture;
material.map.offset.y = 1;
material.map.repeat.y = -1;
const plane = new Mesh( geometry, material );
plane.position.copy( translation );
plane.quaternion.copy( quaternion );
const layer = {
type: 'cylinder',
radius: radius,
centralAngle: centralAngle,
aspectratio: aspectratio,
translation: translation,
quaternion: quaternion,
pixelwidth: pixelwidth,
pixelheight: pixelheight,
samples: renderTarget.samples,
plane: plane,
material: material,
rendercall: rendercall,
renderTarget: renderTarget };
this._layers.push( layer );
if ( this._session !== null ) {
layer.plane.material = new MeshBasicMaterial( { color: 0xffffff, side: BackSide } );
layer.plane.material.blending = CustomBlending;
layer.plane.material.blendEquation = AddEquation;
layer.plane.material.blendSrc = ZeroFactor;
layer.plane.material.blendDst = ZeroFactor;
layer.xrlayer = this._createXRLayer( layer );
const xrlayers = this._session.renderState.layers;
xrlayers.unshift( layer.xrlayer );
this._session.updateRenderState( { layers: xrlayers } );
} else {
renderTarget.isXRRenderTarget = false;
}
return plane;
}
/**
* Renders the XR layers that have been previously added to the scene.
*
* This method is usually called in your animation loop before rendering
* the actual scene via `renderer.render( scene, camera );`.
*/
renderLayers( ) {
const translationObject = new Vector3();
const quaternionObject = new Quaternion();
const renderer = this._renderer;
const wasPresenting = this.isPresenting;
this.isPresenting = false;
const rendererSize = new Vector2();
renderer.getSize( rendererSize );
const currentRenderTarget = renderer.getRenderTarget();
for ( const layer of this._layers ) {
layer.renderTarget.isXRRenderTarget = this._session !== null;
layer.renderTarget._hasExternalTextures = layer.renderTarget.isXRRenderTarget;
const currentContextNode = renderer.contextNode;
let contextNode;
if ( layer.renderTarget.isXRRenderTarget && this._sessionUsesLayers ) {
layer.xrlayer.transform = new XRRigidTransform( layer.plane.getWorldPosition( translationObject ), layer.plane.getWorldQuaternion( quaternionObject ) );
const glSubImage = this._glBinding.getSubImage( layer.xrlayer, this._xrFrame );
renderer.backend.setXRRenderTargetTextures(
layer.renderTarget,
glSubImage.colorTexture,
undefined );
renderer._setXRLayerSize( layer.renderTarget.width, layer.renderTarget.height );
contextNode = _contextNodeLib.get( currentContextNode );
if ( contextNode === undefined ) {
// Apply ToneMapping and OutputColorSpace directly in the material shader
contextNode = context( {
getOutput: ( outputNode, builder ) => {
const renderOutputNode = renderOutput( outputNode, renderer.toneMapping, renderer.outputColorSpace );
renderOutputNode.premultipliedAlpha = builder.material.premultipliedAlpha;
return renderOutputNode;
},
... currentContextNode.getFlowContextData()
} );
_contextNodeLib.set( currentContextNode, contextNode );
}
} else {
contextNode = currentContextNode;
}
renderer.contextNode = contextNode;
renderer.setRenderTarget( layer.renderTarget );
layer.rendercall();
renderer.contextNode = currentContextNode;
}
renderer.setRenderTarget( currentRenderTarget );
renderer._setXRLayerSize( rendererSize.x, rendererSize.y );
this.isPresenting = wasPresenting;
}
/**
* Returns the current XR session.
*
* @return {?XRSession} The XR session. Returns `null` when used outside a XR session.
*/
getSession() {
return this._session;
}
/**
* After a XR session has been requested usually with one of the `*Button` modules, it
* is injected into the renderer with this method. This method triggers the start of
* the actual XR rendering.
*
* @async
* @param {XRSession} session - The XR session to set.
* @return {Promise} A Promise that resolves when the session has been set.
*/
async setSession( session ) {
const renderer = this._renderer;
if ( renderer.initialized === false ) await renderer.init();
this._gl = renderer.getContext();
const gl = this._gl;
this._session = session;
if ( session !== null ) {
session.addEventListener( 'select', this._onSessionEvent );
session.addEventListener( 'selectstart', this._onSessionEvent );
session.addEventListener( 'selectend', this._onSessionEvent );
session.addEventListener( 'squeeze', this._onSessionEvent );
session.addEventListener( 'squeezestart', this._onSessionEvent );
session.addEventListener( 'squeezeend', this._onSessionEvent );
session.addEventListener( 'end', this._onSessionEnd );
session.addEventListener( 'inputsourceschange', this._onInputSourcesChange );
this._validateWebGPUSession();
this._currentPixelRatio = renderer.getPixelRatio();
renderer.getSize( this._currentSize );
this._currentAnimationContext = renderer._animation.getContext();
this._currentAnimationLoop = renderer._animation.getAnimationLoop();
renderer._animation.stop();
//
if ( this._isWebGPUSession() ) {
await this._initWebGPUSession( session );
} else if ( this._supportsLayers === true ) {
// default path using XRProjectionLayer
let depthFormat = null;
let depthType = null;
let glDepthFormat = null;
const attributes = gl.getContextAttributes();
await renderer.backend.makeXRCompatible();
this.setFoveation( this.getFoveation() );
if ( renderer.depth ) {
glDepthFormat = renderer.stencil ? gl.DEPTH24_STENCIL8 : gl.DEPTH_COMPONENT24;
depthFormat = renderer.stencil ? DepthStencilFormat : DepthFormat;
depthType = renderer.stencil ? UnsignedInt248Type : UnsignedIntType;
}
const projectionlayerInit = {
colorFormat: gl.RGBA8,
depthFormat: glDepthFormat,
scaleFactor: this._framebufferScaleFactor,
clearOnAccess: false
};
if ( this._useMultiviewIfPossible && renderer.hasFeature( 'OVR_multiview2' ) ) {
projectionlayerInit.textureType = 'texture-array';
this._useMultiview = true;
}
this._glBinding = this.getBinding();
const glProjLayer = this._glBinding.createProjectionLayer( projectionlayerInit );
const layersArray = [ glProjLayer ];
this._glProjLayer = glProjLayer;
renderer.setPixelRatio( 1 );
renderer._setXRLayerSize( glProjLayer.textureWidth, glProjLayer.textureHeight );
const depth = this._useMultiview ? 2 : 1;
const depthTexture = new DepthTexture( glProjLayer.textureWidth, glProjLayer.textureHeight, depthType, undefined, undefined, undefined, undefined, undefined, undefined, depthFormat, depth );
this._xrRenderTarget = new XRRenderTarget(
glProjLayer.textureWidth,
glProjLayer.textureHeight,
{
format: RGBAFormat,
type: UnsignedByteType,
colorSpace: renderer.outputColorSpace,
depthTexture: depthTexture,
stencilBuffer: renderer.stencil,
samples: attributes.antialias ? 4 : 0,
resolveDepthBuffer: ( glProjLayer.ignoreDepthValues === false ),
resolveStencilBuffer: ( glProjLayer.ignoreDepthValues === false ),
storeMultisampledColorBuffer: false,
storeMultisampledDepthBuffer: ( glProjLayer.ignoreDepthValues === false ),
storeMultisampledStencilBuffer: ( glProjLayer.ignoreDepthValues === false ),
depth: this._useMultiview ? 2 : 1,
multiview: this._useMultiview
} );
this._xrRenderTarget._hasExternalTextures = true;
this._xrRenderTarget.depth = this._useMultiview ? 2 : 1;
this._sessionUsesLayers = session.enabledFeatures.includes( 'layers' );
this._referenceSpace = await session.requestReferenceSpace( this.getReferenceSpaceType() );
if ( this._sessionUsesLayers ) {
// switch layers to native
for ( const layer of this._layers ) {
// change material so it "punches" out a hole to show the XR Layer.
layer.plane.material = new MeshBasicMaterial( { color: 0xffffff, side: layer.type === 'cylinder' ? BackSide : FrontSide } );
layer.plane.material.blending = CustomBlending;
layer.plane.material.blendEquation = AddEquation;
layer.plane.material.blendSrc = ZeroFactor;
layer.plane.material.blendDst = ZeroFactor;
layer.xrlayer = this._createXRLayer( layer );
layersArray.unshift( layer.xrlayer );
}
}
session.updateRenderState( { layers: layersArray } );
} else {
// fallback to XRWebGLLayer
await renderer.backend.makeXRCompatible();
this.setFoveation( this.getFoveation() );
const layerInit = {
antialias: renderer.currentSamples > 0,
alpha: true,
depth: renderer.depth,
stencil: renderer.stencil,
framebufferScaleFactor: this.getFramebufferScaleFactor()
};
const glBaseLayer = new XRWebGLLayer( session, gl, layerInit );
this._glBaseLayer = glBaseLayer;
session.updateRenderState( { baseLayer: glBaseLayer } );
renderer.setPixelRatio( 1 );
renderer._setXRLayerSize( glBaseLayer.framebufferWidth, glBaseLayer.framebufferHeight );
this._xrRenderTarget = new XRRenderTarget(
glBaseLayer.framebufferWidth,
glBaseLayer.framebufferHeight,
{
format: RGBAFormat,
type: UnsignedByteType,
colorSpace: renderer.outputColorSpace,
stencilBuffer: renderer.stencil
}
);
this._xrRenderTarget._isOpaqueFramebuffer = true;
this._referenceSpace = await session.requestReferenceSpace( this.getReferenceSpaceType() );
}
//
renderer._animation.setAnimationLoop( this._onAnimationFrame );
renderer._animation.setContext( session );
renderer._animation.start();
this.isPresenting = true;
this.dispatchEvent( { type: 'sessionstart' } );
}
}
/**
* This method is called by the renderer per frame and updates the XR camera
* and it sub cameras based on the given camera. The given camera is the "user"
* camera created on application level and used for non-XR rendering.
*
* @param {PerspectiveCamera} camera - The camera.
*/
updateCamera( camera ) {
const session = this._session;
if ( session === null ) return;
const depthNear = camera.near;
const depthFar = camera.far;
const cameraXR = this._cameraXR;
const cameraL = this._cameraL;
const cameraR = this._cameraR;
cameraXR.near = cameraR.near = cameraL.near = depthNear;
cameraXR.far = cameraR.far = cameraL.far = depthFar;
cameraXR.isMultiViewCamera = this._useMultiview;
if ( this._currentDepthNear !== cameraXR.near || this._currentDepthFar !== cameraXR.far ) {
// Note that the new renderState won't apply until the next frame. See #18320
session.updateRenderState( {
depthNear: cameraXR.near,
depthFar: cameraXR.far
} );
this._currentDepthNear = cameraXR.near;
this._currentDepthFar = cameraXR.far;
}
// inherit camera layers and enable eye layers (1 = left, 2 = right)
cameraXR.layers.mask = camera.layers.mask | 0b110;
cameraL.layers.mask = cameraXR.layers.mask & -5;
cameraR.layers.mask = cameraXR.layers.mask & -3;
const parent = camera.parent;
const cameras = cameraXR.cameras;
updateCamera( cameraXR, parent );
for ( let i = 0; i < cameras.length; i ++ ) {
updateCamera( cameras[ i ], parent );
}
cameraXR.projectionMatrix.copy( cameraL.projectionMatrix );
cameraXR.projectionMatrixInverse.copy( cameraL.projectionMatrixInverse );
// update user camera and its children
if ( this._currentCameraSettings === null && camera.isPerspectiveCamera ) {
this._currentCameraSettings = { camera: camera, fov: camera.fov, zoom: camera.zoom };
}
updateUserCamera( camera, cameraXR, parent );
}
/**
* Returns a WebXR controller for the given controller index.
*
* @private
* @param {number} index - The controller index.
* @return {WebXRController} The XR controller.
*/
_getController( index ) {
let controller = this._controllers[ index ];
if ( controller === undefined ) {
controller = new WebXRController();
this._controllers[ index ] = controller;
}
return controller;
}
}
/**
* Updates the world matrices for the given camera based on the parent 3D object.
*
* @inner
* @param {Camera} camera - The camera to update.
* @param {Object3D} parent - The parent 3D object.
*/
function updateCamera( camera, parent ) {
if ( parent === null ) {
camera.matrixWorld.copy( camera.matrix );
} else {
camera.matrixWorld.multiplyMatrices( parent.matrixWorld, camera.matrix );
}
camera.matrixWorldInverse.copy( camera.matrixWorld ).invert();
}
/**
* Updates the given camera with the transformation of the XR camera and parent object.
*
* @inner
* @param {Camera} camera - The camera to update.
* @param {ArrayCamera} cameraXR - The XR camera.
* @param {Object3D} parent - The parent 3D object.
*/
function updateUserCamera( camera, cameraXR, parent ) {
if ( parent === null ) {
camera.matrix.copy( cameraXR.matrixWorld );
} else {
camera.matrix.copy( parent.matrixWorld );
camera.matrix.invert();
camera.matrix.multiply( cameraXR.matrixWorld );
}
camera.matrix.decompose( camera.position, camera.quaternion, camera.scale );
camera.updateMatrixWorld( true );
camera.projectionMatrix.copy( cameraXR.projectionMatrix );
camera.projectionMatrixInverse.copy( cameraXR.projectionMatrixInverse );
if ( camera.isPerspectiveCamera ) {
camera.fov = RAD2DEG * 2 * Math.atan( 1 / camera.projectionMatrix.elements[ 5 ] );
camera.zoom = 1;
}
}
function onSessionEvent( event ) {
const controllerIndex = this._controllerInputSources.indexOf( event.inputSource );
if ( controllerIndex === -1 ) {
return;
}
const controller = this._controllers[ controllerIndex ];
if ( controller !== undefined ) {
const referenceSpace = this.getReferenceSpace();
controller.update( event.inputSource, event.frame, referenceSpace );
controller.dispatchEvent( { type: event.type, data: event.inputSource } );
}
}
function onSessionEnd() {
const session = this._session;
const renderer = this._renderer;
session.removeEventListener( 'select', this._onSessionEvent );
session.removeEventListener( 'selectstart', this._onSessionEvent );
session.removeEventListener( 'selectend', this._onSessionEvent );
session.removeEventListener( 'squeeze', this._onSessionEvent );
session.removeEventListener( 'squeezestart', this._onSessionEvent );
session.removeEventListener( 'squeezeend', this._onSessionEvent );
session.removeEventListener( 'end', this._onSessionEnd );
session.removeEventListener( 'inputsourceschange', this._onInputSourcesChange );
for ( let i = 0; i < this._controllers.length; i ++ ) {
const inputSource = this._controllerInputSources[ i ];
if ( inputSource === null ) continue;
this._controllerInputSources[ i ] = null;
this._controllers[ i ].disconnect( inputSource );
}
this._currentDepthNear = null;
this._currentDepthFar = null;
// restore framebuffer/rendering state
renderer._resetXRState();
this._disposeWebGPUSession();
this._session = null;
this._xrRenderTarget = null;
this._glBinding = null;
this._webgpuBinding = null;
this._glBaseLayer = null;
this._glProjLayer = null;
// switch layers back to emulated
if ( this._sessionUsesLayers === true ) {
for ( const layer of this._layers ) {
// Recreate layer render target to reset state
layer.renderTarget = new XRRenderTarget(
layer.pixelwidth,
layer.pixelheight,
{
format: RGBAFormat,
type: UnsignedByteType,
depthTexture: new DepthTexture(
layer.pixelwidth,
layer.pixelheight,
layer.stencilBuffer ? UnsignedInt248Type : UnsignedIntType,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
layer.stencilBuffer ? DepthStencilFormat : DepthFormat
),
stencilBuffer: layer.stencilBuffer,
samples: layer.samples,
resolveDepthBuffer: false,
resolveStencilBuffer: false,
storeMultisampledColorBuffer: false,
storeMultisampledDepthBuffer: false,
storeMultisampledStencilBuffer: false
} );
layer.renderTarget.isXRRenderTarget = false;
layer.plane.material = layer.material;
layer.material.map = layer.renderTarget.texture;
layer.material.map.offset.y = 1;
layer.material.map.repeat.y = -1;
delete layer.xrlayer;
}
}
//
this.isPresenting = false;
this._useMultiview = false;
renderer._animation.stop();
renderer._animation.setAnimationLoop( this._currentAnimationLoop );
renderer._animation.setContext( this._currentAnimationContext );
renderer._animation.start();
renderer.setPixelRatio( this._currentPixelRatio );
renderer.setSize( this._currentSize.width, this._currentSize.height, false );
if ( this._currentCameraSettings !== null ) {
const camera = this._currentCameraSettings.camera;
camera.fov = this._currentCameraSettings.fov;
camera.zoom = this._currentCameraSettings.zoom;
camera.updateProjectionMatrix();
this._currentCameraSettings = null;
}
this.dispatchEvent( { type: 'sessionend' } );
}
function onInputSourcesChange( event ) {
const controllers = this._controllers;
const controllerInputSources = this._controllerInputSources;
// Notify disconnected
for ( let i = 0; i < event.removed.length; i ++ ) {
const inputSource = event.removed[ i ];
const index = controllerInputSources.indexOf( inputSource );
if ( index >= 0 ) {
controllerInputSources[ index ] = null;
controllers[ index ].disconnect( inputSource );
}
}
// Notify connected
for ( let i = 0; i < event.added.length; i ++ ) {
const inputSource = event.added[ i ];
let controllerIndex = controllerInputSources.indexOf( inputSource );
if ( controllerIndex === -1 ) {
// Assign input source a controller that currently has no input source
for ( let i = 0; i < controllers.length; i ++ ) {
if ( i >= controllerInputSources.length ) {
controllerInputSources.push( inputSource );
controllerIndex = i;
break;
} else if ( controllerInputSources[ i ] === null ) {
controllerInputSources[ i ] = inputSource;
controllerIndex = i;
break;
}
}
// If all controllers do currently receive input we ignore new ones
if ( controllerIndex === -1 ) break;
}
const controller = controllers[ controllerIndex ];
if ( controller ) {
controller.connect( inputSource );
}
}
}
// Creation method for native WebXR layers
function createXRLayer( layer ) {
if ( layer.type === 'quad' ) {
return this._glBinding.createQuadLayer( {
transform: new XRRigidTransform( layer.translation, layer.quaternion ),
width: layer.width / 2,
height: layer.height / 2,
space: this._referenceSpace,
viewPixelWidth: layer.pixelwidth,
viewPixelHeight: layer.pixelheight,
clearOnAccess: false
} );
} else {
return this._glBinding.createCylinderLayer( {
transform: new XRRigidTransform( layer.translation, layer.quaternion ),
radius: layer.radius,
centralAngle: layer.centralAngle,
aspectRatio: layer.aspectRatio,
space: this._referenceSpace,
viewPixelWidth: layer.pixelwidth,
viewPixelHeight: layer.pixelheight,
clearOnAccess: false
} );
}
}
// Animation Loop
function onAnimationFrame( time, frame ) {
if ( frame === undefined ) return;
const cameraXR = this._cameraXR;
const renderer = this._renderer;
const backend = renderer.backend;
const glBaseLayer = this._glBaseLayer;
const referenceSpace = this.getReferenceSpace();
const pose = frame.getViewerPose( referenceSpace );
this._xrFrame = frame;
if ( pose !== null ) {
const views = pose.views;
const webgpuViewData = this._isWebGPUSession() ? this._getWebGPUViewData( views ) : null;
if ( this._glBaseLayer !== null && webgpuViewData === null ) {
backend.setXRTarget( glBaseLayer.framebuffer );
}
let cameraXRNeedsUpdate = false;
// check if it's necessary to rebuild cameraXR's camera list
if ( views.length !== cameraXR.cameras.length ) {
cameraXR.cameras.length = 0;
cameraXRNeedsUpdate = true;
}
for ( let i = 0; i < views.length; i ++ ) {
const view = views[ i ];
let viewport;
if ( webgpuViewData !== null ) {
viewport = webgpuViewData.viewports[ i ];
} else if ( this._supportsLayers === true ) {
// WebGL path: Use XRWebGLBinding
const glSubImage = this._glBinding.getViewSubImage( this._glProjLayer, view );
viewport = glSubImage.viewport;
// For side-by-side projection, we only produce a single texture for both eyes.
if ( i === 0 ) {
backend.setXRRenderTargetTextures(
this._xrRenderTarget,
glSubImage.colorTexture,
( this._glProjLayer.ignoreDepthValues && ! this._useMultiview ) ? undefined : glSubImage.depthStencilTexture
);
}
} else {
viewport = glBaseLayer.getViewport( view );
}
let camera = this._cameras[ i ];
if ( camera === undefined ) {
camera = new PerspectiveCamera();
camera.layers.enable( i );
camera.viewport = new Vector4();
camera.matrixWorldAutoUpdate = false;
this._cameras[ i ] = camera;
}
camera.matrix.fromArray( view.transform.matrix );
camera.matrix.decompose( camera.position, camera.quaternion, camera.scale );
camera.projectionMatrix.fromArray( view.projectionMatrix );
camera.projectionMatrixInverse.copy( camera.projectionMatrix ).invert();
camera.viewport.set( viewport.x, viewport.y, viewport.width, viewport.height );
if ( i === 0 ) {
cameraXR.matrix.copy( camera.matrix );
cameraXR.matrix.decompose( cameraXR.position, cameraXR.quaternion, cameraXR.scale );
}
if ( cameraXRNeedsUpdate === true ) {
cameraXR.cameras.push( camera );
}
}
if ( webgpuViewData !== null && webgpuViewData.colorTexture !== null ) {
backend.setXRRenderTargetTextures(
this._xrRenderTarget,
webgpuViewData.colorTexture,
webgpuViewData.viewDescriptors
);
}
renderer.setOutputRenderTarget( this._xrRenderTarget );
const frameBufferTarget = renderer._getFrameBufferTarget();
if ( webgpuViewData !== null ) {
this._xrRenderTarget.samples = frameBufferTarget === null ? renderer.samples : 0;
this._xrRenderTarget.depthBuffer = frameBufferTarget === null;
}
renderer.xr.foveateBoundTexture( frameBufferTarget );
}
//
for ( let i = 0; i < this._controllers.length; i ++ ) {
const inputSource = this._controllerInputSources[ i ];
const controller = this._controllers[ i ];
if ( inputSource !== null && controller !== undefined ) {
controller.update( inputSource, frame, referenceSpace );
}
}
if ( this._currentAnimationLoop ) this._currentAnimationLoop( time, frame );
if ( frame.detectedPlanes ) {
this.dispatchEvent( { type: 'planesdetected', data: frame } );
}
this._xrFrame = null;
}
/**
* CanvasTarget is a class that represents the final output destination of the renderer.
*
* @augments EventDispatcher
*/
class CanvasTarget extends EventDispatcher {
/**
* Constructs a new CanvasTarget.
*
* @param {HTMLCanvasElement|OffscreenCanvas} domElement - The canvas element to render to.
*/
constructor( domElement ) {
super();
/**
* A reference to the canvas element the renderer is drawing to.
* This value of this property will automatically be created by
* the renderer.
*
* @type {HTMLCanvasElement|OffscreenCanvas}
*/
this.domElement = domElement;
/**
* The renderer's pixel ratio.
*
* @private
* @type {number}
* @default 1
*/
this._pixelRatio = 1;
/**
* The width of the renderer's default framebuffer in logical pixel unit.
*
* @private
* @type {number}
*/
this._width = this.domElement.width;
/**
* The height of the renderer's default framebuffer in logical pixel unit.
*
* @private
* @type {number}
*/
this._height = this.domElement.height;
/**
* The viewport of the renderer in logical pixel unit.
*
* @private
* @type {Vector4}
*/
this._viewport = new Vector4( 0, 0, this._width, this._height );
/**
* The scissor rectangle of the renderer in logical pixel unit.
*
* @private
* @type {Vector4}
*/
this._scissor = new Vector4( 0, 0, this._width, this._height );
/**
* Whether the scissor test should be enabled or not.
*
* @private
* @type {boolean}
*/
this._scissorTest = false;
/**
* The color texture of the default framebuffer.
*
* @type {FramebufferTexture}
*/
this.colorTexture = new FramebufferTexture();
/**
* The depth texture of the default framebuffer.
*
* @type {DepthTexture}
*/
this.depthTexture = new DepthTexture();
}
/**
* Returns the pixel ratio.
*
* @return {number} The pixel ratio.
*/
getPixelRatio() {
return this._pixelRatio;
}
/**
* Returns the drawing buffer size in physical pixels. This method honors the pixel ratio.
*
* @param {Vector2} target - The method writes the result in this target object.
* @return {Vector2} The drawing buffer size.
*/
getDrawingBufferSize( target ) {
return target.set( this._width * this._pixelRatio, this._height * this._pixelRatio ).floor();
}
/**
* Returns the renderer's size in logical pixels. This method does not honor the pixel ratio.
*
* @param {Vector2} target - The method writes the result in this target object.
* @return {Vector2} The renderer's size in logical pixels.
*/
getSize( target ) {
return target.set( this._width, this._height );
}
/**
* Sets the given pixel ratio and resizes the canvas if necessary.
*
* @param {number} [value=1] - The pixel ratio.
*/
setPixelRatio( value = 1 ) {
if ( this._pixelRatio === value ) return;
this._pixelRatio = value;
this.setSize( this._width, this._height, false );
}
/**
* This method allows to define the drawing buffer size by specifying
* width, height and pixel ratio all at once. The size of the drawing
* buffer is computed with this formula:
* ```js
* size.x = width * pixelRatio;
* size.y = height * pixelRatio;
* ```
*
* @param {number} width - The width in logical pixels.
* @param {number} height - The height in logical pixels.
* @param {number} pixelRatio - The pixel ratio.
*/
setDrawingBufferSize( width, height, pixelRatio ) {
// Renderer can't be resized while presenting in XR.
if ( this.xr && this.xr.isPresenting ) return;
this._width = width;
this._height = height;
this._pixelRatio = pixelRatio;
this.domElement.width = Math.floor( width * pixelRatio );
this.domElement.height = Math.floor( height * pixelRatio );
this.setViewport( 0, 0, width, height );
this._dispatchResize();
}
/**
* Sets the size of the renderer.
*
* @param {number} width - The width in logical pixels.
* @param {number} height - The height in logical pixels.
* @param {boolean} [updateStyle=true] - Whether to update the `style` attribute of the canvas or not.
*/
setSize( width, height, updateStyle = true ) {
// Renderer can't be resized while presenting in XR.
if ( this.xr && this.xr.isPresenting ) return;
this._width = width;
this._height = height;
this.domElement.width = Math.floor( width * this._pixelRatio );
this.domElement.height = Math.floor( height * this._pixelRatio );
if ( updateStyle === true ) {
this.domElement.style.width = width + 'px';
this.domElement.style.height = height + 'px';
}
this.setViewport( 0, 0, width, height );
this._dispatchResize();
}
/**
* Returns the scissor rectangle.
*
* @param {Vector4} target - The method writes the result in this target object.
* @return {Vector4} The scissor rectangle.
*/
getScissor( target ) {
const scissor = this._scissor;
target.x = scissor.x;
target.y = scissor.y;
target.width = scissor.width;
target.height = scissor.height;
return target;
}
/**
* Defines the scissor rectangle.
*
* @param {number | Vector4} x - The horizontal coordinate for the lower left corner of the box in logical pixel unit.
* Instead of passing four arguments, the method also works with a single four-dimensional vector.
* @param {number} y - The vertical coordinate for the lower left corner of the box in logical pixel unit.
* @param {number} width - The width of the scissor box in logical pixel unit.
* @param {number} height - The height of the scissor box in logical pixel unit.
*/
setScissor( x, y, width, height ) {
const scissor = this._scissor;
if ( x.isVector4 ) {
scissor.copy( x );
} else {
scissor.set( x, y, width, height );
}
}
/**
* Returns the scissor test value.
*
* @return {boolean} Whether the scissor test should be enabled or not.
*/
getScissorTest() {
return this._scissorTest;
}
/**
* Defines the scissor test.
*
* @param {boolean} boolean - Whether the scissor test should be enabled or not.
*/
setScissorTest( boolean ) {
this._scissorTest = boolean;
}
/**
* Returns the viewport definition.
*
* @param {Vector4} target - The method writes the result in this target object.
* @return {Vector4} The viewport definition.
*/
getViewport( target ) {
return target.copy( this._viewport );
}
/**
* Defines the viewport.
*
* @param {number | Vector4} x - The horizontal coordinate for the lower left corner of the viewport origin in logical pixel unit.
* @param {number} y - The vertical coordinate for the lower left corner of the viewport origin in logical pixel unit.
* @param {number} width - The width of the viewport in logical pixel unit.
* @param {number} height - The height of the viewport in logical pixel unit.
* @param {number} minDepth - The minimum depth value of the viewport. WebGPU only.
* @param {number} maxDepth - The maximum depth value of the viewport. WebGPU only.
*/
setViewport( x, y, width, height, minDepth = 0, maxDepth = 1 ) {
const viewport = this._viewport;
if ( x.isVector4 ) {
viewport.copy( x );
} else {
viewport.set( x, y, width, height );
}
viewport.minDepth = minDepth;
viewport.maxDepth = maxDepth;
}
/**
* Dispatches the resize event.
*
* @private
*/
_dispatchResize() {
this.dispatchEvent( { type: 'resize' } );
}
/**
* Frees the GPU-related resources allocated by this instance. Call this
* method whenever this instance is no longer used in your app.
*
* @fires RenderTarget#dispose
*/
dispose() {
this.dispatchEvent( { type: 'dispose' } );
}
}
const _vector4$1 = /*@__PURE__*/ new Vector4();
/**
* Decides which objects of a scene are rendered and how they are submitted to the render list.
* The default implementation performs frustum culling and pushes every renderable object as a
* separate render item. Subclasses can override the hooks {@link StandardDrawingPipeline#isCulled} and
* {@link StandardDrawingPipeline#pushRenderItem} to implement other strategies like GPU-driven rendering.
*
* This class is experimental and its interface might change.
*/
class StandardDrawingPipeline {
/**
* Constructs a new standard drawing pipeline.
*/
constructor() {
/**
* The renderer this pipeline is assigned to. It is set when
* the pipeline is assigned via {@link Renderer#drawingPipeline}.
*
* @type {?Renderer}
* @default null
*/
this.renderer = null;
/**
* The frustum of the current camera.
*
* @protected
* @type {Frustum}
*/
this._frustum = new Frustum();
/**
* The frustum array of the current array camera, kept by the render context.
*
* @protected
* @type {?FrustumArray}
* @default null
*/
this._frustumArray = null;
/**
* The view projection matrix of the current camera.
*
* @protected
* @type {Matrix4}
*/
this._projScreenMatrix = new Matrix4();
/**
* The number of render bundles being recorded in the current projection.
*
* @protected
* @type {number}
* @default 0
*/
this._bundleDepth = 0;
}
/**
* Prepares resources needed to precompile the given scene. Subclasses can use this
* hook to process geometry before the synchronous projection in `Renderer.compileAsync()`.
*
* @param {Object3D} scene - The scene or object to precompile.
* @param {Camera} camera - The camera.
* @return {Promise|undefined} Resolves when the resources are ready.
*/
prepareAsync( /* scene, camera */ ) {}
/**
* Frees the resources of the pipeline. Called by {@link Renderer#dispose}, subclasses holding GPU
* resources release them here.
*/
dispose() {}
/**
* Prepares the pipeline for projecting a scene with the given camera.
*
* @param {Camera} camera - The camera.
* @param {RenderContext} renderContext - The render context.
*/
begin( camera, renderContext ) {
this._projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse );
if ( camera.isArrayCamera ) {
// the frustums of the sub-cameras are kept per render context, the backends cull the draws of each sub-camera with them
if ( renderContext.frustumArray === null ) renderContext.frustumArray = new FrustumArray();
renderContext.frustumArray.setFromArrayCamera( camera );
this._frustumArray = renderContext.frustumArray;
} else {
this._frustum.setFromProjectionMatrix( this._projScreenMatrix, camera.coordinateSystem, camera.reversedDepth );
}
}
/**
* Begins the projection into the given render list.
*
* @param {RenderList} renderList - The render list.
*/
beginRenderList( renderList ) {
renderList.begin();
}
/**
* Finishes the projection into the given render list.
*
* @param {RenderList} renderList - The render list.
*/
finishRenderList( renderList ) {
renderList.finish();
}
/**
* Returns `true` if the given object is culled by the camera and should not be rendered.
* Objects of render bundles are not culled, a bundle is recorded once and drawn with
* any camera.
*
* @param {Object3D} object - The 3D object.
* @param {Camera} camera - The camera.
* @return {boolean} Whether the object is culled or not.
*/
isCulled( object, camera ) {
if ( ! object.frustumCulled || this._bundleDepth > 0 ) return false;
const frustum = camera.isArrayCamera ? this._frustumArray : this._frustum;
return ! object.intersectsFrustum( frustum );
}
/**
* Pushes a render item to the render list.
*
* @param {RenderList} renderList - The current render list.
* @param {Object3D} object - The 3D object.
* @param {BufferGeometry} geometry - The 3D object's geometry.
* @param {Material} material - The 3D object's material.
* @param {number} groupOrder - The current group order.
* @param {number} z - The 3D object's depth value (z value in clip space).
* @param {?Object} group - Only relevant for objects using multiple materials. This represents a group entry from the respective `BufferGeometry`.
* @param {ClippingContext} clippingContext - The current clipping context.
*/
pushRenderItem( renderList, object, geometry, material, groupOrder, z, group, clippingContext ) {
renderList.push( object, geometry, material, groupOrder, z, group, clippingContext );
}
/**
* This method is used to traverse the scene graph and collect all renderable objects
* and lights into the given render list. Call {@link StandardDrawingPipeline#begin} before.
*
* @param {Object3D} object - The 3D object to process (usually a scene).
* @param {Camera} camera - The camera the object should be rendered with.
* @param {number} groupOrder - The group order is derived from the `renderOrder` of groups and is used to group 3D objects within groups.
* @param {RenderList} renderList - The current render list.
* @param {ClippingContext} clippingContext - The current clipping context.
*/
project( object, camera, groupOrder, renderList, clippingContext ) {
if ( object.visible === false ) return;
const renderer = this.renderer;
const visible = object.layers.test( camera.layers );
if ( visible ) {
if ( object.isGroup ) {
groupOrder = object.renderOrder;
if ( object.isClippingGroup && object.enabled ) clippingContext = clippingContext.getGroupContext( object );
} else if ( object.isLOD ) {
if ( object.autoUpdate === true ) object.update( camera );
} else if ( object.isLight ) {
renderList.pushLight( object );
} else if ( object.isSprite ) {
if ( this.isCulled( object, camera ) === false ) {
if ( renderer.sortObjects === true ) {
_vector4$1.setFromMatrixPosition( object.matrixWorld ).applyMatrix4( this._projScreenMatrix );
}
const { geometry, material } = object;
if ( material.visible ) {
this.pushRenderItem( renderList, object, geometry, material, groupOrder, _vector4$1.z, null, clippingContext );
}
}
} else if ( object.isLineLoop ) {
error( 'Renderer: Objects of type THREE.LineLoop are not supported. Please use THREE.Line or THREE.LineSegments.' );
} else if ( object.isMesh || object.isLine || object.isPoints ) {
if ( this.isCulled( object, camera ) === false ) {
const { geometry, material } = object;
if ( renderer.sortObjects === true ) {
if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();
_vector4$1
.copy( geometry.boundingSphere.center )
.applyMatrix4( object.matrixWorld )
.applyMatrix4( this._projScreenMatrix );
}
if ( Array.isArray( material ) ) {
const groups = geometry.groups;
for ( let i = 0, l = groups.length; i < l; i ++ ) {
const group = groups[ i ];
const groupMaterial = material[ group.materialIndex ];
if ( groupMaterial && groupMaterial.visible ) {
this.pushRenderItem( renderList, object, geometry, groupMaterial, groupOrder, _vector4$1.z, group, clippingContext );
}
}
} else if ( material.visible ) {
this.pushRenderItem( renderList, object, geometry, material, groupOrder, _vector4$1.z, null, clippingContext );
}
}
}
}
if ( object.isBundleGroup === true && renderer.backend.beginBundle !== undefined ) {
const baseRenderList = renderList;
// replace render list
renderList = renderer._renderLists.get( object, camera, renderer.lighting );
const renderBundle = renderer._bundles.get( object, camera, renderer._currentRenderContext );
const renderBundleData = renderer.backend.get( renderBundle );
const renderBundleNeedsUpdate = renderer._bundleNeedsUpdate( object, renderBundleData );
if ( renderBundleNeedsUpdate ) {
// update render list if necessary
this.beginRenderList( renderList );
if ( renderBundleData.renderObjects === undefined ) {
renderBundleData.renderObjects = [];
} else {
renderBundleData.renderObjects.length = 0;
}
const children = object.children;
this._bundleDepth ++;
for ( let i = 0, l = children.length; i < l; i ++ ) {
this.project( children[ i ], camera, groupOrder, renderList, clippingContext );
}
this._bundleDepth --;
this.finishRenderList( renderList );
}
baseRenderList.pushBundle( {
bundleGroup: object,
camera,
renderList,
} );
return;
}
//
const children = object.children;
for ( let i = 0, l = children.length; i < l; i ++ ) {
this.project( children[ i ], camera, groupOrder, renderList, clippingContext );
}
}
}
const _scene = /*@__PURE__*/ new Scene();
const _drawingBufferSize = /*@__PURE__*/ new Vector2();
const _screen = /*@__PURE__*/ new Vector4();
const _vector4 = /*@__PURE__*/ new Vector4();
const _shadowSide = { [ FrontSide ]: BackSide, [ BackSide ]: FrontSide, [ DoubleSide ]: DoubleSide };
// The time in milliseconds an async compilation works before it yields to the main thread.
const _compileYieldInterval = 8;
// The maximum number of pipelines an async compilation creates at the same time.
// Too many pipelines in flight stall the rendering of new frames.
const _maxCompilingPipelines = 16;
/**
* Yields to the main thread during an async compilation, but only after a time
* budget, so the compilation does not wait for a new task or frame after every
* small piece of work.
*
* @private
*/
class CompileScheduler {
constructor() {
this.lastYieldTime = performance.now();
}
/**
* Whether the compilation worked longer than the time budget since the last yield.
*
* @return {boolean} Whether the compilation should yield to the main thread.
*/
needsYield() {
return performance.now() - this.lastYieldTime >= _compileYieldInterval;
}
/**
* Yields to the main thread until the next frame was rendered. `scheduler.yield()`
* alone is not enough, since its continuations run before the browser renders.
*
* @return {Promise} A promise that resolves when the compilation can continue.
*/
async yield() {
if ( typeof requestAnimationFrame === 'undefined' || ( typeof document !== 'undefined' && document.hidden === true ) ) {
// no frames are rendered
await yieldToMain();
} else {
await new Promise( ( resolve ) => {
let resolved = false;
const done = () => {
if ( resolved === false ) {
resolved = true;
resolve();
}
};
// continue in a new task after the next frame was rendered
requestAnimationFrame( () => setTimeout( done, 0 ) );
// requestAnimationFrame() stops if the page is hidden in the meantime
setTimeout( done, 100 );
} );
}
this.lastYieldTime = performance.now();
}
}
/**
* Base class for renderers.
*/
class Renderer {
/**
* Renderer options.
*
* @typedef {Object} Renderer~Options
* @property {boolean} [logarithmicDepthBuffer=false] - Whether logarithmic depth buffer is enabled or not. Ignored when a reversed depth buffer is in use.
* @property {boolean} [reversedDepthBuffer=false] - Whether reversed depth buffer is enabled or not.
* @property {boolean} [alpha=true] - Whether the default framebuffer (which represents the final contents of the canvas) should be transparent or opaque.
* @property {boolean} [depth=true] - Whether the default framebuffer should have a depth buffer or not.
* @property {boolean} [stencil=false] - Whether the default framebuffer should have a stencil buffer or not.
* @property {boolean} [antialias=false] - Whether MSAA as the default anti-aliasing should be enabled or not.
* @property {number} [samples=0] - When `antialias` is `true`, `4` samples are used by default. This parameter can set to any other integer value than 0
* to overwrite the default.
* @property {?Function} [getFallback=null] - This callback function can be used to provide a fallback backend, if the primary backend can't be targeted.
* @property {number} [outputBufferType=HalfFloatType] - Defines the type of output buffers. The default `HalfFloatType` is recommend for best
* quality. To save memory and bandwidth, `UnsignedByteType` might be used. This will reduce rendering quality though.
* @property {boolean} [multiview=false] - If set to `true`, the renderer will use multiview during WebXR rendering if supported.
*/
/**
* Constructs a new renderer.
*
* @param {Backend} backend - The backend the renderer is targeting (e.g. WebGPU or WebGL 2).
* @param {Renderer~Options} [parameters] - The configuration parameter.
*/
constructor( backend, parameters = {} ) {
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isRenderer = true;
//
const {
logarithmicDepthBuffer = false,
reversedDepthBuffer = false,
alpha = true,
depth = true,
stencil = false,
antialias = false,
samples = 0,
getFallback = null,
outputBufferType = HalfFloatType,
multiview = false
} = parameters;
/**
* A reference to the current backend.
*
* @type {Backend}
*/
this.backend = backend;
/**
* Whether the renderer should automatically clear the current rendering target
* before execute a `render()` call. The target can be the canvas (default framebuffer)
* or the current bound render target (custom framebuffer).
*
* @type {boolean}
* @default true
*/
this.autoClear = true;
/**
* When `autoClear` is set to `true`, this property defines whether the renderer
* should clear the color buffer.
*
* @type {boolean}
* @default true
*/
this.autoClearColor = true;
/**
* When `autoClear` is set to `true`, this property defines whether the renderer
* should clear the depth buffer.
*
* @type {boolean}
* @default true
*/
this.autoClearDepth = true;
/**
* When `autoClear` is set to `true`, this property defines whether the renderer
* should clear the stencil buffer.
*
* @type {boolean}
* @default true
*/
this.autoClearStencil = true;
/**
* Whether the default framebuffer should be transparent or opaque.
*
* @type {boolean}
* @default true
*/
this.alpha = alpha;
/**
* Whether logarithmic depth buffer is enabled or not. It is disabled
* on initialization when a reversed depth buffer is in use.
*
* @type {boolean}
* @default false
* @readonly
*/
this.logarithmicDepthBuffer = logarithmicDepthBuffer;
/**
* Whether reversed depth buffer is enabled or not.
*
* @type {boolean}
* @default false
* @readonly
*/
this.reversedDepthBuffer = reversedDepthBuffer;
/**
* Defines the output color space of the renderer.
*
* @type {string}
* @default SRGBColorSpace
*/
this.outputColorSpace = SRGBColorSpace;
/**
* Defines the tone mapping of the renderer.
*
* @type {number}
* @default NoToneMapping
*/
this.toneMapping = NoToneMapping;
/**
* Defines the tone mapping exposure.
*
* @type {number}
* @default 1
*/
this.toneMappingExposure = 1.0;
/**
* Whether the renderer should sort its render lists or not.
*
* Note: Sorting is used to attempt to properly render objects that have some degree of transparency.
* By definition, sorting objects may not work in all cases. Depending on the needs of application,
* it may be necessary to turn off sorting and use other methods to deal with transparency rendering
* e.g. manually determining each object's rendering order.
*
* @type {boolean}
* @default true
*/
this.sortObjects = true;
/**
* Whether the default framebuffer should have a depth buffer or not.
*
* @type {boolean}
* @default true
*/
this.depth = depth;
/**
* Whether the default framebuffer should have a stencil buffer or not.
*
* @type {boolean}
* @default false
*/
this.stencil = stencil;
/**
* Holds a series of statistical information about the GPU memory
* and the rendering process. Useful for debugging and monitoring.
*
* @type {Info}
*/
this.info = new Info();
/**
* A global context node that stores override nodes for specific transformations or calculations.
* These nodes can be used to replace default behavior in the rendering pipeline.
*
* @type {ContextNode}
* @property {Object} value - The context value object.
*/
this.contextNode = context();
/**
* The node library defines how certain library objects like materials, lights
* or tone mapping functions are mapped to node types. This is required since
* although instances of classes like `MeshBasicMaterial` or `PointLight` can
* be part of the scene graph, they are internally represented as nodes for
* further processing.
*
* @type {NodeLibrary}
*/
this.library = new NodeLibrary();
/**
* A map-like data structure for managing lights.
*
* @type {Lighting}
*/
this.lighting = new Lighting();
/**
* The current drawing pipeline. See {@link Renderer#drawingPipeline}.
*
* @private
* @type {StandardDrawingPipeline}
*/
this._drawingPipeline = null;
this.drawingPipeline = new StandardDrawingPipeline();
// internals
/**
* The number of MSAA samples.
*
* @private
* @type {number}
* @default 0
*/
this._samples = samples || ( antialias === true ? 4 : 0 );
/**
* OnCanvasTargetResize callback function.
*
* @private
* @type {Function}
*/
this._onCanvasTargetResize = this._onCanvasTargetResize.bind( this );
/**
* OnCanvasTargetDispose callback function.
*
* @private
* @type {Function}
*/
this._onCanvasTargetDispose = this._onCanvasTargetDispose.bind( this );
/**
* The canvas target for rendering.
*
* @private
* @type {CanvasTarget}
*/
this._canvasTarget = new CanvasTarget( backend.getDomElement() );
this._canvasTarget.addEventListener( 'resize', this._onCanvasTargetResize );
this._canvasTarget.addEventListener( 'dispose', this._onCanvasTargetDispose );
this._canvasTarget.isDefaultCanvasTarget = true;
/**
* The inspector provides information about the internal renderer state.
*
* @private
* @type {InspectorBase}
*/
this._inspector = new InspectorBase();
this._inspector.setRenderer( this );
/**
* This callback function can be used to provide a fallback backend, if the primary backend can't be targeted.
*
* @private
* @type {?Function}
*/
this._getFallback = getFallback;
/**
* A reference to a renderer module for managing shader attributes.
*
* @private
* @type {?Attributes}
* @default null
*/
this._attributes = null;
/**
* A reference to a renderer module for managing geometries.
*
* @private
* @type {?Geometries}
* @default null
*/
this._geometries = null;
/**
* A reference to a renderer module for managing node related logic.
*
* @private
* @type {?NodeManager}
* @default null
*/
this._nodes = null;
/**
* A reference to a renderer module for managing the internal animation loop.
*
* @private
* @type {?Animation}
* @default null
*/
this._animation = null;
/**
* A reference to a renderer module for managing shader program bindings.
*
* @private
* @type {?Bindings}
* @default null
*/
this._bindings = null;
/**
* A reference to a renderer module for managing render objects.
*
* @private
* @type {?RenderObjects}
* @default null
*/
this._objects = null;
/**
* A reference to a renderer module for managing render and compute pipelines.
*
* @private
* @type {?Pipelines}
* @default null
*/
this._pipelines = null;
/**
* A reference to a renderer module for managing render bundles.
*
* @private
* @type {?RenderBundles}
* @default null
*/
this._bundles = null;
/**
* A reference to a renderer module for managing render lists.
*
* @private
* @type {?RenderLists}
* @default null
*/
this._renderLists = null;
/**
* A reference to a renderer module for managing render contexts.
*
* @private
* @type {?RenderContexts}
* @default null
*/
this._renderContexts = null;
/**
* A reference to a renderer module for managing textures.
*
* @private
* @type {?Textures}
* @default null
*/
this._textures = null;
/**
* A reference to a renderer module for backgrounds.
*
* @private
* @type {?Background}
* @default null
*/
this._background = null;
/**
* Cache for the fullscreen quad.
* This fullscreen quad is used for internal render passes
* like the tone mapping and color space output pass.
*
* @private
* @type {Map<Texture,QuadMesh>}
*/
this._quadCache = new Map();
/**
* A reference to the current render context.
*
* @private
* @type {?RenderContext}
* @default null
*/
this._currentRenderContext = null;
/**
* A custom sort function for the opaque render list.
*
* @private
* @type {?Function}
* @default null
*/
this._opaqueSort = null;
/**
* A custom sort function for the transparent render list.
*
* @private
* @type {?Function}
* @default null
*/
this._transparentSort = null;
/**
* Cache of framebuffer targets per canvas target.
*
* @private
* @type {Map<CanvasTarget, RenderTarget>}
*/
this._frameBufferTargets = new Map();
const alphaClear = this.alpha === true ? 0 : 1;
/**
* The clear color value.
*
* @private
* @type {Color4}
*/
this._clearColor = new Color4( 0, 0, 0, alphaClear );
/**
* The clear depth value.
*
* @private
* @type {number}
* @default 1
*/
this._clearDepth = 1;
/**
* The clear stencil value.
*
* @private
* @type {number}
* @default 0
*/
this._clearStencil = 0;
/**
* The current render target.
*
* @private
* @type {?RenderTarget}
* @default null
*/
this._renderTarget = null;
/**
* The active cube face.
*
* @private
* @type {number}
* @default 0
*/
this._activeCubeFace = 0;
/**
* The active mipmap level.
*
* @private
* @type {number}
* @default 0
*/
this._activeMipmapLevel = 0;
/**
* The current output render target.
*
* @private
* @type {?RenderTarget}
* @default null
*/
this._outputRenderTarget = null;
/**
* The MRT setting.
*
* @private
* @type {?MRTNode}
* @default null
*/
this._mrt = null;
/**
* This function defines how a render object is going
* to be rendered.
*
* @private
* @type {?Function}
* @default null
*/
this._renderObjectFunction = null;
/**
* Used to keep track of the current render object function.
*
* @private
* @type {?Function}
* @default null
*/
this._currentRenderObjectFunction = null;
/**
* Used to keep track of the current render bundle.
*
* @private
* @type {?RenderBundle}
* @default null
*/
this._currentRenderBundle = null;
/**
* Next to `_renderObjectFunction()`, this function provides another hook
* for influencing the render process of a render object. It is meant for internal
* use and only relevant for `compileAsync()` right now. Instead of using
* the default logic of `_renderObjectDirect()` which actually draws the render object,
* a different function might be used which performs no draw but just the node
* and pipeline updates.
*
* @private
* @type {Function}
*/
this._handleObjectFunction = this._renderObjectDirect;
/**
* Indicates whether the device has been lost or not. In WebGL terms, the device
* lost is considered as a context lost. When this is set to `true`, rendering
* isn't possible anymore.
*
* @private
* @type {boolean}
* @default false
*/
this._isDeviceLost = false;
/**
* A callback function that defines what should happen when a device/context lost occurs.
*
* @type {Function}
*/
this.onDeviceLost = this._onDeviceLost;
/**
* A callback function that defines what should happen when an uncaptured
* backend error is reported (e.g. a WebGPU validation/out-of-memory/internal
* error raised outside an error scope). Applications can override this to
* surface errors in their own UI without letting them escalate to a device
* loss. The default implementation logs to the console.
*
* @type {Function}
*/
this.onError = this._onError;
/**
* Defines the type of output buffers. The default `HalfFloatType` is recommend for
* best quality. To save memory and bandwidth, `UnsignedByteType` might be used.
* This will reduce rendering quality though.
*
* @private
* @type {number}
* @default HalfFloatType
*/
this._outputBufferType = outputBufferType;
/**
* A cache for shadow nodes per material
*
* @private
* @type {WeakMap<Material, Object>}
*/
this._cacheShadowNodes = new WeakMap();
/**
* Whether the renderer has been initialized or not.
*
* @private
* @type {boolean}
* @default false
*/
this._initialized = false;
/**
* The call depth of the renderer. Counts the number of
* nested render calls.
*
* @private
* @type {number}
* @default - 1
*/
this._callDepth = -1;
/**
* A reference to the promise which initializes the renderer.
*
* @private
* @type {?Promise<this>}
* @default null
*/
this._initPromise = null;
/**
* Render and compute work items processed sequentially by `compileAsync()`.
*
* @private
* @type {?Array<Object>}
* @default null
*/
this._compilationPromises = null;
/**
* The state of the current precompilation in `compileAsync()`.
* `null` when the renderer is not precompiling.
*
* @private
* @type {?Object}
* @default null
*/
this._precompilationState = null;
/**
* When an override material is in use, this property points to the current
* source material during the rendering of a render object.
*
* @private
* @type {?Material}
* @default null
*/
this._currentSourceMaterial = null;
/**
* Whether the renderer should render transparent render objects or not.
*
* @type {boolean}
* @default true
*/
this.transparent = true;
/**
* Whether the renderer should render opaque render objects or not.
*
* @type {boolean}
* @default true
*/
this.opaque = true;
/**
* Shadow map configuration
* @typedef {Object} ShadowMapConfig
* @property {boolean} enabled - Whether to globally enable shadows or not.
* @property {boolean} transmitted - Whether to enable light transmission through non-opaque materials.
* @property {number} type - The shadow map type.
*/
/**
* The renderer's shadow configuration.
*
* @type {ShadowMapConfig}
*/
this.shadowMap = {
enabled: false,
transmitted: false,
type: PCFShadowMap
};
/**
* XR configuration.
* @typedef {Object} XRConfig
* @property {boolean} enabled - Whether to globally enable XR or not.
*/
/**
* The renderer's XR manager.
*
* @type {XRManager}
*/
this.xr = new XRManager( this, multiview );
/**
* Debug configuration.
* @typedef {Object} DebugConfig
* @property {boolean} checkShaderErrors - Whether shader errors should be checked or not.
* @property {Object} diagnostics - Diagnostics configuration for the shader generation.
* @property {boolean} diagnostics.keywords - Whether declaration names that collide with reserved keywords should be renamed or not.
* @property {?Function} onNodeBuilderCreated - A callback function that is executed after a node builder has been created and before it is built.
* @property {?Function} onShaderError - A callback function that is executed when a shader error happens. Only supported with WebGL 2 right now.
* @property {Function} getShaderAsync - Allows the get the raw shader code for the given scene, camera and 3D object.
*/
/**
* The renderer's debug configuration.
*
* @type {DebugConfig}
*/
this.debug = {
checkShaderErrors: true,
diagnostics: {
keywords: false
},
onNodeBuilderCreated: null,
onShaderError: null,
getShaderAsync: async ( scene, camera, object ) => {
await this.compileAsync( object, camera, scene );
const useFrameBufferTarget = this.needsFrameBufferTarget && this._renderTarget === null;
const renderTarget = useFrameBufferTarget ? this._getFrameBufferTarget() : ( this._renderTarget || this._outputRenderTarget );
const renderList = this._renderLists.get( scene, camera, this.lighting );
const renderContext = this._renderContexts.get( renderTarget, this._mrt, 0, camera );
const material = scene.overrideMaterial || object.material;
const renderObject = this._objects.get( object, material, scene, camera, renderList.lightsNode, renderContext, renderContext.clippingContext );
const { fragmentShader, vertexShader } = renderObject.getNodeBuilderState();
return { fragmentShader, vertexShader };
}
};
}
/**
* Decides which objects of a scene are rendered and how they are submitted
* to the render list. Assigning a drawing pipeline binds it to this renderer.
* The default is a {@link StandardDrawingPipeline}. This property is experimental.
*
* @type {StandardDrawingPipeline}
*/
set drawingPipeline( pipeline ) {
pipeline.renderer = this;
this._drawingPipeline = pipeline;
}
get drawingPipeline() {
return this._drawingPipeline;
}
/**
* Initializes the renderer so it is ready for usage.
*
* @async
* @return {Promise<this>} A Promise that resolves when the renderer has been initialized.
*/
async init() {
if ( this._initPromise !== null ) {
return this._initPromise;
}
this._initPromise = new Promise( async ( resolve, reject ) => {
let backend = this.backend;
try {
await backend.init( this );
} catch ( error ) {
if ( this._getFallback !== null ) {
// try the fallback
try {
this.backend = backend = this._getFallback( error );
await backend.init( this );
} catch ( error ) {
reject( error );
return;
}
} else {
reject( error );
return;
}
}
if ( this.logarithmicDepthBuffer === true && this.reversedDepthBuffer === true ) {
warn( 'WebGPURenderer: Logarithmic depth buffer is ignored when reversed depth buffer is enabled.' );
this.logarithmicDepthBuffer = false;
}
this._nodes = new NodeManager( this, backend );
this._animation = new Animation( this, this._nodes, this.info );
this._attributes = new Attributes( backend, this.info );
this._background = new Background( this, this._nodes );
this._geometries = new Geometries( backend, this._attributes, this.info );
this._textures = new Textures( this, backend, this.info );
this._pipelines = new Pipelines( backend, this._nodes, this.info );
this._bindings = new Bindings( backend, this._nodes, this._textures, this._attributes, this._pipelines, this.info );
this._objects = new RenderObjects( this, this._nodes, this._geometries, this._pipelines, this._bindings, this.info );
this._renderLists = new RenderLists();
this._bundles = new RenderBundles();
this._renderContexts = new RenderContexts( this );
//
this._initialized = true;
resolve( this );
} );
return this._initPromise;
}
/**
* A reference to the canvas element the renderer is drawing to.
* This value of this property will automatically be created by
* the renderer.
*
* @type {HTMLCanvasElement|OffscreenCanvas}
*/
get domElement() {
return this._canvasTarget.domElement;
}
/**
* The coordinate system of the renderer. The value of this property
* depends on the selected backend. Either `THREE.WebGLCoordinateSystem` or
* `THREE.WebGPUCoordinateSystem`.
*
* @readonly
* @type {number}
*/
get coordinateSystem() {
return this.backend.coordinateSystem;
}
/**
* Compiles all materials in the given scene. This can be useful to avoid a
* phenomenon which is called "shader compilation stutter", which occurs when
* rendering an object with a new shader for the first time.
* The active drawing pipeline also prepares its geometry resources and compute pipelines.
*
* If you want to add a 3D object to an existing scene, use the third optional
* parameter for applying the target scene. Note that the (target) scene's lighting
* and environment must be configured before calling this method.
*
* @async
* @param {Object3D} scene - The scene or 3D object to precompile.
* @param {Camera} camera - The camera that is used to render the scene.
* @param {?Scene} targetScene - If the first argument is a 3D object, this parameter must represent the scene the 3D object is going to be added.
* @param {onProgressCallback} [onProgress] - Executed while the compilation is in progress.
* @return {Promise} A Promise that resolves when the compile has been finished.
*/
async compileAsync( scene, camera, targetScene = null, onProgress = null ) {
if ( this._isDeviceLost === true ) return;
if ( this._initialized === false ) await this.init();
// only awaited when the drawing pipeline prepares something, so the state below is still taken synchronously
const prepare = this.drawingPipeline.prepareAsync( scene, camera );
if ( prepare !== undefined ) await prepare;
if ( this.shadowMap.type === PCFSoftShadowMap ) {
warn( 'WebGPURenderer: PCFSoftShadowMap has been removed. Using PCFShadowMap instead.' );
this.shadowMap.type = PCFShadowMap;
}
// preserve render tree
const nodeFrame = this._nodes.nodeFrame;
const previousRenderId = nodeFrame.renderId;
const previousRenderContext = this._currentRenderContext;
const previousRenderObjectFunction = this._currentRenderObjectFunction;
const previousHandleObjectFunction = this._handleObjectFunction;
const previousCompilationPromises = this._compilationPromises;
//
if ( targetScene === null ) targetScene = scene;
// Use the actual scene for caching when compiling individual objects
// This ensures cache keys match between compileAsync and render
const sceneRef = ( scene.isScene === true ) ? scene : ( targetScene.isScene === true ) ? targetScene : _scene;
// Match render()'s logic: use frameBufferTarget when needsFrameBufferTarget is true
const useFrameBufferTarget = this.needsFrameBufferTarget && this._renderTarget === null;
const outputRenderTarget = this._renderTarget || this._outputRenderTarget;
const useXRCamera = this.xr.isPresenting === true && this.isOutputTarget;
const renderTarget = useFrameBufferTarget ? this._getFrameBufferTarget() : outputRenderTarget;
const activeCubeFace = this._activeCubeFace;
const activeMipmapLevel = this._activeMipmapLevel;
// a state snapshot is taken once at the beginning and reapplied every time precompilation continues.
// `item` is the work item that is currently processed, see `_beginPreCompile()`
// the work items always set their render target, without the frame buffer target the current one is restored
const precompilationState = { useFrameBufferTarget, renderTarget, outputRenderTarget: useFrameBufferTarget ? outputRenderTarget : this._renderTarget, activeCubeFace, activeMipmapLevel, item: null, materialSide: null, mrt: this._mrt, outputMRT: this._mrt };
this._beginPreCompile( precompilationState );
const renderContext = this._renderContexts.get( renderTarget, this._mrt, this._callDepth, camera );
precompilationState.renderContext = renderContext;
const compilationPromises = [];
this._currentRenderContext = renderContext;
this._currentRenderObjectFunction = this.renderObject;
this._handleObjectFunction = this._createObjectPipeline;
this._compilationPromises = compilationPromises;
nodeFrame.renderId ++;
//
nodeFrame.update();
//
renderContext.depth = this.depth;
renderContext.stencil = this.stencil;
if ( ! renderContext.clippingContext ) renderContext.clippingContext = new ClippingContext();
renderContext.clippingContext.updateGlobal( sceneRef, camera );
//
if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();
camera = this._updateCamera( camera, useXRCamera );
//
sceneRef.onBeforeRender( this, scene, camera, renderTarget );
//
this.drawingPipeline.begin( camera, renderContext );
// Use sceneRef for render list to ensure lightsNode matches between compileAsync and render
const renderList = this._renderLists.get( sceneRef, camera, this.lighting );
this.drawingPipeline.beginRenderList( renderList );
this.drawingPipeline.project( scene, camera, 0, renderList, renderContext.clippingContext );
// include lights from target scene
if ( targetScene !== scene ) {
targetScene.traverseVisible( function ( object ) {
if ( object.isLight && object.layers.test( camera.layers ) ) {
renderList.pushLight( object );
}
} );
}
this.drawingPipeline.finishRenderList( renderList );
//
if ( renderTarget !== null ) {
this._textures.updateRenderTarget( renderTarget, activeMipmapLevel );
const renderTargetData = this._textures.get( renderTarget );
renderContext.textures = renderTargetData.textures;
renderContext.depthTexture = renderTargetData.depthTexture;
} else {
renderContext.textures = null;
renderContext.depthTexture = null;
}
//
if ( targetScene !== scene ) {
this._background.update( targetScene, renderList, renderContext );
} else {
this._background.update( sceneRef, renderList, renderContext );
}
// process render lists - _createObjectPipeline queues work in _compilationPromises
const opaqueObjects = renderList.opaque;
const transparentObjects = renderList.transparent;
const transparentDoublePassObjects = renderList.transparentDoublePass;
const lightsNode = renderList.lightsNode;
if ( this.opaque === true && opaqueObjects.length > 0 ) this._renderObjects( opaqueObjects, camera, sceneRef, lightsNode );
if ( this.transparent === true && transparentObjects.length > 0 ) this._renderTransparents( transparentObjects, transparentDoublePassObjects, camera, sceneRef, lightsNode );
// restore render tree
nodeFrame.renderId = previousRenderId;
this._currentRenderContext = previousRenderContext;
this._currentRenderObjectFunction = previousRenderObjectFunction;
this._handleObjectFunction = previousHandleObjectFunction;
this._compilationPromises = previousCompilationPromises;
this._finishPreCompile();
// Process compilation work items sequentially and yield to the main thread after a
// time budget to keep animation smooth. Pipelines are compiled in parallel while
// the next items are processed.
const total = compilationPromises.length;
let loaded = 0;
const scheduler = new CompileScheduler();
const compilingPipelines = new Set();
const onLoaded = () => {
loaded ++;
if ( onProgress !== null ) {
onProgress( new ProgressEvent( 'progress', { lengthComputable: true, loaded, total } ) );
}
};
const yieldPreCompile = async () => {
if ( scheduler.needsYield() === false ) return;
const activePrecompilationState = this._precompilationState;
this._finishPreCompile();
await scheduler.yield();
this._beginPreCompile( activePrecompilationState );
};
for ( const item of compilationPromises ) {
if ( item.computeNodes !== undefined ) {
// compute work queued by the drawing pipeline, e.g. its culling
await this.compileComputeAsync( item.computeNodes );
onLoaded();
continue;
}
const pipelinePromises = [];
// each item is compiled with the state of the render call that queued it, e.g. a nested render into another target
const itemPrecompilationState = item.precompilationState;
itemPrecompilationState.item = item;
this._beginPreCompile( itemPrecompilationState );
const renderObject = this._objects.get( item.object, item.material, item.scene, item.camera, item.lightsNode, item.renderContext, item.clippingContext, item.passId );
renderObject.drawRange = item.object.geometry.drawRange;
renderObject.group = item.group;
await this._nodes.getForRenderAsync( renderObject, yieldPreCompile );
this._nodes.updateBefore( renderObject );
this._geometries.updateForRender( renderObject );
this._nodes.updateForRender( renderObject );
this._bindings.updateForRender( renderObject );
this._pipelines.getForRender( renderObject, pipelinePromises );
this._nodes.updateAfter( renderObject );
this._finishPreCompile();
if ( pipelinePromises.length > 0 ) {
const promise = Promise.all( pipelinePromises ).then( () => {
compilingPipelines.delete( promise );
onLoaded();
} );
compilingPipelines.add( promise );
while ( compilingPipelines.size >= _maxCompilingPipelines ) await Promise.race( compilingPipelines );
} else {
onLoaded();
}
if ( scheduler.needsYield() ) await scheduler.yield();
}
await Promise.all( compilingPipelines );
}
/**
* Sets up the renderer state for precompiling in `compileAsync()`.
* The state must be restored with `_finishPreCompile()` before yielding to the main thread,
* otherwise it leaks into renders executed in the meantime.
*
* @private
* @param {Object} precompilationState - The precompilation state.
*/
_beginPreCompile( precompilationState ) {
const { useFrameBufferTarget, renderTarget, mrt, renderContext, item } = precompilationState;
if ( useFrameBufferTarget === true ) this.setRenderTarget( renderTarget );
this.setMRT( mrt );
if ( item !== null ) {
// apply the material side the work item was queued with since the renderer changes the side
// temporarily, e.g. for the back and front pass of double-sided transparent materials
precompilationState.materialSide = item.material.side;
item.material.side = item.side;
}
this._callDepth ++;
// node builds take their shared bind groups from the current render context
precompilationState.previousRenderContext = this._currentRenderContext;
if ( renderContext !== undefined ) this._currentRenderContext = renderContext;
this._precompilationState = precompilationState;
}
/**
* Restores the renderer state after precompiling in `compileAsync()`.
*
* @private
*/
_finishPreCompile() {
const { useFrameBufferTarget, outputRenderTarget, activeCubeFace, activeMipmapLevel, outputMRT, previousRenderContext, item, materialSide } = this._precompilationState;
if ( useFrameBufferTarget === true ) this.setRenderTarget( outputRenderTarget, activeCubeFace, activeMipmapLevel );
this.setMRT( outputMRT );
if ( item !== null ) item.material.side = materialSide;
this._currentRenderContext = previousRenderContext;
this._callDepth --;
this._precompilationState = null;
}
/**
* Compile compute programs. This can be useful to avoid a
* phenomenon which is called "shader compilation stutter", which occurs when
* rendering an object with a new shader for the first time.
*
* @async
* @param {Node|Array<Node>} computeNodes - The compute node(s).
* @param {onProgressCallback} [onProgress] - Executed while the compilation is in progress.
* @return {Promise} A Promise that resolves when the compile has been finished.
*/
async compileComputeAsync( computeNodes, onProgress = null ) {
if ( this._isDeviceLost === true ) return;
if ( this._initialized === false ) await this.init();
const computeList = Array.isArray( computeNodes ) ? computeNodes : [ computeNodes ];
if ( computeList.length === 0 || computeList.some( ( computeNode ) => computeNode === undefined || computeNode === null || computeNode.isComputeNode !== true ) ) {
throw new Error( 'THREE.Renderer: .compileComputeAsync() expects a ComputeNode.' );
}
const total = computeList.length;
let loaded = 0;
const scheduler = new CompileScheduler();
const compilingPipelines = new Set();
const onLoaded = () => {
loaded ++;
if ( onProgress !== null ) {
onProgress( new ProgressEvent( 'progress', { lengthComputable: true, loaded, total } ) );
}
};
const yieldCompile = async () => {
if ( scheduler.needsYield() ) await scheduler.yield();
};
//
const pipelines = this._pipelines;
const bindings = this._bindings;
const nodes = this._nodes;
for ( const computeNode of computeList ) {
if ( pipelines.has( computeNode ) === false ) {
const dispose = () => {
computeNode.removeEventListener( 'dispose', dispose );
pipelines.delete( computeNode );
bindings.deleteForCompute( computeNode );
nodes.delete( computeNode );
};
computeNode.addEventListener( 'dispose', dispose );
const onInitFn = computeNode.onInitFunction;
if ( onInitFn !== null ) {
onInitFn.call( computeNode, { renderer: this } );
}
}
await nodes.getForComputeAsync( computeNode, yieldCompile );
nodes.updateBeforeForCompute( computeNode );
nodes.updateForCompute( computeNode );
bindings.updateForCompute( computeNode );
const computeBindings = bindings.getForCompute( computeNode );
const compilationPromises = [];
pipelines.getForCompute( computeNode, computeBindings, compilationPromises );
nodes.updateAfterForCompute( computeNode );
// Pipelines are compiled in parallel while the next compute nodes are processed
if ( compilationPromises.length > 0 ) {
const promise = Promise.all( compilationPromises ).then( () => {
compilingPipelines.delete( promise );
onLoaded();
} );
compilingPipelines.add( promise );
while ( compilingPipelines.size >= _maxCompilingPipelines ) await Promise.race( compilingPipelines );
} else {
onLoaded();
}
await yieldCompile();
}
await Promise.all( compilingPipelines );
}
/**
* Renders the scene in an async fashion.
*
* @async
* @deprecated
* @param {Object3D} scene - The scene or 3D object to render.
* @param {Camera} camera - The camera.
* @return {Promise} A Promise that resolves when the render has been finished.
*/
async renderAsync( scene, camera ) {
warnOnce( 'Renderer: "renderAsync()" has been deprecated. Use "render()" and "await renderer.init();" when creating the renderer.' ); // @deprecated r181
await this.init();
this.render( scene, camera );
}
/**
* Can be used to synchronize CPU operations with GPU tasks. So when this method is called,
* the CPU waits for the GPU to complete its operation (e.g. a compute task).
*
* @async
* @deprecated
* @return {Promise} A Promise that resolves when synchronization has been finished.
*/
async waitForGPU() {
error( 'Renderer: waitForGPU() has been removed. Read https://github.com/mrdoob/three.js/issues/32012 for more information.' );
}
//
set inspector( value ) {
if ( this._inspector !== null ) {
this._inspector.setRenderer( null );
}
this._inspector = value;
this._inspector.setRenderer( this );
}
/**
* The inspector instance. The inspector can be any class that extends from `InspectorBase`.
*
* @type {InspectorBase}
*/
get inspector() {
return this._inspector;
}
/**
* Enables or disables high precision for model-view and normal-view matrices.
* When enabled, will use CPU 64-bit precision for higher precision instead of GPU 32-bit for higher performance.
*
* NOTE: 64-bit precision is not compatible with `InstancedMesh` and `SkinnedMesh`.
*
* @param {boolean} value - Whether to enable or disable high precision.
* @type {boolean}
*/
set highPrecision( value ) {
const contextNodeData = this.contextNode.value;
if ( value === true ) {
contextNodeData.modelViewMatrix = highpModelViewMatrix;
contextNodeData.modelNormalViewMatrix = highpModelNormalViewMatrix;
} else if ( this.highPrecision ) {
delete contextNodeData.modelViewMatrix;
delete contextNodeData.modelNormalViewMatrix;
}
}
/**
* Returns whether high precision is enabled or not.
*
* @return {boolean} Whether high precision is enabled or not.
* @type {boolean}
*/
get highPrecision() {
const contextNodeData = this.contextNode.value;
return contextNodeData.modelViewMatrix === highpModelViewMatrix && contextNodeData.modelNormalViewMatrix === highpModelNormalViewMatrix;
}
/**
* Sets the given MRT configuration.
*
* @param {MRTNode} mrt - The MRT node to set.
* @return {Renderer} A reference to this renderer.
*/
setMRT( mrt ) {
this._mrt = mrt;
return this;
}
/**
* Returns the MRT configuration.
*
* @return {MRTNode} The MRT configuration.
*/
getMRT() {
return this._mrt;
}
/**
* Returns the output buffer type.
*
* @return {number} The output buffer type.
*/
getOutputBufferType() {
return this._outputBufferType;
}
/**
* Returns the output buffer type.
*
* @deprecated since r182. Use `.getOutputBufferType()` instead.
* @return {number} The output buffer type.
*/
getColorBufferType() { // @deprecated, r182
warnOnce( 'Renderer: ".getColorBufferType()" has been renamed to ".getOutputBufferType()".' );
return this.getOutputBufferType();
}
/**
* Default implementation of the device lost callback.
*
* @private
* @param {Object} info - Information about the context lost.
*/
_onDeviceLost( info ) {
let errorMessage = `THREE.WebGPURenderer: ${info.api} Device Lost:\n\nMessage: ${info.message}`;
if ( info.reason ) {
errorMessage += `\nReason: ${info.reason}`;
}
error( errorMessage );
this._isDeviceLost = true;
}
/**
* Default implementation of the uncaptured backend error callback.
*
* @private
* @param {Object} info - Information about the uncaptured error.
*/
_onError( info ) {
let errorMessage = `WebGPURenderer: Uncaptured ${ info.api } ${ info.type }`;
if ( info.message ) {
errorMessage += `: ${ info.message }`;
}
error( errorMessage );
}
/**
* Returns `true` if the cached GPU render bundle for the given bundle group is
* out-of-date and must be recorded again. The drawing pipeline decides which objects are
* recorded, so bundles recorded by another drawing pipeline are recorded again.
*
* @private
* @param {BundleGroup} bundleGroup - The bundle group.
* @param {Object} renderBundleData - The backend data of the render bundle.
* @return {boolean} Whether the cached render bundle needs an update.
*/
_bundleNeedsUpdate( bundleGroup, renderBundleData ) {
if ( renderBundleData.bundleGPU === undefined || bundleGroup.version !== renderBundleData.version || renderBundleData.drawingPipeline !== this.drawingPipeline ) return true;
// the recorded bundle references the buffers of its attributes. they are only verified after a disposal
const attributes = this._attributes;
if ( renderBundleData.attributesDisposeVersion !== attributes.disposeVersion ) {
renderBundleData.attributesDisposeVersion = attributes.disposeVersion;
for ( const renderObject of renderBundleData.renderObjects ) {
const index = renderObject.getIndex();
let disposed = index !== null && attributes.has( index ) === false;
for ( const vertexBuffer of renderObject.getVertexBuffers() ) {
if ( attributes.has( vertexBuffer ) === false ) disposed = true;
}
if ( disposed ) {
// invalidate the recorded version so the bundle is recorded again
renderBundleData.version = null;
return true;
}
}
}
return false;
}
/**
* Renders the given render bundle.
*
* @private
* @param {Object} bundle - Render bundle data.
* @param {Scene} sceneRef - The scene the render bundle belongs to.
* @param {LightsNode} lightsNode - The lights node.
*/
_renderBundle( bundle, sceneRef, lightsNode ) {
const { bundleGroup, camera, renderList } = bundle;
const renderContext = this._currentRenderContext;
//
const renderBundle = this._bundles.get( bundleGroup, camera, renderContext );
const renderBundleData = this.backend.get( renderBundle );
const renderBundleNeedsUpdate = this._bundleNeedsUpdate( bundleGroup, renderBundleData );
if ( renderBundleNeedsUpdate ) {
this.backend.beginBundle( renderContext );
// nested renders, like shadow maps updated by the objects of the bundle, record their own bundles
const previousRenderBundle = this._currentRenderBundle;
this._currentRenderBundle = renderBundle;
// objects whose pipelines are not ready yet reset the version, so the bundle is recorded again
renderBundleData.version = bundleGroup.version;
renderBundleData.drawingPipeline = this.drawingPipeline;
const {
transparentDoublePass: transparentDoublePassObjects,
transparent: transparentObjects,
opaque: opaqueObjects
} = renderList;
if ( this.opaque === true && opaqueObjects.length > 0 ) this._renderObjects( opaqueObjects, camera, sceneRef, lightsNode );
if ( this.transparent === true && transparentObjects.length > 0 ) this._renderTransparents( transparentObjects, transparentDoublePassObjects, camera, sceneRef, lightsNode );
this._currentRenderBundle = previousRenderBundle;
//
this.backend.finishBundle( renderContext, renderBundle );
} else {
const { renderObjects } = renderBundleData;
for ( let i = 0, l = renderObjects.length; i < l; i ++ ) {
const renderObject = renderObjects[ i ];
const refreshType = this._nodes.needsRefresh( renderObject );
if ( refreshType === RenderObjectRefreshType.FULL ) {
this._nodes.updateBefore( renderObject );
this._geometries.updateForRender( renderObject );
this._nodes.updateForRender( renderObject );
this._bindings.updateForRender( renderObject );
this._nodes.updateAfter( renderObject );
} else if ( refreshType === RenderObjectRefreshType.SHARED ) {
this._nodes.updateBefore( renderObject );
this._nodes.updateForRender( renderObject );
this._bindings.updateSharedForRender( renderObject );
this._nodes.updateAfter( renderObject );
}
}
}
this.backend.addBundle( renderContext, renderBundle );
}
/**
* Renders the scene or 3D object with the given camera. This method can only be called
* if the renderer has been initialized. When using `render()` inside an animation loop,
* it's guaranteed the renderer will be initialized. The animation loop must be defined
* with {@link Renderer#setAnimationLoop} though.
*
* For all other use cases (like when using on-demand rendering), you must call
* {@link Renderer#init} before rendering.
*
* The target of the method is the default framebuffer (meaning the canvas)
* or alternatively a render target when specified via `setRenderTarget()`.
*
* @param {Object3D} scene - The scene or 3D object to render.
* @param {Camera} camera - The camera to render the scene with.
*/
render( scene, camera ) {
if ( this._initialized === false ) {
throw new Error( 'THREE.Renderer: .render() called before the backend is initialized. Use "await renderer.init();" before rendering.' );
}
this._animation.start();
this._renderScene( scene, camera );
}
/**
* Returns whether the renderer has been initialized or not.
*
* @readonly
* @return {boolean} Whether the renderer has been initialized or not.
*/
get initialized() {
return this._initialized;
}
_renderOutputLayers( quad, renderTarget ) {
const useMultiview = this.backend.isWebGLBackend === true && renderTarget.multiview === true;
if ( useMultiview || renderTarget.texture.isArrayTexture !== true || renderTarget.texture.image.depth <= 1 ) {
this._renderScene( quad, quad.camera, false );
return;
}
const currentActiveCubeFace = this._activeCubeFace;
try {
for ( let layer = 0; layer < renderTarget.texture.image.depth; layer ++ ) {
this._nodes.setOutputLayerIndex( layer );
this._activeCubeFace = layer;
this._renderScene( quad, quad.camera, false );
}
} finally {
this._nodes.setOutputLayerIndex( 0 );
this._activeCubeFace = currentActiveCubeFace;
}
}
/**
* Returns an internal render target which is used when computing the output tone mapping
* and color space conversion. Unlike in `WebGLRenderer`, this is done in a separate render
* pass and not inline to achieve more correct results.
*
* @private
* @return {?RenderTarget} The render target. The method returns `null` if no output conversion should be applied.
*/
_getFrameBufferTarget() {
if ( this.needsFrameBufferTarget === false ) return null;
const { width, height } = this.getDrawingBufferSize( _drawingBufferSize );
const { depth, stencil } = this;
// TODO: Unify CanvasTarget and OutputRenderTarget
const target = this._outputRenderTarget || this._canvasTarget;
let frameBufferTarget = this._frameBufferTargets.get( target );
if ( frameBufferTarget === undefined ) {
frameBufferTarget = new RenderTarget( width, height, {
depthBuffer: depth,
stencilBuffer: stencil,
type: this._outputBufferType,
format: RGBAFormat,
colorSpace: ColorManagement.workingColorSpace,
generateMipmaps: false,
minFilter: LinearFilter,
magFilter: LinearFilter,
samples: this.samples
} );
frameBufferTarget.isPostProcessingRenderTarget = true;
const dispose = () => {
target.removeEventListener( 'dispose', dispose );
frameBufferTarget.dispose();
this._frameBufferTargets.delete( target );
const quadData = this._quadCache.get( frameBufferTarget );
if ( quadData !== undefined ) {
quadData.quad.material.dispose();
this._quadCache.delete( frameBufferTarget );
}
};
target.addEventListener( 'dispose', dispose );
this._frameBufferTargets.set( target, frameBufferTarget );
}
const outputRenderTarget = this.getOutputRenderTarget();
frameBufferTarget.depthBuffer = depth;
frameBufferTarget.stencilBuffer = stencil;
if ( outputRenderTarget !== null ) {
frameBufferTarget.setSize( outputRenderTarget.width, outputRenderTarget.height, outputRenderTarget.depth );
} else {
frameBufferTarget.setSize( width, height, 1 );
}
// RenderTarget || CanvasTarget
const viewport = this._outputRenderTarget ? this._outputRenderTarget.viewport : target._viewport;
const scissor = this._outputRenderTarget ? this._outputRenderTarget.scissor : target._scissor;
const pixelRatio = this._outputRenderTarget ? 1 : target._pixelRatio;
const scissorTest = this._outputRenderTarget ? this._outputRenderTarget.scissorTest : target._scissorTest;
frameBufferTarget.viewport.copy( viewport );
frameBufferTarget.scissor.copy( scissor );
frameBufferTarget.viewport.multiplyScalar( pixelRatio );
frameBufferTarget.scissor.multiplyScalar( pixelRatio );
frameBufferTarget.scissorTest = scissorTest;
frameBufferTarget.multiview = outputRenderTarget !== null ? outputRenderTarget.multiview : false;
frameBufferTarget.useArrayDepthTexture = outputRenderTarget !== null ? outputRenderTarget.useArrayDepthTexture : false;
frameBufferTarget.resolveDepthBuffer = outputRenderTarget !== null ? outputRenderTarget.resolveDepthBuffer : true;
frameBufferTarget.resolveStencilBuffer = outputRenderTarget !== null ? outputRenderTarget.resolveStencilBuffer : true;
frameBufferTarget.storeMultisampledColorBuffer = outputRenderTarget !== null ? outputRenderTarget.storeMultisampledColorBuffer : true;
frameBufferTarget.storeMultisampledDepthBuffer = outputRenderTarget !== null ? outputRenderTarget.storeMultisampledDepthBuffer : true;
frameBufferTarget.storeMultisampledStencilBuffer = outputRenderTarget !== null ? outputRenderTarget.storeMultisampledStencilBuffer : true;
frameBufferTarget._autoAllocateDepthBuffer = outputRenderTarget !== null ? outputRenderTarget._autoAllocateDepthBuffer : false;
return frameBufferTarget;
}
/**
* Renders the scene or 3D object with the given camera.
*
* @private
* @param {Object3D} scene - The scene or 3D object to render.
* @param {Camera} camera - The camera to render the scene with.
* @param {boolean} [useFrameBufferTarget=true] - Whether to use a framebuffer target or not.
* @return {RenderContext} The current render context.
*/
_renderScene( scene, camera, useFrameBufferTarget = true ) {
if ( this._isDeviceLost === true ) return;
if ( this.shadowMap.type === PCFSoftShadowMap ) {
warn( 'WebGPURenderer: PCFSoftShadowMap has been removed. Using PCFShadowMap instead.' );
this.shadowMap.type = PCFShadowMap;
}
//
const frameBufferTarget = useFrameBufferTarget ? this._getFrameBufferTarget() : null;
// preserve render tree
const nodeFrame = this._nodes.nodeFrame;
const previousRenderId = nodeFrame.renderId;
const previousRenderContext = this._currentRenderContext;
const previousRenderObjectFunction = this._currentRenderObjectFunction;
const previousHandleObjectFunction = this._handleObjectFunction;
const previousRenderBundle = this._currentRenderBundle;
// a render nested in the recording of a render bundle draws its objects directly
this._currentRenderBundle = null;
this.lighting.beginRender( scene );
//
this._callDepth ++;
const sceneRef = ( scene.isScene === true ) ? scene : _scene;
const outputRenderTarget = this._renderTarget || this._outputRenderTarget;
const useXRCamera = this.xr.isPresenting === true && this.isOutputTarget;
const activeCubeFace = this._activeCubeFace;
const activeMipmapLevel = this._activeMipmapLevel;
//
let renderTarget;
if ( frameBufferTarget !== null ) {
renderTarget = frameBufferTarget;
this.setRenderTarget( renderTarget );
} else {
renderTarget = outputRenderTarget;
}
// make sure a new render target has correct default depth values
if ( renderTarget !== null && renderTarget.depthBuffer === true ) {
const renderTargetData = this._textures.get( renderTarget );
if ( renderTargetData.depthInitialized !== true ) {
// we need a single manual clear if auto clear depth is disabled
if ( this.autoClear === false || ( this.autoClear === true && this.autoClearDepth === false ) ) {
this.clearDepth();
}
renderTargetData.depthInitialized = true;
}
}
//
const renderContext = this._renderContexts.get( renderTarget, this._mrt, this._callDepth, camera );
this._currentRenderContext = renderContext;
this._currentRenderObjectFunction = this._renderObjectFunction || this.renderObject;
this._handleObjectFunction = this._renderObjectDirect;
//
this.info.calls ++;
this.info.render.calls ++;
this.info.render.frameCalls ++;
nodeFrame.renderId = this.info.calls;
//
this.backend.updateTimeStampUID( renderContext );
this.inspector.beginRender( this.backend.getTimestampUID( renderContext ), scene, camera, renderTarget );
//
if ( scene.matrixWorldAutoUpdate === true ) scene.updateMatrixWorld();
camera = this._updateCamera( camera, useXRCamera );
//
const canvasTarget = this._canvasTarget;
let viewport = canvasTarget._viewport;
let scissor = canvasTarget._scissor;
let scissorTest = canvasTarget._scissorTest;
let pixelRatio = canvasTarget._pixelRatio;
if ( renderTarget !== null ) {
viewport = renderTarget.viewport;
scissor = renderTarget.scissor;
scissorTest = renderTarget.scissorTest;
pixelRatio = 1;
}
const minDepth = ( viewport.minDepth === undefined ) ? 0 : viewport.minDepth;
const maxDepth = ( viewport.maxDepth === undefined ) ? 1 : viewport.maxDepth;
renderContext.viewportValue.copy( viewport ).multiplyScalar( pixelRatio ).floor();
renderContext.viewportValue.width >>= activeMipmapLevel;
renderContext.viewportValue.height >>= activeMipmapLevel;
renderContext.viewportValue.minDepth = minDepth;
renderContext.viewportValue.maxDepth = maxDepth;
renderContext.scissorValue.copy( scissor ).multiplyScalar( pixelRatio ).floor();
renderContext.scissorValue.width >>= activeMipmapLevel;
renderContext.scissorValue.height >>= activeMipmapLevel;
if ( ! renderContext.clippingContext ) renderContext.clippingContext = new ClippingContext();
renderContext.clippingContext.updateGlobal( sceneRef, camera );
//
sceneRef.onBeforeRender( this, scene, camera, renderTarget );
//
this.drawingPipeline.begin( camera, renderContext );
this._renderLists.update( nodeFrame.frameId );
const renderList = this._renderLists.get( scene, camera, this.lighting );
this.drawingPipeline.beginRenderList( renderList );
this.drawingPipeline.project( scene, camera, 0, renderList, renderContext.clippingContext );
this.drawingPipeline.finishRenderList( renderList );
if ( this.sortObjects === true ) {
renderList.sort( this._opaqueSort, this._transparentSort );
}
//
if ( renderTarget !== null ) {
this._textures.updateRenderTarget( renderTarget, activeMipmapLevel );
const renderTargetData = this._textures.get( renderTarget );
renderContext.textures = renderTargetData.textures;
renderContext.depthTexture = renderTargetData.depthTexture;
renderContext.width = renderTargetData.width;
renderContext.height = renderTargetData.height;
renderContext.renderTarget = renderTarget;
renderContext.depth = renderTarget.depthBuffer;
renderContext.stencil = renderTarget.stencilBuffer;
} else {
this.getDrawingBufferSize( _drawingBufferSize );
renderContext.textures = null;
renderContext.depthTexture = null;
renderContext.width = _drawingBufferSize.width;
renderContext.height = _drawingBufferSize.height;
renderContext.depth = this.depth;
renderContext.stencil = this.stencil;
}
renderContext.width >>= activeMipmapLevel;
renderContext.height >>= activeMipmapLevel;
renderContext.activeCubeFace = activeCubeFace;
renderContext.activeMipmapLevel = activeMipmapLevel;
renderContext.occlusionQueryCount = renderList.occlusionQueryCount;
renderContext.fullscreenPass = scene.isQuadMesh === true;
//
renderContext.scissorValue.max( _vector4.set( 0, 0, 0, 0 ) );
if ( renderContext.scissorValue.x + renderContext.scissorValue.width > renderContext.width ) {
renderContext.scissorValue.width = Math.max( renderContext.width - renderContext.scissorValue.x, 0 );
}
if ( renderContext.scissorValue.y + renderContext.scissorValue.height > renderContext.height ) {
renderContext.scissorValue.height = Math.max( renderContext.height - renderContext.scissorValue.y, 0 );
}
_screen.set( 0, 0, renderContext.width, renderContext.height );
renderContext.viewport = renderContext.viewportValue.equals( _screen ) === false;
renderContext.scissor = scissorTest && renderContext.scissorValue.equals( _screen ) === false;
//
this._background.update( sceneRef, renderList, renderContext );
//
renderContext.camera = camera;
this.backend.beginRender( renderContext );
// process render lists
const {
bundles,
lightsNode,
transparentDoublePass: transparentDoublePassObjects,
transparent: transparentObjects,
opaque: opaqueObjects
} = renderList;
if ( bundles.length > 0 ) this._renderBundles( bundles, sceneRef, lightsNode );
if ( this.opaque === true && opaqueObjects.length > 0 ) this._renderObjects( opaqueObjects, camera, sceneRef, lightsNode );
if ( this.transparent === true && transparentObjects.length > 0 ) this._renderTransparents( transparentObjects, transparentDoublePassObjects, camera, sceneRef, lightsNode );
// finish render pass
this.backend.finishRender( renderContext );
// restore render tree
nodeFrame.renderId = previousRenderId;
this._currentRenderContext = previousRenderContext;
this._currentRenderObjectFunction = previousRenderObjectFunction;
this._handleObjectFunction = previousHandleObjectFunction;
this._currentRenderBundle = previousRenderBundle;
this.lighting.finishRender( scene );
//
this._callDepth --;
if ( frameBufferTarget !== null ) {
this.setRenderTarget( outputRenderTarget, activeCubeFace, activeMipmapLevel );
this._renderOutput( renderTarget );
}
//
sceneRef.onAfterRender( this, scene, camera, renderTarget );
//
this.inspector.finishRender( this.backend.getTimestampUID( renderContext ) );
//
return renderContext;
}
_setXRLayerSize( width, height ) {
// TODO: Find a better solution to resize the canvas when in XR.
this._canvasTarget._width = width;
this._canvasTarget._height = height;
this.setViewport( 0, 0, width, height );
}
/**
* The output pass performs tone mapping and color space conversion.
*
* @private
* @param {RenderTarget} renderTarget - The current render target.
*/
_renderOutput( renderTarget ) {
const cacheKey = this._nodes.getOutputCacheKey();
let quadData = this._quadCache.get( renderTarget );
let quad;
if ( quadData === undefined ) {
quad = new QuadMesh( new NodeMaterial() );
quad.name = 'Output Color Transform';
quad.material.name = 'outputColorTransform';
quad.material.fragmentNode = this._nodes.getOutputNode( renderTarget.texture );
quadData = {
quad,
cacheKey
};
this._quadCache.set( renderTarget, quadData );
} else {
quad = quadData.quad;
if ( quadData.cacheKey !== cacheKey ) {
quad.material.fragmentNode = this._nodes.getOutputNode( renderTarget.texture );
quad.material.needsUpdate = true;
quadData.cacheKey = cacheKey;
}
}
// a clear operation clears the intermediate renderTarget texture, but should not update the screen canvas.
const currentAutoClear = this.autoClear;
const currentXR = this.xr.enabled;
this.autoClear = false;
this.xr.enabled = false;
this._renderOutputLayers( quad, renderTarget );
this.autoClear = currentAutoClear;
this.xr.enabled = currentXR;
}
/**
* Returns the maximum available anisotropy for texture filtering.
*
* @return {number} The maximum available anisotropy.
*/
getMaxAnisotropy() {
return this.backend.capabilities.getMaxAnisotropy();
}
/**
* Returns the active cube face.
*
* @return {number} The active cube face.
*/
getActiveCubeFace() {
return this._activeCubeFace;
}
/**
* Returns the active mipmap level.
*
* @return {number} The active mipmap level.
*/
getActiveMipmapLevel() {
return this._activeMipmapLevel;
}
/**
* Applications are advised to always define the animation loop
* with this method and not manually with `requestAnimationFrame()`
* for best compatibility.
*
* @async
* @param {?onAnimationCallback} callback - The application's animation loop.
* @return {Promise} A Promise that resolves when the set has been executed.
*/
async setAnimationLoop( callback ) {
if ( this._initialized === false ) await this.init();
// started before assigning the callback, so it first runs on the next frame
if ( callback !== null ) this._animation.start();
this._animation.setAnimationLoop( callback );
}
/**
* Returns the current animation loop callback.
*
* @return {?Function} The current animation loop callback.
*/
getAnimationLoop() {
return this._animation.getAnimationLoop();
}
/**
* Can be used to transfer buffer data from a storage buffer attribute
* from the GPU to the CPU in context of compute shaders.
*
* @async
* @param {BufferAttribute} attribute - The storage buffer attribute to read from.
* @param {?(ReadbackBuffer|ArrayBuffer)} [target=null] - The readback buffer or array buffer that receives the data. If `null`, a new array buffer is returned.
* @param {number} [offset=0] - The byte offset to start reading from. Must be a multiple of 4.
* @param {number} [count=-1] - The number of bytes to read. Must be a multiple of 4, or `-1` to read to the end of the buffer.
* @return {Promise<ArrayBuffer|ReadbackBuffer>} A promise that resolves with the buffer data when the data are ready.
*/
async getArrayBufferAsync( attribute, target = null, offset = 0, count = -1 ) {
// tally the memory for this readback buffer
if ( target !== null && target.isReadbackBuffer ) {
if ( this.info.memoryMap.has( target ) === false ) {
this.info.createReadbackBuffer( target );
const disposeInfo = () => {
target.removeEventListener( 'dispose', disposeInfo );
this.info.destroyReadbackBuffer( target );
};
target.addEventListener( 'dispose', disposeInfo );
}
}
if ( offset % 4 !== 0 || ( count > 0 && count % 4 !== 0 ) ) {
throw new Error( 'THREE.Renderer: "getArrayBufferAsync()" offset and count must be a multiple of 4.' );
}
return await this.backend.getArrayBufferAsync( attribute, target, offset, count );
}
/**
* Returns the rendering context.
*
* @return {GPUCanvasContext|WebGL2RenderingContext} The rendering context.
*/
getContext() {
return this.backend.getContext();
}
/**
* Returns the pixel ratio.
*
* @return {number} The pixel ratio.
*/
getPixelRatio() {
return this._canvasTarget.getPixelRatio();
}
/**
* Returns the drawing buffer size in physical pixels. This method honors the pixel ratio.
*
* @param {Vector2} target - The method writes the result in this target object.
* @return {Vector2} The drawing buffer size.
*/
getDrawingBufferSize( target ) {
return this._canvasTarget.getDrawingBufferSize( target );
}
/**
* Returns the renderer's size in logical pixels. This method does not honor the pixel ratio.
*
* @param {Vector2} target - The method writes the result in this target object.
* @return {Vector2} The renderer's size in logical pixels.
*/
getSize( target ) {
return this._canvasTarget.getSize( target );
}
/**
* Sets the given pixel ratio and resizes the canvas if necessary.
*
* @param {number} [value=1] - The pixel ratio.
*/
setPixelRatio( value = 1 ) {
this._canvasTarget.setPixelRatio( value );
}
/**
* This method allows to define the drawing buffer size by specifying
* width, height and pixel ratio all at once. The size of the drawing
* buffer is computed with this formula:
* ```js
* size.x = width * pixelRatio;
* size.y = height * pixelRatio;
* ```
*
* @param {number} width - The width in logical pixels.
* @param {number} height - The height in logical pixels.
* @param {number} pixelRatio - The pixel ratio.
*/
setDrawingBufferSize( width, height, pixelRatio ) {
// Renderer can't be resized while presenting in XR.
if ( this.xr && this.xr.isPresenting ) return;
this._canvasTarget.setDrawingBufferSize( width, height, pixelRatio );
}
/**
* Sets the size of the renderer.
*
* @param {number} width - The width in logical pixels.
* @param {number} height - The height in logical pixels.
* @param {boolean} [updateStyle=true] - Whether to update the `style` attribute of the canvas or not.
*/
setSize( width, height, updateStyle = true ) {
// Renderer can't be resized while presenting in XR.
if ( this.xr && this.xr.isPresenting ) return;
this._canvasTarget.setSize( width, height, updateStyle );
}
/**
* Defines a manual sort function for the opaque render list.
* Pass `null` to use the default sort.
*
* @param {Function} method - The sort function.
*/
setOpaqueSort( method ) {
this._opaqueSort = method;
}
/**
* Defines a manual sort function for the transparent render list.
* Pass `null` to use the default sort.
*
* @param {Function} method - The sort function.
*/
setTransparentSort( method ) {
this._transparentSort = method;
}
/**
* Returns the scissor rectangle.
*
* @param {Vector4} target - The method writes the result in this target object.
* @return {Vector4} The scissor rectangle.
*/
getScissor( target ) {
return this._canvasTarget.getScissor( target );
}
/**
* Defines the scissor rectangle.
*
* @param {number | Vector4} x - The horizontal coordinate for the upper left corner of the box in logical pixel unit.
* Instead of passing four arguments, the method also works with a single four-dimensional vector.
* @param {number} y - The vertical coordinate for the upper left corner of the box in logical pixel unit.
* @param {number} width - The width of the scissor box in logical pixel unit.
* @param {number} height - The height of the scissor box in logical pixel unit.
*/
setScissor( x, y, width, height ) {
this._canvasTarget.setScissor( x, y, width, height );
}
/**
* Returns the scissor test value.
*
* @return {boolean} Whether the scissor test should be enabled or not.
*/
getScissorTest() {
return this._canvasTarget.getScissorTest();
}
/**
* Defines the scissor test.
*
* @param {boolean} boolean - Whether the scissor test should be enabled or not.
*/
setScissorTest( boolean ) {
this._canvasTarget.setScissorTest( boolean );
// TODO: Move it to CanvasTarget event listener.
this.backend.setScissorTest( boolean );
}
/**
* Resets the backend's internal state cache. Useful when the rendering context is shared with
* other libraries that change the state. A no-op for the WebGPU backend.
*/
resetState() {
if ( this._initialized === false ) {
throw new Error( 'THREE.Renderer: .resetState() called before the backend is initialized. Use "await renderer.init();" before using this method.' );
}
this.backend.resetState();
}
/**
* Returns the viewport definition.
*
* @param {Vector4} target - The method writes the result in this target object.
* @return {Vector4} The viewport definition.
*/
getViewport( target ) {
return this._canvasTarget.getViewport( target );
}
/**
* Defines the viewport.
*
* @param {number | Vector4} x - The horizontal coordinate for the upper left corner of the viewport origin in logical pixel unit.
* @param {number} y - The vertical coordinate for the upper left corner of the viewport origin in logical pixel unit.
* @param {number} width - The width of the viewport in logical pixel unit.
* @param {number} height - The height of the viewport in logical pixel unit.
* @param {number} minDepth - The minimum depth value of the viewport. WebGPU only.
* @param {number} maxDepth - The maximum depth value of the viewport. WebGPU only.
*/
setViewport( x, y, width, height, minDepth = 0, maxDepth = 1 ) {
this._canvasTarget.setViewport( x, y, width, height, minDepth, maxDepth );
}
/**
* Returns the clear color.
*
* @param {Color} target - The method writes the result in this target object.
* @return {Color} The clear color.
*/
getClearColor( target ) {
return target.copy( this._clearColor );
}
/**
* Defines the clear color and optionally the clear alpha.
*
* @param {Color} color - The clear color.
* @param {number} [alpha=1] - The clear alpha.
*/
setClearColor( color, alpha = 1 ) {
this._clearColor.set( color );
this._clearColor.a = alpha;
}
/**
* Returns the clear alpha.
*
* @return {number} The clear alpha.
*/
getClearAlpha() {
return this._clearColor.a;
}
/**
* Defines the clear alpha.
*
* @param {number} alpha - The clear alpha.
*/
setClearAlpha( alpha ) {
this._clearColor.a = alpha;
}
/**
* Returns the clear depth.
*
* @return {number} The clear depth.
*/
getClearDepth() {
return ( this.reversedDepthBuffer === true ) ? 1 - this._clearDepth : this._clearDepth;
}
/**
* Defines the clear depth.
*
* @param {number} depth - The clear depth.
*/
setClearDepth( depth ) {
this._clearDepth = depth;
}
/**
* Returns the clear stencil.
*
* @return {number} The clear stencil.
*/
getClearStencil() {
return this._clearStencil;
}
/**
* Defines the clear stencil.
*
* @param {number} stencil - The clear stencil.
*/
setClearStencil( stencil ) {
this._clearStencil = stencil;
}
/**
* This method performs an occlusion query for the given 3D object.
* It returns `true` if the given 3D object is fully occluded by other
* 3D objects in the scene.
*
* @param {Object3D} object - The 3D object to test.
* @return {boolean} Whether the 3D object is fully occluded or not.
*/
isOccluded( object ) {
const renderContext = this._currentRenderContext;
return renderContext && this.backend.isOccluded( renderContext, object );
}
/**
* Performs a manual clear operation. This method ignores `autoClear` properties.
*
* @param {boolean} [color=true] - Whether the color buffer should be cleared or not.
* @param {boolean} [depth=true] - Whether the depth buffer should be cleared or not.
* @param {boolean} [stencil=true] - Whether the stencil buffer should be cleared or not.
*/
clear( color = true, depth = true, stencil = true ) {
if ( this._initialized === false ) {
throw new Error( 'THREE.Renderer: .clear() called before the backend is initialized. Use "await renderer.init();" before using this method.' );
}
const renderTarget = this._renderTarget || this._getFrameBufferTarget();
let renderContext = null;
if ( renderTarget !== null ) {
this._textures.updateRenderTarget( renderTarget );
const renderTargetData = this._textures.get( renderTarget );
renderContext = this._renderContexts.get( renderTarget, null, -1 ); // using - 1 for the call depth to get a render context for the clear operation
renderContext.textures = renderTargetData.textures;
renderContext.depthTexture = renderTargetData.depthTexture;
renderContext.width = renderTargetData.width;
renderContext.height = renderTargetData.height;
renderContext.renderTarget = renderTarget;
renderContext.depth = renderTarget.depthBuffer;
renderContext.stencil = renderTarget.stencilBuffer;
// #30329
const color = this.backend.getClearColor();
renderContext.clearColorValue.r = color.r;
renderContext.clearColorValue.g = color.g;
renderContext.clearColorValue.b = color.b;
renderContext.clearColorValue.a = color.a;
renderContext.clearDepthValue = this.getClearDepth();
renderContext.clearStencilValue = this.getClearStencil();
renderContext.activeCubeFace = this.getActiveCubeFace();
renderContext.activeMipmapLevel = this.getActiveMipmapLevel();
if ( renderTarget.depthBuffer === true ) renderTargetData.depthInitialized = true;
}
this.backend.clear( color, depth, stencil, renderContext );
if ( renderTarget !== null && this._renderTarget === null ) {
this._renderOutput( renderTarget );
}
}
/**
* Performs a manual clear operation of the color buffer. This method ignores `autoClear` properties.
*/
clearColor() {
this.clear( true, false, false );
}
/**
* Performs a manual clear operation of the depth buffer. This method ignores `autoClear` properties.
*/
clearDepth() {
this.clear( false, true, false );
}
/**
* Performs a manual clear operation of the stencil buffer. This method ignores `autoClear` properties.
*/
clearStencil() {
this.clear( false, false, true );
}
/**
* Async version of {@link Renderer#clear}.
*
* @async
* @deprecated
* @param {boolean} [color=true] - Whether the color buffer should be cleared or not.
* @param {boolean} [depth=true] - Whether the depth buffer should be cleared or not.
* @param {boolean} [stencil=true] - Whether the stencil buffer should be cleared or not.
* @return {Promise} A Promise that resolves when the clear operation has been executed.
*/
async clearAsync( color = true, depth = true, stencil = true ) {
warnOnce( 'Renderer: "clearAsync()" has been deprecated. Use "clear()" and "await renderer.init();" when creating the renderer.' ); // @deprecated r181
await this.init();
this.clear( color, depth, stencil );
}
/**
* Async version of {@link Renderer#clearColor}.
*
* @async
* @deprecated
* @return {Promise} A Promise that resolves when the clear operation has been executed.
*/
async clearColorAsync() {
warnOnce( 'Renderer: "clearColorAsync()" has been deprecated. Use "clearColor()" and "await renderer.init();" when creating the renderer.' ); // @deprecated r181
this.clear( true, false, false );
}
/**
* Async version of {@link Renderer#clearDepth}.
*
* @async
* @deprecated
* @return {Promise} A Promise that resolves when the clear operation has been executed.
*/
async clearDepthAsync() {
warnOnce( 'Renderer: "clearDepthAsync()" has been deprecated. Use "clearDepth()" and "await renderer.init();" when creating the renderer.' ); // @deprecated r181
this.clear( false, true, false );
}
/**
* Async version of {@link Renderer#clearStencil}.
*
* @async
* @deprecated
* @return {Promise} A Promise that resolves when the clear operation has been executed.
*/
async clearStencilAsync() {
warnOnce( 'Renderer: "clearStencilAsync()" has been deprecated. Use "clearStencil()" and "await renderer.init();" when creating the renderer.' ); // @deprecated r181
this.clear( false, false, true );
}
/**
* Returns `true` if a framebuffer target is needed to perform tone mapping or color space conversion.
* If this is the case, the renderer allocates an internal render target for that purpose.
*
* @type {boolean}
*/
get needsFrameBufferTarget() {
const useToneMapping = this.currentToneMapping !== NoToneMapping;
const useColorSpace = this.currentColorSpace !== ColorManagement.workingColorSpace;
return useToneMapping || useColorSpace;
}
/**
* The number of samples used for multi-sample anti-aliasing (MSAA).
*
* @type {number}
* @default 0
*/
get samples() {
return this._samples;
}
/**
* The current number of samples used for multi-sample anti-aliasing (MSAA).
*
* When rendering to a custom render target, the number of samples of that render target is used.
* The number of samples is set to 0 when the renderer needs an internal framebuffer target for
* tone mapping or color space conversion, or when rendering a fullscreen quad to screen.
*
* @type {number}
*/
get currentSamples() {
let samples = this._samples;
if ( this._renderTarget !== null ) {
samples = this._renderTarget.samples;
} else if ( this.needsFrameBufferTarget || this._currentRenderContext?.fullscreenPass === true ) {
samples = 0;
}
return samples;
}
/**
* The current tone mapping of the renderer. When not producing screen output,
* the tone mapping is always `NoToneMapping`.
*
* @type {number}
*/
get currentToneMapping() {
return this.isOutputTarget ? this.toneMapping : NoToneMapping;
}
/**
* The current color space of the renderer. When not producing screen output,
* the color space is always the working color space.
*
* @type {string}
*/
get currentColorSpace() {
return this.isOutputTarget ? this.outputColorSpace : ColorManagement.workingColorSpace;
}
/**
* Returns `true` if the rendering settings are set to screen output.
*
* @returns {boolean} True if the current render target is the same of output render target or `null`, otherwise false.
*/
get isOutputTarget() {
return this._renderTarget === this._outputRenderTarget || this._renderTarget === null;
}
/**
* Frees all internal resources of the renderer. Call this method if the renderer
* is no longer in use by your app.
*/
async dispose() {
// edge case: If dispose() is called during an init, wait for the finish
if ( this._initPromise !== null ) {
await this.init();
}
if ( this._initialized === true ) {
// the drawing pipeline frees its resources while the managers of the renderer still track them
this.drawingPipeline.dispose();
this.lighting.dispose();
this._inspector.dispose();
this._animation.dispose();
this._objects.dispose();
this._geometries.dispose();
this._attributes.dispose();
this._pipelines.dispose();
this._nodes.dispose();
this._bindings.dispose();
this._renderLists.dispose();
this._renderContexts.dispose();
for ( const canvasTarget of this._frameBufferTargets.keys() ) {
canvasTarget.dispose();
}
this._destroyCanvasTarget( this._canvasTarget );
this._textures.dispose();
this.info.dispose();
await this.backend.dispose();
}
this.setRenderTarget( null );
}
/**
* Sets the given render target. Calling this method means the renderer does not
* target the default framebuffer (meaning the canvas) anymore but a custom framebuffer.
* Use `null` as the first argument to reset the state.
*
* @param {?RenderTarget} renderTarget - The render target to set.
* @param {number} [activeCubeFace=0] - The active cube face.
* @param {number} [activeMipmapLevel=0] - The active mipmap level.
*/
setRenderTarget( renderTarget, activeCubeFace = 0, activeMipmapLevel = 0 ) {
this._renderTarget = renderTarget;
this._activeCubeFace = activeCubeFace;
this._activeMipmapLevel = activeMipmapLevel;
}
/**
* Returns the current render target.
*
* @return {?RenderTarget} The render target. Returns `null` if no render target is set.
*/
getRenderTarget() {
return this._renderTarget;
}
/**
* Sets the output render target for the renderer.
*
* @param {?RenderTarget} renderTarget - The render target to set as the output target.
*/
setOutputRenderTarget( renderTarget ) {
this._outputRenderTarget = renderTarget;
}
/**
* Returns the current output target.
*
* @return {?RenderTarget} The current output render target. Returns `null` if no output target is set.
*/
getOutputRenderTarget() {
return this._outputRenderTarget;
}
/**
* Sets the canvas target. The canvas target manages the HTML canvas
* or the offscreen canvas the renderer draws into.
*
* @param {CanvasTarget} canvasTarget - The canvas target.
*/
setCanvasTarget( canvasTarget ) {
this._canvasTarget.removeEventListener( 'resize', this._onCanvasTargetResize );
// keep any dispose event listener attached for later use
this._canvasTarget = canvasTarget;
this._canvasTarget.addEventListener( 'resize', this._onCanvasTargetResize );
this._canvasTarget.addEventListener( 'dispose', this._onCanvasTargetDispose );
}
/**
* Returns the current canvas target.
*
* @return {CanvasTarget} The current canvas target.
*/
getCanvasTarget() {
return this._canvasTarget;
}
/**
* Resets the renderer to the initial state before WebXR started.
*
* @private
*/
_resetXRState() {
this.backend.setXRTarget( null );
this.setOutputRenderTarget( null );
this.setRenderTarget( null );
for ( const canvasTarget of this._frameBufferTargets.keys() ) {
canvasTarget.dispose();
}
}
/**
* Callback for {@link Renderer#setRenderObjectFunction}.
*
* @callback renderObjectFunction
* @param {Object3D} object - The 3D object.
* @param {Scene} scene - The scene the 3D object belongs to.
* @param {Camera} camera - The camera the object should be rendered with.
* @param {BufferGeometry} geometry - The object's geometry.
* @param {Material} material - The object's material.
* @param {?Object} group - Only relevant for objects using multiple materials. This represents a group entry from the respective `BufferGeometry`.
* @param {LightsNode} lightsNode - The current lights node.
* @param {ClippingContext} clippingContext - The clipping context.
* @param {?string} [passId=null] - An optional ID for identifying the pass.
*/
/**
* Sets the given render object function. Calling this method overwrites the default implementation
* which is {@link Renderer#renderObject}. Defining a custom function can be useful
* if you want to modify the way objects are rendered. For example you can define things like "every
* object that has material of a certain type should perform a pre-pass with a special overwrite material".
* The custom function must always call `renderObject()` in its implementation.
*
* Use `null` as the first argument to reset the state.
*
* @param {?renderObjectFunction} renderObjectFunction - The render object function.
*/
setRenderObjectFunction( renderObjectFunction ) {
this._renderObjectFunction = renderObjectFunction;
}
/**
* Returns the current render object function.
*
* @return {?Function} The current render object function. Returns `null` if no function is set.
*/
getRenderObjectFunction() {
return this._renderObjectFunction;
}
/**
* Execute a single or an array of compute nodes. This method can only be called
* if the renderer has been initialized.
*
* @param {Node|Array<Node>} computeNodes - The compute node(s).
* @param {number|Array<number>|IndirectStorageBufferAttribute} [dispatchSize=null]
* - A single number representing count, or
* - An array [x, y, z] representing dispatch size, or
* - A IndirectStorageBufferAttribute for indirect dispatch size.
* @return {Promise|undefined} A Promise that resolve when the compute has finished. Only returned when the renderer has not been initialized.
*/
compute( computeNodes, dispatchSize = null ) {
if ( this._isDeviceLost === true ) return;
if ( this._initialized === false ) {
warn( 'Renderer: ".compute()" called before the backend is initialized. Try using ".computeAsync()" instead.' );
return this.computeAsync( computeNodes, dispatchSize );
}
const computeList = Array.isArray( computeNodes ) ? computeNodes : [ computeNodes ];
if ( computeList.length === 0 || computeList.some( computeNode => computeNode?.isComputeNode !== true ) ) {
throw new Error( 'THREE.Renderer: ".compute()" expects a "ComputeNode".' );
}
if ( dispatchSize !== null && computeList.some( computeNode => computeNode.count !== null || computeNode.dispatchSize !== null ) ) {
throw new Error( 'THREE.Renderer: A dispatch size cannot be passed to "renderer.compute()" for nodes created with "compute( count )". Use "computeKernel()" instead.' );
}
//
this._animation.start();
const nodeFrame = this._nodes.nodeFrame;
const previousRenderId = nodeFrame.renderId;
//
this.info.calls ++;
this.info.compute.calls ++;
this.info.compute.frameCalls ++;
nodeFrame.renderId = this.info.calls;
//
this.backend.updateTimeStampUID( computeNodes );
this.inspector.beginCompute( this.backend.getTimestampUID( computeNodes ), computeNodes );
//
const backend = this.backend;
const pipelines = this._pipelines;
const bindings = this._bindings;
const nodes = this._nodes;
backend.beginCompute( computeNodes );
for ( const computeNode of computeList ) {
// onInit
if ( pipelines.has( computeNode ) === false ) {
const dispose = () => {
computeNode.removeEventListener( 'dispose', dispose );
pipelines.delete( computeNode );
bindings.deleteForCompute( computeNode );
nodes.delete( computeNode );
};
computeNode.addEventListener( 'dispose', dispose );
//
const onInitFn = computeNode.onInitFunction;
if ( onInitFn !== null ) {
onInitFn.call( computeNode, { renderer: this } );
}
}
nodes.updateBeforeForCompute( computeNode );
nodes.updateForCompute( computeNode );
bindings.updateForCompute( computeNode );
const computeBindings = bindings.getForCompute( computeNode );
const computePipeline = pipelines.getForCompute( computeNode, computeBindings );
this._updateDispatchSize( computeNode, dispatchSize );
backend.compute( computeNodes, computeNode, computeBindings, computePipeline, dispatchSize );
nodes.updateAfterForCompute( computeNode );
}
backend.finishCompute( computeNodes );
//
nodeFrame.renderId = previousRenderId;
//
this.inspector.finishCompute( this.backend.getTimestampUID( computeNodes ) );
}
/**
* Execute a single or an array of compute nodes.
*
* @async
* @param {Node|Array<Node>} computeNodes - The compute node(s).
* @param {number|Array<number>|IndirectStorageBufferAttribute} [dispatchSize=null]
* - A single number representing count, or
* - An array [x, y, z] representing dispatch size, or
* - A IndirectStorageBufferAttribute for indirect dispatch size.
* @return {Promise} A Promise that resolve when the compute has finished.
*/
async computeAsync( computeNodes, dispatchSize = null ) {
if ( this._initialized === false ) await this.init();
this.compute( computeNodes, dispatchSize );
}
/**
* Checks if the given feature is supported by the selected backend.
*
* @async
* @deprecated
* @param {string} name - The feature's name.
* @return {Promise<boolean>} A Promise that resolves with a bool that indicates whether the feature is supported or not.
*/
async hasFeatureAsync( name ) {
warnOnce( 'Renderer: "hasFeatureAsync()" has been deprecated. Use "hasFeature()" and "await renderer.init();" when creating the renderer.' ); // @deprecated r181
await this.init();
return this.hasFeature( name );
}
async resolveTimestampsAsync( type = 'render' ) {
if ( this._initialized === false ) await this.init();
return this.backend.resolveTimestampsAsync( type );
}
/**
* Checks if the given feature is supported by the selected backend. If the
* renderer has not been initialized, this method always returns `false`.
*
* @param {string} name - The feature's name.
* @return {boolean} Whether the feature is supported or not.
*/
hasFeature( name ) {
if ( this._initialized === false ) {
throw new Error( 'THREE.Renderer: .hasFeature() called before the backend is initialized. Use "await renderer.init();" before using this method.' );
}
return this.backend.hasFeature( name );
}
/**
* Returns `true` when the renderer has been initialized.
*
* @return {boolean} Whether the renderer has been initialized or not.
*/
hasInitialized() {
return this._initialized;
}
/**
* Initializes the given textures. Useful for preloading a texture rather than waiting until first render
* (which can cause noticeable lags due to decode and GPU upload overhead).
*
* @async
* @deprecated
* @param {Texture} texture - The texture.
* @return {Promise} A Promise that resolves when the texture has been initialized.
*/
async initTextureAsync( texture ) {
warnOnce( 'Renderer: "initTextureAsync()" has been deprecated. Use "initTexture()" and "await renderer.init();" when creating the renderer.' ); // @deprecated r181
await this.init();
this.initTexture( texture );
}
/**
* Initializes the given texture. Useful for preloading a texture rather than waiting until first render
* (which can cause noticeable lags due to decode and GPU upload overhead).
*
* This method can only be used if the renderer has been initialized.
*
* @param {Texture} texture - The texture.
*/
initTexture( texture ) {
if ( this._initialized === false ) {
throw new Error( 'THREE.Renderer: .initTexture() called before the backend is initialized. Use "await renderer.init();" before using this method.' );
}
this._textures.updateTexture( texture );
}
/**
* Initializes the given render target.
*
* @param {RenderTarget} renderTarget - The render target to intialize.
*/
initRenderTarget( renderTarget ) {
if ( this._initialized === false ) {
throw new Error( 'THREE.Renderer: .initRenderTarget() called before the backend is initialized. Use "await renderer.init();" before using this method.' );
}
this._textures.updateRenderTarget( renderTarget );
const renderTargetData = this._textures.get( renderTarget );
const renderContext = this._renderContexts.get( renderTarget );
renderContext.textures = renderTargetData.textures;
renderContext.depthTexture = renderTargetData.depthTexture;
renderContext.width = renderTargetData.width;
renderContext.height = renderTargetData.height;
renderContext.renderTarget = renderTarget;
renderContext.depth = renderTarget.depthBuffer;
renderContext.stencil = renderTarget.stencilBuffer;
this.backend.initRenderTarget( renderContext );
}
/**
* Copies the current bound framebuffer into the given texture.
*
* @param {FramebufferTexture} framebufferTexture - The texture.
* @param {?(Vector2|Vector4)} [rectangle=null] - A two or four dimensional vector that defines the rectangular portion of the framebuffer that should be copied.
*/
copyFramebufferToTexture( framebufferTexture, rectangle = null ) {
if ( rectangle !== null ) {
if ( rectangle.isVector2 ) {
rectangle = _vector4.set( rectangle.x, rectangle.y, framebufferTexture.image.width, framebufferTexture.image.height ).floor();
} else if ( rectangle.isVector4 ) {
rectangle = _vector4.copy( rectangle ).floor();
} else {
error( 'Renderer.copyFramebufferToTexture: Invalid rectangle.' );
return;
}
} else {
rectangle = _vector4.set( 0, 0, framebufferTexture.image.width, framebufferTexture.image.height );
}
//
let renderContext = this._currentRenderContext;
let renderTarget;
if ( renderContext !== null ) {
renderTarget = renderContext.renderTarget;
} else {
renderTarget = this._renderTarget || this._getFrameBufferTarget();
if ( renderTarget !== null ) {
this._textures.updateRenderTarget( renderTarget );
renderContext = this._textures.get( renderTarget );
}
}
//
this._textures.updateTexture( framebufferTexture, { renderTarget } );
this.backend.copyFramebufferToTexture( framebufferTexture, renderContext, rectangle );
this._inspector.copyFramebufferToTexture( framebufferTexture );
}
/**
* Copies data of the given source texture into a destination texture.
*
* @param {Texture} srcTexture - The source texture.
* @param {Texture} dstTexture - The destination texture.
* @param {Box2|Box3} [srcRegion=null] - A bounding box which describes the source region. Can be two or three-dimensional.
* @param {Vector2|Vector3} [dstPosition=null] - A vector that represents the origin of the destination region. Can be two or three-dimensional.
* @param {number} [srcLevel=0] - The source mip level to copy from.
* @param {number} [dstLevel=0] - The destination mip level to copy to.
*/
copyTextureToTexture( srcTexture, dstTexture, srcRegion = null, dstPosition = null, srcLevel = 0, dstLevel = 0 ) {
this._textures.updateTexture( srcTexture );
this._textures.updateTexture( dstTexture );
this.backend.copyTextureToTexture( srcTexture, dstTexture, srcRegion, dstPosition, srcLevel, dstLevel );
this._inspector.copyTextureToTexture( srcTexture, dstTexture );
}
/**
* Reads pixel data from the given render target.
*
* @async
* @param {RenderTarget} renderTarget - The render target to read from.
* @param {number} x - The `x` coordinate of the copy region's origin.
* @param {number} y - The `y` coordinate of the copy region's origin.
* @param {number} width - The width of the copy region.
* @param {number} height - The height of the copy region.
* @param {number} [textureIndex=0] - The texture index of a MRT render target.
* @param {number} [faceIndex=0] - The cube face, depth slice or array layer index.
* @return {Promise<TypedArray>} A Promise that resolves when the read has been finished. The resolve provides the read data as a typed array.
*/
async readRenderTargetPixelsAsync( renderTarget, x, y, width, height, textureIndex = 0, faceIndex = 0 ) {
return this.backend.copyTextureToBuffer( renderTarget.textures[ textureIndex ], x, y, width, height, faceIndex );
}
/**
* Renders the given render bundles.
*
* @private
* @param {Array<Object>} bundles - Array with render bundle data.
* @param {Scene} sceneRef - The scene the render bundles belong to.
* @param {LightsNode} lightsNode - The current lights node.
*/
_renderBundles( bundles, sceneRef, lightsNode ) {
for ( const bundle of bundles ) {
this._renderBundle( bundle, sceneRef, lightsNode );
}
}
/**
* Renders the transparent objects from the given render lists.
*
* @private
* @param {Array<Object>} renderList - The transparent render list.
* @param {Array<Object>} doublePassList - The list of transparent objects which require a double pass (e.g. because of transmission).
* @param {Camera} camera - The camera the render list should be rendered with.
* @param {Scene} scene - The scene the render list belongs to.
* @param {LightsNode} lightsNode - The current lights node.
*/
_renderTransparents( renderList, doublePassList, camera, scene, lightsNode ) {
if ( doublePassList.length > 0 ) {
// render back side
for ( const { material } of doublePassList ) {
material.side = BackSide;
}
this._renderObjects( doublePassList, camera, scene, lightsNode, 'backSide' );
// render front side
for ( const { material } of doublePassList ) {
material.side = FrontSide;
}
this._renderObjects( renderList, camera, scene, lightsNode );
// restore
for ( const { material } of doublePassList ) {
material.side = DoubleSide;
}
} else {
this._renderObjects( renderList, camera, scene, lightsNode );
}
}
/**
* Renders the objects from the given render list.
*
* @private
* @param {Array<Object>} renderList - The render list.
* @param {Camera} camera - The camera the render list should be rendered with.
* @param {Scene} scene - The scene the render list belongs to.
* @param {LightsNode} lightsNode - The current lights node.
* @param {?string} [passId=null] - An optional ID for identifying the pass.
*/
_renderObjects( renderList, camera, scene, lightsNode, passId = null ) {
for ( let i = 0, il = renderList.length; i < il; i ++ ) {
const { object, geometry, material, group, clippingContext } = renderList[ i ];
this._currentRenderObjectFunction( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId );
}
}
/**
* Retrieves shadow nodes for the given material. This is used to setup shadow passes.
* The result is cached per material and updated when the material's version changes.
*
* @private
* @param {Material} material
* @returns {Object} - The shadow nodes for the material.
*/
_getShadowNodes( material ) {
const version = material.version;
let cache = this._cacheShadowNodes.get( material );
if ( cache === undefined || cache.version !== version ) {
const hasMap = material.map && material.map.isTexture;
const hasColorNode = material.colorNode && material.colorNode.isNode;
const hasCastShadowNode = material.castShadowNode && material.castShadowNode.isNode;
const hasMaskNode = ( material.maskShadowNode && material.maskShadowNode.isNode ) || ( material.maskNode && material.maskNode.isNode );
let positionNode = null;
let colorNode = null;
let depthNode = null;
if ( hasMap || hasColorNode || hasCastShadowNode || hasMaskNode ) {
let shadowRGB;
let shadowAlpha;
if ( hasCastShadowNode ) {
shadowRGB = material.castShadowNode.rgb;
shadowAlpha = material.castShadowNode.a;
if ( this.shadowMap.transmitted !== true ) {
warnOnce( 'Renderer: `shadowMap.transmitted` needs to be set to `true` when using `material.castShadowNode`.' );
}
} else {
shadowRGB = vec3( 0 );
shadowAlpha = float( 1 );
}
if ( hasMap ) {
shadowAlpha = shadowAlpha.mul( reference$1( 'map', 'texture', material ).a );
}
if ( hasColorNode ) {
shadowAlpha = shadowAlpha.mul( material.colorNode.a );
}
colorNode = vec4( shadowRGB, shadowAlpha );
if ( hasMaskNode ) {
const maskNode = material.maskShadowNode || material.maskNode;
colorNode = Fn( ( [ color ] ) => {
maskNode.not().discard();
return color;
} )( colorNode );
}
}
if ( material.depthNode && material.depthNode.isNode ) {
depthNode = material.depthNode;
}
if ( material.castShadowPositionNode && material.castShadowPositionNode.isNode ) {
positionNode = material.castShadowPositionNode;
} else if ( material.positionNode && material.positionNode.isNode ) {
positionNode = material.positionNode;
}
cache = {
version,
colorNode,
depthNode,
positionNode
};
this._cacheShadowNodes.set( material, cache );
}
return cache;
}
/**
* Updates the camera so it's prepared for rendering operations.
*
* @private
* @param {Camera} camera - The camera to update.
* @param {boolean} useXRCamera - Whether the XR camera should be used when presenting.
* @return {Camera} The returned camera might be different depending on whether XR is used or not.
*/
_updateCamera( camera, useXRCamera ) {
const xr = this.xr;
const useXR = xr.enabled === true && xr.isPresenting === true && useXRCamera === true;
if ( useXR === false ) {
let projectionMatrixNeedsUpdate = false;
// reversed depth
if ( this.reversedDepthBuffer === true && camera.reversedDepth !== true ) {
camera._reversedDepth = true;
if ( camera.isArrayCamera ) {
for ( const subCamera of camera.cameras ) {
subCamera._reversedDepth = true;
}
}
projectionMatrixNeedsUpdate = true;
}
// WebGPU/WebGL coordinate system
const coordinateSystem = this.coordinateSystem;
if ( camera.coordinateSystem !== coordinateSystem ) {
camera.coordinateSystem = coordinateSystem;
if ( camera.isArrayCamera ) {
for ( const subCamera of camera.cameras ) {
subCamera.coordinateSystem = coordinateSystem;
}
}
projectionMatrixNeedsUpdate = true;
}
// camera update
if ( projectionMatrixNeedsUpdate === true ) {
camera.updateProjectionMatrix();
if ( camera.isArrayCamera ) {
for ( const subCamera of camera.cameras ) {
subCamera.updateProjectionMatrix();
}
}
}
}
if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld();
// handle XR
if ( useXR === true ) {
if ( xr.cameraAutoUpdate === true ) xr.updateCamera( camera );
camera = xr.getCamera(); // use XR camera for rendering
}
return camera;
}
/**
* Updates the indirect storage attribute that defines the dispatch size
* of the given compute node, if any.
*
* @private
* @param {ComputeNode} computeNode - The compute node.
* @param {?(number|Array<number>|IndirectStorageBufferAttribute)} dispatchSize - The dispatch size of the compute call.
*/
_updateDispatchSize( computeNode, dispatchSize ) {
const indirect = dispatchSize !== null ? dispatchSize : computeNode.dispatchSize;
if ( indirect && indirect.isIndirectStorageBufferAttribute === true ) {
this._attributes.update( indirect, AttributeType.INDIRECT );
}
}
/**
* This method represents the default render object function that manages the render lifecycle
* of the object.
*
* @param {Object3D} object - The 3D object.
* @param {Scene} scene - The scene the 3D object belongs to.
* @param {Camera} camera - The camera the object should be rendered with.
* @param {BufferGeometry} geometry - The object's geometry.
* @param {Material} material - The object's material.
* @param {?Object} group - Only relevant for objects using multiple materials. This represents a group entry from the respective `BufferGeometry`.
* @param {LightsNode} lightsNode - The current lights node.
* @param {?ClippingContext} clippingContext - The clipping context.
* @param {?string} [passId=null] - An optional ID for identifying the pass.
*/
renderObject( object, scene, camera, geometry, material, group, lightsNode, clippingContext = null, passId = null ) {
let materialOverride = false;
let materialColorNode;
let materialDepthNode;
let materialPositionNode;
let materialSide;
let materialDisplacementMap;
let materialDisplacementScale;
let materialDisplacementBias;
const previousSourceMaterial = this._currentSourceMaterial;
//
object.onBeforeRender( this, scene, camera, geometry, material, group );
//
if ( material.allowOverride === true && scene.overrideMaterial !== null ) {
this._currentSourceMaterial = material;
const overrideMaterial = scene.overrideMaterial;
materialOverride = true;
// store original nodes
materialColorNode = ( overrideMaterial.isNodeMaterial ) ? overrideMaterial.colorNode : null;
materialDepthNode = ( overrideMaterial.isNodeMaterial ) ? overrideMaterial.depthNode : null;
materialPositionNode = ( overrideMaterial.isNodeMaterial ) ? overrideMaterial.positionNode : null;
materialSide = scene.overrideMaterial.side;
materialDisplacementMap = overrideMaterial.displacementMap;
materialDisplacementScale = overrideMaterial.displacementScale;
materialDisplacementBias = overrideMaterial.displacementBias;
if ( material.positionNode && material.positionNode.isNode ) {
overrideMaterial.positionNode = material.positionNode;
}
overrideMaterial._alphaTest = material.alphaTest; // bypass the setter so the version doesn't change
overrideMaterial.alphaMap = material.alphaMap;
overrideMaterial.displacementMap = material.displacementMap;
overrideMaterial.displacementScale = material.displacementScale;
overrideMaterial.displacementBias = material.displacementBias;
overrideMaterial.transparent = material.transparent || material.transmission > 0 ||
( material.transmissionNode && material.transmissionNode.isNode ) ||
( material.backdropNode && material.backdropNode.isNode );
if ( overrideMaterial.isShadowPassMaterial ) {
const { colorNode, depthNode, positionNode } = this._getShadowNodes( material );
if ( this.shadowMap.type === VSMShadowMap ) {
overrideMaterial.side = ( material.shadowSide !== null ) ? material.shadowSide : material.side;
} else {
overrideMaterial.side = ( material.shadowSide !== null ) ? material.shadowSide : _shadowSide[ material.side ];
}
if ( colorNode !== null ) overrideMaterial.colorNode = colorNode;
if ( depthNode !== null ) overrideMaterial.depthNode = depthNode;
if ( positionNode !== null ) overrideMaterial.positionNode = positionNode;
}
material = overrideMaterial;
}
//
if ( material.transparent === true && material.side === DoubleSide && material.forceSinglePass === false ) {
material.side = BackSide;
this._handleObjectFunction( object, material, scene, camera, lightsNode, group, clippingContext, 'backSide' ); // create backSide pass id
material.side = FrontSide;
this._handleObjectFunction( object, material, scene, camera, lightsNode, group, clippingContext, passId ); // use default pass id
material.side = DoubleSide;
} else {
this._handleObjectFunction( object, material, scene, camera, lightsNode, group, clippingContext, passId );
}
//
if ( materialOverride ) {
scene.overrideMaterial.colorNode = materialColorNode;
scene.overrideMaterial.depthNode = materialDepthNode;
scene.overrideMaterial.positionNode = materialPositionNode;
scene.overrideMaterial.side = materialSide;
scene.overrideMaterial.displacementMap = materialDisplacementMap;
scene.overrideMaterial.displacementScale = materialDisplacementScale;
scene.overrideMaterial.displacementBias = materialDisplacementBias;
}
this._currentSourceMaterial = previousSourceMaterial;
//
object.onAfterRender( this, scene, camera, geometry, material, group );
}
/**
* Checks if the given compatibility is supported by the selected backend.
*
* @param {string} name - The compatibility's name.
* @return {boolean} Whether the compatibility is supported or not.
*/
hasCompatibility( name ) {
if ( this._initialized === false ) {
throw new Error( 'THREE.Renderer: .hasCompatibility() called before the backend is initialized. Use "await renderer.init();" before using this method.' );
}
return this.backend.hasCompatibility( name );
}
/**
* This method represents the default `_handleObjectFunction` implementation which creates
* a render object from the given data and performs the draw command with the selected backend.
*
* @private
* @param {Object3D} object - The 3D object.
* @param {Material} material - The object's material.
* @param {Scene} scene - The scene the 3D object belongs to.
* @param {Camera} camera - The camera the object should be rendered with.
* @param {LightsNode} lightsNode - The current lights node.
* @param {?{start: number, count: number}} group - Only relevant for objects using multiple materials. This represents a group entry from the respective `BufferGeometry`.
* @param {ClippingContext} clippingContext - The clipping context.
* @param {string} [passId] - An optional ID for identifying the pass.
*/
_renderObjectDirect( object, material, scene, camera, lightsNode, group, clippingContext, passId ) {
const renderObject = this._objects.get( object, material, scene, camera, lightsNode, this._currentRenderContext, clippingContext, passId );
renderObject.drawRange = object.geometry.drawRange;
renderObject.group = group;
if ( this._currentRenderBundle !== null ) {
const renderBundleData = this.backend.get( this._currentRenderBundle );
renderBundleData.renderObjects.push( renderObject );
renderObject.bundle = this._currentRenderBundle.bundleGroup;
}
//
const refreshType = this._nodes.needsRefresh( renderObject );
if ( refreshType === RenderObjectRefreshType.FULL ) {
this._nodes.updateBefore( renderObject );
this._geometries.updateForRender( renderObject );
this._nodes.updateForRender( renderObject );
this._bindings.updateForRender( renderObject );
} else if ( refreshType === RenderObjectRefreshType.SHARED ) {
this._nodes.updateBefore( renderObject );
this._nodes.updateForRender( renderObject );
this._bindings.updateSharedForRender( renderObject );
}
this._pipelines.updateForRender( renderObject );
//
if ( this._pipelines.isReady( renderObject ) ) {
this.backend.draw( renderObject, this.info );
if ( refreshType !== RenderObjectRefreshType.NONE ) this._nodes.updateAfter( renderObject );
} else if ( this._currentRenderBundle !== null ) {
this.backend.get( this._currentRenderBundle ).version = -1;
}
}
/**
* A different implementation for `_handleObjectFunction` which only makes sure the object is ready for rendering.
* Used in `compileAsync()`.
*
* @private
* @param {Object3D} object - The 3D object.
* @param {Material} material - The object's material.
* @param {Scene} scene - The scene the 3D object belongs to.
* @param {Camera} camera - The camera the object should be rendered with.
* @param {LightsNode} lightsNode - The current lights node.
* @param {?{start: number, count: number}} group - Only relevant for objects using multiple materials. This represents a group entry from the respective `BufferGeometry`.
* @param {ClippingContext} clippingContext - The clipping context.
* @param {string} [passId] - An optional ID for identifying the pass.
*/
_createObjectPipeline( object, material, scene, camera, lightsNode, group, clippingContext, passId ) {
// If in async compilation mode, queue the work for sequential execution
if ( this._compilationPromises !== null ) {
// Store work items instead of promises - will be processed sequentially.
// The side is stored since the renderer might have changed it temporarily.
this._compilationPromises.push( {
object,
material,
side: material.side,
scene,
camera,
lightsNode,
group,
clippingContext,
passId,
renderContext: this._currentRenderContext,
precompilationState: {
...this._precompilationState,
useFrameBufferTarget: true,
renderTarget: this.getRenderTarget(),
mrt: this.getMRT(),
renderContext: this._currentRenderContext
}
} );
return;
}
// Sync path
const renderObject = this._objects.get( object, material, scene, camera, lightsNode, this._currentRenderContext, clippingContext, passId );
renderObject.drawRange = object.geometry.drawRange;
renderObject.group = group;
//
this._nodes.updateBefore( renderObject );
this._geometries.updateForRender( renderObject );
this._nodes.updateForRender( renderObject );
this._bindings.updateForRender( renderObject );
this._pipelines.getForRender( renderObject, this._compilationPromises );
this._nodes.updateAfter( renderObject );
}
/**
* Callback when the canvas has been resized.
*
* @private
*/
_onCanvasTargetResize() {
if ( this._initialized ) this.backend.updateSize();
}
/**
* Callback when a canvas target has been disposed.
*
* @private
* @param {Object} event - The dispose event.
*/
_onCanvasTargetDispose( event ) {
this._destroyCanvasTarget( event.target );
}
/**
* Frees the GPU resources of the given canvas target.
*
* @private
* @param {CanvasTarget} canvasTarget - The canvas target.
*/
_destroyCanvasTarget( canvasTarget ) {
canvasTarget.removeEventListener( 'dispose', this._onCanvasTargetDispose );
if ( this._initialized ) this.backend.destroyCanvasTarget( canvasTarget );
}
/**
* Alias for `compileAsync()`.
*
* @method
* @param {Object3D} scene - The scene or 3D object to precompile.
* @param {Camera} camera - The camera that is used to render the scene.
* @param {Scene} targetScene - If the first argument is a 3D object, this parameter must represent the scene the 3D object is going to be added.
* @param {onProgressCallback} [onProgress] - Executed while the compilation is in progress.
* @return {function(Object3D, Camera, ?Scene, ?onProgressCallback): Promise|undefined} A Promise that resolves when the compile has been finished.
*/
get compile() {
return this.compileAsync;
}
}
/**
* A binding represents the connection between a resource (like a texture, sampler
* or uniform buffer) and the resource definition in a shader stage.
*
* This module is an abstract base class for all concrete bindings types.
*
* @abstract
* @private
*/
class Binding {
/**
* Constructs a new binding.
*
* @param {string} [name=''] - The binding's name.
*/
constructor( name = '' ) {
/**
* The binding's name.
*
* @type {string}
*/
this.name = name;
/**
* A bitmask that defines in what shader stages the
* binding's resource is accessible.
*
* @type {number}
*/
this.visibility = 0;
}
/**
* Makes sure binding's resource is visible for the given shader stage.
*
* @param {number} visibility - The shader stage.
*/
setVisibility( visibility ) {
this.visibility |= visibility;
}
/**
* Clones the binding.
*
* @return {Binding} The cloned binding.
*/
clone() {
return Object.assign( new this.constructor(), this );
}
}
/**
* This function is usually called with the length in bytes of an array buffer.
* It returns an padded value which ensure chunk size alignment according to STD140 layout.
*
* @function
* @param {number} floatLength - The buffer length.
* @return {number} The padded length.
*/
function getFloatLength( floatLength ) {
// ensure chunk size alignment (STD140 layout)
return floatLength + ( ( GPU_CHUNK_BYTES - ( floatLength % GPU_CHUNK_BYTES ) ) % GPU_CHUNK_BYTES );
}
/**
* Represents a buffer binding type.
*
* @private
* @abstract
* @augments Binding
*/
class Buffer extends Binding {
/**
* Constructs a new buffer.
*
* @param {string} name - The buffer's name.
* @param {TypedArray} [buffer=null] - The buffer.
*/
constructor( name, buffer = null ) {
super( name );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isBuffer = true;
/**
* The bytes per element.
*
* @type {number}
*/
this.bytesPerElement = Float32Array.BYTES_PER_ELEMENT;
/**
* A reference to the internal buffer.
*
* @private
* @type {TypedArray}
*/
this._buffer = buffer;
/**
* An array of update ranges.
*
* @private
* @type {Array<{start: number, count: number}>}
*/
this._updateRanges = [];
}
/**
* The array of update ranges.
*
* @type {Array<{start: number, count: number}>}
*/
get updateRanges() {
return this._updateRanges;
}
/**
* Adds an update range.
*
* @param {number} start - The start index.
* @param {number} count - The number of elements.
*/
addUpdateRange( start, count ) {
this.updateRanges.push( { start, count } );
}
/**
* Clears all update ranges.
*/
clearUpdateRanges() {
this.updateRanges.length = 0;
}
/**
* The buffer's byte length.
*
* @type {number}
* @readonly
*/
get byteLength() {
return getFloatLength( this.buffer.byteLength );
}
/**
* A reference to the internal buffer.
*
* @type {Float32Array}
* @readonly
*/
get buffer() {
return this._buffer;
}
/**
* Updates the binding.
*
* @return {boolean} Whether the buffer has been updated and must be
* uploaded to the GPU.
*/
update() {
return true;
}
/**
* Releases the buffer.
*/
release() {
this._buffer = null;
}
}
/**
* Represents a uniform buffer binding type.
*
* @private
* @augments Buffer
*/
class UniformBuffer extends Buffer {
/**
* Constructs a new uniform buffer.
*
* @param {string} name - The buffer's name.
* @param {TypedArray} [buffer=null] - The buffer.
*/
constructor( name, buffer = null ) {
super( name, buffer );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isUniformBuffer = true;
}
}
let _id$4 = 0;
/**
* A special form of uniform buffer binding type.
* It's buffer value is managed by a node object.
*
* @private
* @augments UniformBuffer
*/
class NodeUniformBuffer extends UniformBuffer {
/**
* Constructs a new node-based uniform buffer.
*
* @param {BufferNode} nodeUniform - The uniform buffer node.
* @param {UniformGroupNode} groupNode - The uniform group node.
*/
constructor( nodeUniform, groupNode ) {
const label = nodeUniform && nodeUniform.name ? nodeUniform.name : _id$4 ++;
super( 'UniformBuffer_' + label );
/**
* The uniform buffer node.
*
* @type {BufferNode}
*/
this.nodeUniform = nodeUniform;
/**
* The uniform group node.
*
* @type {UniformGroupNode}
*/
this.groupNode = groupNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isNodeUniformBuffer = true;
}
/**
* The array of update ranges.
*
* @param {Array<{start: number, count: number}>} value - The update ranges.
*/
set updateRanges( value ) {
this.nodeUniform.updateRanges = value;
}
/**
* The array of update ranges.
*
* @type {Array<{start: number, count: number}>}
*/
get updateRanges() {
return this.nodeUniform.updateRanges;
}
/**
* Adds a range of data in the data array to be updated on the GPU.
*
* @param {number} start - Position at which to start update.
* @param {number} count - The number of components to update.
*/
addUpdateRange( start, count ) {
this.nodeUniform.addUpdateRange( start, count );
}
/**
* Clears all update ranges.
*/
clearUpdateRanges() {
this.nodeUniform.clearUpdateRanges();
}
/**
* The uniform buffer.
*
* @type {Float32Array}
*/
get buffer() {
return this.nodeUniform.value;
}
}
/**
* This class represents a uniform buffer binding but with
* an API that allows to maintain individual uniform objects.
*
* @private
* @augments UniformBuffer
*/
class UniformsGroup extends UniformBuffer {
/**
* Constructs a new uniforms group.
*
* @param {string} name - The group's name.
*/
constructor( name ) {
super( name );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isUniformsGroup = true;
/**
* An array with the raw uniform values.
*
* @private
* @type {?Array<number>}
* @default null
*/
this._values = null;
/**
* An array of uniform objects.
*
* The order of uniforms in this array must match the order of uniforms in the shader.
*
* @type {Array<Uniform>}
*/
this.uniforms = [];
/**
* A cache for the uniform update ranges.
*
* @private
* @type {Map<number, {start: number, count: number}>}
*/
this._updateRangeCache = new Map();
/**
* Uniform indices whose range has already been pushed into `updateRanges`
* during the current update cycle. Reset on `clearUpdateRanges()`.
*
* @private
* @type {Set<number>}
*/
this._addedIndices = new Set();
}
/**
* Adds a uniform's update range to this buffer.
*
* @param {Uniform} uniform - The uniform.
*/
addUniformUpdateRange( uniform ) {
const index = uniform.index;
if ( this._addedIndices.has( index ) ) return;
let range = this._updateRangeCache.get( index );
if ( range === undefined ) {
range = { start: 0, count: 0 };
this._updateRangeCache.set( index, range );
}
range.start = uniform.offset;
range.count = uniform.itemSize;
this._addedIndices.add( index );
this.updateRanges.push( range );
}
/**
* Clears all update ranges of this buffer.
*/
clearUpdateRanges() {
this._addedIndices.clear();
super.clearUpdateRanges();
}
/**
* Adds a uniform to this group.
*
* @param {Uniform} uniform - The uniform to add.
* @return {UniformsGroup} A reference to this group.
*/
addUniform( uniform ) {
this.uniforms.push( uniform );
return this;
}
/**
* Removes a uniform from this group.
*
* @param {Uniform} uniform - The uniform to remove.
* @return {UniformsGroup} A reference to this group.
*/
removeUniform( uniform ) {
const index = this.uniforms.indexOf( uniform );
if ( index !== -1 ) {
this.uniforms.splice( index, 1 );
}
return this;
}
/**
* An array with the raw uniform values.
*
* @type {Array<number>}
*/
get values() {
if ( this._values === null ) {
this._values = Array.from( this.buffer );
}
return this._values;
}
/**
* A Float32 array buffer with the uniform values.
*
* @type {Float32Array}
*/
get buffer() {
let buffer = this._buffer;
if ( buffer === null ) {
const byteLength = this.byteLength;
buffer = new Float32Array( new ArrayBuffer( byteLength ) );
this._buffer = buffer;
}
return buffer;
}
/**
* The byte length of the buffer with correct buffer alignment.
*
* @type {number}
*/
get byteLength() {
const bytesPerElement = this.bytesPerElement;
let offset = 0; // global buffer offset in bytes
for ( let i = 0, l = this.uniforms.length; i < l; i ++ ) {
const uniform = this.uniforms[ i ];
const boundary = uniform.boundary;
const itemSize = uniform.itemSize * bytesPerElement; // size of the uniform in bytes
const chunkOffset = offset % GPU_CHUNK_BYTES; // offset in the current chunk
const chunkPadding = chunkOffset % boundary; // required padding to match boundary
const chunkStart = chunkOffset + chunkPadding; // start position in the current chunk for the data
offset += chunkPadding;
// Check for chunk overflow
if ( chunkStart !== 0 && ( GPU_CHUNK_BYTES - chunkStart ) < itemSize ) {
// Add padding to the end of the chunk
offset += ( GPU_CHUNK_BYTES - chunkStart );
}
uniform.offset = offset / bytesPerElement;
uniform.index = i;
offset += itemSize;
}
return Math.ceil( offset / GPU_CHUNK_BYTES ) * GPU_CHUNK_BYTES;
}
/**
* Updates this group by updating each uniform object of
* the internal uniform list. The uniform objects check if their
* values has actually changed so this method only returns
* `true` if there is a real value change.
*
* @return {boolean} Whether the uniforms have been updated and
* must be uploaded to the GPU.
*/
update() {
let updated = false;
for ( const uniform of this.uniforms ) {
if ( this.updateByType( uniform ) === true ) {
updated = true;
}
}
return updated;
}
/**
* Releases the buffer.
*/
release() {
super.release();
this._values = null;
}
/**
* Returns a clone of this uniforms group. The clone has its own buffer and update state.
*
* @return {UniformsGroup} The cloned uniforms group.
*/
clone() {
const clone = super.clone();
clone._buffer = null;
clone._values = null;
clone._updateRanges = [];
clone._updateRangeCache = new Map();
clone._addedIndices = new Set();
return clone;
}
/**
* Updates a given uniform by calling an update method matching
* the uniforms type.
*
* @param {Uniform} uniform - The uniform to update.
* @return {boolean} Whether the uniform has been updated or not.
*/
updateByType( uniform ) {
if ( uniform.isNumberUniform ) return this.updateNumber( uniform );
if ( uniform.isVector2Uniform ) return this.updateVector2( uniform );
if ( uniform.isVector3Uniform ) return this.updateVector3( uniform );
if ( uniform.isVector4Uniform ) return this.updateVector4( uniform );
if ( uniform.isColorUniform ) return this.updateColor( uniform );
if ( uniform.isMatrix3Uniform ) return this.updateMatrix3( uniform );
if ( uniform.isMatrix4Uniform ) return this.updateMatrix4( uniform );
error( 'WebGPUUniformsGroup: Unsupported uniform type.', uniform );
}
/**
* Updates a given Number uniform.
*
* @param {NumberUniform} uniform - The Number uniform.
* @return {boolean} Whether the uniform has been updated or not.
*/
updateNumber( uniform ) {
let updated = false;
const a = this.values;
const v = uniform.getValue();
const offset = uniform.offset;
const type = uniform.getType();
if ( a[ offset ] !== v ) {
const b = this._getBufferForType( type );
b[ offset ] = a[ offset ] = v;
updated = true;
this.addUniformUpdateRange( uniform );
}
return updated;
}
/**
* Updates a given Vector2 uniform.
*
* @param {Vector2Uniform} uniform - The Vector2 uniform.
* @return {boolean} Whether the uniform has been updated or not.
*/
updateVector2( uniform ) {
let updated = false;
const a = this.values;
const v = uniform.getValue();
const offset = uniform.offset;
const type = uniform.getType();
if ( a[ offset + 0 ] !== v.x || a[ offset + 1 ] !== v.y ) {
const b = this._getBufferForType( type );
b[ offset + 0 ] = a[ offset + 0 ] = v.x;
b[ offset + 1 ] = a[ offset + 1 ] = v.y;
updated = true;
this.addUniformUpdateRange( uniform );
}
return updated;
}
/**
* Updates a given Vector3 uniform.
*
* @param {Vector3Uniform} uniform - The Vector3 uniform.
* @return {boolean} Whether the uniform has been updated or not.
*/
updateVector3( uniform ) {
let updated = false;
const a = this.values;
const v = uniform.getValue();
const offset = uniform.offset;
const type = uniform.getType();
if ( a[ offset + 0 ] !== v.x || a[ offset + 1 ] !== v.y || a[ offset + 2 ] !== v.z ) {
const b = this._getBufferForType( type );
b[ offset + 0 ] = a[ offset + 0 ] = v.x;
b[ offset + 1 ] = a[ offset + 1 ] = v.y;
b[ offset + 2 ] = a[ offset + 2 ] = v.z;
updated = true;
this.addUniformUpdateRange( uniform );
}
return updated;
}
/**
* Updates a given Vector4 uniform.
*
* @param {Vector4Uniform} uniform - The Vector4 uniform.
* @return {boolean} Whether the uniform has been updated or not.
*/
updateVector4( uniform ) {
let updated = false;
const a = this.values;
const v = uniform.getValue();
const offset = uniform.offset;
const type = uniform.getType();
if ( a[ offset + 0 ] !== v.x || a[ offset + 1 ] !== v.y || a[ offset + 2 ] !== v.z || a[ offset + 3 ] !== v.w ) {
const b = this._getBufferForType( type );
b[ offset + 0 ] = a[ offset + 0 ] = v.x;
b[ offset + 1 ] = a[ offset + 1 ] = v.y;
b[ offset + 2 ] = a[ offset + 2 ] = v.z;
b[ offset + 3 ] = a[ offset + 3 ] = v.w;
updated = true;
this.addUniformUpdateRange( uniform );
}
return updated;
}
/**
* Updates a given Color uniform.
*
* @param {ColorUniform} uniform - The Color uniform.
* @return {boolean} Whether the uniform has been updated or not.
*/
updateColor( uniform ) {
let updated = false;
const a = this.values;
const c = uniform.getValue();
const offset = uniform.offset;
if ( a[ offset + 0 ] !== c.r || a[ offset + 1 ] !== c.g || a[ offset + 2 ] !== c.b ) {
const b = this.buffer;
b[ offset + 0 ] = a[ offset + 0 ] = c.r;
b[ offset + 1 ] = a[ offset + 1 ] = c.g;
b[ offset + 2 ] = a[ offset + 2 ] = c.b;
updated = true;
this.addUniformUpdateRange( uniform );
}
return updated;
}
/**
* Updates a given Matrix3 uniform.
*
* @param {Matrix3Uniform} uniform - The Matrix3 uniform.
* @return {boolean} Whether the uniform has been updated or not.
*/
updateMatrix3( uniform ) {
let updated = false;
const a = this.values;
const e = uniform.getValue().elements;
const offset = uniform.offset;
if ( a[ offset + 0 ] !== e[ 0 ] || a[ offset + 1 ] !== e[ 1 ] || a[ offset + 2 ] !== e[ 2 ] ||
a[ offset + 4 ] !== e[ 3 ] || a[ offset + 5 ] !== e[ 4 ] || a[ offset + 6 ] !== e[ 5 ] ||
a[ offset + 8 ] !== e[ 6 ] || a[ offset + 9 ] !== e[ 7 ] || a[ offset + 10 ] !== e[ 8 ] ) {
const b = this.buffer;
b[ offset + 0 ] = a[ offset + 0 ] = e[ 0 ];
b[ offset + 1 ] = a[ offset + 1 ] = e[ 1 ];
b[ offset + 2 ] = a[ offset + 2 ] = e[ 2 ];
b[ offset + 4 ] = a[ offset + 4 ] = e[ 3 ];
b[ offset + 5 ] = a[ offset + 5 ] = e[ 4 ];
b[ offset + 6 ] = a[ offset + 6 ] = e[ 5 ];
b[ offset + 8 ] = a[ offset + 8 ] = e[ 6 ];
b[ offset + 9 ] = a[ offset + 9 ] = e[ 7 ];
b[ offset + 10 ] = a[ offset + 10 ] = e[ 8 ];
updated = true;
this.addUniformUpdateRange( uniform );
}
return updated;
}
/**
* Updates a given Matrix4 uniform.
*
* @param {Matrix4Uniform} uniform - The Matrix4 uniform.
* @return {boolean} Whether the uniform has been updated or not.
*/
updateMatrix4( uniform ) {
let updated = false;
const a = this.values;
const e = uniform.getValue().elements;
const offset = uniform.offset;
if ( arraysEqual( a, e, offset ) === false ) {
const b = this.buffer;
b.set( e, offset );
setArray( a, e, offset );
updated = true;
this.addUniformUpdateRange( uniform );
}
return updated;
}
/**
* Returns a typed array that matches the given data type.
*
* @private
* @param {string} type - The data type.
* @return {TypedArray} The typed array.
*/
_getBufferForType( type ) {
if ( type === 'int' || type === 'ivec2' || type === 'ivec3' || type === 'ivec4' ) return new Int32Array( this.buffer.buffer );
if ( type === 'uint' || type === 'uvec2' || type === 'uvec3' || type === 'uvec4' ) return new Uint32Array( this.buffer.buffer );
return this.buffer;
}
}
/**
* Sets the values of the second array to the first array.
*
* @private
* @param {TypedArray} a - The first array.
* @param {TypedArray} b - The second array.
* @param {number} offset - An index offset for the first array.
*/
function setArray( a, b, offset ) {
for ( let i = 0, l = b.length; i < l; i ++ ) {
a[ offset + i ] = b[ i ];
}
}
/**
* Returns `true` if the given arrays are equal.
*
* @private
* @param {TypedArray} a - The first array.
* @param {TypedArray} b - The second array.
* @param {number} offset - An index offset for the first array.
* @return {boolean} Whether the given arrays are equal or not.
*/
function arraysEqual( a, b, offset ) {
for ( let i = 0, l = b.length; i < l; i ++ ) {
if ( a[ offset + i ] !== b[ i ] ) return false;
}
return true;
}
let _id$3 = 0;
/**
* A special form of uniforms group that represents
* the individual uniforms as node-based uniforms.
*
* @private
* @augments UniformsGroup
*/
class NodeUniformsGroup extends UniformsGroup {
/**
* Constructs a new node-based uniforms group.
*
* @param {string} name - The group's name.
* @param {UniformGroupNode} groupNode - The uniform group node.
*/
constructor( name, groupNode ) {
super( name );
/**
* The group's ID.
*
* @type {number}
*/
this.id = _id$3 ++;
/**
* The uniform group node.
*
* @type {UniformGroupNode}
*/
this.groupNode = groupNode;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isNodeUniformsGroup = true;
}
}
/**
* Represents a sampler binding type.
*
* @private
* @augments Binding
*/
class Sampler extends Binding {
/**
* Constructs a new sampler.
*
* @param {string} name - The samplers's name.
* @param {?Texture} texture - The texture this binding is referring to.
*/
constructor( name, texture ) {
super( name );
/**
* The texture the sampler is referring to.
*
* @private
* @type {?Texture}
*/
this._texture = texture;
/**
* The binding's version.
*
* @type {number}
*/
this.version = -1;
/**
* The binding's generation which is an additional version
* qualifier.
*
* @type {?number}
* @default null
*/
this.generation = null;
/**
* The binding's sampler key.
*
* @type {string}
* @default ''
*/
this.samplerKey = '';
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isSampler = true;
}
/**
* Sets the texture of this sampler.
*
* @param {Texture} value - The texture to set.
*/
set texture( value ) {
if ( this._texture === value ) return;
this._texture = value;
this.reset();
}
/**
* Gets the texture of this sampler.
* @return {?Texture} The texture.
*/
get texture() {
return this._texture;
}
/**
* Updates the binding.
*
* @return {boolean} Whether the texture has been updated and must be
* uploaded to the GPU.
*/
update() {
const { texture, version } = this;
if ( version !== texture.version ) {
this.version = texture.version;
return true;
}
return false;
}
/**
* Resets the version and generation. This is used when the texture
* the binding is pointing to is disposed or exchanged.
*/
reset() {
this.generation = null;
this.version = -1;
}
/**
* Releases the texture reference.
*/
release() {
this._texture = null;
}
}
let _id$2 = 0;
/**
* Represents a sampled texture binding type.
*
* @private
* @augments Sampler
*/
class SampledTexture extends Sampler {
/**
* Constructs a new sampled texture.
*
* @param {string} name - The sampled texture's name.
* @param {?Texture} texture - The texture this binding is referring to.
*/
constructor( name, texture ) {
super( name, texture );
/**
* This identifier.
*
* @type {number}
*/
this.id = _id$2 ++;
/**
* Whether the texture is a storage texture or not.
*
* @type {boolean}
* @default false
*/
this.store = false;
/**
* The mip level to bind for storage textures.
*
* @type {number}
* @default 0
*/
this.mipLevel = 0;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isSampledTexture = true;
}
}
/**
* A special form of sampled texture binding type.
* It's texture value is managed by a node object.
*
* @private
* @augments SampledTexture
*/
class NodeSampledTexture extends SampledTexture {
/**
* Constructs a new node-based sampled texture.
*
* @param {string} name - The textures's name.
* @param {TextureNode} textureNode - The texture node.
* @param {UniformGroupNode} groupNode - The uniform group node.
* @param {?string} [access=null] - The access type.
*/
constructor( name, textureNode, groupNode, access = null ) {
super( name, textureNode ? textureNode.value : null );
/**
* The texture node.
*
* @type {TextureNode}
*/
this.textureNode = textureNode;
/**
* The uniform group node.
*
* @type {UniformGroupNode}
*/
this.groupNode = groupNode;
/**
* The access type.
*
* @type {?string}
* @default null
*/
this.access = access;
}
/**
* Updates the binding.
*
* @return {boolean} Whether the texture has been updated and must be
* uploaded to the GPU.
*/
update() {
const { textureNode } = this;
if ( this.texture !== textureNode.value ) {
this.texture = textureNode.value;
return true;
}
return super.update();
}
/**
* Clones the binding.
*
* @return {NodeSampledTexture} The cloned binding.
*/
clone() {
const binding = super.clone();
binding.texture = this.textureNode.value; // copy current node value to avoid stale references
return binding;
}
}
/**
* A special form of sampled cube texture binding type.
* It's texture value is managed by a node object.
*
* @private
* @augments NodeSampledTexture
*/
class NodeSampledCubeTexture extends NodeSampledTexture {
/**
* Constructs a new node-based sampled cube texture.
*
* @param {string} name - The textures's name.
* @param {TextureNode} textureNode - The texture node.
* @param {UniformGroupNode} groupNode - The uniform group node.
* @param {?string} [access=null] - The access type.
*/
constructor( name, textureNode, groupNode, access = null ) {
super( name, textureNode, groupNode, access );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isSampledCubeTexture = true;
}
}
/**
* A special form of sampled 3D texture binding type.
* It's texture value is managed by a node object.
*
* @private
* @augments NodeSampledTexture
*/
class NodeSampledTexture3D extends NodeSampledTexture {
/**
* Constructs a new node-based sampled 3D texture.
*
* @param {string} name - The textures's name.
* @param {TextureNode} textureNode - The texture node.
* @param {UniformGroupNode} groupNode - The uniform group node.
* @param {?string} [access=null] - The access type.
*/
constructor( name, textureNode, groupNode, access = null ) {
super( name, textureNode, groupNode, access );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isSampledTexture3D = true;
}
}
const glslPolyfills = {
textureGather: new CodeNode( /* glsl */`
vec4 tsl_textureGather( const int comp, sampler2D map, vec2 coord, ivec2 offset, bool flipY ) {
if ( flipY ) offset.y = - offset.y;
vec2 size = vec2( textureSize( map, 0 ) );
vec2 st = floor( coord * size + vec2( offset ) - 0.5 );
vec4 ij = vec4( st + 0.5, st + 1.5 ) / size.xyxy;
vec4 ret = vec4(
textureLod( map, ij.xw, 0.0 )[ comp ],
textureLod( map, ij.zw, 0.0 )[ comp ],
textureLod( map, ij.zy, 0.0 )[ comp ],
textureLod( map, ij.xy, 0.0 )[ comp ]
);
return flipY ? ret.wzyx : ret;
}
` ),
textureGatherArray: new CodeNode( /* glsl */`
vec4 tsl_textureGather_array( const int comp, sampler2DArray map, vec3 coord, ivec2 offset, bool flipY ) {
if ( flipY ) offset.y = - offset.y;
vec2 size = vec2( textureSize( map, 0 ).xy );
vec2 st = floor( coord.xy * size + vec2( offset ) - 0.5 );
vec4 ij = vec4( st + 0.5, st + 1.5 ) / size.xyxy;
vec4 ret = vec4(
textureLod( map, vec3( ij.xw, coord.z ), 0.0 )[ comp ],
textureLod( map, vec3( ij.zw, coord.z ), 0.0 )[ comp ],
textureLod( map, vec3( ij.zy, coord.z ), 0.0 )[ comp ],
textureLod( map, vec3( ij.xy, coord.z ), 0.0 )[ comp ]
);
return flipY ? ret.wzyx : ret;
}
` ),
textureGatherCompare: new CodeNode( /* glsl */`
vec4 tsl_textureGatherCompare( sampler2DShadow map, vec2 coord, ivec2 offset, float ref, bool flipY ) {
if ( flipY ) offset.y = - offset.y;
vec2 size = vec2( textureSize( map, 0 ) );
vec2 st = floor( coord * size + vec2( offset ) - 0.5 );
vec4 ij = vec4( st + 0.5, st + 1.5 ) / size.xyxy;
vec4 ret = vec4(
textureLod( map, vec3( ij.xw, ref ), 0.0 ),
textureLod( map, vec3( ij.zw, ref ), 0.0 ),
textureLod( map, vec3( ij.zy, ref ), 0.0 ),
textureLod( map, vec3( ij.xy, ref ), 0.0 )
);
return flipY ? ret.wzyx : ret;
}
` ),
textureGatherCompareArray: new CodeNode( /* glsl */`
vec4 tsl_textureGatherCompare_array( sampler2DArrayShadow map, vec3 coord, ivec2 offset, float ref, bool flipY ) {
if ( flipY ) offset.y = - offset.y;
vec2 size = vec2( textureSize( map, 0 ).xy );
vec2 st = floor( coord.xy * size + vec2( offset ) - 0.5 );
vec4 ij = vec4( st + 0.5, st + 1.5 ) / size.xyxy;
vec4 ret = vec4(
texture( map, vec4( ij.xw, coord.z, ref ) ),
texture( map, vec4( ij.zw, coord.z, ref ) ),
texture( map, vec4( ij.zy, coord.z, ref ) ),
texture( map, vec4( ij.xy, coord.z, ref ) )
);
return flipY ? ret.wzyx : ret;
}
` ),
floatpack_snorm_4x8: new CodeNode( /* glsl */'uint tsl_packSnorm4x8( vec4 v ) { uvec4 u = uvec4( ivec4( round( clamp( v, -1.0, 1.0 ) * 127.0 ) ) & 0xff ); return u.x | u.y << 8 | u.z << 16 | u.w << 24; }' ),
floatpack_unorm_4x8: new CodeNode( /* glsl */'uint tsl_packUnorm4x8( vec4 v ) { uvec4 u = uvec4( round( clamp( v, 0.0, 1.0 ) * 255.0 ) ); return u.x | u.y << 8 | u.z << 16 | u.w << 24; }' ),
floatunpack_snorm_4x8: new CodeNode( /* glsl */'vec4 tsl_unpackSnorm4x8( uint x ) { return max( vec4( ivec4( x << 24, x << 16, x << 8, x ) >> 24 ) / 127.0, -1.0 ); }' ),
floatunpack_unorm_4x8: new CodeNode( /* glsl */'vec4 tsl_unpackUnorm4x8( uint x ) { return vec4( uvec4( x, x >> 8, x >> 16, x >> 24 ) & 0xffu ) / 255.0; }' )
};
const glslMethods = {
equals: 'equal',
bitcast_float_int: 'floatBitsToInt',
bitcast_int_float: 'intBitsToFloat',
bitcast_uint_float: 'uintBitsToFloat',
bitcast_float_uint: 'floatBitsToUint',
floatpack_snorm_2x16: 'packSnorm2x16',
floatpack_unorm_2x16: 'packUnorm2x16',
floatpack_float16_2x16: 'packHalf2x16',
floatpack_snorm_4x8: 'tsl_packSnorm4x8',
floatpack_unorm_4x8: 'tsl_packUnorm4x8',
floatunpack_snorm_2x16: 'unpackSnorm2x16',
floatunpack_unorm_2x16: 'unpackUnorm2x16',
floatunpack_float16_2x16: 'unpackHalf2x16',
floatunpack_snorm_4x8: 'tsl_unpackSnorm4x8',
floatunpack_unorm_4x8: 'tsl_unpackUnorm4x8'
};
const precisionLib = {
low: 'lowp',
medium: 'mediump',
high: 'highp'
};
const supports$1 = {
swizzleAssign: true,
storageBuffer: false
};
const interpolationTypeMap = {
perspective: 'smooth',
linear: 'noperspective'
};
const interpolationModeMap = {
'centroid': 'centroid'
};
const defaultPrecisions = `
precision highp float;
precision highp int;
precision highp sampler2D;
precision highp sampler3D;
precision highp samplerCube;
precision highp sampler2DArray;
precision highp usampler2D;
precision highp usampler3D;
precision highp usamplerCube;
precision highp usampler2DArray;
precision highp isampler2D;
precision highp isampler3D;
precision highp isamplerCube;
precision highp isampler2DArray;
precision highp sampler2DShadow;
precision highp sampler2DArrayShadow;
precision highp samplerCubeShadow;
`;
const glslReservedKeywords = new Set( [
// keywords
'const', 'uniform', 'buffer', 'shared', 'attribute', 'varying', 'coherent', 'volatile', 'restrict',
'readonly', 'writeonly', 'atomic_uint', 'layout', 'centroid', 'flat', 'smooth', 'noperspective',
'patch', 'sample', 'invariant', 'precise', 'break', 'continue', 'do', 'for', 'while', 'switch',
'case', 'default', 'if', 'else', 'subroutine', 'in', 'out', 'inout', 'int', 'void', 'bool', 'true',
'false', 'float', 'double', 'discard', 'return', 'vec2', 'vec3', 'vec4', 'ivec2', 'ivec3', 'ivec4',
'bvec2', 'bvec3', 'bvec4', 'uint', 'uvec2', 'uvec3', 'uvec4', 'dvec2', 'dvec3', 'dvec4', 'mat2',
'mat3', 'mat4', 'mat2x2', 'mat2x3', 'mat2x4', 'mat3x2', 'mat3x3', 'mat3x4', 'mat4x2', 'mat4x3',
'mat4x4', 'dmat2', 'dmat3', 'dmat4', 'dmat2x2', 'dmat2x3', 'dmat2x4', 'dmat3x2', 'dmat3x3',
'dmat3x4', 'dmat4x2', 'dmat4x3', 'dmat4x4', 'lowp', 'mediump', 'highp', 'precision', 'sampler2D',
'sampler3D', 'samplerCube', 'sampler2DShadow', 'samplerCubeShadow', 'sampler2DArray',
'sampler2DArrayShadow', 'isampler2D', 'isampler3D', 'isamplerCube', 'isampler2DArray', 'usampler2D',
'usampler3D', 'usamplerCube', 'usampler2DArray', 'struct',
// reserved for future use
'common', 'partition', 'active', 'asm', 'class', 'union', 'enum', 'typedef', 'template', 'this',
'resource', 'goto', 'inline', 'noinline', 'public', 'static', 'extern', 'external', 'interface',
'long', 'short', 'half', 'fixed', 'unsigned', 'superp', 'input', 'output', 'hvec2', 'hvec3',
'hvec4', 'fvec2', 'fvec3', 'fvec4', 'sampler3DRect', 'filter', 'sizeof', 'cast', 'namespace',
'using',
// generated entry points
'main'
] );
/**
* A node builder targeting GLSL.
*
* This module generates GLSL shader code from node materials and also
* generates the respective bindings and vertex buffer definitions. These
* data are later used by the renderer to create render and compute pipelines
* for render objects.
*
* @augments NodeBuilder
*/
class GLSLNodeBuilder extends NodeBuilder {
/**
* Constructs a new GLSL node builder renderer.
*
* @param {Object3D} object - The 3D object.
* @param {Renderer} renderer - The renderer.
*/
constructor( object, renderer ) {
super( object, renderer, new GLSLNodeParser() );
/**
* A dictionary holds for each shader stage ('vertex', 'fragment', 'compute')
* another dictionary which manages UBOs per group ('render','frame','object').
*
* @type {Object<string,Object<string,NodeUniformsGroup>>}
*/
this.uniformGroups = {};
/**
* An array that holds objects defining the varying and attribute data in
* context of Transform Feedback.
*
* @type {Array<Object<string,AttributeNode|string>>}
*/
this.transforms = [];
/**
* A dictionary that holds for each shader stage a Map of used extensions.
*
* @type {Object<string,Map<string,Object>>}
*/
this.extensions = {};
/**
* A dictionary that holds for each shader stage an Array of used builtins.
*
* @type {Object<string,Array<string>>}
*/
this.builtins = { vertex: [], fragment: [], compute: [] };
}
/**
* Checks if the given texture requires a manual conversion to the working color space.
*
* @param {Texture} texture - The texture to check.
* @return {boolean} Whether the given texture requires a conversion to working color space or not.
*/
needsToWorkingColorSpace( texture ) {
return texture.isVideoTexture === true && texture.colorSpace !== NoColorSpace;
}
/**
* Includes the given method name into the current
* function node.
*
* @private
* @param {string} name - The method name to include.
* @return {CodeNode} The respective code node.
*/
_include( name ) {
const codeNode = glslPolyfills[ name ];
codeNode.build( this );
this.addInclude( codeNode );
return codeNode;
}
/**
* Returns the native shader method name for a given generic name.
*
* @param {string} method - The method name to resolve.
* @return {string} The resolved GLSL method name.
*/
getMethod( method ) {
if ( glslPolyfills[ method ] !== undefined ) {
this._include( method );
}
return glslMethods[ method ] || method;
}
/**
* Returns the bitcast method name for a given input and outputType.
*
* @param {string} type - The output type to bitcast to.
* @param {string} inputType - The input type of the.
* @return {string} The resolved GLSL bitcast invocation.
*/
getBitcastMethod( type, inputType ) {
const inputComponentType = this.getComponentType( inputType );
const componentType = this.getComponentType( type );
// integer types support bitcast implicitly via the type constructor
if ( inputComponentType !== 'float' && componentType !== 'float' &&
this.getTypeLength( inputType ) === this.getTypeLength( type ) ) {
return this.getType( type );
}
return this.getMethod( `bitcast_${ inputComponentType }_${ componentType }` );
}
/**
* Returns the float packing method name for a given numeric encoding.
*
* @param {string} encoding - The numeric encoding that describes how the float values are mapped to the integer range.
* @param {string} [layout='2x16'] - The component layout of the packed integer.
* @returns {string} The resolved GLSL float packing method name.
*/
getFloatPackingMethod( encoding, layout = '2x16' ) {
return this.getMethod( `floatpack_${ encoding }_${ layout }` );
}
/**
* Returns the float unpacking method name for a given numeric encoding.
*
* @param {string} encoding - The numeric encoding that describes how the integer values are mapped to the float range.
* @param {string} [layout='2x16'] - The component layout of the packed integer.
* @returns {string} The resolved GLSL float unpacking method name.
*/
getFloatUnpackingMethod( encoding, layout = '2x16' ) {
return this.getMethod( `floatunpack_${ encoding }_${ layout }` );
}
/**
* Returns the native snippet for a ternary operation.
*
* @param {string} condSnippet - The condition determining which expression gets resolved.
* @param {string} ifSnippet - The expression to resolve to if the condition is true.
* @param {string} elseSnippet - The expression to resolve to if the condition is false.
* @return {string} The resolved method name.
*/
getTernary( condSnippet, ifSnippet, elseSnippet ) {
return `${condSnippet} ? ${ifSnippet} : ${elseSnippet}`;
}
/**
* Returns the native snippet for a genuinely per-component vector select.
* GLSL has no vector ternary, so `float` types use `mix()`'s `bvecN`-selector
* overload; `int`/`uint`/`bool` types use an arithmetic select instead, since
* that overload doesn't exist for them.
*
* @param {string} condSnippet - The per-component boolean (`bvecN`) condition.
* @param {string} ifSnippet - The vector expression selected where `condSnippet` is `true`.
* @param {string} elseSnippet - The vector expression selected where `condSnippet` is `false`.
* @param {string} type - The (vector) type of `ifSnippet`/`elseSnippet`.
* @return {string} The resolved method name.
*/
getVectorSelect( condSnippet, ifSnippet, elseSnippet, type ) {
const componentType = this.getComponentType( type );
if ( componentType === 'float' ) {
return `mix( ${elseSnippet}, ${ifSnippet}, ${condSnippet} )`;
}
const glslType = this.getType( type );
if ( componentType === 'bool' ) {
const intType = this.getType( this.getTypeFromLength( this.getTypeLength( type ), 'int' ) );
const maskSnippet = `${intType}( ${condSnippet} )`;
const ifIntSnippet = `${intType}( ${ifSnippet} )`;
const elseIntSnippet = `${intType}( ${elseSnippet} )`;
return `${glslType}( ${elseIntSnippet} + ${maskSnippet} * ( ${ifIntSnippet} - ${elseIntSnippet} ) )`;
}
const maskSnippet = `${glslType}( ${condSnippet} )`;
return `${elseSnippet} + ${maskSnippet} * ( ${ifSnippet} - ${elseSnippet} )`;
}
/**
* Returns the output struct name. Not relevant for GLSL.
*
* @return {string}
*/
getOutputStructName() {
return '';
}
/**
* Builds the given shader node.
*
* @param {ShaderNodeInternal} shaderNode - The shader node.
* @return {string} The GLSL function code.
*/
buildFunctionCode( shaderNode ) {
const layout = shaderNode.layout;
const flowData = this.flowShaderNode( shaderNode );
const parameters = [];
for ( const input of layout.inputs ) {
parameters.push( this.getType( input.type ) + ' ' + input.name );
}
//
const code = `${ this.getType( layout.type ) } ${ layout.name }( ${ parameters.join( ', ' ) } ) {
${ flowData.vars }
${ flowData.code }
return ${ flowData.result };
}`;
//
return code;
}
/**
* Setups the Pixel Buffer Object (PBO) for the given storage
* buffer node.
*
* @param {StorageBufferNode} storageBufferNode - The storage buffer node.
*/
setupPBO( storageBufferNode ) {
const attribute = storageBufferNode.value;
if ( attribute.pbo === undefined ) {
const originalArray = attribute.array;
const numElements = attribute.count * attribute.itemSize;
const { itemSize } = attribute;
const isInteger = attribute.array.constructor.name.toLowerCase().includes( 'int' );
let format = isInteger ? RedIntegerFormat : RedFormat;
if ( itemSize === 2 ) {
format = isInteger ? RGIntegerFormat : RGFormat;
} else if ( itemSize === 3 ) {
format = isInteger ? RGBIntegerFormat : RGBFormat;
} else if ( itemSize === 4 ) {
format = isInteger ? RGBAIntegerFormat : RGBAFormat;
}
const typeMap = {
Float32Array: FloatType,
Uint8Array: UnsignedByteType,
Uint16Array: UnsignedShortType,
Uint32Array: UnsignedIntType,
Int8Array: ByteType,
Int16Array: ShortType,
Int32Array: IntType,
Uint8ClampedArray: UnsignedByteType,
};
const width = Math.pow( 2, Math.ceil( Math.log2( Math.sqrt( numElements / itemSize ) ) ) );
let height = Math.ceil( ( numElements / itemSize ) / width );
if ( width * height * itemSize < numElements ) height ++; // Ensure enough space
const newSize = width * height * itemSize;
const newArray = new originalArray.constructor( newSize );
newArray.set( originalArray, 0 );
attribute.array = newArray;
const pboTexture = new DataTexture( attribute.array, width, height, format, typeMap[ attribute.array.constructor.name ] || FloatType );
pboTexture.needsUpdate = true;
pboTexture.isPBOTexture = true;
const pbo = new TextureNode( pboTexture, null, null );
pbo.setPrecision( 'high' );
attribute.pboNode = pbo;
attribute.pbo = pbo.value;
this.getUniformFromNode( attribute.pboNode, 'texture', this.shaderStage, this.context.nodeName );
}
}
/**
* Returns a GLSL snippet that represents the property name of the given node.
*
* @param {Node} node - The node.
* @param {string} [shaderStage=this.shaderStage] - The shader stage this code snippet is generated for.
* @return {string} The property name.
*/
getPropertyName( node, shaderStage = this.shaderStage ) {
if ( node.isNodeUniform && node.node.isTextureNode !== true && node.node.isBufferNode !== true ) {
return node.name;
}
return super.getPropertyName( node, shaderStage );
}
/**
* Returns whether the given name is a reserved keyword of GLSL.
*
* @param {string} name - The name to test.
* @return {boolean} Whether the name is a reserved keyword or not.
*/
isReservedKeyword( name ) {
return glslReservedKeywords.has( name );
}
/**
* Setups the Pixel Buffer Object (PBO) for the given storage
* buffer node.
*
* @param {StorageArrayElementNode} storageArrayElementNode - The storage array element node.
* @return {string} The property name.
*/
generatePBO( storageArrayElementNode ) {
const { node, indexNode } = storageArrayElementNode;
const attribute = node.value;
if ( this.renderer.backend.has( attribute ) ) {
const attributeData = this.renderer.backend.get( attribute );
attributeData.pbo = attribute.pbo;
}
const nodeUniform = this.getUniformFromNode( attribute.pboNode, 'texture', this.shaderStage, this.context.nodeName );
const textureName = this.getPropertyName( nodeUniform );
this.increaseUsage( indexNode ); // force cache generate to be used as index in x,y
const indexSnippet = indexNode.build( this, 'uint' );
const elementNodeData = this.getDataFromNode( storageArrayElementNode );
let propertyName = elementNodeData.propertyName;
if ( propertyName === undefined ) {
// property element
const nodeVar = this.getVarFromNode( storageArrayElementNode );
propertyName = this.getPropertyName( nodeVar );
// property size
const bufferNodeData = this.getDataFromNode( node );
let propertySizeName = bufferNodeData.propertySizeName;
if ( propertySizeName === undefined ) {
propertySizeName = propertyName + 'Size';
this.getVarFromNode( node, propertySizeName, 'uint' );
this.addLineFlowCode( `${ propertySizeName } = uint( textureSize( ${ textureName }, 0 ).x )`, storageArrayElementNode );
bufferNodeData.propertySizeName = propertySizeName;
}
//
const { itemSize } = attribute;
const channel = '.' + vectorComponents.join( '' ).slice( 0, itemSize );
const uvSnippet = `ivec2(${indexSnippet} % ${ propertySizeName }, ${indexSnippet} / ${ propertySizeName })`;
const snippet = this.generateTextureLoad( null, textureName, uvSnippet, '0', null, null );
//
let prefix = 'vec4';
if ( attribute.pbo.type === UnsignedIntType ) {
prefix = 'uvec4';
} else if ( attribute.pbo.type === IntType ) {
prefix = 'ivec4';
}
const texelType = this.getTypeFromLength( itemSize, this.getComponentType( prefix ) );
const elementType = storageArrayElementNode.getNodeType( this );
this.addLineFlowCode( `${ propertyName } = ${ this.format( `${prefix}(${ snippet })${channel}`, texelType, elementType ) }`, storageArrayElementNode );
elementNodeData.propertyName = propertyName;
}
return propertyName;
}
/**
* Generates the GLSL snippet that reads a single texel from a texture without sampling or filtering.
*
* @param {?Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvIndexSnippet - A GLSL snippet that represents texture coordinates used for sampling.
* @param {?string} levelSnippet - A GLSL snippet that represents the mip level, with level 0 containing a full size version of the texture.
* @param {?string} depthSnippet - A GLSL snippet that represents the 0-based texture array index to sample.
* @param {?string} offsetSnippet - A GLSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @return {string} The GLSL snippet.
*/
generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) {
if ( levelSnippet === null ) levelSnippet = '0';
let snippet;
if ( depthSnippet ) {
if ( offsetSnippet ) {
snippet = `texelFetchOffset( ${ textureProperty }, ivec3( ${ uvIndexSnippet }, ${ depthSnippet } ), int( ${ levelSnippet } ), ${ offsetSnippet } )`;
} else {
snippet = `texelFetch( ${ textureProperty }, ivec3( ${ uvIndexSnippet }, ${ depthSnippet } ), int( ${ levelSnippet } ) )`;
}
} else {
if ( offsetSnippet ) {
snippet = `texelFetchOffset( ${ textureProperty }, ${ uvIndexSnippet }, int( ${ levelSnippet } ), ${ offsetSnippet } )`;
} else {
snippet = `texelFetch( ${ textureProperty }, ${ uvIndexSnippet }, int( ${ levelSnippet } ) )`;
}
}
if ( texture !== null && texture.isDepthTexture ) {
snippet += '.x';
}
return snippet;
}
/**
* Generates the GLSL snippet for sampling/loading the given texture.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A GLSL snippet that represents texture coordinates used for sampling.
* @param {?string} depthSnippet - A GLSL snippet that represents the 0-based texture array index to sample.
* @param {?string} offsetSnippet - A GLSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @return {string} The GLSL snippet.
*/
generateTexture( texture, textureProperty, uvSnippet, depthSnippet, offsetSnippet ) {
if ( depthSnippet ) uvSnippet = `vec3( ${ uvSnippet }, ${ depthSnippet } )`;
if ( texture.isDepthTexture ) {
if ( offsetSnippet ) return `textureOffset( ${ textureProperty }, ${ uvSnippet }, ${ offsetSnippet } ).x`;
return `texture( ${ textureProperty }, ${ uvSnippet } ).x`;
}
if ( offsetSnippet ) return `textureOffset( ${ textureProperty }, ${ uvSnippet }, ${ offsetSnippet } )`;
return `texture( ${ textureProperty }, ${ uvSnippet } )`;
}
/**
* Generates the GLSL snippet that resolves the dimensions of the given texture.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} levelSnippet - A GLSL snippet that represents the mip level.
* @return {string} The GLSL snippet.
*/
generateTextureSize( texture, textureProperty, levelSnippet ) {
return `textureSize( ${ textureProperty }, ${ levelSnippet } )`;
}
/**
* Generates the GLSL snippet when sampling textures with explicit mip level.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A GLSL snippet that represents texture coordinates used for sampling.
* @param {string} levelSnippet - A GLSL snippet that represents the mip level, with level 0 containing a full size version of the texture.
* @param {?string} depthSnippet - A GLSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A GLSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @return {string} The GLSL snippet.
*/
generateTextureLevel( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ) {
if ( depthSnippet ) uvSnippet = `vec3( ${ uvSnippet }, ${ depthSnippet } )`;
if ( texture.isDepthTexture ) {
if ( offsetSnippet ) {
return `textureLodOffset( ${ textureProperty }, ${ uvSnippet }, ${ levelSnippet }, ${ offsetSnippet } ).x`;
}
return `textureLod( ${ textureProperty }, ${ uvSnippet }, ${ levelSnippet } ).x`;
}
if ( offsetSnippet ) {
return `textureLodOffset( ${ textureProperty }, ${ uvSnippet }, ${ levelSnippet }, ${ offsetSnippet } )`;
}
return `textureLod( ${ textureProperty }, ${ uvSnippet }, ${ levelSnippet } )`;
}
/**
* Generates the GLSL snippet when sampling textures with a bias to the mip level.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A GLSL snippet that represents texture coordinates used for sampling.
* @param {string} biasSnippet - A GLSL snippet that represents the bias to apply to the mip level before sampling.
* @param {?string} depthSnippet - A GLSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A GLSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @return {string} The GLSL snippet.
*/
generateTextureBias( texture, textureProperty, uvSnippet, biasSnippet, depthSnippet, offsetSnippet ) {
if ( depthSnippet ) uvSnippet = `vec3( ${ uvSnippet }, ${ depthSnippet } )`;
if ( offsetSnippet ) {
return `textureOffset( ${ textureProperty }, ${ uvSnippet }, ${ offsetSnippet }, ${ biasSnippet } )`;
}
return `texture( ${ textureProperty }, ${ uvSnippet }, ${ biasSnippet } )`;
}
/**
* Generates the GLSL snippet for sampling/loading the given texture using explicit gradients.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A GLSL snippet that represents texture coordinates used for sampling.
* @param {Array<string>} gradSnippet - An array holding both gradient GLSL snippets.
* @param {?string} depthSnippet - A GLSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A GLSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @return {string} The GLSL snippet.
*/
generateTextureGrad( texture, textureProperty, uvSnippet, gradSnippet, depthSnippet, offsetSnippet ) {
if ( depthSnippet ) uvSnippet = `vec3( ${ uvSnippet }, ${ depthSnippet } )`;
if ( offsetSnippet ) {
return `textureGradOffset( ${ textureProperty }, ${ uvSnippet }, ${ gradSnippet[ 0 ] }, ${ gradSnippet[ 1 ] }, ${ offsetSnippet } )`;
}
return `textureGrad( ${ textureProperty }, ${ uvSnippet }, ${ gradSnippet[ 0 ] }, ${ gradSnippet[ 1 ] } )`;
}
/**
* Generates the GLSL snippet for sampling a depth texture and comparing the sampled depth values
* against a reference value.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A GLSL snippet that represents texture coordinates used for sampling.
* @param {string} compareSnippet - A GLSL snippet that represents the reference value.
* @param {?string} depthSnippet - A GLSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A GLSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @param {string} [shaderStage=this.shaderStage] - The shader stage this code snippet is generated for.
* @return {string} The GLSL snippet.
*/
generateTextureCompare( texture, textureProperty, uvSnippet, compareSnippet, depthSnippet, offsetSnippet, shaderStage = this.shaderStage ) {
if ( shaderStage === 'fragment' ) {
// Cube shadow maps use vec4(direction, compareValue)
if ( texture.isCubeTexture ) {
return `texture( ${ textureProperty }, vec4( ${ uvSnippet }, ${ compareSnippet } ) )`;
}
if ( depthSnippet ) {
if ( offsetSnippet ) {
return `textureOffset( ${ textureProperty }, vec4( ${ uvSnippet }, ${ depthSnippet }, ${ compareSnippet } ), ${ offsetSnippet } )`;
}
return `texture( ${ textureProperty }, vec4( ${ uvSnippet }, ${ depthSnippet }, ${ compareSnippet } ) )`;
}
if ( offsetSnippet ) {
return `textureOffset( ${ textureProperty }, vec3( ${ uvSnippet }, ${ compareSnippet } ), ${ offsetSnippet } )`;
}
return `texture( ${ textureProperty }, vec3( ${ uvSnippet }, ${ compareSnippet } ) )`;
} else {
error( `WebGPURenderer: THREE.DepthTexture.compareFunction() does not support ${ shaderStage } shader.` );
}
}
/**
* Generates the GLSL snippet for gathering four texels from the given texture.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A GLSL snippet that represents texture coordinates used for sampling.
* @param {string} gatherSnippet - A GLSL snippet that represents the index of the channel to read.
* @param {?string} depthSnippet - A GLSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A GLSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @param {?string} flipYSnippet - A GLSL snippet that represents the y-flip. Only used for WebGL.
* @return {string} The GLSL snippet.
*/
generateTextureGather( texture, textureProperty, uvSnippet, gatherSnippet, depthSnippet, offsetSnippet, flipYSnippet ) {
if ( texture.isDepthTexture ) gatherSnippet = '0';
if ( offsetSnippet === null ) offsetSnippet = 'ivec2( 0 )';
if ( flipYSnippet === null ) flipYSnippet = 'false';
if ( depthSnippet ) {
this._include( 'textureGatherArray' );
return `tsl_textureGather_array( ${gatherSnippet}, ${ textureProperty }, vec3( ${ uvSnippet }, ${ depthSnippet } ), ${ offsetSnippet }, ${ flipYSnippet } )`;
}
this._include( 'textureGather' );
return `tsl_textureGather( ${gatherSnippet}, ${ textureProperty }, ${ uvSnippet }, ${ offsetSnippet }, ${ flipYSnippet } )`;
}
/**
* Generates the GLSL snippet for performing a depth comparison on four texels in the given depth texture.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A GLSL snippet that represents texture coordinates used for sampling.
* @param {string} compareSnippet - A GLSL snippet that represents the reference value.
* @param {?string} depthSnippet - A GLSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A GLSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @param {?string} flipYSnippet - A GLSL snippet that represents the y-flip. Only used for WebGL.
* @return {string} The GLSL snippet.
*/
generateTextureGatherCompare( texture, textureProperty, uvSnippet, compareSnippet, depthSnippet, offsetSnippet, flipYSnippet ) {
if ( offsetSnippet === null ) offsetSnippet = 'ivec2( 0 )';
if ( flipYSnippet === null ) flipYSnippet = 'false';
if ( depthSnippet ) {
this._include( 'textureGatherCompareArray' );
return `tsl_textureGatherCompare_array( ${ textureProperty }, vec3( ${ uvSnippet }, ${depthSnippet} ), ${ offsetSnippet }, ${ compareSnippet }, ${ flipYSnippet } )`;
}
this._include( 'textureGatherCompare' );
return `tsl_textureGatherCompare( ${ textureProperty }, ${ uvSnippet }, ${ offsetSnippet }, ${ compareSnippet }, ${ flipYSnippet } )`;
}
/**
* Returns the uniform block name of the given buffer uniform.
*
* @param {NodeUniform} uniform - The buffer uniform.
* @param {string} shaderStage - The shader stage.
* @return {string} The uniform block name.
*/
getUniformBufferName( uniform, shaderStage ) {
return this.getDataFromNode( uniform.node, shaderStage, this.globalCache ).uniformGPU.name;
}
/**
* Returns a bind group for the given group name and bindings. Uniform blocks are
* named after their position in the group since shared bind groups are created
* by one build but looked up by name in the programs of every build sharing them.
*
* @private
* @param {string} groupName - The group name.
* @param {Array<NodeUniformsGroup>} bindings - List of bindings.
* @return {BindGroup} The bind group
*/
_getBindGroup( groupName, bindings ) {
for ( let i = 0; i < bindings.length; i ++ ) {
if ( bindings[ i ].isNodeUniformBuffer === true ) bindings[ i ].name = 'NodeBuffer_' + groupName + '_' + i;
}
return super._getBindGroup( groupName, bindings );
}
/**
* Returns the uniforms of the given shader stage as a GLSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} The GLSL snippet that defines the uniforms.
*/
getUniforms( shaderStage ) {
const uniforms = this.uniforms[ shaderStage ];
const bindingSnippets = [];
const uniformGroups = {};
for ( const uniform of uniforms ) {
let snippet = null;
let group = false;
if ( uniform.type === 'texture' || uniform.type === 'texture3D' ) {
const textureNode = uniform.node;
const texture = textureNode.value;
const componentType = this.getComponentTypeFromTexture( texture );
const typePrefix = componentType === 'float' ? '' : componentType.charAt( 0 );
if ( uniform.type === 'texture3D' && texture.isArrayTexture === false ) {
snippet = `${typePrefix}sampler3D ${ uniform.name };`;
} else if ( texture.compareFunction && textureNode.isSampleCompare() ) {
if ( texture.isArrayTexture === true ) {
snippet = `sampler2DArrayShadow ${ uniform.name };`;
} else {
snippet = `sampler2DShadow ${ uniform.name };`;
}
} else if ( texture.isArrayTexture === true || texture.isDataArrayTexture === true || texture.isCompressedArrayTexture === true ) {
snippet = `${typePrefix}sampler2DArray ${ uniform.name };`;
} else {
snippet = `${typePrefix}sampler2D ${ uniform.name };`;
}
} else if ( uniform.type === 'cubeTexture' ) {
snippet = `samplerCube ${ uniform.name };`;
} else if ( uniform.type === 'cubeDepthTexture' ) {
const texture = uniform.node.value;
if ( texture.compareFunction ) {
snippet = `samplerCubeShadow ${ uniform.name };`;
} else {
snippet = `samplerCube ${ uniform.name };`;
}
} else if ( uniform.type === 'buffer' ) {
const bufferNode = uniform.node;
const bufferType = this.getType( bufferNode.bufferType );
const bufferCount = bufferNode.bufferCount;
const bufferCountSnippet = bufferCount > 0 ? bufferCount : '';
snippet = `${ this.getUniformBufferName( uniform, shaderStage ) } {\n\t${ bufferType } ${ uniform.name }[${ bufferCountSnippet }];\n};\n`;
} else {
const groupName = uniform.groupNode.name;
// Check if this group has already been processed
if ( uniformGroups[ groupName ] === undefined ) {
// Get the shared uniform group that contains uniforms from all stages
const sharedUniformGroup = this.uniformGroups[ groupName ];
if ( sharedUniformGroup !== undefined ) {
// Generate snippets for ALL uniforms in this shared group
const snippets = [];
for ( const sharedUniform of sharedUniformGroup.uniforms ) {
const type = sharedUniform.getType();
const vectorType = this.getVectorType( type );
const precision = sharedUniform.nodeUniform.node.precision;
let uniformSnippet = `${ vectorType } ${ sharedUniform.name };`;
if ( precision !== null ) {
uniformSnippet = precisionLib[ precision ] + ' ' + uniformSnippet;
}
snippets.push( '\t' + uniformSnippet );
}
uniformGroups[ groupName ] = snippets;
}
}
group = true;
}
if ( ! group ) {
const precision = uniform.node.precision;
if ( precision !== null ) {
snippet = precisionLib[ precision ] + ' ' + snippet;
}
snippet = 'uniform ' + snippet;
bindingSnippets.push( snippet );
}
}
let output = '';
for ( const name in uniformGroups ) {
const groupSnippets = uniformGroups[ name ];
output += this._getGLSLUniformStruct( name, groupSnippets.join( '\n' ) ) + '\n';
}
output += bindingSnippets.join( '\n' );
return output;
}
/**
* Returns the type for a given buffer attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
* @return {string} The type.
*/
getTypeFromAttribute( attribute ) {
let nodeType = super.getTypeFromAttribute( attribute );
if ( /^[iu]/.test( nodeType ) && attribute.gpuType !== IntType ) {
let dataAttribute = attribute;
if ( attribute.isInterleavedBufferAttribute ) dataAttribute = attribute.data;
const array = dataAttribute.array;
if ( ( array instanceof Uint32Array || array instanceof Int32Array ) === false ) {
nodeType = nodeType.slice( 1 );
}
}
return nodeType;
}
/**
* Returns the shader attributes of the given shader stage as a GLSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} The GLSL snippet that defines the shader attributes.
*/
getAttributes( shaderStage ) {
let snippet = '';
if ( shaderStage === 'vertex' || shaderStage === 'compute' ) {
const attributes = this.getAttributesArray();
let location = 0;
for ( const attribute of attributes ) {
snippet += `layout( location = ${ location ++ } ) in ${ attribute.type } ${ attribute.name };\n`;
}
}
return snippet;
}
/**
* Returns the members of the given struct type node as a GLSL string.
*
* @param {StructTypeNode} struct - The struct type node.
* @return {string} The GLSL snippet that defines the struct members.
*/
getStructMembers( struct ) {
const snippets = [];
for ( const member of struct.members ) {
snippets.push( `\t${ member.type } ${ member.name };` );
}
return snippets.join( '\n' );
}
/**
* Returns the structs of the given shader stage as a GLSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} The GLSL snippet that defines the structs.
*/
getStructs( shaderStage ) {
const snippets = [];
const structs = this.structs[ shaderStage ];
const outputSnippet = [];
for ( const struct of structs ) {
if ( struct.output ) {
for ( const member of struct.members ) {
outputSnippet.push( `layout( location = ${ member.index } ) out ${ member.type } ${ member.name };` );
}
} else {
let snippet = 'struct ' + struct.name + ' {\n';
snippet += this.getStructMembers( struct );
snippet += '\n};\n';
snippets.push( snippet );
}
}
if ( shaderStage === 'fragment' && outputSnippet.length === 0 ) {
outputSnippet.push( `layout( location = 0 ) out ${ this.getOutputType() } fragColor;` );
}
return '\n' + outputSnippet.join( '\n' ) + '\n\n' + snippets.join( '\n' );
}
/**
* Returns a single const variable statement as a GLSL string for the given variable type and name.
*
* @param {string} type - The variable's type.
* @param {string} name - The variable's name.
* @param {?number} [count=null] - The array length.
* @return {string} The GLSL snippet that defines a const variable.
*/
generateConstStatement( type, name, count = null ) {
return `const ${ this.getVar( type, name, count ) }`;
}
/**
* Returns a single variable statement as a GLSL string for the given variable type and name.
*
* @param {string} type - The variable's type.
* @param {string} name - The variable's name.
* @param {?number} [count=null] - The array length.
* @return {string} The GLSL snippet that defines a variable.
*/
generateVarStatement( type, name, count = null ) {
return this.getVar( type, name, count );
}
/**
* Returns the varyings of the given shader stage as a GLSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} The GLSL snippet that defines the varyings.
*/
getVaryings( shaderStage ) {
let snippet = shaderStage === 'vertex' && this.context.invariant === true ? 'invariant gl_Position;\n' : '';
const varyings = this.varyings;
if ( shaderStage === 'vertex' || shaderStage === 'compute' ) {
for ( const varying of varyings ) {
if ( shaderStage === 'compute' ) varying.needsInterpolation = true;
const type = this.getType( varying.type );
if ( varying.needsInterpolation ) {
if ( varying.interpolationType ) {
const interpolationType = interpolationTypeMap[ varying.interpolationType ] || varying.interpolationType;
const sampling = interpolationModeMap[ varying.interpolationSampling ] || '';
snippet += `${ interpolationType } ${ sampling } out ${ type } ${ varying.name };\n`;
} else {
const flat = type.includes( 'int' ) || type.includes( 'uv' ) || type.includes( 'iv' ) ? 'flat ' : '';
snippet += `${ flat }out ${ type } ${ varying.name };\n`;
}
} else {
snippet += `${type} ${varying.name};\n`; // generate variable (no varying required)
}
}
} else if ( shaderStage === 'fragment' ) {
for ( const varying of varyings ) {
if ( varying.needsInterpolation ) {
const type = this.getType( varying.type );
if ( varying.interpolationType ) {
const interpolationType = interpolationTypeMap[ varying.interpolationType ] || varying.interpolationType;
const sampling = interpolationModeMap[ varying.interpolationSampling ] || '';
snippet += `${ interpolationType } ${ sampling } in ${ type } ${ varying.name };\n`;
} else {
const flat = type.includes( 'int' ) || type.includes( 'uv' ) || type.includes( 'iv' ) ? 'flat ' : '';
snippet += `${ flat }in ${ type } ${ varying.name };\n`;
}
}
}
}
for ( const builtin of this.builtins[ shaderStage ] ) {
snippet += `${builtin};\n`;
}
return snippet;
}
/**
* Returns the vertex index builtin.
*
* @return {string} The vertex index.
*/
getVertexIndex() {
return 'uint( gl_VertexID )';
}
/**
* Contextually returns either the vertex stage instance index builtin
* or the linearized index of an compute invocation within a grid of workgroups.
*
* @return {string} The instance index.
*/
getInstanceIndex() {
return 'uint( gl_InstanceID )';
}
/**
* Returns a builtin representing the index of an invocation within its workgroup.
*
* @return {string} The invocation local index.
*/
getInvocationLocalIndex() {
const workgroupSize = this.compute.workgroupSize;
const size = workgroupSize.reduce( ( acc, curr ) => acc * curr, 1 );
return `uint( gl_InstanceID ) % ${size}u`;
}
/**
* Returns a builtin representing the size of a subgroup within the current shader.
*/
getSubgroupSize() {
error( 'GLSLNodeBuilder: WebGLBackend does not support the subgroupSize node' );
}
/**
* Returns a builtin representing the index of an invocation within its subgroup.
*/
getInvocationSubgroupIndex() {
error( 'GLSLNodeBuilder: WebGLBackend does not support the invocationSubgroupIndex node' );
}
/**
* Returns a builtin representing the index of the current invocation's subgroup within its workgroup.
*/
getSubgroupIndex() {
error( 'GLSLNodeBuilder: WebGLBackend does not support the subgroupIndex node' );
}
/**
* Enables subgroups.
*/
enableSubGroups() {
error( 'GLSLNodeBuilder: WebGLBackend does not support subgroup operations' );
}
/**
* Enables 16 bit floats.
*/
enableShaderF16() {
error( 'GLSLNodeBuilder: WebGLBackend does not support 16 bit floats' );
}
/**
* Enables dual source blending.
*
* WebGL 2 can express this through the `WEBGL_blend_func_extended` extension,
* but WebGLBackend does not implement it.
*/
enableDualSourceBlending() {
error( 'GLSLNodeBuilder: WebGLBackend does not support dual source blending' );
}
/**
* Returns the draw index builtin.
*
* @return {?string} The drawIndex shader string. Returns `null` if `WEBGL_multi_draw` isn't supported by the device.
*/
getDrawIndex() {
const extensions = this.renderer.backend.extensions;
if ( extensions.has( 'WEBGL_multi_draw' ) ) {
return 'uint( gl_DrawID )';
} else {
return 'nodeUniformDrawId'; // fallback to uniform
}
}
/**
* Returns the front facing builtin.
*
* @return {string} The front facing builtin.
*/
getFrontFacing() {
return 'gl_FrontFacing';
}
/**
* Returns the frag coord builtin.
*
* @return {string} The frag coord builtin.
*/
getFragCoord() {
return 'gl_FragCoord.xy';
}
/**
* Returns the frag depth builtin.
*
* @return {string} The frag depth builtin.
*/
getFragDepth() {
return 'gl_FragDepth';
}
/**
* Enables the given extension.
*
* @param {string} name - The extension name.
* @param {string} behavior - The extension behavior.
* @param {string} [shaderStage=this.shaderStage] - The shader stage.
*/
enableExtension( name, behavior, shaderStage = this.shaderStage ) {
const map = this.extensions[ shaderStage ] || ( this.extensions[ shaderStage ] = new Map() );
if ( map.has( name ) === false ) {
map.set( name, {
name,
behavior
} );
}
}
/**
* Returns the enabled extensions of the given shader stage as a GLSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} The GLSL snippet that defines the enabled extensions.
*/
getExtensions( shaderStage ) {
const snippets = [];
if ( shaderStage === 'vertex' ) {
const ext = this.renderer.backend.extensions;
const isBatchedMesh = this.object.isBatchedMesh;
if ( isBatchedMesh && ext.has( 'WEBGL_multi_draw' ) ) {
this.enableExtension( 'GL_ANGLE_multi_draw', 'require', shaderStage );
}
}
const extensions = this.extensions[ shaderStage ];
if ( extensions !== undefined ) {
for ( const { name, behavior } of extensions.values() ) {
snippets.push( `#extension ${name} : ${behavior}` );
}
}
return snippets.join( '\n' );
}
/**
* Returns the clip distances builtin.
*
* @return {string} The clip distances builtin.
*/
getClipDistance() {
return 'gl_ClipDistance';
}
/**
* Whether the requested feature is available or not.
*
* @param {string} name - The requested feature.
* @return {boolean} Whether the requested feature is supported or not.
*/
isAvailable( name ) {
let result = supports$1[ name ];
if ( result === undefined ) {
let extensionName;
result = false;
switch ( name ) {
case 'float32Filterable':
extensionName = 'OES_texture_float_linear';
break;
case 'clipDistance':
extensionName = 'WEBGL_clip_cull_distance';
break;
}
if ( extensionName !== undefined ) {
const extensions = this.renderer.backend.extensions;
if ( extensions.has( extensionName ) ) {
extensions.get( extensionName );
result = true;
}
}
supports$1[ name ] = result;
}
return result;
}
/**
* Whether to flip texture data along its vertical axis or not.
*
* @return {boolean} Returns always `true` in context of GLSL.
*/
isFlipY() {
return true;
}
/**
* Enables hardware clipping.
*
* @param {string} planeCount - The clipping plane count.
*/
enableHardwareClipping( planeCount ) {
this.enableExtension( 'GL_ANGLE_clip_cull_distance', 'require' );
this.builtins[ 'vertex' ].push( `out float gl_ClipDistance[ ${ planeCount } ]` );
}
/**
* Enables multiview.
*/
enableMultiview() {
this.enableExtension( 'GL_OVR_multiview2', 'require', 'fragment' );
this.enableExtension( 'GL_OVR_multiview2', 'require', 'vertex' );
this.builtins[ 'vertex' ].push( 'layout(num_views = 2) in' );
}
/**
* Registers a transform in context of Transform Feedback.
*
* @param {string} varyingName - The varying name.
* @param {AttributeNode} attributeNode - The attribute node.
*/
registerTransform( varyingName, attributeNode ) {
this.transforms.push( { varyingName, attributeNode } );
}
/**
* Returns the transforms of the given shader stage as a GLSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} The GLSL snippet that defines the transforms.
*/
getTransforms( /* shaderStage */ ) {
const transforms = this.transforms;
let snippet = '';
for ( let i = 0; i < transforms.length; i ++ ) {
const transform = transforms[ i ];
const attributeName = this.getPropertyName( transform.attributeNode );
if ( attributeName ) snippet += `${ transform.varyingName } = ${ attributeName };\n\t`;
}
return snippet;
}
/**
* Returns a GLSL struct based on the given name and variables.
*
* @private
* @param {string} name - The struct name.
* @param {string} vars - The struct variables.
* @return {string} The GLSL snippet representing a struct.
*/
_getGLSLUniformStruct( name, vars ) {
return `
layout( std140 ) uniform ${name} {
${vars}
};`;
}
/**
* Returns a GLSL vertex shader based on the given shader data.
*
* @private
* @param {Object} shaderData - The shader data.
* @return {string} The vertex shader.
*/
_getGLSLVertexCode( shaderData ) {
return `#version 300 es
${ this.getSignature() }
// extensions
${shaderData.extensions}
// precision
${ defaultPrecisions }
// structs
${shaderData.structs}
// uniforms
${shaderData.uniforms}
// varyings
${shaderData.varyings}
// attributes
${shaderData.attributes}
// vars
${shaderData.vars}
// codes
${shaderData.codes}
void main() {
// transforms
${shaderData.transforms}
// flow
${shaderData.flow}
gl_PointSize = 1.0;
}
`;
}
/**
* Returns a GLSL fragment shader based on the given shader data.
*
* @private
* @param {Object} shaderData - The shader data.
* @return {string} The vertex shader.
*/
_getGLSLFragmentCode( shaderData ) {
return `#version 300 es
${ this.getSignature() }
// extensions
${shaderData.extensions}
// precision
${ defaultPrecisions }
// structs
${shaderData.structs}
// uniforms
${shaderData.uniforms}
// varyings
${shaderData.varyings}
// vars
${shaderData.vars}
// codes
${shaderData.codes}
void main() {
// flow
${shaderData.flow}
}
`;
}
/**
* Controls the code build of the shader stages.
*/
buildCode() {
const shadersData = this.material !== null ? { fragment: {}, vertex: {} } : { compute: {} };
this.sortBindingGroups();
for ( const shaderStage in shadersData ) {
let flow = '// code\n\n';
flow += this.flowCode[ shaderStage ];
const flowNodes = this.flowNodes[ shaderStage ];
const mainNode = flowNodes[ flowNodes.length - 1 ];
for ( const node of flowNodes ) {
const flowSlotData = this.getFlowData( node/*, shaderStage*/ );
const slotName = node.name;
if ( slotName ) {
if ( flow.length > 0 ) flow += '\n';
flow += `\t// flow -> ${ slotName }\n\t`;
}
flow += `${ flowSlotData.code }\n\t`;
if ( node === mainNode && shaderStage !== 'compute' ) {
flow += '// result\n\t';
if ( shaderStage === 'vertex' ) {
flow += 'gl_Position = ';
flow += `${ this.format( flowSlotData.result, mainNode.getNodeType( this ), 'vec4' ) };`;
} else if ( shaderStage === 'fragment' ) {
if ( ! node.outputNode.isOutputStructNode ) {
flow += 'fragColor = ';
flow += `${ this.format( flowSlotData.result, mainNode.getNodeType( this ), this.getOutputType() ) };`;
}
}
}
}
const stageData = shadersData[ shaderStage ];
stageData.extensions = this.getExtensions( shaderStage );
stageData.uniforms = this.getUniforms( shaderStage );
stageData.attributes = this.getAttributes( shaderStage );
stageData.varyings = this.getVaryings( shaderStage );
stageData.vars = this.getVars( shaderStage, true );
stageData.structs = this.getStructs( shaderStage );
stageData.codes = this.getCodes( shaderStage );
stageData.transforms = this.getTransforms( shaderStage );
stageData.flow = flow;
// fallbacks
if ( shaderStage === 'vertex' ) {
const ext = this.renderer.backend.extensions;
if ( this.object.isBatchedMesh && ext.has( 'WEBGL_multi_draw' ) === false ) {
stageData.uniforms += '\nuniform uint nodeUniformDrawId;\n';
}
}
}
if ( this.material !== null ) {
this.vertexShader = this._getGLSLVertexCode( shadersData.vertex );
this.fragmentShader = this._getGLSLFragmentCode( shadersData.fragment );
} else {
this.computeShader = this._getGLSLVertexCode( shadersData.compute );
}
}
/**
* This method is one of the more important ones since it's responsible
* for generating a matching binding instance for the given uniform node.
*
* These bindings are later used in the renderer to create bind groups
* and layouts.
*
* @param {UniformNode} node - The uniform node.
* @param {string} type - The node data type.
* @param {string} shaderStage - The shader stage.
* @param {?string} [name=null] - An optional uniform name.
* @return {NodeUniform} The node uniform object.
*/
getUniformFromNode( node, type, shaderStage, name = null ) {
const uniformNode = super.getUniformFromNode( node, type, shaderStage, type === 'buffer' ? null : name );
const nodeData = this.getDataFromNode( node, shaderStage, this.globalCache );
let uniformGPU = nodeData.uniformGPU;
if ( uniformGPU === undefined ) {
const group = node.groupNode;
const groupName = group.name;
const bindings = this.getBindGroupArray( groupName, shaderStage );
if ( type === 'texture' ) {
uniformGPU = new NodeSampledTexture( uniformNode.name, uniformNode.node, group );
bindings.push( uniformGPU );
} else if ( type === 'cubeTexture' || type === 'cubeDepthTexture' ) {
uniformGPU = new NodeSampledCubeTexture( uniformNode.name, uniformNode.node, group );
bindings.push( uniformGPU );
} else if ( type === 'texture3D' ) {
uniformGPU = new NodeSampledTexture3D( uniformNode.name, uniformNode.node, group );
bindings.push( uniformGPU );
} else if ( type === 'buffer' ) {
const buffer = new NodeUniformBuffer( node, group );
bindings.push( buffer );
uniformGPU = buffer;
} else {
let uniformsGroup = this.uniformGroups[ groupName ];
if ( uniformsGroup === undefined ) {
uniformsGroup = new NodeUniformsGroup( groupName, group );
this.uniformGroups[ groupName ] = uniformsGroup;
bindings.push( uniformsGroup );
} else {
// Add to bindings for this stage if not already present
if ( bindings.indexOf( uniformsGroup ) === -1 ) {
bindings.push( uniformsGroup );
}
}
uniformGPU = this.getNodeUniform( uniformNode, type );
// Only add uniform if not already present in the group (check by name to avoid duplicates across stages)
const uniformName = uniformGPU.name;
const alreadyExists = uniformsGroup.uniforms.some( u => u.name === uniformName );
if ( ! alreadyExists ) {
uniformsGroup.addUniform( uniformGPU );
}
}
nodeData.uniformGPU = uniformGPU;
}
return uniformNode;
}
}
let _vector2 = null;
let _color4 = null;
/**
* Most of the rendering related logic is implemented in the
* {@link Renderer} module and related management components.
* Sometimes it is required though to execute commands which are
* specific to the current 3D backend (which is WebGPU or WebGL 2).
* This abstract base class defines an interface that encapsulates
* all backend-related logic. Derived classes for each backend must
* implement the interface.
*
* @abstract
*/
class Backend {
/**
* Constructs a new backend.
*
* @param {Object} parameters - An object holding parameters for the backend.
*/
constructor( parameters = {} ) {
/**
* The parameters of the backend.
*
* @type {Object}
*/
this.parameters = Object.assign( {}, parameters );
/**
* This weak map holds backend-specific data of objects
* like textures, attributes or render targets.
*
* @type {WeakMap<Object, Object>}
*/
this.data = new WeakMap();
/**
* A reference to the renderer.
*
* @type {?Renderer}
* @default null
*/
this.renderer = null;
/**
* A reference to the canvas element the renderer is drawing to.
*
* @type {?(HTMLCanvasElement|OffscreenCanvas)}
* @default null
*/
this.domElement = null;
/**
* A reference to the timestamp query pool.
*
* @type {{render: ?TimestampQueryPool, compute: ?TimestampQueryPool}}
*/
this.timestampQueryPool = {
[ TimestampQuery.RENDER ]: null,
[ TimestampQuery.COMPUTE ]: null
};
/**
* Whether to track timestamps with a Timestamp Query API or not.
*
* @type {boolean}
* @default false
*/
this.trackTimestamp = ( parameters.trackTimestamp === true );
}
/**
* Initializes the backend so it is ready for usage. Concrete backends
* are supposed to implement their rendering context creation and related
* operations in this method.
*
* @async
* @param {Renderer} renderer - The renderer.
* @return {Promise} A Promise that resolves when the backend has been initialized.
*/
async init( renderer ) {
this.renderer = renderer;
}
/**
* The coordinate system of the backend.
*
* @abstract
* @type {number}
* @readonly
*/
get coordinateSystem() {}
// render context
/**
* This method is executed at the beginning of a render call and
* can be used by the backend to prepare the state for upcoming
* draw calls.
*
* @abstract
* @param {RenderContext} renderContext - The render context.
*/
beginRender( /*renderContext*/ ) {}
/**
* This method is executed at the end of a render call and
* can be used by the backend to finalize work after draw
* calls.
*
* @abstract
* @param {RenderContext} renderContext - The render context.
*/
finishRender( /*renderContext*/ ) {}
/**
* Sets the XR rendering destination.
*
* Backends that render directly into XR framebuffers can override this hook.
*
* @param {?Object} xrTarget - The XR rendering destination.
*/
setXRTarget( /*xrTarget*/ ) {}
/**
* This method is executed at the beginning of a compute call and
* can be used by the backend to prepare the state for upcoming
* compute tasks.
*
* @abstract
* @param {Node|Array<Node>} computeGroup - The compute node(s).
*/
beginCompute( /*computeGroup*/ ) {}
/**
* This method is executed at the end of a compute call and
* can be used by the backend to finalize work after compute
* tasks.
*
* @abstract
* @param {Node|Array<Node>} computeGroup - The compute node(s).
*/
finishCompute( /*computeGroup*/ ) {}
// render object
/**
* Executes a draw command for the given render object.
*
* @abstract
* @param {RenderObject} renderObject - The render object to draw.
* @param {Info} info - Holds a series of statistical information about the GPU memory and the rendering process.
*/
draw( /*renderObject, info*/ ) { }
/**
* Deletes the vertex state for the given render object.
*
* @abstract
* @param {RenderObject} renderObject - The render object.
*/
deleteVertexState( /*renderObject*/ ) { }
// compute node
/**
* Executes a compute command for the given compute node.
*
* @abstract
* @param {Node|Array<Node>} computeGroup - The group of compute nodes of a compute call. Can be a single compute node.
* @param {Node} computeNode - The compute node.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {ComputePipeline} computePipeline - The compute pipeline.
*/
compute( /*computeGroup, computeNode, computeBindings, computePipeline*/ ) { }
// program
/**
* Creates a shader program from the given programmable stage.
*
* @abstract
* @param {ProgrammableStage} program - The programmable stage.
*/
createProgram( /*program*/ ) { }
/**
* Destroys the shader program of the given programmable stage.
*
* @abstract
* @param {ProgrammableStage} program - The programmable stage.
*/
destroyProgram( /*program*/ ) { }
// bindings
/**
* Creates bindings from the given bind group definition.
*
* @abstract
* @param {BindGroup} bindGroup - The bind group.
* @param {Array<BindGroup>} bindings - Array of bind groups.
* @param {string} cacheKey - The cache key.
* @param {number} version - The version.
*/
createBindings( /*bindGroup, bindings, cacheKey, version*/ ) { }
/**
* Updates the given bind group definition.
*
* @abstract
* @param {BindGroup} bindGroup - The bind group.
* @param {Array<BindGroup>} bindings - Array of bind groups.
* @param {string} cacheKey - The cache key.
* @param {number} version - The version.
*/
updateBindings( /*bindGroup, bindings, cacheKey, version*/ ) { }
/**
* Updates a buffer binding.
*
* @abstract
* @param {Buffer} binding - The buffer binding to update.
*/
updateBinding( /*binding*/ ) { }
// pipeline
/**
* Creates a render pipeline for the given render object.
*
* @abstract
* @param {RenderObject} renderObject - The render object.
* @param {Array<Promise>} promises - An array of compilation promises which are used in `compileAsync()`.
*/
createRenderPipeline( /*renderObject, promises*/ ) { }
/**
* Creates a compute pipeline for the given compute node.
*
* @abstract
* @param {ComputePipeline} computePipeline - The compute pipeline.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {?Array<Promise>} [promises=null] - Optional compilation promises.
*/
createComputePipeline( /*computePipeline, bindings, promises*/ ) { }
/**
* Destroys the given pipeline.
*
* @abstract
* @param {Pipeline} pipeline - The pipeline.
*/
destroyPipeline( /*pipeline*/ ) { }
// cache key
/**
* Returns `true` if the render pipeline requires an update.
*
* @abstract
* @param {RenderObject} renderObject - The render object.
* @return {boolean} Whether the render pipeline requires an update or not.
*/
needsRenderUpdate( /*renderObject*/ ) { }
/**
* Returns a cache key that is used to identify render pipelines.
*
* @abstract
* @param {RenderObject} renderObject - The render object.
* @return {string} The cache key.
*/
getRenderCacheKey( /*renderObject*/ ) { }
// node builder
/**
* Returns a node builder for the given render object.
*
* @abstract
* @param {RenderObject} renderObject - The render object.
* @param {Renderer} renderer - The renderer.
* @return {NodeBuilder} The node builder.
*/
createNodeBuilder( /*renderObject, renderer*/ ) { }
// textures
/**
* Updates a GPU sampler for the given texture.
*
* @abstract
* @param {Sampler} binding - The sampler binding to update.
* @return {string} The current sampler key.
*/
updateSampler( /*binding*/ ) { }
/**
* Frees the GPU sampler for the given sampler binding.
*
* @abstract
* @param {Sampler} binding - The sampler binding to free.
*/
destroySampler( /*binding*/ ) { }
/**
* Creates a default texture for the given texture that can be used
* as a placeholder until the actual texture is ready for usage.
*
* @abstract
* @param {Texture} texture - The texture to create a default texture for.
*/
createDefaultTexture( /*texture*/ ) { }
/**
* Defines a texture on the GPU for the given texture object.
*
* @abstract
* @param {Texture} texture - The texture.
* @param {Object} [options={}] - Optional configuration parameter.
*/
createTexture( /*texture, options={}*/ ) { }
/**
* Uploads the updated texture data to the GPU.
*
* @abstract
* @param {Texture} texture - The texture.
* @param {Object} [options={}] - Optional configuration parameter.
*/
updateTexture( /*texture, options = {}*/ ) { }
/**
* Generates mipmaps for the given texture.
*
* @abstract
* @param {Texture} texture - The texture.
*/
generateMipmaps( /*texture*/ ) { }
/**
* Destroys the GPU data for the given texture object.
*
* @abstract
* @param {Texture} texture - The texture.
* @param {boolean} [isDefaultTexture=false] - Whether the texture uses a default GPU texture or not.
*/
destroyTexture( /*texture, isDefaultTexture*/ ) { }
/**
* Returns texture data as a typed array.
*
* @abstract
* @async
* @param {Texture} texture - The texture to copy.
* @param {number} x - The x coordinate of the copy origin.
* @param {number} y - The y coordinate of the copy origin.
* @param {number} width - The width of the copy.
* @param {number} height - The height of the copy.
* @param {number} faceIndex - The cube face, depth slice or array layer index.
* @return {Promise<TypedArray>} A Promise that resolves with a typed array when the copy operation has finished.
*/
async copyTextureToBuffer( /*texture, x, y, width, height, faceIndex*/ ) {}
/**
* Copies data of the given source texture to the given destination texture.
*
* @abstract
* @param {Texture} srcTexture - The source texture.
* @param {Texture} dstTexture - The destination texture.
* @param {?(Box3|Box2)} [srcRegion=null] - The region of the source texture to copy.
* @param {?(Vector2|Vector3)} [dstPosition=null] - The destination position of the copy.
* @param {number} [srcLevel=0] - The source mip level to copy from.
* @param {number} [dstLevel=0] - The destination mip level to copy to.
*/
copyTextureToTexture( /*srcTexture, dstTexture, srcRegion = null, dstPosition = null, srcLevel = 0, dstLevel = 0*/ ) {}
/**
* Copies the current bound framebuffer to the given texture.
*
* @abstract
* @param {Texture} texture - The destination texture.
* @param {RenderContext} renderContext - The render context.
* @param {Vector4} rectangle - A four dimensional vector defining the origin and dimension of the copy.
*/
copyFramebufferToTexture( /*texture, renderContext, rectangle*/ ) {}
// attributes
/**
* Creates the GPU buffer of a shader attribute.
*
* @abstract
* @param {BufferAttribute} attribute - The buffer attribute.
*/
createAttribute( /*attribute*/ ) { }
/**
* Creates the GPU buffer of an indexed shader attribute.
*
* @abstract
* @param {BufferAttribute} attribute - The indexed buffer attribute.
*/
createIndexAttribute( /*attribute*/ ) { }
/**
* Creates the GPU buffer of a storage attribute.
*
* @abstract
* @param {BufferAttribute} attribute - The buffer attribute.
*/
createStorageAttribute( /*attribute*/ ) { }
/**
* Creates the GPU buffer of an indirect storage attribute.
*
* @abstract
* @param {BufferAttribute} attribute - The buffer attribute.
*/
createIndirectStorageAttribute( /*attribute*/ ) { }
/**
* Creates a uniform buffer.
*
* @abstract
* @param {Buffer} uniformBuffer - The uniform buffer.
*/
createUniformBuffer( /*uniformBuffer*/ ) { }
/**
* Destroys a uniform buffer.
*
* @abstract
* @param {Buffer} uniformBuffer - The uniform buffer.
*/
destroyUniformBuffer( /*uniformBuffer*/ ) { }
/**
* Updates the GPU buffer of a shader attribute.
*
* @abstract
* @param {BufferAttribute} attribute - The buffer attribute to update.
*/
updateAttribute( /*attribute*/ ) { }
/**
* Destroys the GPU buffer of a shader attribute.
*
* @abstract
* @param {BufferAttribute} attribute - The buffer attribute to destroy.
*/
destroyAttribute( /*attribute*/ ) { }
/**
* Utility method for handling interleaved buffer attributes correctly.
* To process them, their `InterleavedBuffer` is returned.
*
* @param {BufferAttribute|InterleavedBufferAttribute} attribute - The attribute.
* @return {BufferAttribute|InterleavedBuffer}
*/
getBufferAttribute( attribute ) {
if ( attribute.isInterleavedBufferAttribute ) attribute = attribute.data;
return attribute;
}
// canvas
/**
* Returns the backend's rendering context.
*
* @abstract
* @return {Object} The rendering context.
*/
getContext() { }
/**
* Backends can use this method if they have to run
* logic when the renderer gets resized.
*
* @abstract
*/
updateSize() { }
/**
* Updates the viewport with the values from the given render context.
*
* @abstract
* @param {RenderContext} renderContext - The render context.
*/
updateViewport( /*renderContext*/ ) {}
/**
* Frees the GPU resources of the given canvas target.
*
* @abstract
* @param {CanvasTarget} canvasTarget - The canvas target.
*/
destroyCanvasTarget( /*canvasTarget*/ ) {}
// utils
/**
* Updates a unique identifier for the given render context that can be used
* to allocate resources like occlusion queries or timestamp queries.
*
* @param {RenderContext|ComputeNode|Array<ComputeNode>} abstractRenderContext - The render context.
*/
updateTimeStampUID( abstractRenderContext ) {
const contextData = this.get( abstractRenderContext );
const frame = this.renderer.info.frame;
let prefix;
let id = abstractRenderContext.id;
if ( Array.isArray( abstractRenderContext ) ) {
id = abstractRenderContext.map( c => c.id ).join( ',' );
prefix = 'c:' + this.renderer.info.compute.frameCalls;
} else if ( abstractRenderContext.isComputeNode === true ) {
prefix = 'c:' + this.renderer.info.compute.frameCalls;
} else {
prefix = 'r:' + this.renderer.info.render.frameCalls;
}
contextData.timestampUID = prefix + ':' + id + ':f' + frame;
}
/**
* Returns a unique identifier for the given render context that can be used
* to allocate resources like occlusion queries or timestamp queries.
*
* @param {RenderContext|ComputeNode} abstractRenderContext - The render context.
* @return {string} The unique identifier.
*/
getTimestampUID( abstractRenderContext ) {
return this.get( abstractRenderContext ).timestampUID;
}
/**
* Returns all timestamp frames for the given type.
*
* @param {string} type - The type of the time stamp.
* @return {Array<number>} The timestamp frames.
*/
getTimestampFrames( type ) {
const queryPool = this.timestampQueryPool[ type ];
return queryPool ? queryPool.getTimestampFrames() : [];
}
/**
* Returns the query pool for the given uid.
*
* @param {string} uid - The unique identifier.
* @return {TimestampQueryPool} The query pool.
*/
_getQueryPool( uid ) {
const type = uid.startsWith( 'c:' ) ? TimestampQuery.COMPUTE : TimestampQuery.RENDER;
const queryPool = this.timestampQueryPool[ type ];
return queryPool;
}
/**
* Returns the timestamp for the given uid.
*
* @param {string} uid - The unique identifier.
* @return {number} The timestamp.
*/
getTimestamp( uid ) {
const queryPool = this._getQueryPool( uid );
return queryPool.getTimestamp( uid );
}
/**
* Whether the backend supports query timestamps or not.
*
* @type {boolean}
* @readonly
*/
get hasTimestamp() {
return false;
}
/**
* Returns `true` if a timestamp for the given uid is available.
*
* @param {string} uid - The unique identifier.
* @return {boolean} Whether the timestamp is available or not.
*/
hasTimestampQuery( uid ) {
const queryPool = this._getQueryPool( uid );
return queryPool.hasTimestampQuery( uid );
}
/**
* Returns `true` if the given 3D object is fully occluded by other
* 3D objects in the scene. Backends must implement this method by using
* a Occlusion Query API.
*
* @abstract
* @param {RenderContext} renderContext - The render context.
* @param {Object3D} object - The 3D object to test.
* @return {boolean} Whether the 3D object is fully occluded or not.
*/
isOccluded( /*renderContext, object*/ ) {}
/**
* Returns the occlusion query data of the given render context for its current camera.
*
* @param {RenderContext} renderContext - The render context.
* @return {Object} The occlusion query data.
*/
getOcclusionData( renderContext ) {
const renderContextData = this.get( renderContext );
if ( renderContextData.occlusionData === undefined ) renderContextData.occlusionData = new WeakMap();
let occlusionData = renderContextData.occlusionData.get( renderContext.camera );
if ( occlusionData === undefined ) {
occlusionData = {};
renderContextData.occlusionData.set( renderContext.camera, occlusionData );
}
return occlusionData;
}
/**
* Resolves the time stamp for the given render context and type.
*
* @async
* @abstract
* @param {string} [type='render'] - The type of the time stamp.
* @return {Promise<number>} A Promise that resolves with the time stamp.
*/
async resolveTimestampsAsync( type = 'render' ) {
if ( ! this.trackTimestamp ) {
warnOnce( 'WebGPURenderer: Timestamp tracking is disabled.' );
return;
}
const queryPool = this.timestampQueryPool[ type ];
if ( ! queryPool ) {
return;
}
const duration = await queryPool.resolveQueriesAsync();
this.renderer.info[ type ].timestamp = duration;
return duration;
}
/**
* This method performs a readback operation by moving buffer data from
* a storage buffer attribute from the GPU to the CPU.
*
* @async
* @param {BufferAttribute} attribute - The storage buffer attribute to read from.
* @param {?(ReadbackBuffer|ArrayBuffer)} [target=null] - The readback buffer or array buffer that receives the data. If `null`, a new array buffer is returned.
* @param {number} [offset=0] - The byte offset to start reading from. Must be a multiple of 4.
* @param {number} [count=-1] - The number of bytes to read. Must be a multiple of 4, or `-1` to read to the end of the buffer.
* @return {Promise<ArrayBuffer|ReadbackBuffer>} A promise that resolves with the buffer data when the data are ready.
*/
async getArrayBufferAsync( /* attribute, target, offset, count */ ) {}
/**
* Checks if the given feature is supported by the backend.
*
* @async
* @abstract
* @param {string} name - The feature's name.
* @return {Promise<boolean>} A Promise that resolves with a bool that indicates whether the feature is supported or not.
*/
async hasFeatureAsync( /*name*/ ) { }
/**
* Checks if the given feature is supported by the backend.
*
* @abstract
* @param {string} name - The feature's name.
* @return {boolean} Whether the feature is supported or not.
*/
hasFeature( /*name*/ ) {}
/**
* Returns the drawing buffer size.
*
* @return {Vector2} The drawing buffer size.
*/
getDrawingBufferSize() {
_vector2 = _vector2 || new Vector2();
return this.renderer.getDrawingBufferSize( _vector2 );
}
/**
* Defines the scissor test.
*
* @abstract
* @param {boolean} boolean - Whether the scissor test should be enabled or not.
*/
setScissorTest( /*boolean*/ ) { }
/**
* Resets the backend's internal state. A no-op for backends without a state cache (e.g. WebGPU).
*
* @abstract
*/
resetState() { }
/**
* Returns the clear color and alpha into a single
* color object.
*
* @return {Color4} The clear color.
*/
getClearColor() {
const renderer = this.renderer;
_color4 = _color4 || new Color4();
renderer.getClearColor( _color4 );
_color4.getRGB( _color4 );
return _color4;
}
/**
* Returns the DOM element. If no DOM element exists, the backend
* creates a new one.
*
* @return {HTMLCanvasElement} The DOM element.
*/
getDomElement() {
let domElement = this.domElement;
if ( domElement === null ) {
domElement = ( this.parameters.canvas !== undefined ) ? this.parameters.canvas : createCanvasElement();
// OffscreenCanvas does not have setAttribute, see #22811
if ( 'setAttribute' in domElement ) domElement.setAttribute( 'data-engine', `three.js r${REVISION} webgpu` );
this.domElement = domElement;
}
return domElement;
}
/**
* Checks if the backend has the given compatibility.
*
* @abstract
* @param {string} name - The compatibility.
* @return {boolean} Whether the backend has the given compatibility or not.
*/
hasCompatibility( /*name*/ ) {
return false;
}
/**
* Initializes the render target defined in the given render context.
*
* @abstract
* @param {RenderContext} renderContext - The render context.
*/
initRenderTarget( /*renderContext*/ ) {}
/**
* Sets a dictionary for the given object into the
* internal data structure.
*
* @param {Object} object - The object.
* @param {Object} value - The dictionary to set.
*/
set( object, value ) {
this.data.set( object, value );
}
/**
* Returns the dictionary for the given object.
*
* @param {Object} object - The object.
* @return {Object} The object's dictionary.
*/
get( object ) {
let map = this.data.get( object );
if ( map === undefined ) {
map = {};
this.data.set( object, map );
}
return map;
}
/**
* Checks if the given object has a dictionary
* with data defined.
*
* @param {Object} object - The object.
* @return {boolean} Whether a dictionary for the given object as been defined or not.
*/
has( object ) {
return this.data.has( object );
}
/**
* Deletes an object from the internal data structure.
*
* @param {Object} object - The object to delete.
*/
delete( object ) {
this.data.delete( object );
}
/**
* Delete GPU data associated with a bind group.
*
* @abstract
* @param {BindGroup} bindGroup - The bind group.
*/
deleteBindGroupData( /*bindGroup*/ ) { }
/**
* Frees internal resources.
*
* @abstract
*/
async dispose() {
for ( const queryPool of Object.values( this.timestampQueryPool ) ) {
if ( queryPool !== null ) {
await queryPool.dispose();
}
}
}
}
let _id$1 = 0;
/**
* This module is internally used in context of compute shaders.
* This type of shader is not natively supported in WebGL 2 and
* thus implemented via Transform Feedback. `DualAttributeData`
* manages the related data.
*
* @private
*/
class DualAttributeData {
constructor( attributeData, dualBuffer ) {
this.buffers = [ attributeData.bufferGPU, dualBuffer ];
this.type = attributeData.type;
this.bufferType = attributeData.bufferType;
this.pbo = attributeData.pbo;
this.byteLength = attributeData.byteLength;
this.bytesPerElement = attributeData.BYTES_PER_ELEMENT;
this.version = attributeData.version;
this.isInteger = attributeData.isInteger;
this.activeBufferIndex = 0;
this.id = attributeData.id;
}
get bufferGPU() {
return this.buffers[ this.activeBufferIndex ];
}
get transformBuffer() {
return this.buffers[ this.activeBufferIndex ^ 1 ];
}
switchBuffers() {
this.activeBufferIndex ^= 1;
}
}
/**
* A WebGL 2 backend utility module for managing shader attributes.
*
* @private
*/
class WebGLAttributeUtils {
/**
* Constructs a new utility object.
*
* @param {WebGLBackend} backend - The WebGL 2 backend.
*/
constructor( backend ) {
/**
* A reference to the WebGL 2 backend.
*
* @type {WebGLBackend}
*/
this.backend = backend;
}
/**
* Creates the GPU buffer for the given buffer attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
* @param {GLenum } bufferType - A flag that indicates the buffer type and thus binding point target.
*/
createAttribute( attribute, bufferType ) {
const backend = this.backend;
const { gl } = backend;
const bufferAttribute = backend.getBufferAttribute( attribute );
if ( backend.get( bufferAttribute ).bufferGPU !== undefined ) return;
const array = attribute.array;
const usage = attribute.usage || gl.STATIC_DRAW;
const bufferGPU = this._createBuffer( gl, bufferType, array, usage );
//attribute.onUploadCallback();
let type;
if ( array instanceof Float32Array ) {
type = gl.FLOAT;
} else if ( typeof Float16Array !== 'undefined' && array instanceof Float16Array ) {
type = gl.HALF_FLOAT;
} else if ( array instanceof Uint16Array ) {
if ( attribute.isFloat16BufferAttribute ) {
type = gl.HALF_FLOAT;
} else {
type = gl.UNSIGNED_SHORT;
}
} else if ( array instanceof Int16Array ) {
type = gl.SHORT;
} else if ( array instanceof Uint32Array ) {
type = gl.UNSIGNED_INT;
} else if ( array instanceof Int32Array ) {
type = gl.INT;
} else if ( array instanceof Int8Array ) {
type = gl.BYTE;
} else if ( array instanceof Uint8Array ) {
type = gl.UNSIGNED_BYTE;
} else if ( array instanceof Uint8ClampedArray ) {
type = gl.UNSIGNED_BYTE;
} else {
throw new Error( 'THREE.WebGLBackend: Unsupported buffer data format: ' + array );
}
let attributeData = {
bufferGPU,
bufferType,
type,
byteLength: array.byteLength,
bytesPerElement: array.BYTES_PER_ELEMENT,
version: bufferAttribute.version,
pbo: attribute.pbo,
isInteger: type === gl.INT || type === gl.UNSIGNED_INT || attribute.gpuType === IntType,
id: _id$1 ++
};
if ( attribute.isStorageBufferAttribute || attribute.isStorageInstancedBufferAttribute ) {
// create buffer for transform feedback use
const bufferGPUDual = this._createBuffer( gl, bufferType, array, usage );
attributeData = new DualAttributeData( attributeData, bufferGPUDual );
}
backend.set( bufferAttribute, attributeData );
}
/**
* Updates the GPU buffer of the given buffer attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
updateAttribute( attribute ) {
const backend = this.backend;
const { gl } = backend;
const array = attribute.array;
const bufferAttribute = backend.getBufferAttribute( attribute );
const bufferData = backend.get( bufferAttribute );
const updateRanges = bufferAttribute.updateRanges;
// the element buffer binding is part of the VAO state, so index buffers are updated
// via the copy target to keep the bound VAO intact
const target = bufferData.bufferType === gl.ELEMENT_ARRAY_BUFFER ? gl.COPY_WRITE_BUFFER : bufferData.bufferType;
gl.bindBuffer( target, bufferData.bufferGPU );
if ( updateRanges.length === 0 ) {
// Not using update ranges
gl.bufferSubData( target, 0, array );
} else {
for ( let i = 0, l = updateRanges.length; i < l; i ++ ) {
const range = updateRanges[ i ];
gl.bufferSubData( target, range.start * array.BYTES_PER_ELEMENT,
array, range.start, range.count );
}
bufferAttribute.clearUpdateRanges();
}
gl.bindBuffer( target, null );
bufferData.version = bufferAttribute.version;
}
/**
* Destroys the GPU buffer of the given buffer attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
destroyAttribute( attribute ) {
const backend = this.backend;
const { gl } = backend;
const bufferAttribute = backend.getBufferAttribute( attribute );
const attributeData = backend.get( bufferAttribute );
if ( attributeData.buffers !== undefined ) {
// storage attributes hold a second buffer for transform feedback
for ( const buffer of attributeData.buffers ) {
gl.deleteBuffer( buffer );
}
} else {
gl.deleteBuffer( attributeData.bufferGPU );
}
backend.delete( bufferAttribute );
}
/**
* This method performs a readback operation by moving buffer data from
* a storage buffer attribute from the GPU to the CPU. ReadbackBuffer can
* be used to retain and reuse handles to the intermediate buffers and prevent
* new allocation.
*
* @async
* @param {BufferAttribute} attribute - The storage buffer attribute to read from.
* @param {?(ReadbackBuffer|ArrayBuffer)} [target=null] - The readback buffer or array buffer that receives the data. If `null`, a new array buffer is returned.
* @param {number} [offset=0] - The byte offset to start reading from. Must be a multiple of 4.
* @param {number} [count=-1] - The number of bytes to read. Must be a multiple of 4, or `-1` to read to the end of the buffer.
* @return {Promise<ArrayBuffer|ReadbackBuffer>} A promise that resolves with the buffer data when the data are ready.
*/
async getArrayBufferAsync( attribute, target = null, offset = 0, count = -1 ) {
const backend = this.backend;
const { gl } = backend;
const bufferAttribute = backend.getBufferAttribute( attribute );
const attributeInfo = backend.get( bufferAttribute );
const { bufferGPU } = attributeInfo;
const byteLength = count === -1 ? attributeInfo.byteLength - offset : count;
// read the data back
let dstBuffer;
if ( target === null ) {
dstBuffer = new Uint8Array( new ArrayBuffer( byteLength ) );
} else if ( target.isReadbackBuffer ) {
if ( target._mapped === true ) {
throw new Error( 'THREE.WebGPURenderer: ReadbackBuffer must be released before being used again.' );
}
const releaseCallback = () => {
target.buffer = null;
target._mapped = false;
target.removeEventListener( 'release', releaseCallback );
target.removeEventListener( 'dispose', releaseCallback );
};
target.addEventListener( 'release', releaseCallback );
target.addEventListener( 'dispose', releaseCallback );
// WebGL has no concept of a "mapped" data buffer so we create a new buffer, instead.
dstBuffer = new Uint8Array( new ArrayBuffer( byteLength ) );
target.buffer = dstBuffer.buffer;
} else {
dstBuffer = new Uint8Array( target );
}
// Ensure the buffer is bound before reading
gl.bindBuffer( gl.COPY_READ_BUFFER, bufferGPU );
gl.getBufferSubData( gl.COPY_READ_BUFFER, offset, dstBuffer );
gl.bindBuffer( gl.COPY_READ_BUFFER, null );
gl.bindBuffer( gl.COPY_WRITE_BUFFER, null );
// return the appropriate type
if ( target && target.isReadbackBuffer ) {
return target;
} else {
return dstBuffer.buffer;
}
}
/**
* Creates a WebGL buffer with the given data.
*
* @private
* @param {WebGL2RenderingContext} gl - The rendering context.
* @param {GLenum } bufferType - A flag that indicates the buffer type and thus binding point target.
* @param {TypedArray} array - The array of the buffer attribute.
* @param {GLenum} usage - The usage.
* @return {WebGLBuffer} The WebGL buffer.
*/
_createBuffer( gl, bufferType, array, usage ) {
const bufferGPU = gl.createBuffer();
// the element buffer binding is part of the VAO state, so the bound VAO must not be affected.
// the first binding defines the buffer type so the copy target can not be used here
const state = this.backend.state;
if ( bufferType === gl.ELEMENT_ARRAY_BUFFER && state.currentVAO !== null ) state.resetVertexState();
gl.bindBuffer( bufferType, bufferGPU );
gl.bufferData( bufferType, array, usage );
gl.bindBuffer( bufferType, null );
return bufferGPU;
}
}
/**
* A WebGL 2 backend utility module for managing vertex array objects (VAOs).
*
* VAOs are shared between all owners (render objects and compute pipelines)
* that use the same GPU buffers and deleted when the last owner releases them
* or when one of their buffers is destroyed.
*
* @private
*/
class WebGLVertexArrayUtils {
/**
* Constructs a new utility object.
*
* @param {WebGLBackend} backend - The WebGL 2 backend.
*/
constructor( backend ) {
/**
* A reference to the WebGL 2 backend.
*
* @type {WebGLBackend}
*/
this.backend = backend;
/**
* The VAO cache. Entries are keyed by the IDs of the attribute buffers
* and hold one VAO per combination of active storage buffers as well
* as a usage counter.
*
* @type {Map<string,Object>}
*/
this.cache = new Map();
}
/**
* Returns a VAO for the given attributes.
*
* @param {RenderObject|ComputePipeline} owner - The render object or compute pipeline using the VAO.
* @param {Array<BufferAttribute>} attributes - An array of buffer attributes.
* @param {?BufferAttribute} [index=null] - The index attribute.
* @return {WebGLVertexArrayObject} The VAO.
*/
getVAO( owner, attributes, index = null ) {
const backend = this.backend;
const ownerData = backend.get( owner );
const indexData = ( index !== null ) ? backend.get( index ) : null;
const indexGPU = ( indexData !== null ) ? indexData.bufferGPU : null;
if ( this._needsUpdate( ownerData, attributes, indexGPU ) === false ) return ownerData.vaoGPU;
// determine key, variant and attribute buffers
let key = '';
let variant = '';
const buffers = [];
const buffersData = [];
for ( let i = 0; i < attributes.length; i ++ ) {
const attribute = attributes[ i ];
const attributeData = backend.get( backend.getBufferAttribute( attribute ) );
key += ':' + attributeData.id;
if ( attribute.isInterleavedBufferAttribute ) key += '.' + attribute.offset;
variant += ':' + ( attributeData.activeBufferIndex || 0 );
buffers.push( attributeData.bufferGPU );
buffersData.push( attributeData );
}
// the element buffer binding is part of the VAO state
if ( indexData !== null ) {
key += '|' + indexData.id;
buffersData.push( indexData );
}
// get cache entry
let entry = this.cache.get( key );
if ( key !== ownerData.vaoKey ) {
if ( ownerData.vaoKey !== undefined ) this._releaseEntry( ownerData.vaoKey ); // release old VAO
if ( entry === undefined ) {
entry = { vaos: new Map(), buffersData, usedTimes: 0 };
this.cache.set( key, entry );
// each buffer knows its VAOs so they can be deleted with the buffer
for ( const bufferData of buffersData ) {
if ( bufferData.vaoKeys === undefined ) bufferData.vaoKeys = new Set();
bufferData.vaoKeys.add( key );
}
}
entry.usedTimes ++;
ownerData.vaoKey = key;
}
// get correct VAO variant (variants are needed for dual buffer attributes)
let vaoGPU = entry.vaos.get( variant );
if ( vaoGPU === undefined ) {
vaoGPU = this._createVAO( attributes, indexGPU );
entry.vaos.set( variant, vaoGPU );
}
ownerData.vaoGPU = vaoGPU;
ownerData.vertexBuffers = buffers;
ownerData.indexBuffer = indexGPU;
return vaoGPU;
}
/**
* Releases the VAO used by the given owner.
*
* @param {RenderObject|ComputePipeline} owner - The render object or compute pipeline.
*/
releaseVAO( owner ) {
const ownerData = this.backend.get( owner );
if ( ownerData.vaoKey === undefined ) return;
this._releaseEntry( ownerData.vaoKey );
ownerData.vaoKey = undefined;
ownerData.vaoGPU = undefined;
ownerData.vertexBuffers = undefined;
ownerData.indexBuffer = undefined;
}
/**
* Deletes all VAOs referencing the given attribute buffer. Must be called
* when the buffer is destroyed since VAOs keep the memory of attached buffers
* allocated. Owners of deleted VAOs request a new one with their next draw.
*
* @param {Object} bufferData - The backend data of the attribute buffer.
*/
releaseBuffer( bufferData ) {
if ( bufferData.vaoKeys === undefined ) return;
for ( const key of bufferData.vaoKeys ) {
this._deleteEntry( key );
}
}
/**
* Frees internal resources.
*/
dispose() {
const { gl } = this.backend;
for ( const entry of this.cache.values() ) {
for ( const vaoGPU of entry.vaos.values() ) {
gl.deleteVertexArray( vaoGPU );
}
}
this.cache.clear();
}
/**
* Returns `true` if the given attributes refer to other GPU buffers than
* the VAO of the given owner.
*
* @private
* @param {Object} ownerData - The backend data of the owner.
* @param {Array<BufferAttribute>} attributes - An array of buffer attributes.
* @param {?WebGLBuffer} indexGPU - The index buffer.
* @return {boolean} Whether the owner requires a different VAO or not.
*/
_needsUpdate( ownerData, attributes, indexGPU ) {
const buffers = ownerData.vertexBuffers;
if ( buffers === undefined || buffers.length !== attributes.length || ownerData.indexBuffer !== indexGPU ) return true;
for ( let i = 0; i < attributes.length; i ++ ) {
const bufferAttribute = this.backend.getBufferAttribute( attributes[ i ] );
if ( this.backend.get( bufferAttribute ).bufferGPU !== buffers[ i ] ) return true;
}
return false;
}
/**
* Releases the VAO entry. If possible, the all VAO variants are deleted.
*
* @private
* @param {string} key - The cache key.
*/
_releaseEntry( key ) {
const entry = this.cache.get( key );
if ( entry === undefined ) return; // already deleted with one of its buffers
entry.usedTimes --;
if ( entry.usedTimes === 0 ) this._deleteEntry( key );
}
/**
* Deletes all VAO variants of the given cache entry.
*
* @private
* @param {string} key - The cache key.
*/
_deleteEntry( key ) {
const { gl, state } = this.backend;
const entry = this.cache.get( key );
for ( const vaoGPU of entry.vaos.values() ) {
if ( state.currentVAO === vaoGPU ) state.resetVertexState();
gl.deleteVertexArray( vaoGPU );
}
for ( const bufferData of entry.buffersData ) {
bufferData.vaoKeys.delete( key );
}
this.cache.delete( key );
}
/**
* Creates a VAO from the given attributes.
*
* @private
* @param {Array<BufferAttribute>} attributes - An array of buffer attributes.
* @param {?WebGLBuffer} indexGPU - The index buffer.
* @return {WebGLVertexArrayObject} The VAO.
*/
_createVAO( attributes, indexGPU ) {
const { gl, state } = this.backend;
const vaoGPU = gl.createVertexArray();
state.setVertexState( vaoGPU, indexGPU );
for ( let i = 0; i < attributes.length; i ++ ) {
const attribute = attributes[ i ];
const bufferAttribute = this.backend.getBufferAttribute( attribute );
const attributeData = this.backend.get( bufferAttribute );
gl.bindBuffer( gl.ARRAY_BUFFER, attributeData.bufferGPU );
gl.enableVertexAttribArray( i );
let stride, offset;
if ( attribute.isInterleavedBufferAttribute === true ) {
// interleaved attributes can also view a buffer attribute
const bufferStride = bufferAttribute.isInterleavedBuffer === true ? bufferAttribute.stride : bufferAttribute.itemSize;
stride = bufferStride * attributeData.bytesPerElement;
offset = attribute.offset * attributeData.bytesPerElement;
} else {
stride = 0;
offset = 0;
}
if ( attributeData.isInteger ) {
gl.vertexAttribIPointer( i, attribute.itemSize, attributeData.type, stride, offset );
} else {
gl.vertexAttribPointer( i, attribute.itemSize, attributeData.type, attribute.normalized, stride, offset );
}
if ( bufferAttribute.isInstancedBufferAttribute === true || bufferAttribute.isInstancedInterleavedBuffer === true ) {
gl.vertexAttribDivisor( i, bufferAttribute.meshPerAttribute );
}
}
gl.bindBuffer( gl.ARRAY_BUFFER, null );
return vaoGPU;
}
}
let equationToGL, factorToGL;
/**
* A WebGL 2 backend utility module for managing the WebGL state.
*
* The major goal of this module is to reduce the number of state changes
* by caching the WEbGL state with a series of variables. In this way, the
* renderer only executes state change commands when necessary which
* improves the overall performance.
*
* @private
*/
class WebGLState {
/**
* Constructs a new utility object.
*
* @param {WebGLBackend} backend - The WebGL 2 backend.
*/
constructor( backend ) {
/**
* A reference to the WebGL 2 backend.
*
* @type {WebGLBackend}
*/
this.backend = backend;
/**
* A reference to the rendering context.
*
* @type {WebGL2RenderingContext}
*/
this.gl = this.backend.gl;
// Below properties are intended to cache
// the WebGL state and are not explicitly
// documented for convenience reasons.
this.enabled = {};
this.parameters = {};
this.currentFlipSided = null;
this.currentCullFace = null;
this.currentProgram = null;
this.currentBlendingEnabled = false;
this.currentBlending = null;
this.currentBlendEquation = null;
this.currentBlendEquationAlpha = null;
this.currentBlendSrc = null;
this.currentBlendDst = null;
this.currentBlendSrcAlpha = null;
this.currentBlendDstAlpha = null;
this.currentBlendColor = new Color( 0, 0, 0 );
this.currentBlendAlpha = 0;
this.currentPremultipledAlpha = null;
this.currentPolygonOffsetFactor = null;
this.currentPolygonOffsetUnits = null;
this.currentColorMask = null;
this.currentDepthReversed = false;
this.currentDepthFunc = null;
this.currentDepthMask = null;
this.currentStencilFunc = null;
this.currentStencilRef = null;
this.currentStencilFuncMask = null;
this.currentStencilFail = null;
this.currentStencilZFail = null;
this.currentStencilZPass = null;
this.currentStencilMask = null;
this.currentLineWidth = null;
this.currentClippingPlanes = 0;
this.currentVAO = null;
this.currentIndex = null;
this.currentBoundFramebuffers = {};
this.currentDrawbuffers = new WeakMap();
this.maxTextures = this.gl.getParameter( this.gl.MAX_TEXTURE_IMAGE_UNITS );
this.currentTextureSlot = null;
this.currentBoundTextures = {};
this.currentBoundBufferBases = {};
this._init();
}
/**
* Inits the state of the utility.
*
* @private
*/
_init() {
const gl = this.gl;
// Store only WebGL constants here.
equationToGL = {
[ AddEquation ]: gl.FUNC_ADD,
[ SubtractEquation ]: gl.FUNC_SUBTRACT,
[ ReverseSubtractEquation ]: gl.FUNC_REVERSE_SUBTRACT,
[ MinEquation ]: gl.MIN,
[ MaxEquation ]: gl.MAX
};
factorToGL = {
[ ZeroFactor ]: gl.ZERO,
[ OneFactor ]: gl.ONE,
[ SrcColorFactor ]: gl.SRC_COLOR,
[ SrcAlphaFactor ]: gl.SRC_ALPHA,
[ SrcAlphaSaturateFactor ]: gl.SRC_ALPHA_SATURATE,
[ DstColorFactor ]: gl.DST_COLOR,
[ DstAlphaFactor ]: gl.DST_ALPHA,
[ OneMinusSrcColorFactor ]: gl.ONE_MINUS_SRC_COLOR,
[ OneMinusSrcAlphaFactor ]: gl.ONE_MINUS_SRC_ALPHA,
[ OneMinusDstColorFactor ]: gl.ONE_MINUS_DST_COLOR,
[ OneMinusDstAlphaFactor ]: gl.ONE_MINUS_DST_ALPHA,
[ ConstantColorFactor ]: gl.CONSTANT_COLOR,
[ OneMinusConstantColorFactor ]: gl.ONE_MINUS_CONSTANT_COLOR,
[ ConstantAlphaFactor ]: gl.CONSTANT_ALPHA,
[ OneMinusConstantAlphaFactor ]: gl.ONE_MINUS_CONSTANT_ALPHA
};
const scissorParam = gl.getParameter( gl.SCISSOR_BOX );
const viewportParam = gl.getParameter( gl.VIEWPORT );
this.currentScissor = new Vector4().fromArray( scissorParam );
this.currentViewport = new Vector4().fromArray( viewportParam );
this._tempVec4 = new Vector4();
}
/**
* Enables the given WebGL capability.
*
* This method caches the capability state so
* `gl.enable()` is only called when necessary.
*
* @param {GLenum} id - The capability to enable.
*/
enable( id ) {
const { enabled } = this;
if ( enabled[ id ] !== true ) {
this.gl.enable( id );
enabled[ id ] = true;
}
}
/**
* Disables the given WebGL capability.
*
* This method caches the capability state so
* `gl.disable()` is only called when necessary.
*
* @param {GLenum} id - The capability to enable.
*/
disable( id ) {
const { enabled } = this;
if ( enabled[ id ] !== false ) {
this.gl.disable( id );
enabled[ id ] = false;
}
}
/**
* Specifies whether polygons are front- or back-facing
* by setting the winding orientation.
*
* This method caches the state so `gl.frontFace()` is only
* called when necessary.
*
* @param {boolean} flipSided - Whether triangles flipped their sides or not.
*/
setFlipSided( flipSided ) {
if ( this.currentFlipSided !== flipSided ) {
const { gl } = this;
if ( flipSided ) {
gl.frontFace( gl.CW );
} else {
gl.frontFace( gl.CCW );
}
this.currentFlipSided = flipSided;
}
}
/**
* Specifies whether or not front- and/or back-facing
* polygons can be culled.
*
* This method caches the state so `gl.cullFace()` is only
* called when necessary.
*
* @param {number} cullFace - Defines which polygons are candidates for culling.
*/
setCullFace( cullFace ) {
const { gl } = this;
if ( cullFace !== CullFaceNone ) {
this.enable( gl.CULL_FACE );
if ( cullFace !== this.currentCullFace ) {
if ( cullFace === CullFaceBack ) {
gl.cullFace( gl.BACK );
} else if ( cullFace === CullFaceFront ) {
gl.cullFace( gl.FRONT );
} else {
gl.cullFace( gl.FRONT_AND_BACK );
}
}
} else {
this.disable( gl.CULL_FACE );
}
this.currentCullFace = cullFace;
}
/**
* Specifies the width of line primitives.
*
* This method caches the state so `gl.lineWidth()` is only
* called when necessary.
*
* @param {number} width - The line width.
*/
setLineWidth( width ) {
const { currentLineWidth, gl } = this;
if ( width !== currentLineWidth ) {
gl.lineWidth( width );
this.currentLineWidth = width;
}
}
/**
* Defines the blending for each draw buffer of a MRT-configured render target.
*
* @param {Array<Texture>} textures - The textures of the render target.
* @param {?MRTNode} mrt - The MRT configuration.
* @param {Material} material - The material whose blending is used for outputs with `MaterialBlending`.
*/
setMRTBlending( textures, mrt, material ) {
const gl = this.gl;
const drawBuffersIndexedExt = this.backend.drawBuffersIndexedExt;
if ( ! drawBuffersIndexedExt ) {
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.' );
return;
}
for ( let i = 0; i < textures.length; i ++ ) {
const texture = textures[ i ];
let blending = null;
if ( mrt !== null ) {
const blendMode = mrt.getBlendMode( texture.name );
if ( blendMode.blending === MaterialBlending ) {
// use material blending
blending = material;
} else if ( blendMode.blending !== NoBlending ) {
blending = blendMode;
}
} else {
// use material blending
blending = material;
}
if ( blending !== null ) {
this._setMRTBlendingIndex( i, blending );
} else {
// use opaque blending (no blending)
drawBuffersIndexedExt.blendFuncSeparateiOES( i, gl.ONE, gl.ZERO, gl.ONE, gl.ZERO );
}
}
// invalidate cache
this.currentBlending = null;
this.currentBlendEquation = null;
this.currentBlendEquationAlpha = null;
this.currentBlendSrc = null;
this.currentBlendDst = null;
this.currentBlendSrcAlpha = null;
this.currentBlendDstAlpha = null;
this.currentPremultipledAlpha = null;
}
/**
* Applies blending configuration for a specific draw buffer index.
*
* @private
* @param {number} index - The draw buffer index.
* @param {Object} blending - The blending configuration (material or BlendMode).
*/
_setMRTBlendingIndex( index, blending ) {
const { gl } = this;
const drawBuffersIndexedExt = this.backend.drawBuffersIndexedExt;
const blendingType = blending.blending;
const blendSrc = blending.blendSrc;
const blendDst = blending.blendDst;
const blendEquation = blending.blendEquation;
const premultipliedAlpha = blending.premultipliedAlpha;
if ( blendingType === CustomBlending ) {
const blendSrcAlpha = blending.blendSrcAlpha !== null ? blending.blendSrcAlpha : blendSrc;
const blendDstAlpha = blending.blendDstAlpha !== null ? blending.blendDstAlpha : blendDst;
const blendEquationAlpha = blending.blendEquationAlpha !== null ? blending.blendEquationAlpha : blendEquation;
drawBuffersIndexedExt.blendEquationSeparateiOES( index, equationToGL[ blendEquation ], equationToGL[ blendEquationAlpha ] );
drawBuffersIndexedExt.blendFuncSeparateiOES( index, factorToGL[ blendSrc ], factorToGL[ blendDst ], factorToGL[ blendSrcAlpha ], factorToGL[ blendDstAlpha ] );
} else {
drawBuffersIndexedExt.blendEquationSeparateiOES( index, gl.FUNC_ADD, gl.FUNC_ADD );
if ( premultipliedAlpha ) {
switch ( blendingType ) {
case NormalBlending:
drawBuffersIndexedExt.blendFuncSeparateiOES( index, gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA );
break;
case AdditiveBlending:
drawBuffersIndexedExt.blendFuncSeparateiOES( index, gl.ONE, gl.ONE, gl.ONE, gl.ONE );
break;
case SubtractiveBlending:
drawBuffersIndexedExt.blendFuncSeparateiOES( index, gl.ZERO, gl.ONE_MINUS_SRC_COLOR, gl.ZERO, gl.ONE );
break;
case MultiplyBlending:
drawBuffersIndexedExt.blendFuncSeparateiOES( index, gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE );
break;
default:
drawBuffersIndexedExt.blendFuncSeparateiOES( index, gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA );
break;
}
} else {
switch ( blendingType ) {
case NormalBlending:
drawBuffersIndexedExt.blendFuncSeparateiOES( index, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA );
break;
case AdditiveBlending:
drawBuffersIndexedExt.blendFuncSeparateiOES( index, gl.SRC_ALPHA, gl.ONE, gl.ONE, gl.ONE );
break;
case SubtractiveBlending:
drawBuffersIndexedExt.blendFuncSeparateiOES( index, gl.ZERO, gl.ONE_MINUS_SRC_COLOR, gl.ZERO, gl.ONE );
break;
case MultiplyBlending:
drawBuffersIndexedExt.blendFuncSeparateiOES( index, gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE );
break;
default:
drawBuffersIndexedExt.blendFuncSeparateiOES( index, gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA );
break;
}
}
}
}
/**
* Defines the blending.
*
* This method caches the state so `gl.blendEquation()`, `gl.blendEquationSeparate()`,
* `gl.blendFunc()`, `gl.blendFuncSeparate()` and `gl.blendColor()` are only called when necessary.
*
* @param {number} blending - The blending type.
* @param {number} blendEquation - The blending equation.
* @param {number} blendSrc - Only relevant for custom blending. The RGB source blending factor.
* @param {number} blendDst - Only relevant for custom blending. The RGB destination blending factor.
* @param {number} blendEquationAlpha - Only relevant for custom blending. The blending equation for alpha.
* @param {number} blendSrcAlpha - Only relevant for custom blending. The alpha source blending factor.
* @param {number} blendDstAlpha - Only relevant for custom blending. The alpha destination blending factor.
* @param {Color} blendColor - Only relevant for custom blending. The RGB values of the constant blend color.
* @param {number} blendAlpha - Only relevant for custom blending. The alpha value of the constant blend color.
* @param {boolean} premultipliedAlpha - Whether premultiplied alpha is enabled or not.
*/
setBlending( blending, blendEquation, blendSrc, blendDst, blendEquationAlpha, blendSrcAlpha, blendDstAlpha, blendColor, blendAlpha, premultipliedAlpha ) {
const { gl } = this;
if ( blending === NoBlending ) {
if ( this.currentBlendingEnabled === true ) {
this.disable( gl.BLEND );
this.currentBlendingEnabled = false;
}
return;
}
if ( this.currentBlendingEnabled === false ) {
this.enable( gl.BLEND );
this.currentBlendingEnabled = true;
}
if ( blending !== CustomBlending ) {
if ( blending !== this.currentBlending || premultipliedAlpha !== this.currentPremultipledAlpha ) {
if ( this.currentBlendEquation !== AddEquation || this.currentBlendEquationAlpha !== AddEquation ) {
gl.blendEquation( gl.FUNC_ADD );
this.currentBlendEquation = AddEquation;
this.currentBlendEquationAlpha = AddEquation;
}
if ( premultipliedAlpha ) {
switch ( blending ) {
case NormalBlending:
gl.blendFuncSeparate( gl.ONE, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA );
break;
case AdditiveBlending:
gl.blendFunc( gl.ONE, gl.ONE );
break;
case SubtractiveBlending:
gl.blendFuncSeparate( gl.ZERO, gl.ONE_MINUS_SRC_COLOR, gl.ZERO, gl.ONE );
break;
case MultiplyBlending:
gl.blendFuncSeparate( gl.DST_COLOR, gl.ONE_MINUS_SRC_ALPHA, gl.ZERO, gl.ONE );
break;
default:
error( 'WebGLState: Invalid blending: ', blending );
break;
}
} else {
switch ( blending ) {
case NormalBlending:
gl.blendFuncSeparate( gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA );
break;
case AdditiveBlending:
gl.blendFuncSeparate( gl.SRC_ALPHA, gl.ONE, gl.ONE, gl.ONE );
break;
case SubtractiveBlending:
error( 'WebGLState: SubtractiveBlending requires material.premultipliedAlpha = true' );
break;
case MultiplyBlending:
error( 'WebGLState: MultiplyBlending requires material.premultipliedAlpha = true' );
break;
default:
error( 'WebGLState: Invalid blending: ', blending );
break;
}
}
this.currentBlendSrc = null;
this.currentBlendDst = null;
this.currentBlendSrcAlpha = null;
this.currentBlendDstAlpha = null;
this.currentBlending = blending;
this.currentPremultipledAlpha = premultipliedAlpha;
}
return;
}
// custom blending
blendEquationAlpha = blendEquationAlpha || blendEquation;
blendSrcAlpha = blendSrcAlpha || blendSrc;
blendDstAlpha = blendDstAlpha || blendDst;
if ( blendEquation !== this.currentBlendEquation || blendEquationAlpha !== this.currentBlendEquationAlpha ) {
gl.blendEquationSeparate( equationToGL[ blendEquation ], equationToGL[ blendEquationAlpha ] );
this.currentBlendEquation = blendEquation;
this.currentBlendEquationAlpha = blendEquationAlpha;
}
if ( blendSrc !== this.currentBlendSrc || blendDst !== this.currentBlendDst || blendSrcAlpha !== this.currentBlendSrcAlpha || blendDstAlpha !== this.currentBlendDstAlpha ) {
gl.blendFuncSeparate( factorToGL[ blendSrc ], factorToGL[ blendDst ], factorToGL[ blendSrcAlpha ], factorToGL[ blendDstAlpha ] );
this.currentBlendSrc = blendSrc;
this.currentBlendDst = blendDst;
this.currentBlendSrcAlpha = blendSrcAlpha;
this.currentBlendDstAlpha = blendDstAlpha;
}
if ( blendColor.equals( this.currentBlendColor ) === false || blendAlpha !== this.currentBlendAlpha ) {
gl.blendColor( blendColor.r, blendColor.g, blendColor.b, blendAlpha );
this.currentBlendColor.copy( blendColor );
this.currentBlendAlpha = blendAlpha;
}
this.currentBlending = blending;
this.currentPremultipledAlpha = false;
}
/**
* Specifies whether colors can be written when rendering
* into a framebuffer or not.
*
* This method caches the state so `gl.colorMask()` is only
* called when necessary.
*
* @param {boolean} colorMask - The color mask.
*/
setColorMask( colorMask ) {
if ( this.currentColorMask !== colorMask ) {
this.gl.colorMask( colorMask, colorMask, colorMask, colorMask );
this.currentColorMask = colorMask;
}
}
/**
* Specifies whether the depth test is enabled or not.
*
* @param {boolean} depthTest - Whether the depth test is enabled or not.
*/
setDepthTest( depthTest ) {
const { gl } = this;
if ( depthTest ) {
this.enable( gl.DEPTH_TEST );
} else {
this.disable( gl.DEPTH_TEST );
}
}
/**
* Configures the WebGL state to use a reversed depth buffer.
*
* @param {boolean} reversed - Whether the depth buffer is reversed or not.
*/
setReversedDepth( reversed ) {
if ( this.currentDepthReversed !== reversed ) {
const ext = this.backend.extensions.get( 'EXT_clip_control' );
if ( reversed ) {
ext.clipControlEXT( ext.LOWER_LEFT_EXT, ext.ZERO_TO_ONE_EXT );
} else {
ext.clipControlEXT( ext.LOWER_LEFT_EXT, ext.NEGATIVE_ONE_TO_ONE_EXT );
}
this.currentDepthReversed = reversed;
}
}
/**
* Specifies whether depth values can be written when rendering
* into a framebuffer or not.
*
* This method caches the state so `gl.depthMask()` is only
* called when necessary.
*
* @param {boolean} depthMask - The depth mask.
*/
setDepthMask( depthMask ) {
if ( this.currentDepthMask !== depthMask ) {
this.gl.depthMask( depthMask );
this.currentDepthMask = depthMask;
}
}
/**
* Specifies the depth compare function.
*
* This method caches the state so `gl.depthFunc()` is only
* called when necessary.
*
* @param {number} depthFunc - The depth compare function.
*/
setDepthFunc( depthFunc ) {
if ( this.currentDepthReversed ) depthFunc = ReversedDepthFuncs[ depthFunc ];
if ( this.currentDepthFunc !== depthFunc ) {
const { gl } = this;
switch ( depthFunc ) {
case NeverDepth:
gl.depthFunc( gl.NEVER );
break;
case AlwaysDepth:
gl.depthFunc( gl.ALWAYS );
break;
case LessDepth:
gl.depthFunc( gl.LESS );
break;
case LessEqualDepth:
gl.depthFunc( gl.LEQUAL );
break;
case EqualDepth:
gl.depthFunc( gl.EQUAL );
break;
case GreaterEqualDepth:
gl.depthFunc( gl.GEQUAL );
break;
case GreaterDepth:
gl.depthFunc( gl.GREATER );
break;
case NotEqualDepth:
gl.depthFunc( gl.NOTEQUAL );
break;
default:
gl.depthFunc( gl.LEQUAL );
}
this.currentDepthFunc = depthFunc;
}
}
/**
* Specifies the scissor box.
*
* @param {number} x - The x-coordinate of the lower left corner of the viewport.
* @param {number} y - The y-coordinate of the lower left corner of the viewport.
* @param {number} width - The width of the viewport.
* @param {number} height - The height of the viewport.
*
*/
scissor( x, y, width, height ) {
const scissor = this._tempVec4.set( x, y, width, height );
if ( this.currentScissor.equals( scissor ) === false ) {
const { gl } = this;
gl.scissor( scissor.x, scissor.y, scissor.z, scissor.w );
this.currentScissor.copy( scissor );
}
}
/**
* Specifies the viewport.
*
* @param {number} x - The x-coordinate of the lower left corner of the viewport.
* @param {number} y - The y-coordinate of the lower left corner of the viewport.
* @param {number} width - The width of the viewport.
* @param {number} height - The height of the viewport.
*
*/
viewport( x, y, width, height ) {
const viewport = this._tempVec4.set( x, y, width, height );
if ( this.currentViewport.equals( viewport ) === false ) {
const { gl } = this;
gl.viewport( viewport.x, viewport.y, viewport.z, viewport.w );
this.currentViewport.copy( viewport );
}
}
/**
* Defines the scissor test.
*
* @param {boolean} boolean - Whether the scissor test should be enabled or not.
*/
setScissorTest( boolean ) {
const gl = this.gl;
if ( boolean ) {
this.enable( gl.SCISSOR_TEST );
} else {
this.disable( gl.SCISSOR_TEST );
}
}
/**
* Specifies whether the stencil test is enabled or not.
*
* @param {boolean} stencilTest - Whether the stencil test is enabled or not.
*/
setStencilTest( stencilTest ) {
const { gl } = this;
if ( stencilTest ) {
this.enable( gl.STENCIL_TEST );
} else {
this.disable( gl.STENCIL_TEST );
}
}
/**
* Specifies whether stencil values can be written when rendering
* into a framebuffer or not.
*
* This method caches the state so `gl.stencilMask()` is only
* called when necessary.
*
* @param {boolean} stencilMask - The stencil mask.
*/
setStencilMask( stencilMask ) {
if ( this.currentStencilMask !== stencilMask ) {
this.gl.stencilMask( stencilMask );
this.currentStencilMask = stencilMask;
}
}
/**
* Specifies whether the stencil test functions.
*
* This method caches the state so `gl.stencilFunc()` is only
* called when necessary.
*
* @param {number} stencilFunc - The stencil compare function.
* @param {number} stencilRef - The reference value for the stencil test.
* @param {number} stencilMask - A bit-wise mask that is used to AND the reference value and the stored stencil value when the test is done.
*/
setStencilFunc( stencilFunc, stencilRef, stencilMask ) {
if ( this.currentStencilFunc !== stencilFunc ||
this.currentStencilRef !== stencilRef ||
this.currentStencilFuncMask !== stencilMask ) {
this.gl.stencilFunc( stencilFunc, stencilRef, stencilMask );
this.currentStencilFunc = stencilFunc;
this.currentStencilRef = stencilRef;
this.currentStencilFuncMask = stencilMask;
}
}
/**
* Specifies whether the stencil test operation.
*
* This method caches the state so `gl.stencilOp()` is only
* called when necessary.
*
* @param {number} stencilFail - The function to use when the stencil test fails.
* @param {number} stencilZFail - The function to use when the stencil test passes, but the depth test fail.
* @param {number} stencilZPass - The function to use when both the stencil test and the depth test pass,
* or when the stencil test passes and there is no depth buffer or depth testing is disabled.
*/
setStencilOp( stencilFail, stencilZFail, stencilZPass ) {
if ( this.currentStencilFail !== stencilFail ||
this.currentStencilZFail !== stencilZFail ||
this.currentStencilZPass !== stencilZPass ) {
this.gl.stencilOp( stencilFail, stencilZFail, stencilZPass );
this.currentStencilFail = stencilFail;
this.currentStencilZFail = stencilZFail;
this.currentStencilZPass = stencilZPass;
}
}
/**
* Configures the WebGL state for the given material.
*
* @param {Material} material - The material to configure the state for.
* @param {number} frontFaceCW - Whether the front faces are counter-clockwise or not.
* @param {number} hardwareClippingPlanes - The number of hardware clipping planes.
*/
setMaterial( material, frontFaceCW, hardwareClippingPlanes ) {
const { gl } = this;
material.side === DoubleSide
? this.disable( gl.CULL_FACE )
: this.enable( gl.CULL_FACE );
let flipSided = ( material.side === BackSide );
if ( frontFaceCW ) flipSided = ! flipSided;
this.setFlipSided( flipSided );
( material.blending === NormalBlending && material.transparent === false )
? this.setBlending( NoBlending )
: this.setBlending( material.blending, material.blendEquation, material.blendSrc, material.blendDst, material.blendEquationAlpha, material.blendSrcAlpha, material.blendDstAlpha, material.blendColor, material.blendAlpha, material.premultipliedAlpha );
this.setDepthFunc( material.depthFunc );
this.setDepthTest( material.depthTest );
this.setDepthMask( material.depthWrite );
this.setColorMask( material.colorWrite );
const stencilWrite = material.stencilWrite;
this.setStencilTest( stencilWrite );
if ( stencilWrite ) {
this.setStencilMask( material.stencilWriteMask );
this.setStencilFunc( material.stencilFunc, material.stencilRef, material.stencilFuncMask );
this.setStencilOp( material.stencilFail, material.stencilZFail, material.stencilZPass );
}
this.setPolygonOffset( material.polygonOffset, material.polygonOffsetFactor, material.polygonOffsetUnits );
material.alphaToCoverage === true && this.backend.renderer.currentSamples > 0
? this.enable( gl.SAMPLE_ALPHA_TO_COVERAGE )
: this.disable( gl.SAMPLE_ALPHA_TO_COVERAGE );
if ( this.currentClippingPlanes !== hardwareClippingPlanes ) {
const CLIP_DISTANCE0_WEBGL = 0x3000;
for ( let i = 0; i < 8; i ++ ) {
if ( i < hardwareClippingPlanes ) {
this.enable( CLIP_DISTANCE0_WEBGL + i );
} else {
this.disable( CLIP_DISTANCE0_WEBGL + i );
}
}
this.currentClippingPlanes = hardwareClippingPlanes;
}
}
/**
* Specifies the polygon offset.
*
* This method caches the state so `gl.polygonOffset()` is only
* called when necessary.
*
* @param {boolean} polygonOffset - Whether polygon offset is enabled or not.
* @param {number} factor - The scale factor for the variable depth offset for each polygon.
* @param {number} units - The multiplier by which an implementation-specific value is multiplied with to create a constant depth offset.
*/
setPolygonOffset( polygonOffset, factor, units ) {
const { gl } = this;
if ( polygonOffset ) {
this.enable( gl.POLYGON_OFFSET_FILL );
if ( this.currentPolygonOffsetFactor !== factor || this.currentPolygonOffsetUnits !== units ) {
this.currentPolygonOffsetFactor = factor;
this.currentPolygonOffsetUnits = units;
if ( this.currentDepthReversed ) {
factor = - factor;
units = - units;
}
gl.polygonOffset( factor, units );
}
} else {
this.disable( gl.POLYGON_OFFSET_FILL );
}
}
/**
* Defines the usage of the given WebGL program.
*
* This method caches the state so `gl.useProgram()` is only
* called when necessary.
*
* @param {WebGLProgram} program - The WebGL program to use.
* @return {boolean} Whether a program change has been executed or not.
*/
useProgram( program ) {
if ( this.currentProgram !== program ) {
this.gl.useProgram( program );
this.currentProgram = program;
return true;
}
return false;
}
/**
* Sets the vertex state by binding the given VAO and element buffer.
*
* @param {WebGLVertexArrayObject} vao - The VAO.
* @param {?WebGLBuffer} indexBuffer - The index buffer.
* @return {boolean} Whether a vertex state has been changed or not.
*/
setVertexState( vao, indexBuffer = null ) {
const gl = this.gl;
if ( this.currentVAO !== vao || this.currentIndex !== indexBuffer ) {
gl.bindVertexArray( vao );
if ( indexBuffer !== null ) {
gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, indexBuffer );
}
this.currentVAO = vao;
this.currentIndex = indexBuffer;
return true;
}
return false;
}
/**
* Resets the vertex array state by resetting the VAO and element buffer.
*/
resetVertexState() {
const gl = this.gl;
gl.bindVertexArray( null );
gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, null );
this.currentVAO = null;
this.currentIndex = null;
}
// framebuffer
/**
* Binds the given framebuffer.
*
* This method caches the state so `gl.bindFramebuffer()` is only
* called when necessary.
*
* @param {number} target - The binding point (target).
* @param {WebGLFramebuffer} framebuffer - The WebGL framebuffer to bind.
* @return {boolean} Whether a bind has been executed or not.
*/
bindFramebuffer( target, framebuffer ) {
const { gl, currentBoundFramebuffers } = this;
if ( currentBoundFramebuffers[ target ] !== framebuffer ) {
gl.bindFramebuffer( target, framebuffer );
currentBoundFramebuffers[ target ] = framebuffer;
// gl.DRAW_FRAMEBUFFER is equivalent to gl.FRAMEBUFFER
if ( target === gl.DRAW_FRAMEBUFFER ) {
currentBoundFramebuffers[ gl.FRAMEBUFFER ] = framebuffer;
}
if ( target === gl.FRAMEBUFFER ) {
currentBoundFramebuffers[ gl.DRAW_FRAMEBUFFER ] = framebuffer;
}
return true;
}
return false;
}
/**
* Defines draw buffers to which fragment colors are written into.
* Configures the MRT setup of custom framebuffers.
*
* This method caches the state so `gl.drawBuffers()` is only
* called when necessary.
*
* @param {RenderContext} renderContext - The render context.
* @param {WebGLFramebuffer} framebuffer - The WebGL framebuffer.
*/
drawBuffers( renderContext, framebuffer ) {
const { gl } = this;
let drawBuffers = [];
let needsUpdate = false;
if ( renderContext.textures !== null ) {
drawBuffers = this.currentDrawbuffers.get( framebuffer );
if ( drawBuffers === undefined ) {
drawBuffers = [ gl.COLOR_ATTACHMENT0 ];
this.currentDrawbuffers.set( framebuffer, drawBuffers );
}
const textures = renderContext.textures;
if ( drawBuffers.length !== textures.length ) {
for ( let i = 0, il = textures.length; i < il; i ++ ) {
drawBuffers[ i ] = gl.COLOR_ATTACHMENT0 + i;
}
drawBuffers.length = textures.length;
needsUpdate = true;
}
} else {
if ( drawBuffers[ 0 ] !== gl.BACK ) {
drawBuffers[ 0 ] = gl.BACK;
needsUpdate = true;
}
}
if ( needsUpdate ) {
gl.drawBuffers( drawBuffers );
}
}
// texture
/**
* Makes the given texture unit active.
*
* This method caches the state so `gl.activeTexture()` is only
* called when necessary.
*
* @param {number} webglSlot - The texture unit to make active.
*/
activeTexture( webglSlot ) {
const { gl, currentTextureSlot, maxTextures } = this;
if ( webglSlot === undefined ) webglSlot = gl.TEXTURE0 + maxTextures - 1;
if ( currentTextureSlot !== webglSlot ) {
gl.activeTexture( webglSlot );
this.currentTextureSlot = webglSlot;
}
}
/**
* Binds the given WebGL texture to a target.
*
* This method caches the state so `gl.bindTexture()` is only
* called when necessary.
*
* @param {number} webglType - The binding point (target).
* @param {WebGLTexture} webglTexture - The WebGL texture to bind.
* @param {number} webglSlot - The texture.
*/
bindTexture( webglType, webglTexture, webglSlot ) {
const { gl, currentTextureSlot, currentBoundTextures, maxTextures } = this;
if ( webglSlot === undefined ) {
if ( currentTextureSlot === null ) {
webglSlot = gl.TEXTURE0 + maxTextures - 1;
} else {
webglSlot = currentTextureSlot;
}
}
let boundTexture = currentBoundTextures[ webglSlot ];
if ( boundTexture === undefined ) {
boundTexture = { type: undefined, texture: undefined };
currentBoundTextures[ webglSlot ] = boundTexture;
}
if ( boundTexture.type !== webglType || boundTexture.texture !== webglTexture ) {
if ( currentTextureSlot !== webglSlot ) {
gl.activeTexture( webglSlot );
this.currentTextureSlot = webglSlot;
}
gl.bindTexture( webglType, webglTexture );
boundTexture.type = webglType;
boundTexture.texture = webglTexture;
}
}
/**
* Binds a given WebGL buffer to a given binding point (target) at a given index.
*
* This method caches the state so `gl.bindBufferBase()` is only
* called when necessary.
*
* @param {number} target - The target for the bind operation.
* @param {number} index - The index of the target.
* @param {WebGLBuffer} buffer - The WebGL buffer.
* @return {boolean} Whether a bind has been executed or not.
*/
bindBufferBase( target, index, buffer ) {
const { gl } = this;
const key = `${target}-${index}`;
if ( this.currentBoundBufferBases[ key ] !== buffer ) {
gl.bindBufferBase( target, index, buffer );
this.currentBoundBufferBases[ key ] = buffer;
return true;
}
return false;
}
/**
* Unbinds the current bound texture.
*
* This method caches the state so `gl.bindTexture()` is only
* called when necessary.
*/
unbindTexture() {
const { gl, currentTextureSlot, currentBoundTextures } = this;
const boundTexture = currentBoundTextures[ currentTextureSlot ];
if ( boundTexture !== undefined && boundTexture.type !== undefined ) {
gl.bindTexture( boundTexture.type, null );
boundTexture.type = undefined;
boundTexture.texture = undefined;
}
}
/**
* Returns the value for the given parameter.
*
* @param {number} name - The parameter to get the value for.
* @return {any} The value for the given parameter.
*/
getParameter( name ) {
const { gl, parameters } = this;
if ( parameters[ name ] !== undefined ) {
return parameters[ name ];
} else {
return gl.getParameter( name );
}
}
/**
* Specifies a pixel storage mode.
*
* @param {number} name - The parameter to set.
* @param {any} value - A value to set the parameter to.
*/
pixelStorei( name, value ) {
const { gl, parameters } = this;
if ( parameters[ name ] !== value ) {
gl.pixelStorei( name, value );
parameters[ name ] = value;
}
}
/**
* Restores the WebGL state to its default and clears the cache so subsequent renderings
* re-apply the required state. Useful when the WebGL context is shared with other libraries.
*/
reset() {
const { gl } = this;
// reset WebGL state
gl.disable( gl.BLEND );
gl.disable( gl.CULL_FACE );
gl.disable( gl.DEPTH_TEST );
gl.disable( gl.POLYGON_OFFSET_FILL );
gl.disable( gl.SCISSOR_TEST );
gl.disable( gl.STENCIL_TEST );
gl.disable( gl.SAMPLE_ALPHA_TO_COVERAGE );
gl.blendEquation( gl.FUNC_ADD );
gl.blendFunc( gl.ONE, gl.ZERO );
gl.blendFuncSeparate( gl.ONE, gl.ZERO, gl.ONE, gl.ZERO );
gl.blendColor( 0, 0, 0, 0 );
gl.colorMask( true, true, true, true );
gl.clearColor( 0, 0, 0, 0 );
gl.depthMask( true );
gl.depthFunc( gl.LESS );
gl.clearDepth( 1 );
gl.stencilMask( 0xffffffff );
gl.stencilFunc( gl.ALWAYS, 0, 0xffffffff );
gl.stencilOp( gl.KEEP, gl.KEEP, gl.KEEP );
gl.clearStencil( 0 );
gl.cullFace( gl.BACK );
gl.frontFace( gl.CCW );
gl.polygonOffset( 0, 0 );
gl.activeTexture( gl.TEXTURE0 );
gl.bindFramebuffer( gl.FRAMEBUFFER, null );
gl.bindFramebuffer( gl.DRAW_FRAMEBUFFER, null );
gl.bindFramebuffer( gl.READ_FRAMEBUFFER, null );
gl.useProgram( null );
gl.lineWidth( 1 );
gl.scissor( 0, 0, gl.canvas.width, gl.canvas.height );
gl.viewport( 0, 0, gl.canvas.width, gl.canvas.height );
gl.pixelStorei( gl.PACK_ALIGNMENT, 4 );
gl.pixelStorei( gl.UNPACK_ALIGNMENT, 4 );
gl.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, false );
gl.pixelStorei( gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false );
gl.pixelStorei( gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.BROWSER_DEFAULT_WEBGL );
gl.pixelStorei( gl.PACK_ROW_LENGTH, 0 );
gl.pixelStorei( gl.PACK_SKIP_PIXELS, 0 );
gl.pixelStorei( gl.PACK_SKIP_ROWS, 0 );
gl.pixelStorei( gl.UNPACK_ROW_LENGTH, 0 );
gl.pixelStorei( gl.UNPACK_IMAGE_HEIGHT, 0 );
gl.pixelStorei( gl.UNPACK_SKIP_PIXELS, 0 );
gl.pixelStorei( gl.UNPACK_SKIP_ROWS, 0 );
gl.pixelStorei( gl.UNPACK_SKIP_IMAGES, 0 );
this.resetVertexState();
// reset internal cache
this.enabled = {};
this.parameters = {};
this.currentFlipSided = null;
this.currentCullFace = null;
this.currentProgram = null;
this.currentBlendingEnabled = false;
this.currentBlending = null;
this.currentBlendEquation = null;
this.currentBlendEquationAlpha = null;
this.currentBlendSrc = null;
this.currentBlendDst = null;
this.currentBlendSrcAlpha = null;
this.currentBlendDstAlpha = null;
this.currentBlendColor.set( 0, 0, 0 );
this.currentBlendAlpha = 0;
this.currentPremultipledAlpha = null;
this.currentPolygonOffsetFactor = null;
this.currentPolygonOffsetUnits = null;
this.currentColorMask = null;
this.currentDepthFunc = null;
this.currentDepthMask = null;
this.currentStencilFunc = null;
this.currentStencilRef = null;
this.currentStencilFuncMask = null;
this.currentStencilFail = null;
this.currentStencilZFail = null;
this.currentStencilZPass = null;
this.currentStencilMask = null;
this.currentLineWidth = null;
this.currentClippingPlanes = 0;
this.currentBoundFramebuffers = {};
this.currentDrawbuffers = new WeakMap();
this.currentTextureSlot = null;
this.currentBoundTextures = {};
this.currentBoundBufferBases = {};
this.currentScissor.set( 0, 0, gl.canvas.width, gl.canvas.height );
this.currentViewport.set( 0, 0, gl.canvas.width, gl.canvas.height );
// re-apply reversed depth if used by the renderer
this.currentDepthReversed = false;
if ( this.backend.renderer.reversedDepthBuffer === true ) {
this.setReversedDepth( true );
}
}
}
/**
* A WebGL 2 backend utility module with common helpers.
*
* @private
*/
class WebGLUtils {
/**
* Constructs a new utility object.
*
* @param {WebGLBackend} backend - The WebGL 2 backend.
*/
constructor( backend ) {
/**
* A reference to the WebGL 2 backend.
*
* @type {WebGLBackend}
*/
this.backend = backend;
/**
* A reference to the rendering context.
*
* @type {WebGL2RenderingContext}
*/
this.gl = this.backend.gl;
/**
* A reference to a backend module holding extension-related
* utility functions.
*
* @type {WebGLExtensions}
*/
this.extensions = backend.extensions;
}
/**
* Converts the given three.js constant into a WebGL constant.
* The method currently supports the conversion of texture formats
* and types.
*
* @param {number} p - The three.js constant.
* @param {string} [colorSpace=NoColorSpace] - The color space.
* @return {?number} The corresponding WebGL constant.
*/
convert( p, colorSpace = NoColorSpace ) {
const { gl, extensions } = this;
let extension;
const transfer = ColorManagement.getTransfer( colorSpace );
if ( p === UnsignedByteType ) return gl.UNSIGNED_BYTE;
if ( p === UnsignedShort4444Type ) return gl.UNSIGNED_SHORT_4_4_4_4;
if ( p === UnsignedShort5551Type ) return gl.UNSIGNED_SHORT_5_5_5_1;
if ( p === UnsignedInt5999Type ) return gl.UNSIGNED_INT_5_9_9_9_REV;
if ( p === UnsignedInt101111Type ) return gl.UNSIGNED_INT_10F_11F_11F_REV;
if ( p === ByteType ) return gl.BYTE;
if ( p === ShortType ) return gl.SHORT;
if ( p === UnsignedShortType ) return gl.UNSIGNED_SHORT;
if ( p === IntType ) return gl.INT;
if ( p === UnsignedIntType ) return gl.UNSIGNED_INT;
if ( p === FloatType ) return gl.FLOAT;
if ( p === HalfFloatType ) {
return gl.HALF_FLOAT;
}
if ( p === AlphaFormat ) return gl.ALPHA;
if ( p === RGBFormat ) return gl.RGB;
if ( p === RGBAFormat ) return gl.RGBA;
if ( p === DepthFormat ) return gl.DEPTH_COMPONENT;
if ( p === DepthStencilFormat ) return gl.DEPTH_STENCIL;
// WebGL2 formats.
if ( p === RedFormat ) return gl.RED;
if ( p === RedIntegerFormat ) return gl.RED_INTEGER;
if ( p === RGFormat ) return gl.RG;
if ( p === RGIntegerFormat ) return gl.RG_INTEGER;
if ( p === RGBAIntegerFormat ) return gl.RGBA_INTEGER;
// S3TC
if ( p === RGB_S3TC_DXT1_Format || p === RGBA_S3TC_DXT1_Format || p === RGBA_S3TC_DXT3_Format || p === RGBA_S3TC_DXT5_Format ) {
if ( transfer === SRGBTransfer ) {
extension = extensions.get( 'WEBGL_compressed_texture_s3tc_srgb' );
if ( extension !== null ) {
if ( p === RGB_S3TC_DXT1_Format ) return extension.COMPRESSED_SRGB_S3TC_DXT1_EXT;
if ( p === RGBA_S3TC_DXT1_Format ) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
if ( p === RGBA_S3TC_DXT3_Format ) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;
if ( p === RGBA_S3TC_DXT5_Format ) return extension.COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;
} else {
return null;
}
} else {
extension = extensions.get( 'WEBGL_compressed_texture_s3tc' );
if ( extension !== null ) {
if ( p === RGB_S3TC_DXT1_Format ) return extension.COMPRESSED_RGB_S3TC_DXT1_EXT;
if ( p === RGBA_S3TC_DXT1_Format ) return extension.COMPRESSED_RGBA_S3TC_DXT1_EXT;
if ( p === RGBA_S3TC_DXT3_Format ) return extension.COMPRESSED_RGBA_S3TC_DXT3_EXT;
if ( p === RGBA_S3TC_DXT5_Format ) return extension.COMPRESSED_RGBA_S3TC_DXT5_EXT;
} else {
return null;
}
}
}
// PVRTC
if ( p === RGB_PVRTC_4BPPV1_Format || p === RGB_PVRTC_2BPPV1_Format || p === RGBA_PVRTC_4BPPV1_Format || p === RGBA_PVRTC_2BPPV1_Format ) {
extension = extensions.get( 'WEBGL_compressed_texture_pvrtc' );
if ( extension !== null ) {
if ( p === RGB_PVRTC_4BPPV1_Format ) return extension.COMPRESSED_RGB_PVRTC_4BPPV1_IMG;
if ( p === RGB_PVRTC_2BPPV1_Format ) return extension.COMPRESSED_RGB_PVRTC_2BPPV1_IMG;
if ( p === RGBA_PVRTC_4BPPV1_Format ) return extension.COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
if ( p === RGBA_PVRTC_2BPPV1_Format ) return extension.COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
} else {
return null;
}
}
// ETC
if ( p === RGB_ETC1_Format || p === RGB_ETC2_Format || p === RGBA_ETC2_EAC_Format || p === R11_EAC_Format || p === SIGNED_R11_EAC_Format || p === RG11_EAC_Format || p === SIGNED_RG11_EAC_Format ) {
extension = extensions.get( 'WEBGL_compressed_texture_etc' );
if ( extension !== null ) {
if ( p === RGB_ETC1_Format || p === RGB_ETC2_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ETC2 : extension.COMPRESSED_RGB8_ETC2;
if ( p === RGBA_ETC2_EAC_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ETC2_EAC : extension.COMPRESSED_RGBA8_ETC2_EAC;
if ( p === R11_EAC_Format ) return extension.COMPRESSED_R11_EAC;
if ( p === SIGNED_R11_EAC_Format ) return extension.COMPRESSED_SIGNED_R11_EAC;
if ( p === RG11_EAC_Format ) return extension.COMPRESSED_RG11_EAC;
if ( p === SIGNED_RG11_EAC_Format ) return extension.COMPRESSED_SIGNED_RG11_EAC;
} else {
return null;
}
}
// ASTC
if ( p === RGBA_ASTC_4x4_Format || p === RGBA_ASTC_5x4_Format || p === RGBA_ASTC_5x5_Format ||
p === RGBA_ASTC_6x5_Format || p === RGBA_ASTC_6x6_Format || p === RGBA_ASTC_8x5_Format ||
p === RGBA_ASTC_8x6_Format || p === RGBA_ASTC_8x8_Format || p === RGBA_ASTC_10x5_Format ||
p === RGBA_ASTC_10x6_Format || p === RGBA_ASTC_10x8_Format || p === RGBA_ASTC_10x10_Format ||
p === RGBA_ASTC_12x10_Format || p === RGBA_ASTC_12x12_Format ) {
extension = extensions.get( 'WEBGL_compressed_texture_astc' );
if ( extension !== null ) {
if ( p === RGBA_ASTC_4x4_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_4x4_KHR : extension.COMPRESSED_RGBA_ASTC_4x4_KHR;
if ( p === RGBA_ASTC_5x4_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_5x4_KHR : extension.COMPRESSED_RGBA_ASTC_5x4_KHR;
if ( p === RGBA_ASTC_5x5_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_5x5_KHR : extension.COMPRESSED_RGBA_ASTC_5x5_KHR;
if ( p === RGBA_ASTC_6x5_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_6x5_KHR : extension.COMPRESSED_RGBA_ASTC_6x5_KHR;
if ( p === RGBA_ASTC_6x6_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_6x6_KHR : extension.COMPRESSED_RGBA_ASTC_6x6_KHR;
if ( p === RGBA_ASTC_8x5_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x5_KHR : extension.COMPRESSED_RGBA_ASTC_8x5_KHR;
if ( p === RGBA_ASTC_8x6_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x6_KHR : extension.COMPRESSED_RGBA_ASTC_8x6_KHR;
if ( p === RGBA_ASTC_8x8_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_8x8_KHR : extension.COMPRESSED_RGBA_ASTC_8x8_KHR;
if ( p === RGBA_ASTC_10x5_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x5_KHR : extension.COMPRESSED_RGBA_ASTC_10x5_KHR;
if ( p === RGBA_ASTC_10x6_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x6_KHR : extension.COMPRESSED_RGBA_ASTC_10x6_KHR;
if ( p === RGBA_ASTC_10x8_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x8_KHR : extension.COMPRESSED_RGBA_ASTC_10x8_KHR;
if ( p === RGBA_ASTC_10x10_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_10x10_KHR : extension.COMPRESSED_RGBA_ASTC_10x10_KHR;
if ( p === RGBA_ASTC_12x10_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_12x10_KHR : extension.COMPRESSED_RGBA_ASTC_12x10_KHR;
if ( p === RGBA_ASTC_12x12_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR : extension.COMPRESSED_RGBA_ASTC_12x12_KHR;
} else {
return null;
}
}
// BPTC
if ( p === RGBA_BPTC_Format || p === RGB_BPTC_SIGNED_Format || p === RGB_BPTC_UNSIGNED_Format ) {
extension = extensions.get( 'EXT_texture_compression_bptc' );
if ( extension !== null ) {
if ( p === RGBA_BPTC_Format ) return ( transfer === SRGBTransfer ) ? extension.COMPRESSED_SRGB_ALPHA_BPTC_UNORM_EXT : extension.COMPRESSED_RGBA_BPTC_UNORM_EXT;
if ( p === RGB_BPTC_SIGNED_Format ) return extension.COMPRESSED_RGB_BPTC_SIGNED_FLOAT_EXT;
if ( p === RGB_BPTC_UNSIGNED_Format ) return extension.COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_EXT;
} else {
return null;
}
}
// RGTC
if ( p === RED_RGTC1_Format || p === SIGNED_RED_RGTC1_Format || p === RED_GREEN_RGTC2_Format || p === SIGNED_RED_GREEN_RGTC2_Format ) {
extension = extensions.get( 'EXT_texture_compression_rgtc' );
if ( extension !== null ) {
if ( p === RED_RGTC1_Format ) return extension.COMPRESSED_RED_RGTC1_EXT;
if ( p === SIGNED_RED_RGTC1_Format ) return extension.COMPRESSED_SIGNED_RED_RGTC1_EXT;
if ( p === RED_GREEN_RGTC2_Format ) return extension.COMPRESSED_RED_GREEN_RGTC2_EXT;
if ( p === SIGNED_RED_GREEN_RGTC2_Format ) return extension.COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT;
} else {
return null;
}
}
//
if ( p === UnsignedInt248Type ) {
return gl.UNSIGNED_INT_24_8;
}
// if "p" can't be resolved, assume the user defines a WebGL constant as a string (fallback/workaround for packed RGB formats)
return ( gl[ p ] !== undefined ) ? gl[ p ] : null;
}
/**
* This method can be used to synchronize the CPU with the GPU by waiting until
* ongoing GPU commands have been completed.
*
* @private
* @return {Promise} A promise that resolves when all ongoing GPU commands have been completed.
*/
_clientWaitAsync() {
const { gl } = this;
const sync = gl.fenceSync( gl.SYNC_GPU_COMMANDS_COMPLETE, 0 );
gl.flush();
return new Promise( ( resolve, reject ) => {
function test() {
const res = gl.clientWaitSync( sync, gl.SYNC_FLUSH_COMMANDS_BIT, 0 );
if ( res === gl.WAIT_FAILED ) {
gl.deleteSync( sync );
reject();
return;
}
if ( res === gl.TIMEOUT_EXPIRED ) {
requestAnimationFrame( test );
return;
}
gl.deleteSync( sync );
resolve();
}
test();
} );
}
}
let initialized = false, wrappingToGL, filterToGL, compareToGL;
/**
* A WebGL 2 backend utility module for managing textures.
*
* @private
*/
class WebGLTextureUtils {
/**
* Constructs a new utility object.
*
* @param {WebGLBackend} backend - The WebGL 2 backend.
*/
constructor( backend ) {
/**
* A reference to the WebGL 2 backend.
*
* @type {WebGLBackend}
*/
this.backend = backend;
/**
* A reference to the rendering context.
*
* @type {WebGL2RenderingContext}
*/
this.gl = backend.gl;
/**
* A reference to a backend module holding extension-related
* utility functions.
*
* @type {WebGLExtensions}
*/
this.extensions = backend.extensions;
/**
* A dictionary for managing default textures. The key
* is the binding point (target), the value the WEbGL texture object.
*
* @type {Object<GLenum,WebGLTexture>}
*/
this.defaultTextures = {};
/**
* A scratch framebuffer used for attaching the source texture in
* {@link copyTextureToTexture}.
*
* @private
* @type {?WebGLFramebuffer}
*/
this._srcFramebuffer = null;
/**
* A scratch framebuffer used for attaching the destination texture in
* {@link copyTextureToTexture}.
*
* @private
* @type {?WebGLFramebuffer}
*/
this._dstFramebuffer = null;
if ( initialized === false ) {
this._init();
initialized = true;
}
}
/**
* Inits the state of the utility.
*
* @private
*/
_init() {
const gl = this.gl;
// Store only WebGL constants here.
wrappingToGL = {
[ RepeatWrapping ]: gl.REPEAT,
[ ClampToEdgeWrapping ]: gl.CLAMP_TO_EDGE,
[ MirroredRepeatWrapping ]: gl.MIRRORED_REPEAT
};
filterToGL = {
[ NearestFilter ]: gl.NEAREST,
[ NearestMipmapNearestFilter ]: gl.NEAREST_MIPMAP_NEAREST,
[ NearestMipmapLinearFilter ]: gl.NEAREST_MIPMAP_LINEAR,
[ LinearFilter ]: gl.LINEAR,
[ LinearMipmapNearestFilter ]: gl.LINEAR_MIPMAP_NEAREST,
[ LinearMipmapLinearFilter ]: gl.LINEAR_MIPMAP_LINEAR
};
compareToGL = {
[ NeverCompare ]: gl.NEVER,
[ AlwaysCompare ]: gl.ALWAYS,
[ LessCompare ]: gl.LESS,
[ LessEqualCompare ]: gl.LEQUAL,
[ EqualCompare ]: gl.EQUAL,
[ GreaterEqualCompare ]: gl.GEQUAL,
[ GreaterCompare ]: gl.GREATER,
[ NotEqualCompare ]: gl.NOTEQUAL
};
}
/**
* Returns the native texture type for the given texture.
*
* @param {Texture} texture - The texture.
* @return {GLenum} The native texture type.
*/
getGLTextureType( texture ) {
const { gl } = this;
let glTextureType;
if ( texture.isCubeTexture === true ) {
glTextureType = gl.TEXTURE_CUBE_MAP;
} else if ( texture.isArrayTexture === true || texture.isDataArrayTexture === true || texture.isCompressedArrayTexture === true ) {
glTextureType = gl.TEXTURE_2D_ARRAY;
} else if ( texture.isData3DTexture === true ) { // TODO: isCompressed3DTexture, wait for #26642
glTextureType = gl.TEXTURE_3D;
} else {
glTextureType = gl.TEXTURE_2D;
}
return glTextureType;
}
/**
* Returns the native texture type for the given texture.
*
* @param {?string} internalFormatName - The internal format name. When `null`, the internal format is derived from the subsequent parameters.
* @param {GLenum} glFormat - The WebGL format.
* @param {GLenum} glType - The WebGL type.
* @param {string} colorSpace - The texture's color space.
* @param {boolean} [forceLinearTransfer=false] - Whether to force a linear transfer or not.
* @return {GLenum} The internal format.
*/
getInternalFormat( internalFormatName, glFormat, glType, normalized, colorSpace, forceLinearTransfer = false ) {
const { gl, extensions } = this;
if ( internalFormatName !== null ) {
if ( gl[ internalFormatName ] !== undefined ) return gl[ internalFormatName ];
warn( 'WebGLBackend: Attempt to use non-existing WebGL internal format \'' + internalFormatName + '\'' );
}
let extTextureNorm16 = null;
if ( normalized ) {
extTextureNorm16 = extensions.get( 'EXT_texture_norm16' );
if ( ! extTextureNorm16 ) {
warn( 'WebGLRenderer: Unable to use normalized textures without EXT_texture_norm16 extension' );
}
}
let internalFormat = glFormat;
if ( glFormat === gl.RED ) {
if ( glType === gl.FLOAT ) internalFormat = gl.R32F;
if ( glType === gl.HALF_FLOAT ) internalFormat = gl.R16F;
if ( glType === gl.UNSIGNED_BYTE ) internalFormat = gl.R8;
if ( glType === gl.BYTE ) internalFormat = gl.R8_SNORM;
if ( glType === gl.UNSIGNED_SHORT && extTextureNorm16 ) internalFormat = extTextureNorm16.R16_EXT;
if ( glType === gl.SHORT && extTextureNorm16 ) internalFormat = extTextureNorm16.R16_SNORM_EXT;
}
if ( glFormat === gl.RED_INTEGER ) {
if ( glType === gl.UNSIGNED_BYTE ) internalFormat = gl.R8UI;
if ( glType === gl.UNSIGNED_SHORT ) internalFormat = gl.R16UI;
if ( glType === gl.UNSIGNED_INT ) internalFormat = gl.R32UI;
if ( glType === gl.BYTE ) internalFormat = gl.R8I;
if ( glType === gl.SHORT ) internalFormat = gl.R16I;
if ( glType === gl.INT ) internalFormat = gl.R32I;
}
if ( glFormat === gl.RG ) {
if ( glType === gl.FLOAT ) internalFormat = gl.RG32F;
if ( glType === gl.HALF_FLOAT ) internalFormat = gl.RG16F;
if ( glType === gl.UNSIGNED_BYTE ) internalFormat = gl.RG8;
if ( glType === gl.BYTE ) internalFormat = gl.RG8_SNORM;
if ( glType === gl.UNSIGNED_SHORT && extTextureNorm16 ) internalFormat = extTextureNorm16.RG16_EXT;
if ( glType === gl.SHORT && extTextureNorm16 ) internalFormat = extTextureNorm16.RG16_SNORM_EXT;
}
if ( glFormat === gl.RG_INTEGER ) {
if ( glType === gl.UNSIGNED_BYTE ) internalFormat = gl.RG8UI;
if ( glType === gl.UNSIGNED_SHORT ) internalFormat = gl.RG16UI;
if ( glType === gl.UNSIGNED_INT ) internalFormat = gl.RG32UI;
if ( glType === gl.BYTE ) internalFormat = gl.RG8I;
if ( glType === gl.SHORT ) internalFormat = gl.RG16I;
if ( glType === gl.INT ) internalFormat = gl.RG32I;
}
if ( glFormat === gl.RGB ) {
const transfer = forceLinearTransfer ? LinearTransfer : ColorManagement.getTransfer( colorSpace );
if ( glType === gl.FLOAT ) internalFormat = gl.RGB32F;
if ( glType === gl.HALF_FLOAT ) internalFormat = gl.RGB16F;
if ( glType === gl.UNSIGNED_BYTE ) internalFormat = ( transfer === SRGBTransfer ) ? gl.SRGB8 : gl.RGB8;
if ( glType === gl.BYTE ) internalFormat = gl.RGB8_SNORM;
if ( glType === gl.UNSIGNED_SHORT && extTextureNorm16 ) internalFormat = extTextureNorm16.RGB16_EXT;
if ( glType === gl.SHORT && extTextureNorm16 ) internalFormat = extTextureNorm16.RGB16_SNORM_EXT;
if ( glType === gl.UNSIGNED_SHORT_5_6_5 ) internalFormat = gl.RGB565;
if ( glType === gl.UNSIGNED_SHORT_5_5_5_1 ) internalFormat = gl.RGB5_A1;
if ( glType === gl.UNSIGNED_SHORT_4_4_4_4 ) internalFormat = gl.RGB4;
if ( glType === gl.UNSIGNED_INT_5_9_9_9_REV ) internalFormat = gl.RGB9_E5;
if ( glType === gl.UNSIGNED_INT_10F_11F_11F_REV ) internalFormat = gl.R11F_G11F_B10F;
}
if ( glFormat === gl.RGB_INTEGER ) {
if ( glType === gl.UNSIGNED_BYTE ) internalFormat = gl.RGB8UI;
if ( glType === gl.UNSIGNED_SHORT ) internalFormat = gl.RGB16UI;
if ( glType === gl.UNSIGNED_INT ) internalFormat = gl.RGB32UI;
if ( glType === gl.BYTE ) internalFormat = gl.RGB8I;
if ( glType === gl.SHORT ) internalFormat = gl.RGB16I;
if ( glType === gl.INT ) internalFormat = gl.RGB32I;
}
if ( glFormat === gl.RGBA ) {
const transfer = forceLinearTransfer ? LinearTransfer : ColorManagement.getTransfer( colorSpace );
if ( glType === gl.FLOAT ) internalFormat = gl.RGBA32F;
if ( glType === gl.HALF_FLOAT ) internalFormat = gl.RGBA16F;
if ( glType === gl.UNSIGNED_BYTE ) internalFormat = ( transfer === SRGBTransfer ) ? gl.SRGB8_ALPHA8 : gl.RGBA8;
if ( glType === gl.BYTE ) internalFormat = gl.RGBA8_SNORM;
if ( glType === gl.UNSIGNED_SHORT && extTextureNorm16 ) internalFormat = extTextureNorm16.RGBA16_EXT;
if ( glType === gl.SHORT && extTextureNorm16 ) internalFormat = extTextureNorm16.RGBA16_SNORM_EXT;
if ( glType === gl.UNSIGNED_SHORT_4_4_4_4 ) internalFormat = gl.RGBA4;
if ( glType === gl.UNSIGNED_SHORT_5_5_5_1 ) internalFormat = gl.RGB5_A1;
}
if ( glFormat === gl.RGBA_INTEGER ) {
if ( glType === gl.UNSIGNED_BYTE ) internalFormat = gl.RGBA8UI;
if ( glType === gl.UNSIGNED_SHORT ) internalFormat = gl.RGBA16UI;
if ( glType === gl.UNSIGNED_INT ) internalFormat = gl.RGBA32UI;
if ( glType === gl.BYTE ) internalFormat = gl.RGBA8I;
if ( glType === gl.SHORT ) internalFormat = gl.RGBA16I;
if ( glType === gl.INT ) internalFormat = gl.RGBA32I;
}
if ( glFormat === gl.DEPTH_COMPONENT ) {
if ( glType === gl.UNSIGNED_SHORT ) internalFormat = gl.DEPTH_COMPONENT16;
if ( glType === gl.UNSIGNED_INT ) internalFormat = gl.DEPTH_COMPONENT24;
if ( glType === gl.FLOAT ) internalFormat = gl.DEPTH_COMPONENT32F;
}
if ( glFormat === gl.DEPTH_STENCIL ) {
if ( glType === gl.UNSIGNED_INT_24_8 ) internalFormat = gl.DEPTH24_STENCIL8;
if ( glType === gl.FLOAT ) internalFormat = gl.DEPTH32F_STENCIL8;
}
if ( internalFormat === gl.R16F || internalFormat === gl.R32F ||
internalFormat === gl.RG16F || internalFormat === gl.RG32F ||
internalFormat === gl.RGBA16F || internalFormat === gl.RGBA32F ) {
extensions.get( 'EXT_color_buffer_float' );
}
return internalFormat;
}
/**
* Sets the texture parameters for the given texture.
*
* @param {GLenum} textureType - The texture type.
* @param {Texture} texture - The texture.
*/
setTextureParameters( textureType, texture ) {
const { gl, extensions, backend } = this;
const { state } = this.backend;
const workingPrimaries = ColorManagement.getPrimaries( ColorManagement.workingColorSpace );
const texturePrimaries = texture.colorSpace === NoColorSpace ? null : ColorManagement.getPrimaries( texture.colorSpace );
const unpackConversion = texture.colorSpace === NoColorSpace || workingPrimaries === texturePrimaries ? gl.NONE : gl.BROWSER_DEFAULT_WEBGL;
state.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, texture.flipY );
state.pixelStorei( gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, texture.premultiplyAlpha );
state.pixelStorei( gl.UNPACK_ALIGNMENT, texture.unpackAlignment );
state.pixelStorei( gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, unpackConversion );
gl.texParameteri( textureType, gl.TEXTURE_WRAP_S, wrappingToGL[ texture.wrapS ] );
gl.texParameteri( textureType, gl.TEXTURE_WRAP_T, wrappingToGL[ texture.wrapT ] );
if ( textureType === gl.TEXTURE_3D || textureType === gl.TEXTURE_2D_ARRAY ) {
// WebGL 2 does not support wrapping for depth 2D array textures
if ( ! texture.isArrayTexture ) {
gl.texParameteri( textureType, gl.TEXTURE_WRAP_R, wrappingToGL[ texture.wrapR ] );
}
}
gl.texParameteri( textureType, gl.TEXTURE_MAG_FILTER, filterToGL[ texture.magFilter ] );
const hasMipmaps = texture.mipmaps.length > 0;
// follow WebGPU backend mapping for texture filtering
const minFilter = texture.minFilter === LinearFilter && hasMipmaps ? LinearMipmapLinearFilter : texture.minFilter;
gl.texParameteri( textureType, gl.TEXTURE_MIN_FILTER, filterToGL[ minFilter ] );
if ( texture.compareFunction ) {
gl.texParameteri( textureType, gl.TEXTURE_COMPARE_MODE, gl.COMPARE_REF_TO_TEXTURE );
gl.texParameteri( textureType, gl.TEXTURE_COMPARE_FUNC, compareToGL[ texture.compareFunction ] );
}
if ( extensions.has( 'EXT_texture_filter_anisotropic' ) === true ) {
if ( texture.magFilter === NearestFilter ) return;
if ( texture.minFilter !== NearestMipmapLinearFilter && texture.minFilter !== LinearMipmapLinearFilter ) return;
if ( texture.type === FloatType && extensions.has( 'OES_texture_float_linear' ) === false ) return; // verify extension for WebGL 1 and WebGL 2
if ( texture.anisotropy > 1 ) {
const extension = extensions.get( 'EXT_texture_filter_anisotropic' );
gl.texParameterf( textureType, extension.TEXTURE_MAX_ANISOTROPY_EXT, Math.min( texture.anisotropy, backend.capabilities.getMaxAnisotropy() ) );
}
}
}
/**
* Creates a default texture for the given texture that can be used
* as a placeholder until the actual texture is ready for usage.
*
* @param {Texture} texture - The texture to create a default texture for.
*/
createDefaultTexture( texture ) {
const { gl, backend, defaultTextures } = this;
const glTextureType = this.getGLTextureType( texture );
let textureGPU = defaultTextures[ glTextureType ];
if ( textureGPU === undefined ) {
textureGPU = gl.createTexture();
backend.state.bindTexture( glTextureType, textureGPU );
gl.texParameteri( glTextureType, gl.TEXTURE_MIN_FILTER, gl.NEAREST );
gl.texParameteri( glTextureType, gl.TEXTURE_MAG_FILTER, gl.NEAREST );
// gl.texImage2D( glTextureType, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, data );
defaultTextures[ glTextureType ] = textureGPU;
}
backend.set( texture, {
textureGPU,
glTextureType
} );
}
/**
* Defines a texture on the GPU for the given texture object.
*
* @param {Texture} texture - The texture.
* @param {Object} [options={}] - Optional configuration parameter.
* @return {undefined}
*/
createTexture( texture, options ) {
const { gl, backend } = this;
let textureGPU, glTextureType, glFormat, glType, glInternalFormat;
if ( texture.isExternalTexture === true ) {
textureGPU = texture.sourceTexture;
glTextureType = this.getGLTextureType( texture );
} else {
const { levels, width, height, depth } = options;
glFormat = backend.utils.convert( texture.format, texture.colorSpace );
glType = backend.utils.convert( texture.type );
glInternalFormat = this.getInternalFormat( texture.internalFormat, glFormat, glType, texture.normalized, texture.colorSpace, texture.isVideoTexture );
textureGPU = gl.createTexture();
glTextureType = this.getGLTextureType( texture );
backend.state.bindTexture( glTextureType, textureGPU );
this.setTextureParameters( glTextureType, texture );
if ( texture.isArrayTexture || texture.isDataArrayTexture || texture.isCompressedArrayTexture ) {
gl.texStorage3D( gl.TEXTURE_2D_ARRAY, levels, glInternalFormat, width, height, depth );
} else if ( texture.isData3DTexture ) {
gl.texStorage3D( gl.TEXTURE_3D, levels, glInternalFormat, width, height, depth );
} else if ( ! texture.isVideoTexture ) {
gl.texStorage2D( glTextureType, levels, glInternalFormat, width, height );
}
}
backend.set( texture, {
textureGPU,
glTextureType,
glFormat,
glType,
glInternalFormat
} );
}
/**
* Uploads texture buffer data to the GPU memory.
*
* @param {WebGLBuffer} buffer - The buffer data.
* @param {Texture} texture - The texture,
*/
copyBufferToTexture( buffer, texture ) {
const { gl, backend } = this;
const { state } = backend;
const { textureGPU, glTextureType, glFormat, glType } = backend.get( texture );
const { width, height } = texture.source.data;
gl.bindBuffer( gl.PIXEL_UNPACK_BUFFER, buffer );
backend.state.bindTexture( glTextureType, textureGPU );
state.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, false );
state.pixelStorei( gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false );
gl.texSubImage2D( glTextureType, 0, 0, 0, width, height, glFormat, glType, 0 );
gl.bindBuffer( gl.PIXEL_UNPACK_BUFFER, null );
backend.state.unbindTexture();
// debug
// const framebuffer = gl.createFramebuffer();
// backend.state.bindFramebuffer( gl.FRAMEBUFFER, framebuffer );
// gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, glTextureType, textureGPU, 0 );
// const readout = new Float32Array( width * height * 4 );
// const altFormat = gl.getParameter( gl.IMPLEMENTATION_COLOR_READ_FORMAT );
// const altType = gl.getParameter( gl.IMPLEMENTATION_COLOR_READ_TYPE );
// gl.readPixels( 0, 0, width, height, altFormat, altType, readout );
// backend.state.bindFramebuffer( gl.FRAMEBUFFER, null );
// log( readout );
}
/**
* Uploads the updated texture data to the GPU.
*
* @param {Texture} texture - The texture.
* @param {Object} [options={}] - Optional configuration parameter.
*/
updateTexture( texture, options ) {
const { gl } = this;
const { width, height } = options;
const { textureGPU, glTextureType, glFormat, glType, glInternalFormat } = this.backend.get( texture );
if ( texture.isRenderTargetTexture || ( textureGPU === undefined /* unsupported texture format */ ) )
return;
this.backend.state.bindTexture( glTextureType, textureGPU );
this.setTextureParameters( glTextureType, texture );
if ( texture.isCompressedCubeTexture ) {
for ( let i = 0; i < 6; i ++ ) {
const mipmaps = texture.image[ i ].mipmaps;
for ( let j = 0; j < mipmaps.length; j ++ ) {
const mipmap = mipmaps[ j ];
if ( glFormat === gl.RGBA ) {
gl.texSubImage2D( gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j, 0, 0, mipmap.width, mipmap.height, glFormat, glType, mipmap.data );
} else if ( glFormat !== null ) {
gl.compressedTexSubImage2D( gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j, 0, 0, mipmap.width, mipmap.height, glFormat, mipmap.data );
} else {
warn( 'WebGLBackend: Unsupported compressed texture format' );
}
}
}
} else if ( texture.isCompressedTexture ) {
const mipmaps = texture.mipmaps;
const image = options.image;
const isCompressedFormat = ( glFormat !== null && glFormat !== gl.RGBA );
for ( let i = 0; i < mipmaps.length; i ++ ) {
const mipmap = mipmaps[ i ];
if ( texture.isCompressedArrayTexture ) {
if ( isCompressedFormat ) {
if ( texture.layerUpdates.size > 0 ) {
const layerByteLength = getByteLength( mipmap.width, mipmap.height, texture.format, texture.type );
for ( const layerIndex of texture.layerUpdates ) {
const layerData = mipmap.data.subarray(
layerIndex * layerByteLength / mipmap.data.BYTES_PER_ELEMENT,
( layerIndex + 1 ) * layerByteLength / mipmap.data.BYTES_PER_ELEMENT
);
gl.compressedTexSubImage3D( gl.TEXTURE_2D_ARRAY, i, 0, 0, layerIndex, mipmap.width, mipmap.height, 1, glFormat, layerData );
}
} else {
gl.compressedTexSubImage3D( gl.TEXTURE_2D_ARRAY, i, 0, 0, 0, mipmap.width, mipmap.height, image.depth, glFormat, mipmap.data );
}
} else if ( glFormat === gl.RGBA ) {
gl.texSubImage3D( gl.TEXTURE_2D_ARRAY, i, 0, 0, 0, mipmap.width, mipmap.height, image.depth, glFormat, glType, mipmap.data );
} else {
warn( 'WebGLBackend: Unsupported compressed texture format.' );
}
} else {
if ( isCompressedFormat ) {
gl.compressedTexSubImage2D( gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, mipmap.data );
} else if ( glFormat === gl.RGBA ) {
gl.texSubImage2D( gl.TEXTURE_2D, i, 0, 0, mipmap.width, mipmap.height, glFormat, glType, mipmap.data );
} else {
warn( 'WebGLBackend: Unsupported compressed texture format.' );
}
}
}
if ( texture.isCompressedArrayTexture && texture.layerUpdates.size > 0 ) texture.clearLayerUpdates();
} else if ( texture.isCubeTexture ) {
const images = options.images;
const mipmaps = texture.mipmaps;
for ( let i = 0; i < 6; i ++ ) {
const image = getImage( images[ i ] );
gl.texSubImage2D( gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, 0, 0, width, height, glFormat, glType, image );
for ( let j = 0; j < mipmaps.length; j ++ ) {
const mipmap = mipmaps[ j ];
const image = getImage( mipmap.images[ i ] );
gl.texSubImage2D( gl.TEXTURE_CUBE_MAP_POSITIVE_X + i, j + 1, 0, 0, image.width, image.height, glFormat, glType, image );
}
}
} else if ( texture.isDataArrayTexture || texture.isArrayTexture ) {
const image = options.image;
if ( texture.layerUpdates.size > 0 ) {
const layerByteLength = getByteLength( image.width, image.height, texture.format, texture.type );
for ( const layerIndex of texture.layerUpdates ) {
const layerData = image.data.subarray(
layerIndex * layerByteLength / image.data.BYTES_PER_ELEMENT,
( layerIndex + 1 ) * layerByteLength / image.data.BYTES_PER_ELEMENT
);
gl.texSubImage3D( gl.TEXTURE_2D_ARRAY, 0, 0, 0, layerIndex, image.width, image.height, 1, glFormat, glType, layerData );
}
texture.clearLayerUpdates();
} else {
gl.texSubImage3D( gl.TEXTURE_2D_ARRAY, 0, 0, 0, 0, image.width, image.height, image.depth, glFormat, glType, image.data );
}
} else if ( texture.isData3DTexture ) {
const image = options.image;
gl.texSubImage3D( gl.TEXTURE_3D, 0, 0, 0, 0, image.width, image.height, image.depth, glFormat, glType, image.data );
} else if ( texture.isVideoTexture ) {
texture.update();
gl.texImage2D( glTextureType, 0, glInternalFormat, glFormat, glType, options.image );
} else if ( texture.isHTMLTexture ) {
if ( typeof gl.texElementImage2D === 'function' ) {
if ( gl.texElementImage2D.length === 3 ) {
// Chrome 150+
gl.texElementImage2D( gl.TEXTURE_2D, gl.RGBA8, options.image );
} else {
// Chrome 138 - 149
gl.texElementImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, options.image );
}
}
} else {
const mipmaps = texture.mipmaps;
if ( mipmaps.length > 0 ) {
for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {
const mipmap = mipmaps[ i ];
const image = getImage( mipmap );
gl.texSubImage2D( glTextureType, i, 0, 0, mipmap.width, mipmap.height, glFormat, glType, image );
}
} else {
const image = getImage( options.image );
gl.texSubImage2D( glTextureType, 0, 0, 0, width, height, glFormat, glType, image );
}
}
}
/**
* Generates mipmaps for the given texture.
*
* @param {Texture} texture - The texture.
*/
generateMipmaps( texture ) {
const { gl, backend } = this;
const { textureGPU, glTextureType } = backend.get( texture );
backend.state.bindTexture( glTextureType, textureGPU );
gl.generateMipmap( glTextureType );
}
/**
* Deallocates the render buffers of the given render target.
*
* @param {RenderTarget} renderTarget - The render target.
*/
deallocateRenderBuffers( renderTarget ) {
const { gl, backend } = this;
// remove framebuffer reference
if ( renderTarget ) {
const renderContextData = backend.get( renderTarget );
renderContextData.renderBufferStorageSetup = undefined;
if ( renderContextData.framebuffers ) {
for ( const cacheKey in renderContextData.framebuffers ) {
gl.deleteFramebuffer( renderContextData.framebuffers[ cacheKey ] );
}
delete renderContextData.framebuffers;
}
if ( renderContextData.depthRenderbuffer ) {
gl.deleteRenderbuffer( renderContextData.depthRenderbuffer );
delete renderContextData.depthRenderbuffer;
}
if ( renderContextData.stencilRenderbuffer ) {
gl.deleteRenderbuffer( renderContextData.stencilRenderbuffer );
delete renderContextData.stencilRenderbuffer;
}
if ( renderContextData.msaaFrameBuffer ) {
gl.deleteFramebuffer( renderContextData.msaaFrameBuffer );
delete renderContextData.msaaFrameBuffer;
}
if ( renderContextData.msaaRenderbuffers ) {
for ( let i = 0; i < renderContextData.msaaRenderbuffers.length; i ++ ) {
gl.deleteRenderbuffer( renderContextData.msaaRenderbuffers[ i ] );
}
delete renderContextData.msaaRenderbuffers;
}
}
}
/**
* Destroys the GPU data for the given texture object.
*
* @param {Texture} texture - The texture.
* @param {boolean} [isDefaultTexture=false] - Whether the texture uses a default GPU texture or not.
*/
destroyTexture( texture, isDefaultTexture = false ) {
const { gl, backend } = this;
const { textureGPU, renderTarget } = backend.get( texture );
this.deallocateRenderBuffers( renderTarget );
if ( isDefaultTexture === false && texture.isExternalTexture !== true ) {
gl.deleteTexture( textureGPU );
}
backend.delete( texture );
}
/**
* Copies data of the given source texture to the given destination texture.
*
* @param {Texture} srcTexture - The source texture.
* @param {Texture} dstTexture - The destination texture.
* @param {?(Box3|Box2)} [srcRegion=null] - The region of the source texture to copy.
* @param {?(Vector2|Vector3)} [dstPosition=null] - The destination position of the copy.
* @param {number} [srcLevel=0] - The source mip level to copy from.
* @param {number} [dstLevel=0] - The destination mip level to copy to.
*/
copyTextureToTexture( srcTexture, dstTexture, srcRegion = null, dstPosition = null, srcLevel = 0, dstLevel = 0 ) {
const { gl, backend } = this;
const { state } = this.backend;
const { textureGPU: dstTextureGPU, glTextureType, glType, glFormat } = backend.get( dstTexture );
state.bindTexture( glTextureType, dstTextureGPU );
// gather the necessary dimensions to copy
let width, height, depth, minX, minY, minZ;
let dstX, dstY, dstZ;
const image = srcTexture.isCompressedTexture ? srcTexture.mipmaps[ dstLevel ] : srcTexture.image;
if ( srcRegion !== null ) {
width = srcRegion.max.x - srcRegion.min.x;
height = srcRegion.max.y - srcRegion.min.y;
depth = srcRegion.isBox3 ? srcRegion.max.z - srcRegion.min.z : 1;
minX = srcRegion.min.x;
minY = srcRegion.min.y;
minZ = srcRegion.isBox3 ? srcRegion.min.z : 0;
} else {
const levelScale = Math.pow( 2, - srcLevel );
width = Math.floor( image.width * levelScale );
height = Math.floor( image.height * levelScale );
if ( srcTexture.isDataArrayTexture || srcTexture.isArrayTexture ) {
depth = image.depth;
} else if ( srcTexture.isData3DTexture ) {
depth = Math.floor( image.depth * levelScale );
} else {
depth = 1;
}
minX = 0;
minY = 0;
minZ = 0;
}
if ( dstPosition !== null ) {
dstX = dstPosition.x;
dstY = dstPosition.y;
dstZ = dstPosition.z;
} else {
dstX = 0;
dstY = 0;
dstZ = 0;
}
state.pixelStorei( gl.UNPACK_FLIP_Y_WEBGL, dstTexture.flipY );
state.pixelStorei( gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, dstTexture.premultiplyAlpha );
state.pixelStorei( gl.UNPACK_ALIGNMENT, dstTexture.unpackAlignment );
// used for copying data from cpu
const currentUnpackRowLen = state.getParameter( gl.UNPACK_ROW_LENGTH );
const currentUnpackImageHeight = state.getParameter( gl.UNPACK_IMAGE_HEIGHT );
const currentUnpackSkipPixels = state.getParameter( gl.UNPACK_SKIP_PIXELS );
const currentUnpackSkipRows = state.getParameter( gl.UNPACK_SKIP_ROWS );
const currentUnpackSkipImages = state.getParameter( gl.UNPACK_SKIP_IMAGES );
state.pixelStorei( gl.UNPACK_ROW_LENGTH, image.width );
state.pixelStorei( gl.UNPACK_IMAGE_HEIGHT, image.height );
state.pixelStorei( gl.UNPACK_SKIP_PIXELS, minX );
state.pixelStorei( gl.UNPACK_SKIP_ROWS, minY );
state.pixelStorei( gl.UNPACK_SKIP_IMAGES, minZ );
// set up the src texture
const isSrc3D = srcTexture.isDataArrayTexture || srcTexture.isData3DTexture || srcTexture.isArrayTexture;
const isDst3D = dstTexture.isDataArrayTexture || dstTexture.isData3DTexture || dstTexture.isArrayTexture;
if ( srcTexture.isDepthTexture ) {
const srcTextureData = backend.get( srcTexture );
if ( this._srcFramebuffer === null ) this._srcFramebuffer = gl.createFramebuffer();
if ( this._dstFramebuffer === null ) this._dstFramebuffer = gl.createFramebuffer();
const attachment = srcTexture.format === DepthStencilFormat ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT;
const prevReadFramebuffer = state.currentBoundFramebuffers[ gl.READ_FRAMEBUFFER ] ?? null;
const prevDrawFramebuffer = state.currentBoundFramebuffers[ gl.DRAW_FRAMEBUFFER ] ?? null;
state.bindFramebuffer( gl.READ_FRAMEBUFFER, this._srcFramebuffer );
state.bindFramebuffer( gl.DRAW_FRAMEBUFFER, this._dstFramebuffer );
// color attachments of previous copies would make the framebuffers incomplete
gl.framebufferTexture2D( gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, null, 0 );
gl.framebufferTexture2D( gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, null, 0 );
for ( let i = 0; i < depth; i ++ ) {
// if the source or destination are a 3d target then a layer needs to be bound
if ( isSrc3D ) {
gl.framebufferTextureLayer( gl.READ_FRAMEBUFFER, attachment, srcTextureData.textureGPU, srcLevel, minZ + i );
} else {
gl.framebufferTexture2D( gl.READ_FRAMEBUFFER, attachment, gl.TEXTURE_2D, srcTextureData.textureGPU, srcLevel );
}
if ( isDst3D ) {
gl.framebufferTextureLayer( gl.DRAW_FRAMEBUFFER, attachment, dstTextureGPU, dstLevel, dstZ + i );
} else {
gl.framebufferTexture2D( gl.DRAW_FRAMEBUFFER, attachment, gl.TEXTURE_2D, dstTextureGPU, dstLevel );
}
gl.blitFramebuffer( minX, minY, minX + width, minY + height, dstX, dstY, dstX + width, dstY + height, gl.DEPTH_BUFFER_BIT, gl.NEAREST );
}
// detach the depth textures so subsequent color copies can reuse the framebuffers
gl.framebufferTexture2D( gl.READ_FRAMEBUFFER, attachment, gl.TEXTURE_2D, null, 0 );
gl.framebufferTexture2D( gl.DRAW_FRAMEBUFFER, attachment, gl.TEXTURE_2D, null, 0 );
state.bindFramebuffer( gl.READ_FRAMEBUFFER, prevReadFramebuffer );
state.bindFramebuffer( gl.DRAW_FRAMEBUFFER, prevDrawFramebuffer );
} else if ( srcLevel !== 0 || srcTexture.isRenderTargetTexture || backend.has( srcTexture ) ) {
// get the appropriate frame buffers
const srcTextureData = backend.get( srcTexture );
if ( this._srcFramebuffer === null ) this._srcFramebuffer = gl.createFramebuffer();
if ( this._dstFramebuffer === null ) this._dstFramebuffer = gl.createFramebuffer();
const prevReadFramebuffer = state.currentBoundFramebuffers[ gl.READ_FRAMEBUFFER ] ?? null;
const prevDrawFramebuffer = state.currentBoundFramebuffers[ gl.DRAW_FRAMEBUFFER ] ?? null;
// bind the frame buffer targets
state.bindFramebuffer( gl.READ_FRAMEBUFFER, this._srcFramebuffer );
state.bindFramebuffer( gl.DRAW_FRAMEBUFFER, this._dstFramebuffer );
for ( let i = 0; i < depth; i ++ ) {
// assign the correct layers and mip maps to the frame buffers
if ( isSrc3D ) {
gl.framebufferTextureLayer( gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, srcTextureData.textureGPU, srcLevel, minZ + i );
} else {
gl.framebufferTexture2D( gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, srcTextureData.textureGPU, srcLevel );
}
if ( isDst3D ) {
gl.framebufferTextureLayer( gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, dstTextureGPU, dstLevel, dstZ + i );
} else {
gl.framebufferTexture2D( gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, dstTextureGPU, dstLevel );
}
// copy the data using the fastest function that can achieve the copy
if ( srcLevel !== 0 ) {
gl.blitFramebuffer( minX, minY, minX + width, minY + height, dstX, dstY, dstX + width, dstY + height, gl.COLOR_BUFFER_BIT, gl.NEAREST );
} else if ( isDst3D ) {
gl.copyTexSubImage3D( glTextureType, dstLevel, dstX, dstY, dstZ + i, minX, minY, width, height );
} else {
gl.copyTexSubImage2D( glTextureType, dstLevel, dstX, dstY, minX, minY, width, height );
}
}
// restore previous read, draw framebuffer bindings
state.bindFramebuffer( gl.READ_FRAMEBUFFER, prevReadFramebuffer );
state.bindFramebuffer( gl.DRAW_FRAMEBUFFER, prevDrawFramebuffer );
} else {
if ( isDst3D ) {
// copy data into the 3d texture
if ( srcTexture.isDataTexture || srcTexture.isData3DTexture ) {
gl.texSubImage3D( glTextureType, dstLevel, dstX, dstY, dstZ, width, height, depth, glFormat, glType, image.data );
} else if ( dstTexture.isCompressedArrayTexture ) {
gl.compressedTexSubImage3D( glTextureType, dstLevel, dstX, dstY, dstZ, width, height, depth, glFormat, image.data );
} else {
gl.texSubImage3D( glTextureType, dstLevel, dstX, dstY, dstZ, width, height, depth, glFormat, glType, image );
}
} else {
// copy data into the 2d texture
if ( srcTexture.isDataTexture ) {
gl.texSubImage2D( gl.TEXTURE_2D, dstLevel, dstX, dstY, width, height, glFormat, glType, image.data );
} else if ( srcTexture.isCompressedTexture ) {
gl.compressedTexSubImage2D( gl.TEXTURE_2D, dstLevel, dstX, dstY, image.width, image.height, glFormat, image.data );
} else {
gl.texSubImage2D( gl.TEXTURE_2D, dstLevel, dstX, dstY, width, height, glFormat, glType, image );
}
}
}
// reset values
state.pixelStorei( gl.UNPACK_ROW_LENGTH, currentUnpackRowLen );
state.pixelStorei( gl.UNPACK_IMAGE_HEIGHT, currentUnpackImageHeight );
state.pixelStorei( gl.UNPACK_SKIP_PIXELS, currentUnpackSkipPixels );
state.pixelStorei( gl.UNPACK_SKIP_ROWS, currentUnpackSkipRows );
state.pixelStorei( gl.UNPACK_SKIP_IMAGES, currentUnpackSkipImages );
// Generate mipmaps only when copying level 0
if ( dstLevel === 0 && dstTexture.generateMipmaps === true && dstTexture.mipmapsAutoUpdate === true ) {
gl.generateMipmap( glTextureType );
}
state.unbindTexture();
}
/**
* Copies the current bound framebuffer to the given texture.
*
* @param {Texture} texture - The destination texture.
* @param {RenderContext} renderContext - The render context.
* @param {Vector4} rectangle - A four dimensional vector defining the origin and dimension of the copy.
*/
copyFramebufferToTexture( texture, renderContext, rectangle ) {
const { gl } = this;
const { state } = this.backend;
const { textureGPU } = this.backend.get( texture );
const { x, y, z: width, w: height } = rectangle;
const requireDrawFrameBuffer = texture.isDepthTexture === true || ( renderContext.renderTarget && renderContext.renderTarget.samples > 0 );
const srcHeight = renderContext.renderTarget ? renderContext.renderTarget.height : this.backend.getDrawingBufferSize().y;
if ( requireDrawFrameBuffer ) {
const partial = ( x !== 0 || y !== 0 );
let mask;
let attachment;
if ( texture.isDepthTexture === true ) {
mask = gl.DEPTH_BUFFER_BIT;
attachment = gl.DEPTH_ATTACHMENT;
if ( renderContext.stencil ) {
mask |= gl.STENCIL_BUFFER_BIT;
}
} else {
mask = gl.COLOR_BUFFER_BIT;
attachment = gl.COLOR_ATTACHMENT0;
}
if ( partial ) {
const renderTargetContextData = this.backend.get( renderContext.renderTarget );
const fb = renderTargetContextData.framebuffers[ renderContext.getCacheKey() ];
const msaaFrameBuffer = renderTargetContextData.msaaFrameBuffer;
state.bindFramebuffer( gl.DRAW_FRAMEBUFFER, fb );
state.bindFramebuffer( gl.READ_FRAMEBUFFER, msaaFrameBuffer );
const flippedY = srcHeight - y - height;
gl.blitFramebuffer( x, flippedY, x + width, flippedY + height, x, flippedY, x + width, flippedY + height, mask, gl.NEAREST );
state.bindFramebuffer( gl.READ_FRAMEBUFFER, fb );
state.bindTexture( gl.TEXTURE_2D, textureGPU );
gl.copyTexSubImage2D( gl.TEXTURE_2D, 0, 0, 0, x, flippedY, width, height );
state.unbindTexture();
} else {
const fb = gl.createFramebuffer();
state.bindFramebuffer( gl.DRAW_FRAMEBUFFER, fb );
gl.framebufferTexture2D( gl.DRAW_FRAMEBUFFER, attachment, gl.TEXTURE_2D, textureGPU, 0 );
gl.blitFramebuffer( 0, 0, width, height, 0, 0, width, height, mask, gl.NEAREST );
gl.deleteFramebuffer( fb );
}
} else {
state.bindTexture( gl.TEXTURE_2D, textureGPU );
gl.copyTexSubImage2D( gl.TEXTURE_2D, 0, 0, 0, x, srcHeight - height - y, width, height );
state.unbindTexture();
}
if ( texture.generateMipmaps === true && texture.mipmapsAutoUpdate === true ) this.generateMipmaps( texture );
this.backend._setFramebuffer( renderContext );
}
/**
* SetupS storage for internal depth/stencil buffers and bind to correct framebuffer.
*
* @param {WebGLRenderbuffer} renderbuffer - The render buffer.
* @param {RenderContext} renderContext - The render context.
* @param {number} samples - The MSAA sample count.
* @param {boolean} [useMultisampledRTT=false] - Whether to use WEBGL_multisampled_render_to_texture or not.
*/
setupRenderBufferStorage( renderbuffer, renderContext, samples, useMultisampledRTT = false ) {
const { gl } = this;
const { depthTexture, renderTarget } = renderContext;
const { depthBuffer, stencilBuffer, width, height } = renderTarget;
gl.bindRenderbuffer( gl.RENDERBUFFER, renderbuffer );
if ( depthBuffer && ! stencilBuffer ) {
let glInternalFormat = gl.DEPTH_COMPONENT24;
if ( useMultisampledRTT === true ) {
const multisampledRTTExt = this.extensions.get( 'WEBGL_multisampled_render_to_texture' );
multisampledRTTExt.renderbufferStorageMultisampleEXT( gl.RENDERBUFFER, renderTarget.samples, glInternalFormat, width, height );
} else if ( samples > 0 ) {
if ( depthTexture && depthTexture.isDepthTexture ) {
if ( depthTexture.type === FloatType ) {
glInternalFormat = gl.DEPTH_COMPONENT32F;
}
}
gl.renderbufferStorageMultisample( gl.RENDERBUFFER, samples, glInternalFormat, width, height );
} else {
gl.renderbufferStorage( gl.RENDERBUFFER, glInternalFormat, width, height );
}
gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderbuffer );
} else if ( depthBuffer && stencilBuffer ) {
if ( samples > 0 ) {
let glInternalFormat = gl.DEPTH24_STENCIL8;
if ( depthTexture && depthTexture.isDepthTexture ) {
if ( depthTexture.type === FloatType ) {
glInternalFormat = gl.DEPTH32F_STENCIL8;
}
}
gl.renderbufferStorageMultisample( gl.RENDERBUFFER, samples, glInternalFormat, width, height );
} else {
gl.renderbufferStorage( gl.RENDERBUFFER, gl.DEPTH_STENCIL, width, height );
}
gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, renderbuffer );
}
gl.bindRenderbuffer( gl.RENDERBUFFER, null );
}
/**
* Returns texture data as a typed array.
*
* @async
* @param {Texture} texture - The texture to copy.
* @param {number} x - The x coordinate of the copy origin.
* @param {number} y - The y coordinate of the copy origin.
* @param {number} width - The width of the copy.
* @param {number} height - The height of the copy.
* @param {number} faceIndex - The cube face, depth slice or array layer index.
* @return {Promise<TypedArray>} A Promise that resolves with a typed array when the copy operation has finished.
*/
async copyTextureToBuffer( texture, x, y, width, height, faceIndex ) {
const { backend, gl } = this;
const { textureGPU, glFormat, glType } = this.backend.get( texture );
const fb = gl.createFramebuffer();
backend.state.bindFramebuffer( gl.READ_FRAMEBUFFER, fb );
if ( texture.isData3DTexture || texture.isDataArrayTexture || texture.isArrayTexture ) {
gl.framebufferTextureLayer( gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, textureGPU, 0, faceIndex );
} else {
const target = texture.isCubeTexture ? gl.TEXTURE_CUBE_MAP_POSITIVE_X + faceIndex : gl.TEXTURE_2D;
gl.framebufferTexture2D( gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, target, textureGPU, 0 );
}
const typedArrayType = this._getTypedArrayType( glType );
const bytesPerTexel = this._getBytesPerTexel( glType, glFormat );
const elementCount = width * height;
const byteLength = elementCount * bytesPerTexel;
const buffer = gl.createBuffer();
backend.state.pixelStorei( gl.PACK_ALIGNMENT, 1 );
gl.bindBuffer( gl.PIXEL_PACK_BUFFER, buffer );
gl.bufferData( gl.PIXEL_PACK_BUFFER, byteLength, gl.STREAM_READ );
gl.readPixels( x, y, width, height, glFormat, glType, 0 );
gl.bindBuffer( gl.PIXEL_PACK_BUFFER, null );
await backend.utils._clientWaitAsync();
const dstBuffer = new typedArrayType( byteLength / typedArrayType.BYTES_PER_ELEMENT );
gl.bindBuffer( gl.PIXEL_PACK_BUFFER, buffer );
gl.getBufferSubData( gl.PIXEL_PACK_BUFFER, 0, dstBuffer );
gl.bindBuffer( gl.PIXEL_PACK_BUFFER, null );
backend.state.bindFramebuffer( gl.READ_FRAMEBUFFER, null );
gl.deleteBuffer( buffer );
gl.deleteFramebuffer( fb );
return dstBuffer;
}
/**
* Returns the corresponding typed array type for the given WebGL data type.
*
* @private
* @param {GLenum} glType - The WebGL data type.
* @return {TypedArray.constructor} The typed array type.
*/
_getTypedArrayType( glType ) {
const { gl } = this;
if ( glType === gl.UNSIGNED_BYTE ) return Uint8Array;
if ( glType === gl.BYTE ) return Int8Array;
if ( glType === gl.UNSIGNED_SHORT_4_4_4_4 ) return Uint16Array;
if ( glType === gl.UNSIGNED_SHORT_5_5_5_1 ) return Uint16Array;
if ( glType === gl.UNSIGNED_SHORT_5_6_5 ) return Uint16Array;
if ( glType === gl.UNSIGNED_SHORT ) return Uint16Array;
if ( glType === gl.SHORT ) return Int16Array;
if ( glType === gl.UNSIGNED_INT ) return Uint32Array;
if ( glType === gl.INT ) return Int32Array;
if ( glType === gl.UNSIGNED_INT_5_9_9_9_REV ) return Uint32Array;
if ( glType === gl.UNSIGNED_INT_10F_11F_11F_REV ) return Uint32Array;
if ( glType === gl.HALF_FLOAT ) return Uint16Array;
if ( glType === gl.FLOAT ) return Float32Array;
throw new Error( `THREE.WebGLTextureUtils: Unsupported WebGL type: ${glType}` );
}
/**
* Returns the bytes-per-texel value for the given WebGL data type and texture format.
*
* @private
* @param {GLenum} glType - The WebGL data type.
* @param {GLenum} glFormat - The WebGL texture format.
* @return {number} The bytes-per-texel.
*/
_getBytesPerTexel( glType, glFormat ) {
const { gl } = this;
// packed types store all components of a texel in a single value
if ( glType === gl.UNSIGNED_SHORT_4_4_4_4 ||
glType === gl.UNSIGNED_SHORT_5_5_5_1 ||
glType === gl.UNSIGNED_SHORT_5_6_5 ) return 2;
if ( glType === gl.UNSIGNED_INT_5_9_9_9_REV ||
glType === gl.UNSIGNED_INT_10F_11F_11F_REV ) return 4;
let bytesPerComponent = 0;
if ( glType === gl.UNSIGNED_BYTE ||
glType === gl.BYTE ) bytesPerComponent = 1;
if ( glType === gl.UNSIGNED_SHORT ||
glType === gl.SHORT ||
glType === gl.HALF_FLOAT ) bytesPerComponent = 2;
if ( glType === gl.UNSIGNED_INT ||
glType === gl.INT ||
glType === gl.FLOAT ) bytesPerComponent = 4;
if ( glFormat === gl.RGBA || glFormat === gl.RGBA_INTEGER ) return bytesPerComponent * 4;
if ( glFormat === gl.RGB ) return bytesPerComponent * 3;
if ( glFormat === gl.RG || glFormat === gl.RG_INTEGER ) return bytesPerComponent * 2;
if ( glFormat === gl.RED || glFormat === gl.RED_INTEGER || glFormat === gl.ALPHA ) return bytesPerComponent;
}
/**
* Frees the internal resources.
*/
dispose() {
const { gl } = this;
if ( this._srcFramebuffer !== null ) gl.deleteFramebuffer( this._srcFramebuffer );
if ( this._dstFramebuffer !== null ) gl.deleteFramebuffer( this._dstFramebuffer );
}
}
function getImage( source ) {
if ( source.isDataTexture ) {
return source.image.data;
} else if ( ( typeof HTMLImageElement !== 'undefined' && source instanceof HTMLImageElement ) ||
( typeof HTMLCanvasElement !== 'undefined' && source instanceof HTMLCanvasElement ) ||
( typeof ImageBitmap !== 'undefined' && source instanceof ImageBitmap ) ||
( typeof OffscreenCanvas !== 'undefined' && source instanceof OffscreenCanvas ) ) {
return source;
}
return source.data;
}
/**
* A WebGL 2 backend utility module for managing extensions.
*
* @private
*/
class WebGLExtensions {
/**
* Constructs a new utility object.
*
* @param {WebGLBackend} backend - The WebGL 2 backend.
*/
constructor( backend ) {
/**
* A reference to the WebGL 2 backend.
*
* @type {WebGLBackend}
*/
this.backend = backend;
/**
* A reference to the rendering context.
*
* @type {WebGL2RenderingContext}
*/
this.gl = this.backend.gl;
/**
* A list with all the supported WebGL extensions.
*
* @type {Array<string>}
*/
this.availableExtensions = this.gl.getSupportedExtensions();
/**
* A dictionary with requested WebGL extensions.
* The key is the name of the extension, the value
* the requested extension object.
*
* @type {Object<string,Object>}
*/
this.extensions = {};
}
/**
* Returns the extension object for the given extension name.
*
* @param {string} name - The extension name.
* @return {Object} The extension object.
*/
get( name ) {
let extension = this.extensions[ name ];
if ( extension === undefined ) {
extension = this.gl.getExtension( name );
this.extensions[ name ] = extension;
}
return extension;
}
/**
* Returns `true` if the requested extension is available.
*
* @param {string} name - The extension name.
* @return {boolean} Whether the given extension is available or not.
*/
has( name ) {
return this.availableExtensions.includes( name );
}
}
/**
* A WebGL 2 backend utility module for managing the device's capabilities.
*
* @private
*/
class WebGLCapabilities {
/**
* Constructs a new utility object.
*
* @param {WebGLBackend} backend - The WebGL 2 backend.
*/
constructor( backend ) {
/**
* A reference to the WebGL 2 backend.
*
* @type {WebGLBackend}
*/
this.backend = backend;
/**
* This value holds the cached max anisotropy value.
*
* @type {?number}
* @default null
*/
this.maxAnisotropy = null;
/**
* This value holds the cached max uniform block size value.
*
* @type {?number}
* @default null
*/
this.maxUniformBlockSize = null;
}
/**
* Returns the maximum anisotropy texture filtering value. This value
* depends on the device and is reported by the `EXT_texture_filter_anisotropic`
* WebGL extension.
*
* @return {number} The maximum anisotropy texture filtering value.
*/
getMaxAnisotropy() {
if ( this.maxAnisotropy !== null ) return this.maxAnisotropy;
const gl = this.backend.gl;
const extensions = this.backend.extensions;
if ( extensions.has( 'EXT_texture_filter_anisotropic' ) === true ) {
const extension = extensions.get( 'EXT_texture_filter_anisotropic' );
this.maxAnisotropy = gl.getParameter( extension.MAX_TEXTURE_MAX_ANISOTROPY_EXT );
} else {
this.maxAnisotropy = 0;
}
return this.maxAnisotropy;
}
/**
* Returns the maximum number of bytes available for uniform buffers.
*
* @return {number} The maximum number of bytes available for uniform buffers.
*/
getUniformBufferLimit() {
if ( this.maxUniformBlockSize !== null ) return this.maxUniformBlockSize;
const gl = this.backend.gl;
this.maxUniformBlockSize = gl.getParameter( gl.MAX_UNIFORM_BLOCK_SIZE );
return this.maxUniformBlockSize;
}
}
const GLFeatureName = {
'WEBGL_multi_draw': 'WEBGL_multi_draw',
'WEBGL_compressed_texture_astc': 'texture-compression-astc',
'WEBGL_compressed_texture_etc': 'texture-compression-etc2',
'WEBGL_compressed_texture_etc1': 'texture-compression-etc1',
'WEBGL_compressed_texture_pvrtc': 'texture-compression-pvrtc',
'WEBGL_compressed_texture_s3tc': 'texture-compression-s3tc',
'EXT_texture_compression_bptc': 'texture-compression-bc',
'EXT_disjoint_timer_query_webgl2': 'timestamp-query',
'OVR_multiview2': 'OVR_multiview2'
};
class WebGLBufferRenderer {
constructor( backend ) {
this.gl = backend.gl;
this.extensions = backend.extensions;
this.info = backend.renderer.info;
this.mode = null;
this.index = 0;
this.type = null;
this.object = null;
}
render( start, count ) {
const { gl, mode, object, type, info, index } = this;
if ( index !== 0 ) {
gl.drawElements( mode, count, type, start );
} else {
gl.drawArrays( mode, start, count );
}
info.update( object, count, 1 );
}
renderInstances( start, count, primcount ) {
const { gl, mode, type, index, object, info } = this;
if ( primcount === 0 ) return;
if ( index !== 0 ) {
gl.drawElementsInstanced( mode, count, type, start, primcount );
} else {
gl.drawArraysInstanced( mode, start, count, primcount );
}
info.update( object, count, primcount );
}
renderMultiDraw( starts, counts, drawCount ) {
const { extensions, mode, object, info } = this;
if ( drawCount === 0 ) return;
const extension = extensions.get( 'WEBGL_multi_draw' );
if ( extension === null ) {
for ( let i = 0; i < drawCount; i ++ ) {
this.render( starts[ i ], counts[ i ] );
}
} else {
if ( this.index !== 0 ) {
extension.multiDrawElementsWEBGL( mode, counts, 0, this.type, starts, 0, drawCount );
} else {
extension.multiDrawArraysWEBGL( mode, starts, 0, counts, 0, drawCount );
}
let elementCount = 0;
for ( let i = 0; i < drawCount; i ++ ) {
elementCount += counts[ i ];
}
info.update( object, elementCount, 1 );
}
}
//
}
/**
* Abstract base class of a timestamp query pool.
*
* @abstract
*/
class TimestampQueryPool {
/**
* Creates a new timestamp query pool.
*
* @param {number} [maxQueries=256] - Maximum number of queries this pool can hold.
*/
constructor( maxQueries = 256 ) {
/**
* Whether to track timestamps or not.
*
* @type {boolean}
* @default true
*/
this.trackTimestamp = true;
/**
* Maximum number of queries this pool can hold.
*
* @type {number}
* @default 256
*/
this.maxQueries = maxQueries;
/**
* How many queries allocated so far.
*
* @type {number}
* @default 0
*/
this.currentQueryIndex = 0;
/**
* Tracks offsets for different contexts.
*
* @type {Map<string, number>}
*/
this.queryOffsets = new Map();
/**
* Whether the pool has been disposed or not.
*
* @type {boolean}
* @default false
*/
this.isDisposed = false;
/**
* The total frame duration until the next update.
*
* @type {number}
* @default 0
*/
this.lastValue = 0;
/**
* Stores all timestamp frames.
*
* @type {Array<number>}
*/
this.frames = [];
/**
* This property is used to avoid multiple concurrent resolve operations.
* The WebGL backend uses it as a boolean flag. In context of WebGPU, it holds
* the promise of the current resolve operation.
*
* @type {boolean|Promise<number>}
* @default false
*/
this.pendingResolve = false;
/**
* Stores the latest timestamp for each render context.
*
* @type {Map<string, number>}
*/
this.timestamps = new Map();
}
/**
* Returns all timestamp frames.
*
* @return {Array<number>} The timestamp frames.
*/
getTimestampFrames() {
return this.frames;
}
/**
* Returns the timestamp for a given render context.
*
* @param {string} uid - A unique identifier for the render context.
* @return {?number} The timestamp, or undefined if not available.
*/
getTimestamp( uid ) {
let timestamp = this.timestamps.get( uid );
if ( timestamp === undefined ) {
warn( `TimestampQueryPool: No timestamp available for uid ${ uid }.` );
timestamp = 0;
}
return timestamp;
}
/**
* Returns whether a timestamp is available for a given render context.
*
* @param {string} uid - A unique identifier for the render context.
* @return {boolean} True if a timestamp is available, false otherwise.
*/
hasTimestampQuery( uid ) {
return this.timestamps.has( uid );
}
/**
* Allocate queries for a specific uid.
*
* @abstract
* @param {string} uid - A unique identifier for the render context.
* @param {number} frameId - The current frame identifier.
* @returns {?number}
*/
allocateQueriesForContext( /* uid, frameId */ ) {}
/**
* Resolve all timestamps and return data (or process them).
*
* @abstract
* @async
* @returns {Promise<number>|number} The resolved timestamp value.
*/
async resolveQueriesAsync() {}
/**
* Dispose of the query pool.
*
* @abstract
*/
dispose() {}
}
/**
* Manages a pool of WebGL timestamp queries for performance measurement.
* Handles creation, execution, and resolution of timer queries using WebGL extensions.
*
* @augments TimestampQueryPool
*/
class WebGLTimestampQueryPool extends TimestampQueryPool {
/**
* Creates a new WebGL timestamp query pool.
*
* @param {WebGLRenderingContext|WebGL2RenderingContext} gl - The WebGL context.
* @param {string} type - The type identifier for this query pool.
* @param {number} [maxQueries=2048] - Maximum number of queries this pool can hold.
*/
constructor( gl, type, maxQueries = 2048 ) {
super( maxQueries );
this.gl = gl;
this.type = type;
// Check for timer query extensions
this.ext = gl.getExtension( 'EXT_disjoint_timer_query_webgl2' ) ||
gl.getExtension( 'EXT_disjoint_timer_query' );
if ( ! this.ext ) {
warn( 'EXT_disjoint_timer_query not supported; timestamps will be disabled.' );
this.trackTimestamp = false;
return;
}
// Create query objects
this.queries = [];
for ( let i = 0; i < this.maxQueries; i ++ ) {
this.queries.push( gl.createQuery() );
}
this.activeQuery = null;
this.queryStates = new Map(); // Track state of each query: 'inactive', 'started', 'ended'
}
/**
* Allocates a pair of queries for a given render context.
*
* @param {string} uid - A unique identifier for the render context.
* @returns {?number} The base offset for the allocated queries, or null if allocation failed.
*/
allocateQueriesForContext( uid ) {
if ( ! this.trackTimestamp ) return null;
if ( this.currentQueryIndex + 2 > this.maxQueries ) {
this.resolveQueriesAsync();
this.currentQueryIndex = 0;
this.queryOffsets.clear();
this.queryStates.clear();
this.activeQuery = null;
}
const baseOffset = this.currentQueryIndex;
this.currentQueryIndex += 2;
// Initialize query states
this.queryStates.set( baseOffset, 'inactive' );
this.queryOffsets.set( uid, baseOffset );
return baseOffset;
}
/**
* Begins a timestamp query for the specified render context.
*
* @param {string} uid - A unique identifier for the render context.
*/
beginQuery( uid ) {
if ( ! this.trackTimestamp || this.isDisposed ) {
return;
}
const baseOffset = this.queryOffsets.get( uid );
if ( baseOffset == null ) {
return;
}
// Don't start a new query if there's an active one
if ( this.activeQuery !== null ) {
return;
}
const query = this.queries[ baseOffset ];
if ( ! query ) {
return;
}
try {
// Only begin if query is inactive
if ( this.queryStates.get( baseOffset ) === 'inactive' ) {
this.gl.beginQuery( this.ext.TIME_ELAPSED_EXT, query );
this.activeQuery = baseOffset;
this.queryStates.set( baseOffset, 'started' );
}
} catch ( e ) {
error( 'Error in beginQuery:', e );
this.activeQuery = null;
this.queryStates.set( baseOffset, 'inactive' );
}
}
/**
* Ends the active timestamp query for the specified render context.
*
* @param {string} uid - A unique identifier for the render context.
*/
endQuery( uid ) {
if ( ! this.trackTimestamp || this.isDisposed ) {
return;
}
const baseOffset = this.queryOffsets.get( uid );
if ( baseOffset == null ) {
return;
}
// Only end if this is the active query
if ( this.activeQuery !== baseOffset ) {
return;
}
try {
this.gl.endQuery( this.ext.TIME_ELAPSED_EXT );
this.queryStates.set( baseOffset, 'ended' );
this.activeQuery = null;
} catch ( e ) {
error( 'Error in endQuery:', e );
// Reset state on error
this.queryStates.set( baseOffset, 'inactive' );
this.activeQuery = null;
}
}
/**
* Asynchronously resolves all completed queries and returns the total duration.
*
* @async
* @returns {Promise<number>} The total duration in milliseconds, or the last valid value if resolution fails.
*/
async resolveQueriesAsync() {
if ( ! this.trackTimestamp || this.pendingResolve ) {
return this.lastValue;
}
this.pendingResolve = true;
try {
// Wait for all ended queries to complete
const resolvePromises = new Map();
for ( const [ uid, baseOffset ] of this.queryOffsets ) {
const state = this.queryStates.get( baseOffset );
if ( state === 'ended' ) {
const query = this.queries[ baseOffset ];
resolvePromises.set( uid, this.resolveQuery( query ) );
}
}
if ( resolvePromises.size === 0 ) {
return this.lastValue;
}
//
const framesDuration = {};
const frames = [];
this.timestamps.clear();
for ( const [ uid, promise ] of resolvePromises ) {
const match = uid.match( /^(.*):f(\d+)$/ );
const frame = parseInt( match[ 2 ] );
if ( frames.includes( frame ) === false ) {
frames.push( frame );
}
if ( framesDuration[ frame ] === undefined ) framesDuration[ frame ] = 0;
const duration = await promise;
this.timestamps.set( uid, duration );
framesDuration[ frame ] += duration;
}
// Return the total duration of the last frame
const totalDuration = framesDuration[ frames[ frames.length - 1 ] ];
// Store the last valid result
this.lastValue = totalDuration;
this.frames = frames;
// Reset states
this.currentQueryIndex = 0;
this.queryOffsets.clear();
this.queryStates.clear();
this.activeQuery = null;
return totalDuration;
} catch ( e ) {
error( 'Error resolving queries:', e );
return this.lastValue;
} finally {
this.pendingResolve = false;
}
}
/**
* Resolves a single query, checking for completion and disjoint operation.
*
* @async
* @param {WebGLQuery} query - The query object to resolve.
* @returns {Promise<number>} The elapsed time in milliseconds.
*/
async resolveQuery( query ) {
return new Promise( ( resolve ) => {
if ( this.isDisposed ) {
resolve( this.lastValue );
return;
}
let timeoutId;
let isResolved = false;
const cleanup = () => {
if ( timeoutId ) {
clearTimeout( timeoutId );
timeoutId = null;
}
};
const finalizeResolution = ( value ) => {
if ( ! isResolved ) {
isResolved = true;
cleanup();
resolve( value );
}
};
const checkQuery = () => {
if ( this.isDisposed ) {
finalizeResolution( this.lastValue );
return;
}
try {
// Check if the GPU timer was disjoint (i.e., timing was unreliable)
const disjoint = this.gl.getParameter( this.ext.GPU_DISJOINT_EXT );
if ( disjoint ) {
finalizeResolution( this.lastValue );
return;
}
const available = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT_AVAILABLE );
if ( ! available ) {
timeoutId = setTimeout( checkQuery, 1 );
return;
}
const elapsed = this.gl.getQueryParameter( query, this.gl.QUERY_RESULT );
resolve( Number( elapsed ) / 1e6 ); // Convert nanoseconds to milliseconds
} catch ( e ) {
error( 'Error checking query:', e );
resolve( this.lastValue );
}
};
checkQuery();
} );
}
/**
* Releases all resources held by this query pool.
* This includes deleting all query objects and clearing internal state.
*/
dispose() {
if ( this.isDisposed ) {
return;
}
this.isDisposed = true;
if ( ! this.trackTimestamp ) return;
for ( const query of this.queries ) {
this.gl.deleteQuery( query );
}
this.queries = [];
this.queryStates.clear();
this.queryOffsets.clear();
this.timestamps.clear();
this.frames = [];
this.lastValue = 0;
this.activeQuery = null;
}
}
const _invalidationArray = [];
/**
* A backend implementation targeting WebGL 2.
*
* @private
* @augments Backend
*/
class WebGLBackend extends Backend {
/**
* WebGLBackend options.
*
* @typedef {Object} WebGLBackend~Options
* @property {boolean} [logarithmicDepthBuffer=false] - Whether logarithmic depth buffer is enabled or not.
* @property {boolean} [reversedDepthBuffer=false] - Whether reversed depth buffer is enabled or not.
* @property {boolean} [alpha=true] - Whether the default framebuffer (which represents the final contents of the canvas) should be transparent or opaque.
* @property {boolean} [depth=true] - Whether the default framebuffer should have a depth buffer or not.
* @property {boolean} [stencil=false] - Whether the default framebuffer should have a stencil buffer or not.
* @property {boolean} [antialias=false] - Whether MSAA as the default anti-aliasing should be enabled or not.
* @property {number} [samples=0] - When `antialias` is `true`, `4` samples are used by default. Set this parameter to any other integer value than 0 to overwrite the default.
* @property {boolean} [forceWebGL=false] - If set to `true`, the renderer uses a WebGL 2 backend no matter if WebGPU is supported or not.
* @property {WebGL2RenderingContext} [context=undefined] - A WebGL 2 rendering context.
*/
/**
* Constructs a new WebGPU backend.
*
* @param {WebGLBackend~Options} [parameters] - The configuration parameter.
*/
constructor( parameters = {} ) {
super( parameters );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isWebGLBackend = true;
/**
* A reference to a backend module holding shader attribute-related
* utility functions.
*
* @type {?WebGLAttributeUtils}
* @default null
*/
this.attributeUtils = null;
/**
* A reference to a backend module holding vertex array object-related
* utility functions.
*
* @type {?WebGLVertexArrayUtils}
* @default null
*/
this.vertexArrayUtils = null;
/**
* A reference to a backend module holding extension-related
* utility functions.
*
* @type {?WebGLExtensions}
* @default null
*/
this.extensions = null;
/**
* A reference to a backend module holding capability-related
* utility functions.
*
* @type {?WebGLCapabilities}
* @default null
*/
this.capabilities = null;
/**
* A reference to a backend module holding texture-related
* utility functions.
*
* @type {?WebGLTextureUtils}
* @default null
*/
this.textureUtils = null;
/**
* A reference to a backend module holding renderer-related
* utility functions.
*
* @type {?WebGLBufferRenderer}
* @default null
*/
this.bufferRenderer = null;
/**
* A reference to the rendering context.
*
* @type {?WebGL2RenderingContext}
* @default null
*/
this.gl = null;
/**
* A reference to a backend module holding state-related
* utility functions.
*
* @type {?WebGLState}
* @default null
*/
this.state = null;
/**
* A reference to a backend module holding common
* utility functions.
*
* @type {?WebGLUtils}
* @default null
*/
this.utils = null;
/**
* Controls if `gl.RASTERIZER_DISCARD` should be enabled or not.
* Only relevant when using compute shaders.
*
* @type {boolean}
* @default false
*/
this.discard = false;
/**
* A reference to the `EXT_disjoint_timer_query_webgl2` extension. `null` if the
* device does not support the extension.
*
* @type {?EXTDisjointTimerQueryWebGL2}
* @default null
*/
this.disjoint = null;
/**
* A reference to the `KHR_parallel_shader_compile` extension. `null` if the
* device does not support the extension.
*
* @type {?KHRParallelShaderCompile}
* @default null
*/
this.parallel = null;
/**
* A reference to the current render context.
*
* @private
* @type {RenderContext}
* @default null
*/
this._currentContext = null;
/**
* A unique collection of bindings.
*
* @private
* @type {WeakSet<Array<BindGroup>>}
*/
this._knownBindings = new WeakSet();
/**
* Whether the device supports framebuffers invalidation or not.
*
* @private
* @type {boolean}
*/
this._supportsInvalidateFramebuffer = typeof navigator === 'undefined' ? false : /OculusBrowser/g.test( navigator.userAgent );
/**
* The target framebuffer when rendering with
* the WebXR device API.
*
* @private
* @type {?WebGLFramebuffer}
* @default null
*/
this._xrFramebuffer = null;
}
/**
* Initializes the backend so it is ready for usage.
*
* @param {Renderer} renderer - The renderer.
*/
init( renderer ) {
super.init( renderer );
//
const parameters = this.parameters;
const contextAttributes = {
antialias: renderer.currentSamples > 0,
alpha: true, // always true for performance reasons
depth: renderer.depth,
stencil: renderer.stencil
};
const glContext = ( parameters.context !== undefined ) ? parameters.context : renderer.domElement.getContext( 'webgl2', contextAttributes );
function onContextLost( event ) {
event.preventDefault();
const contextLossInfo = {
api: 'WebGL',
message: event.statusMessage || 'Unknown reason',
reason: null,
originalEvent: event
};
renderer.onDeviceLost( contextLossInfo );
}
this._onContextLost = onContextLost;
renderer.domElement.addEventListener( 'webglcontextlost', onContextLost, false );
this.gl = glContext;
this.extensions = new WebGLExtensions( this );
this.capabilities = new WebGLCapabilities( this );
this.attributeUtils = new WebGLAttributeUtils( this );
this.vertexArrayUtils = new WebGLVertexArrayUtils( this );
this.textureUtils = new WebGLTextureUtils( this );
this.bufferRenderer = new WebGLBufferRenderer( this );
this.state = new WebGLState( this );
this.utils = new WebGLUtils( this );
this.extensions.get( 'EXT_color_buffer_float' );
this.extensions.get( 'WEBGL_clip_cull_distance' );
this.extensions.get( 'OES_texture_float_linear' );
this.extensions.get( 'EXT_color_buffer_half_float' );
this.extensions.get( 'WEBGL_multisampled_render_to_texture' );
this.extensions.get( 'WEBGL_render_shared_exponent' );
this.extensions.get( 'WEBGL_multi_draw' );
this.extensions.get( 'OVR_multiview2' );
this.extensions.get( 'EXT_clip_control' );
this.disjoint = this.extensions.get( 'EXT_disjoint_timer_query_webgl2' );
this.parallel = this.extensions.get( 'KHR_parallel_shader_compile' );
this.drawBuffersIndexedExt = this.extensions.get( 'OES_draw_buffers_indexed' );
if ( parameters.reversedDepthBuffer ) {
if ( this.extensions.has( 'EXT_clip_control' ) ) {
renderer.reversedDepthBuffer = true;
} else {
warn( 'WebGPURenderer: Unable to use reversed depth buffer due to missing EXT_clip_control extension. Fallback to default depth buffer.' );
renderer.reversedDepthBuffer = false;
}
}
if ( renderer.reversedDepthBuffer ) {
this.state.setReversedDepth( true );
}
}
/**
* The coordinate system of the backend.
*
* @type {number}
* @readonly
*/
get coordinateSystem() {
return WebGLCoordinateSystem;
}
/**
* Whether the backend supports query timestamps or not.
*
* @type {boolean}
* @readonly
*/
get hasTimestamp() {
return this.disjoint !== null;
}
/**
* This method performs a readback operation by moving buffer data from
* a storage buffer attribute from the GPU to the CPU. ReadbackBuffer can
* be used to retain and reuse handles to the intermediate buffers and prevent
* new allocation.
*
* @async
* @param {BufferAttribute} attribute - The storage buffer attribute to read from.
* @param {?(ReadbackBuffer|ArrayBuffer)} [target=null] - The readback buffer or array buffer that receives the data. If `null`, a new array buffer is returned.
* @param {number} [offset=0] - The byte offset to start reading from. Must be a multiple of 4.
* @param {number} [count=-1] - The number of bytes to read. Must be a multiple of 4, or `-1` to read to the end of the buffer.
* @return {Promise<ArrayBuffer|ReadbackBuffer>} A promise that resolves with the buffer data when the data are ready.
*/
async getArrayBufferAsync( attribute, target = null, offset = 0, count = -1 ) {
return await this.attributeUtils.getArrayBufferAsync( attribute, target, offset, count );
}
/**
* Ensures the backend is XR compatible.
*
* @async
* @return {Promise} A Promise that resolve when the renderer is XR compatible.
*/
async makeXRCompatible() {
const attributes = this.gl.getContextAttributes();
if ( attributes.xrCompatible !== true ) {
await this.gl.makeXRCompatible();
}
}
/**
* Sets the XR rendering destination.
*
* @param {WebGLFramebuffer} xrFramebuffer - The XR framebuffer.
*/
setXRTarget( xrFramebuffer ) {
this._xrFramebuffer = xrFramebuffer;
}
/**
* Configures the given XR render target with external textures.
*
* This method is only relevant when using the WebXR Layers API.
*
* @param {XRRenderTarget} renderTarget - The XR render target.
* @param {WebGLTexture} colorTexture - A native color texture.
* @param {?WebGLTexture} [depthTexture=null] - A native depth texture.
*/
setXRRenderTargetTextures( renderTarget, colorTexture, depthTexture = null ) {
const gl = this.gl;
this.set( renderTarget.texture, {
textureGPU: colorTexture,
glTextureType: this.textureUtils.getGLTextureType( renderTarget.texture ),
glInternalFormat: gl.RGBA8 // see #24698 why RGBA8 and not SRGB8_ALPHA8 is used
} );
if ( depthTexture !== null ) {
this.set( renderTarget.depthTexture, {
textureGPU: depthTexture,
glTextureType: this.textureUtils.getGLTextureType( renderTarget.depthTexture ),
glInternalFormat: renderTarget.stencilBuffer ? gl.DEPTH24_STENCIL8 : gl.DEPTH_COMPONENT24
} );
// The multisample_render_to_texture extension doesn't work properly if there
// are midframe flushes and an external depth texture.
if ( ( this.extensions.has( 'WEBGL_multisampled_render_to_texture' ) === true ) && renderTarget._autoAllocateDepthBuffer === true && renderTarget.multiview === false ) {
warn( 'WebGLBackend: Render-to-texture extension was disabled because an external texture was provided' );
}
renderTarget._autoAllocateDepthBuffer = false;
}
}
/**
* Inits a time stamp query for the given render context.
*
* @param {string} type - The type of the timestamp query.
* @param {string} uid - A unique identifier for the timestamp query.
*/
initTimestampQuery( type, uid ) {
if ( ! this.disjoint || ! this.trackTimestamp ) return;
if ( ! this.timestampQueryPool[ type ] ) {
// TODO: Variable maxQueries?
this.timestampQueryPool[ type ] = new WebGLTimestampQueryPool( this.gl, type, 2048 );
}
const timestampQueryPool = this.timestampQueryPool[ type ];
const baseOffset = timestampQueryPool.allocateQueriesForContext( uid );
if ( baseOffset !== null ) {
timestampQueryPool.beginQuery( uid );
}
}
// timestamp utils
/**
* Prepares the timestamp buffer.
*
* @param {string} type - The type of the timestamp query.
* @param {string} uid - A unique identifier for the timestamp query.
*/
prepareTimestampBuffer( type, uid ) {
if ( ! this.disjoint || ! this.trackTimestamp ) return;
const timestampQueryPool = this.timestampQueryPool[ type ];
timestampQueryPool.endQuery( uid );
}
/**
* Returns the backend's rendering context.
*
* @return {WebGL2RenderingContext} The rendering context.
*/
getContext() {
return this.gl;
}
/**
* This method is executed at the beginning of a render call and prepares
* the WebGL state for upcoming render calls
*
* @param {RenderContext} renderContext - The render context.
*/
beginRender( renderContext ) {
const { state } = this;
const renderContextData = this.get( renderContext );
//
if ( renderContext.viewport ) {
this.updateViewport( renderContext );
} else {
const { width, height } = renderContext;
state.viewport( 0, 0, width, height );
}
if ( renderContext.scissor ) {
this.updateScissor( renderContext );
} else {
const { width, height } = renderContext;
state.scissor( 0, 0, width, height );
}
state.setScissorTest( renderContext.scissor );
//
this.initTimestampQuery( TimestampQuery.RENDER, this.getTimestampUID( renderContext ) );
renderContextData.previousContext = this._currentContext;
this._currentContext = renderContext;
this._setFramebuffer( renderContext );
this.clear( renderContext.clearColor, renderContext.clearDepth, renderContext.clearStencil, renderContext, false );
const occlusionQueryCount = renderContext.occlusionQueryCount;
const occlusionData = this.getOcclusionData( renderContext );
if ( occlusionQueryCount > 0 ) {
// Get a reference to the array of objects with queries. The occlusionData property
// can be changed by another render pass before the async reading of all previous queries complete
occlusionData.currentOcclusionQueries = occlusionData.occlusionQueries;
occlusionData.currentOcclusionQueryObjects = occlusionData.occlusionQueryObjects;
occlusionData.lastOcclusionObject = null;
occlusionData.occlusionQueries = new Array( occlusionQueryCount );
occlusionData.occlusionQueryObjects = new Array( occlusionQueryCount );
occlusionData.occlusionQueryIndex = 0;
} else if ( occlusionData.lastOcclusionObject !== undefined ) {
// invalidate if there is a stale query
occlusionData.lastOcclusionObject = undefined;
}
}
/**
* This method is executed at the end of a render call and finalizes work
* after draw calls.
*
* @param {RenderContext} renderContext - The render context.
*/
finishRender( renderContext ) {
const { gl, state } = this;
const renderContextData = this.get( renderContext );
const previousContext = renderContextData.previousContext;
state.resetVertexState();
const occlusionQueryCount = renderContext.occlusionQueryCount;
if ( occlusionQueryCount > 0 ) {
const lastOcclusionObject = this.getOcclusionData( renderContext ).lastOcclusionObject;
if ( lastOcclusionObject && lastOcclusionObject.occlusionTest === true ) {
gl.endQuery( gl.ANY_SAMPLES_PASSED );
}
this.resolveOccludedAsync( renderContext );
}
const textures = renderContext.textures;
if ( textures !== null ) {
for ( let i = 0; i < textures.length; i ++ ) {
const texture = textures[ i ];
if ( texture.generateMipmaps === true && texture.mipmapsAutoUpdate === true ) {
this.generateMipmaps( texture );
}
}
}
this._currentContext = previousContext;
this._resolveRenderTarget( renderContext );
if ( previousContext !== null ) {
this._setFramebuffer( previousContext );
if ( previousContext.viewport ) {
this.updateViewport( previousContext );
} else {
const { width, height } = previousContext;
state.viewport( 0, 0, width, height );
}
if ( previousContext.scissor ) {
this.updateScissor( previousContext );
} else {
const { width, height } = previousContext;
state.scissor( 0, 0, width, height );
}
state.setScissorTest( previousContext.scissor );
}
this.prepareTimestampBuffer( TimestampQuery.RENDER, this.getTimestampUID( renderContext ) );
}
/**
* This method processes the result of occlusion queries and writes it
* into the occlusion data of the render context.
*
* @async
* @param {RenderContext} renderContext - The render context.
*/
resolveOccludedAsync( renderContext ) {
const occlusionData = this.getOcclusionData( renderContext );
// handle occlusion query results
const { currentOcclusionQueries, currentOcclusionQueryObjects } = occlusionData;
if ( currentOcclusionQueries && currentOcclusionQueryObjects ) {
const occluded = new WeakSet();
const { gl } = this;
occlusionData.currentOcclusionQueryObjects = null;
occlusionData.currentOcclusionQueries = null;
const check = () => {
let completed = true;
// check all queries and requeue as appropriate
for ( let i = 0; i < currentOcclusionQueries.length; i ++ ) {
const query = currentOcclusionQueries[ i ];
if ( ! query ) continue;
if ( gl.getQueryParameter( query, gl.QUERY_RESULT_AVAILABLE ) ) {
if ( gl.getQueryParameter( query, gl.QUERY_RESULT ) === 0 ) occluded.add( currentOcclusionQueryObjects[ i ] );
currentOcclusionQueries[ i ] = null;
gl.deleteQuery( query );
} else {
completed = false;
}
}
if ( completed === false ) {
requestAnimationFrame( check );
} else {
occlusionData.occluded = occluded;
}
};
check();
}
}
/**
* Returns `true` if the given 3D object is fully occluded by other
* 3D objects in the scene.
*
* @param {RenderContext} renderContext - The render context.
* @param {Object3D} object - The 3D object to test.
* @return {boolean} Whether the 3D object is fully occluded or not.
*/
isOccluded( renderContext, object ) {
const occlusionData = this.getOcclusionData( renderContext );
return occlusionData.occluded && occlusionData.occluded.has( object );
}
/**
* Updates the viewport with the values from the given render context.
*
* @param {RenderContext} renderContext - The render context.
*/
updateViewport( renderContext ) {
const { state } = this;
const { x, y, width, height } = renderContext.viewportValue;
state.viewport( x, renderContext.height - height - y, width, height );
}
/**
* Updates the scissor with the values from the given render context.
*
* @param {RenderContext} renderContext - The render context.
*/
updateScissor( renderContext ) {
const { state } = this;
const { x, y, width, height } = renderContext.scissorValue;
state.scissor( x, renderContext.height - height - y, width, height );
}
/**
* Defines the scissor test.
*
* @param {boolean} boolean - Whether the scissor test should be enabled or not.
*/
setScissorTest( boolean ) {
const state = this.state;
state.setScissorTest( boolean );
}
/**
* Restores the WebGL state to its default and invalidates the internal state cache.
*/
resetState() {
this.state.reset();
}
/**
* Returns the clear color and alpha into a single
* color object.
*
* @return {Color4} The clear color.
*/
getClearColor() {
const clearColor = super.getClearColor();
// Since the canvas is always created with alpha: true,
// WebGL must always premultiply the clear color.
clearColor.r *= clearColor.a;
clearColor.g *= clearColor.a;
clearColor.b *= clearColor.a;
return clearColor;
}
/**
* Performs a clear operation.
*
* @param {boolean} color - Whether the color buffer should be cleared or not.
* @param {boolean} depth - Whether the depth buffer should be cleared or not.
* @param {boolean} stencil - Whether the stencil buffer should be cleared or not.
* @param {?Object} [descriptor=null] - The render context of the current set render target.
* @param {boolean} [setFrameBuffer=true] - Controls whether the intermediate framebuffer should be set or not.
* @param {boolean} [resolveRenderTarget=true] - Controls whether an active render target should be resolved
* or not. Only relevant for explicit clears.
*/
clear( color, depth, stencil, descriptor = null, setFrameBuffer = true, resolveRenderTarget = true ) {
const { gl, renderer } = this;
if ( descriptor === null ) {
const clearColor = this.getClearColor();
descriptor = {
textures: null,
clearColorValue: clearColor
};
}
//
let clear = 0;
if ( color ) clear |= gl.COLOR_BUFFER_BIT;
if ( depth ) clear |= gl.DEPTH_BUFFER_BIT;
if ( stencil ) clear |= gl.STENCIL_BUFFER_BIT;
if ( clear !== 0 ) {
let clearColor;
if ( descriptor.clearColorValue ) {
clearColor = descriptor.clearColorValue;
} else {
clearColor = this.getClearColor();
}
const clearDepth = renderer.getClearDepth();
const clearStencil = renderer.getClearStencil();
if ( color ) this.state.setColorMask( true );
if ( depth ) this.state.setDepthMask( true );
if ( stencil ) this.state.setStencilMask( 0xffffffff );
if ( descriptor.textures === null ) {
if ( color ) gl.clearColor( clearColor.r, clearColor.g, clearColor.b, clearColor.a );
if ( depth ) gl.clearDepth( clearDepth );
if ( stencil ) gl.clearStencil( clearStencil );
gl.clear( clear );
} else {
if ( setFrameBuffer ) this._setFramebuffer( descriptor );
if ( color ) {
for ( let i = 0; i < descriptor.textures.length; i ++ ) {
const mrtClearColor = descriptor.mrt ? descriptor.mrt.getClearColor( descriptor.textures[ i ].name ) : null;
if ( mrtClearColor !== null ) {
gl.clearBufferfv( gl.COLOR, i, [ mrtClearColor.r, mrtClearColor.g, mrtClearColor.b, mrtClearColor.a ] );
} else if ( i === 0 ) {
gl.clearBufferfv( gl.COLOR, i, [ clearColor.r, clearColor.g, clearColor.b, clearColor.a ] );
} else {
gl.clearBufferfv( gl.COLOR, i, [ 0, 0, 0, 1 ] );
}
}
}
if ( depth && stencil ) {
gl.clearBufferfi( gl.DEPTH_STENCIL, 0, clearDepth, clearStencil );
} else if ( depth ) {
gl.clearBufferfv( gl.DEPTH, 0, [ clearDepth ] );
} else if ( stencil ) {
gl.clearBufferiv( gl.STENCIL, 0, [ clearStencil ] );
}
if ( setFrameBuffer && resolveRenderTarget ) this._resolveRenderTarget( descriptor );
// Restore the framebuffer of the active render pass when clearing an unrelated
// render target, so subsequent draws in the pass don't bind to the cleared target.
if ( setFrameBuffer && this._currentContext !== null && this._currentContext !== descriptor ) {
this._setFramebuffer( this._currentContext );
}
}
}
}
/**
* This method is executed at the beginning of a compute call and
* prepares the state for upcoming compute tasks.
*
* @param {Node|Array<Node>} computeGroup - The compute node(s).
*/
beginCompute( computeGroup ) {
const { state, gl } = this;
//
state.bindFramebuffer( gl.FRAMEBUFFER, null );
this.initTimestampQuery( TimestampQuery.COMPUTE, this.getTimestampUID( computeGroup ) );
}
/**
* Executes a compute command for the given compute node.
*
* @param {Node|Array<Node>} computeGroup - The group of compute nodes of a compute call. Can be a single compute node.
* @param {Node} computeNode - The compute node.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {ComputePipeline} pipeline - The compute pipeline.
* @param {?number} [count=null] - The count of compute invocations. If `null`, the count is determined by the compute node.
*/
compute( computeGroup, computeNode, bindings, pipeline, count = null ) {
const { state, gl } = this;
if ( this.discard === false ) {
// required here to handle async behaviour of render.compute()
state.enable( gl.RASTERIZER_DISCARD );
this.discard = true;
}
const { programGPU, transformAttributes, attributes } = this.get( pipeline );
// the buffers are managed by the renderer's attributes so they are re-created after disposal
for ( let i = 0; i < attributes.length; i ++ ) this._updateComputeAttribute( attributes[ i ] );
for ( let i = 0; i < transformAttributes.length; i ++ ) this._updateComputeAttribute( transformAttributes[ i ] );
const vaoGPU = this.vertexArrayUtils.getVAO( pipeline, attributes );
state.setVertexState( vaoGPU );
state.useProgram( programGPU );
this._bindUniforms( bindings );
// the buffers are only bound to the default transform feedback object during the dispatch so
// no transform feedback object keeps the memory of disposed buffers allocated
for ( let i = 0; i < transformAttributes.length; i ++ ) {
gl.bindBufferBase( gl.TRANSFORM_FEEDBACK_BUFFER, i, this.get( transformAttributes[ i ] ).transformBuffer );
}
gl.beginTransformFeedback( gl.POINTS );
count = ( count !== null ) ? count : computeNode.count;
if ( Array.isArray( count ) ) {
warnOnce( 'WebGLBackend.compute(): The count parameter must be a single number, not an array.' );
count = count[ 0 ];
} else if ( count && typeof count === 'object' && count.isIndirectStorageBufferAttribute ) {
warnOnce( 'WebGLBackend.compute(): The count parameter must be a single number, not IndirectStorageBufferAttribute' );
count = computeNode.count;
}
if ( attributes[ 0 ].isStorageInstancedBufferAttribute ) {
gl.drawArraysInstanced( gl.POINTS, 0, 1, count );
} else {
gl.drawArrays( gl.POINTS, 0, count );
}
gl.endTransformFeedback();
for ( let i = 0; i < transformAttributes.length; i ++ ) {
gl.bindBufferBase( gl.TRANSFORM_FEEDBACK_BUFFER, i, null );
}
// switch active buffers
for ( let i = 0; i < transformAttributes.length; i ++ ) {
const dualAttributeData = this.get( transformAttributes[ i ] );
if ( dualAttributeData.pbo && this.has( dualAttributeData.pbo ) ) {
this.textureUtils.copyBufferToTexture( dualAttributeData.transformBuffer, dualAttributeData.pbo );
}
dualAttributeData.switchBuffers();
}
}
/**
* Creates or updates the GPU buffer of the given compute attribute via the renderer's
* attributes so it is tracked like other attributes.
*
* @private
* @param {BufferAttribute|InterleavedBufferAttribute} attribute - The buffer attribute.
*/
_updateComputeAttribute( attribute ) {
const bufferAttribute = this.getBufferAttribute( attribute );
const attributeType = ( attribute.isStorageBufferAttribute || attribute.isStorageInstancedBufferAttribute ) ? AttributeType.STORAGE : AttributeType.VERTEX;
this.renderer._attributes.update( bufferAttribute, attributeType );
}
/**
* This method is executed at the end of a compute call and
* finalizes work after compute tasks.
*
* @param {Node|Array<Node>} computeGroup - The compute node(s).
*/
finishCompute( computeGroup ) {
const { state, gl } = this;
this.discard = false;
state.disable( gl.RASTERIZER_DISCARD );
this.prepareTimestampBuffer( TimestampQuery.COMPUTE, this.getTimestampUID( computeGroup ) );
if ( this._currentContext ) {
this._setFramebuffer( this._currentContext );
}
}
/**
* Internal to determine if the current render target is a render target array with depth 2D array texture.
*
* @param {RenderContext} renderContext - The render context.
* @return {boolean} Whether the render target is a render target array with depth 2D array texture.
*
* @private
*/
_isRenderCameraDepthArray( renderContext ) {
return renderContext.depthTexture && renderContext.depthTexture.isArrayTexture && renderContext.camera.isArrayCamera;
}
/**
* Internal draw function.
*
* @private
* @param {Object3D} object - The object to render.
* @param {WebGLBufferRenderer} renderer - The internal renderer.
* @param {number} firstVertex - The first vertex to render.
* @param {number} vertexCount - The vertex count.
* @param {number} instanceCount - The intance count.
* @param {WebGLProgram} programGPU - The raw WebGL shader program.
*/
_draw( object, renderer, firstVertex, vertexCount, instanceCount, programGPU ) {
if ( object.isBatchedMesh ) {
if ( this.hasFeature( 'WEBGL_multi_draw' ) === false ) {
const { gl } = this;
const drawIdLocation = gl.getUniformLocation( programGPU, 'nodeUniformDrawId' );
const starts = object._multiDrawStarts;
const counts = object._multiDrawCounts;
const drawCount = object._multiDrawCount;
for ( let i = 0; i < drawCount; i ++ ) {
gl.uniform1ui( drawIdLocation, i );
renderer.render( starts[ i ], counts[ i ] );
}
} else {
renderer.renderMultiDraw( object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount );
}
} else if ( instanceCount > 1 ) {
renderer.renderInstances( firstVertex, vertexCount, instanceCount );
} else {
renderer.render( firstVertex, vertexCount );
}
}
/**
* Executes a draw command for the given render object.
*
* @param {RenderObject} renderObject - The render object to draw.
* @param {Info} info - Holds a series of statistical information about the GPU memory and the rendering process.
*/
draw( renderObject/*, info*/ ) {
const { object, pipeline, material, context, hardwareClippingPlanes } = renderObject;
const { programGPU } = this.get( pipeline );
const { gl, state } = this;
const drawParams = renderObject.getDrawParameters();
if ( drawParams === null ) return;
//
this._bindUniforms( renderObject.getBindings() );
const frontFaceCW = ( object.isMesh && object.matrixWorld.determinantAffine() < 0 );
state.setMaterial( material, frontFaceCW, hardwareClippingPlanes );
if ( context.mrt !== null && context.textures !== null ) {
state.setMRTBlending( context.textures, context.mrt, material );
}
state.useProgram( programGPU );
// vertex state
const index = renderObject.getIndex();
const vaoGPU = this.vertexArrayUtils.getVAO( renderObject, renderObject.getAttributes(), index );
state.setVertexState( vaoGPU );
//
if ( context.occlusionQueryCount > 0 ) {
const occlusionData = this.getOcclusionData( context );
const lastObject = occlusionData.lastOcclusionObject;
if ( lastObject !== object ) {
if ( lastObject !== null && lastObject.occlusionTest === true ) {
gl.endQuery( gl.ANY_SAMPLES_PASSED );
occlusionData.occlusionQueryIndex ++;
}
if ( object.occlusionTest === true ) {
const query = gl.createQuery();
gl.beginQuery( gl.ANY_SAMPLES_PASSED, query );
occlusionData.occlusionQueries[ occlusionData.occlusionQueryIndex ] = query;
occlusionData.occlusionQueryObjects[ occlusionData.occlusionQueryIndex ] = object;
}
occlusionData.lastOcclusionObject = object;
}
}
//
const renderer = this.bufferRenderer;
if ( object.isPoints ) renderer.mode = gl.POINTS;
else if ( object.isLineSegments ) renderer.mode = gl.LINES;
else if ( object.isLine ) renderer.mode = gl.LINE_STRIP;
else if ( object.isLineLoop ) renderer.mode = gl.LINE_LOOP;
else {
if ( material.wireframe === true ) {
state.setLineWidth( material.wireframeLinewidth * this.renderer.getPixelRatio() );
renderer.mode = gl.LINES;
} else {
renderer.mode = gl.TRIANGLES;
}
}
//
const { vertexCount, instanceCount } = drawParams;
let { firstVertex } = drawParams;
renderer.object = object;
if ( index !== null ) {
firstVertex *= index.array.BYTES_PER_ELEMENT;
const indexData = this.get( index );
renderer.index = index.count;
renderer.type = indexData.type;
} else {
renderer.index = 0;
}
if ( renderObject.camera.isArrayCamera === true && renderObject.camera.cameras.length > 0 && renderObject.camera.isMultiViewCamera === false ) {
const cameraData = this.get( renderObject.camera );
const cameras = renderObject.camera.cameras;
const cameraIndex = renderObject.getBindingGroup( 'cameraIndex' ).bindings[ 0 ];
if ( cameraData.indexesGPU === undefined || cameraData.indexesGPU.length !== cameras.length ) {
const data = new Uint32Array( [ 0, 0, 0, 0 ] );
const indexesGPU = [];
for ( let i = 0, len = cameras.length; i < len; i ++ ) {
const bufferGPU = gl.createBuffer();
data[ 0 ] = i;
gl.bindBuffer( gl.UNIFORM_BUFFER, bufferGPU );
gl.bufferData( gl.UNIFORM_BUFFER, data, gl.STATIC_DRAW );
indexesGPU.push( bufferGPU );
}
cameraData.indexesGPU = indexesGPU; // TODO: Create a global library for this
}
let cameraIndexBufferIndex = 0;
bindingsSearch: for ( const bindGroup of renderObject.getBindings() ) {
for ( const binding of bindGroup.bindings ) {
if ( binding === cameraIndex ) break bindingsSearch;
if ( binding.isUniformsGroup || binding.isUniformBuffer ) cameraIndexBufferIndex ++;
}
}
const renderTarget = this._currentContext.renderTarget;
const pixelRatio = ( renderTarget === null || renderTarget.isPostProcessingRenderTarget === true ) ? this.renderer.getPixelRatio() : 1;
const isRenderCameraDepthArray = this._isRenderCameraDepthArray( this._currentContext );
const prevActiveCubeFace = this._currentContext.activeCubeFace;
if ( isRenderCameraDepthArray ) {
// Clear the depth texture
const textureData = this.get( this._currentContext.depthTexture );
if ( textureData.clearedRenderId !== this.renderer._nodes.nodeFrame.renderId ) {
textureData.clearedRenderId = this.renderer._nodes.nodeFrame.renderId;
const { stencilBuffer } = renderTarget;
for ( let i = 0, len = cameras.length; i < len; i ++ ) {
this.renderer._activeCubeFace = i;
this._currentContext.activeCubeFace = i;
this._setFramebuffer( this._currentContext );
this.clear( false, true, stencilBuffer, this._currentContext, false, false );
}
this.renderer._activeCubeFace = prevActiveCubeFace;
this._currentContext.activeCubeFace = prevActiveCubeFace;
}
}
const frustumArray = context.frustumArray;
for ( let i = 0, len = cameras.length; i < len; i ++ ) {
const subCamera = cameras[ i ];
if ( object.layers.test( subCamera.layers ) && ( renderObject.camera.perCameraFrustumCulled === false || object.frustumCulled === false || object.intersectsFrustum( frustumArray.getFrustum( i ) ) ) ) {
if ( isRenderCameraDepthArray ) {
// Update the active layer
this.renderer._activeCubeFace = i;
this._currentContext.activeCubeFace = i;
this._setFramebuffer( this._currentContext );
}
const vp = subCamera.viewport;
if ( vp !== undefined ) {
const x = vp.x * pixelRatio;
const y = vp.y * pixelRatio;
const width = vp.width * pixelRatio;
const height = vp.height * pixelRatio;
state.viewport(
Math.floor( x ),
Math.floor( renderObject.context.height - height - y ),
Math.floor( width ),
Math.floor( height )
);
}
state.bindBufferBase( gl.UNIFORM_BUFFER, cameraIndexBufferIndex, cameraData.indexesGPU[ i ] );
this._draw( object, renderer, firstVertex, vertexCount, instanceCount, programGPU );
}
this._currentContext.activeCubeFace = prevActiveCubeFace;
this.renderer._activeCubeFace = prevActiveCubeFace;
}
} else {
this._draw( object, renderer, firstVertex, vertexCount, instanceCount, programGPU );
}
}
/**
* Releases the VAO of the given render object.
*
* @param {RenderObject} renderObject - The render object.
*/
deleteVertexState( renderObject ) {
this.vertexArrayUtils.releaseVAO( renderObject );
}
/**
* Always returns `false` since WebGL has no pipeline state objects. Render state
* like blending, depth/stencil or the vertex layout is set per draw, so a render
* pipeline only represents a linked shader program which never needs an update.
*
* @param {RenderObject} renderObject - The render object.
* @return {boolean} Whether the render pipeline requires an update or not.
*/
needsRenderUpdate( /*renderObject*/ ) {
return false;
}
/**
* Returns an empty string since a render pipeline only represents a linked shader
* program in WebGL. The program is fully identified by its shader stages, which are
* already part of the pipeline cache key.
*
* @param {RenderObject} renderObject - The render object.
* @return {string} The cache key.
*/
getRenderCacheKey( /*renderObject*/ ) {
return '';
}
// textures
/**
* Creates a default texture for the given texture that can be used
* as a placeholder until the actual texture is ready for usage.
*
* @param {Texture} texture - The texture to create a default texture for.
*/
createDefaultTexture( texture ) {
this.textureUtils.createDefaultTexture( texture );
}
/**
* Defines a texture on the GPU for the given texture object.
*
* @param {Texture} texture - The texture.
* @param {Object} [options={}] - Optional configuration parameter.
*/
createTexture( texture, options ) {
this.textureUtils.createTexture( texture, options );
}
/**
* Uploads the updated texture data to the GPU.
*
* @param {Texture} texture - The texture.
* @param {Object} [options={}] - Optional configuration parameter.
*/
updateTexture( texture, options ) {
this.textureUtils.updateTexture( texture, options );
}
/**
* Generates mipmaps for the given texture.
*
* @param {Texture} texture - The texture.
*/
generateMipmaps( texture ) {
this.textureUtils.generateMipmaps( texture );
}
/**
* Destroys the GPU data for the given texture object.
*
* @param {Texture} texture - The texture.
* @param {boolean} [isDefaultTexture=false] - Whether the texture uses a default GPU texture or not.
*/
destroyTexture( texture, isDefaultTexture = false ) {
this.textureUtils.destroyTexture( texture, isDefaultTexture );
}
/**
* Returns texture data as a typed array.
*
* @async
* @param {Texture} texture - The texture to copy.
* @param {number} x - The x coordinate of the copy origin.
* @param {number} y - The y coordinate of the copy origin.
* @param {number} width - The width of the copy.
* @param {number} height - The height of the copy.
* @param {number} faceIndex - The cube face, depth slice or array layer index.
* @return {Promise<TypedArray>} A Promise that resolves with a typed array when the copy operation has finished.
*/
async copyTextureToBuffer( texture, x, y, width, height, faceIndex ) {
return this.textureUtils.copyTextureToBuffer( texture, x, y, width, height, faceIndex );
}
/**
* This method does nothing since WebGL 2 has no concept of samplers.
*
* @param {Sampler} binding - The sampler binding to update.
* @return {string} The current sampler key.
*/
updateSampler( /*binding*/ ) {
return '';
}
// node builder
/**
* Returns a node builder for the given render object.
*
* @param {RenderObject} object - The render object.
* @param {Renderer} renderer - The renderer.
* @return {GLSLNodeBuilder} The node builder.
*/
createNodeBuilder( object, renderer ) {
return new GLSLNodeBuilder( object, renderer );
}
// program
/**
* Creates a shader program from the given programmable stage.
*
* @param {ProgrammableStage} program - The programmable stage.
*/
createProgram( program ) {
const gl = this.gl;
const { stage, code } = program;
const shader = stage === 'fragment' ? gl.createShader( gl.FRAGMENT_SHADER ) : gl.createShader( gl.VERTEX_SHADER );
gl.shaderSource( shader, code );
gl.compileShader( shader );
this.set( program, {
shaderGPU: shader
} );
}
/**
* Destroys the shader program of the given programmable stage.
*
* @param {ProgrammableStage} program - The programmable stage.
*/
destroyProgram( program ) {
const gl = this.gl;
const data = this.get( program );
gl.deleteShader( data.shaderGPU );
this.delete( program );
}
/**
* 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 gl = this.gl;
const pipeline = renderObject.pipeline;
// Program
const { fragmentProgram, vertexProgram } = pipeline;
const programGPU = gl.createProgram();
const fragmentShader = this.get( fragmentProgram ).shaderGPU;
const vertexShader = this.get( vertexProgram ).shaderGPU;
gl.attachShader( programGPU, fragmentShader );
gl.attachShader( programGPU, vertexShader );
gl.linkProgram( programGPU );
this.set( pipeline, {
programGPU,
fragmentShader,
vertexShader
} );
if ( promises !== null && this.parallel ) {
const p = new Promise( ( resolve /*, reject*/ ) => {
const parallel = this.parallel;
const checkStatus = () => {
if ( gl.getProgramParameter( programGPU, parallel.COMPLETION_STATUS_KHR ) ) {
this._completeCompile( renderObject, pipeline );
resolve();
} else {
requestAnimationFrame( checkStatus );
}
};
checkStatus();
} );
promises.push( p );
return;
}
this._completeCompile( renderObject, pipeline );
}
/**
* Destroys the given pipeline.
*
* @param {Pipeline} pipeline - The pipeline.
*/
destroyPipeline( pipeline ) {
const gl = this.gl;
const data = this.get( pipeline );
if ( pipeline.isComputePipeline === true ) {
// Compute pipeline manage their own VAOs (without render objects)
this.vertexArrayUtils.releaseVAO( pipeline );
}
gl.deleteProgram( data.programGPU );
this.delete( pipeline );
}
/**
* Formats the source code of error messages.
*
* @private
* @param {string} string - The code.
* @param {number} errorLine - The error line.
* @return {string} The formatted code.
*/
_handleSource( string, errorLine ) {
const lines = string.split( '\n' );
const lines2 = [];
const from = Math.max( errorLine - 6, 0 );
const to = Math.min( errorLine + 6, lines.length );
for ( let i = from; i < to; i ++ ) {
const line = i + 1;
lines2.push( `${line === errorLine ? '>' : ' '} ${line}: ${lines[ i ]}` );
}
return lines2.join( '\n' );
}
/**
* Gets the shader compilation errors from the info log.
*
* @private
* @param {WebGL2RenderingContext} gl - The rendering context.
* @param {WebGLShader} shader - The WebGL shader object.
* @param {string} type - The shader type.
* @return {string} The shader errors.
*/
_getShaderErrors( gl, shader, type ) {
const status = gl.getShaderParameter( shader, gl.COMPILE_STATUS );
const shaderInfoLog = gl.getShaderInfoLog( shader ) || '';
const errors = shaderInfoLog.trim();
if ( status && errors === '' ) return '';
const errorMatches = /ERROR: 0:(\d+)/.exec( errors );
if ( errorMatches ) {
const errorLine = parseInt( errorMatches[ 1 ] );
return type.toUpperCase() + '\n\n' + errors + '\n\n' + this._handleSource( gl.getShaderSource( shader ), errorLine );
} else {
return errors;
}
}
/**
* Logs shader compilation errors.
*
* @private
* @param {WebGLProgram} programGPU - The WebGL program.
* @param {WebGLShader} glFragmentShader - The fragment shader as a native WebGL shader object.
* @param {WebGLShader} glVertexShader - The vertex shader as a native WebGL shader object.
*/
_logProgramError( programGPU, glFragmentShader, glVertexShader ) {
if ( this.renderer.debug.checkShaderErrors ) {
const gl = this.gl;
const programInfoLog = gl.getProgramInfoLog( programGPU ) || '';
const programLog = programInfoLog.trim();
if ( gl.getProgramParameter( programGPU, gl.LINK_STATUS ) === false ) {
if ( typeof this.renderer.debug.onShaderError === 'function' ) {
this.renderer.debug.onShaderError( gl, programGPU, glVertexShader, glFragmentShader );
} else {
// default error reporting
const vertexErrors = this._getShaderErrors( gl, glVertexShader, 'vertex' );
const fragmentErrors = this._getShaderErrors( gl, glFragmentShader, 'fragment' );
error(
'WebGLProgram: Shader Error ' + gl.getError() + ' - ' +
'VALIDATE_STATUS ' + gl.getProgramParameter( programGPU, gl.VALIDATE_STATUS ) + '\n\n' +
'Program Info Log: ' + programLog + '\n' +
vertexErrors + '\n' +
fragmentErrors
);
}
} else if ( programLog !== '' ) {
warn( 'WebGLProgram: Program Info Log:', programLog );
}
}
}
/**
* Completes the shader program setup for the given render object.
*
* @private
* @param {RenderObject} renderObject - The render object.
* @param {RenderPipeline} pipeline - The render pipeline.
*/
_completeCompile( renderObject, pipeline ) {
const { state, gl } = this;
const pipelineData = this.get( pipeline );
const { programGPU, fragmentShader, vertexShader } = pipelineData;
if ( gl.getProgramParameter( programGPU, gl.LINK_STATUS ) === false ) {
this._logProgramError( programGPU, fragmentShader, vertexShader );
}
state.useProgram( programGPU );
// Bindings
const bindings = renderObject.getBindings();
this._setupBindings( bindings, programGPU );
//
this.set( pipeline, {
programGPU,
pipeline: programGPU
} );
}
/**
* Creates a compute pipeline for the given compute node.
*
* @param {ComputePipeline} computePipeline - The compute pipeline.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {?Array<Promise>} [promises=null] - Optional compilation promises.
*/
createComputePipeline( computePipeline, bindings, promises = null ) {
const { gl } = this;
// Program
const fragmentProgram = {
stage: 'fragment',
code: '#version 300 es\nprecision highp float;\nvoid main() {}'
};
this.createProgram( fragmentProgram );
const { computeProgram } = computePipeline;
const programGPU = gl.createProgram();
const fragmentShader = this.get( fragmentProgram ).shaderGPU;
const vertexShader = this.get( computeProgram ).shaderGPU;
const transforms = computePipeline.transforms;
const transformVaryingNames = [];
const transformAttributeNodes = [];
for ( let i = 0; i < transforms.length; i ++ ) {
const transform = transforms[ i ];
transformVaryingNames.push( transform.varyingName );
transformAttributeNodes.push( transform.attributeNode );
}
gl.attachShader( programGPU, fragmentShader );
gl.attachShader( programGPU, vertexShader );
gl.transformFeedbackVaryings(
programGPU,
transformVaryingNames,
gl.SEPARATE_ATTRIBS
);
gl.linkProgram( programGPU );
// only the attributes are stored since their buffers are re-created when they are disposed (see compute())
const attributeNodes = computePipeline.attributes;
const attributes = [];
const transformAttributes = [];
for ( let i = 0; i < attributeNodes.length; i ++ ) {
attributes.push( attributeNodes[ i ].node.attribute );
}
for ( let i = 0; i < transformAttributeNodes.length; i ++ ) {
transformAttributes.push( transformAttributeNodes[ i ].attribute );
}
// Store pipeline data
this.set( computePipeline, {
programGPU,
fragmentShader,
vertexShader,
transformAttributes,
attributes
} );
if ( promises !== null && this.parallel ) {
const parallel = this.parallel;
const p = new Promise( ( resolve ) => {
const checkStatus = () => {
if ( gl.getProgramParameter( programGPU, parallel.COMPLETION_STATUS_KHR ) ) {
this._completeComputeCompile( computePipeline, bindings );
resolve();
} else {
requestAnimationFrame( checkStatus );
}
};
checkStatus();
} );
promises.push( p );
return;
}
// Sync fallback
this._completeComputeCompile( computePipeline, bindings );
}
/**
* Completes the compute pipeline setup for the given compute pipeline.
*
* @param {ComputePipeline} computePipeline - The compute pipeline.
* @param {Array<BindGroup>} bindings - Array of bind groups.
*/
_completeComputeCompile( computePipeline, bindings ) {
const { state, gl } = this;
const { programGPU, fragmentShader, vertexShader } = this.get( computePipeline );
if ( gl.getProgramParameter( programGPU, gl.LINK_STATUS ) === false ) {
this._logProgramError( programGPU, fragmentShader, vertexShader );
}
state.useProgram( programGPU );
// Bindings (must be after link completion)
this._setupBindings( bindings, programGPU );
}
/**
* Creates bindings from the given bind group definition.
*
* @param {BindGroup} bindGroup - The bind group.
* @param {Array<BindGroup>} bindings - Array of bind groups.
* @param {string} cacheKey - The cache key.
* @param {number} version - The version.
*/
createBindings( bindGroup, bindings /*, cacheKey, version*/ ) {
if ( this._knownBindings.has( bindings ) === false ) {
this._knownBindings.add( bindings );
let uniformBuffers = 0;
let textures = 0;
for ( const bindGroup of bindings ) {
this.set( bindGroup, {
textures: textures,
uniformBuffers: uniformBuffers
} );
for ( const binding of bindGroup.bindings ) {
if ( binding.isUniformBuffer ) uniformBuffers ++;
if ( binding.isSampledTexture ) textures ++;
}
}
}
this.updateBindings( bindGroup, bindings );
}
/**
* Updates the given bind group definition.
*
* @param {BindGroup} bindGroup - The bind group.
* @param {Array<BindGroup>} bindings - Array of bind groups.
* @param {string} cacheKey - The cache key.
* @param {number} version - The version.
*/
updateBindings( bindGroup /*, bindings, cacheKey, version*/ ) {
const { gl } = this;
for ( const binding of bindGroup.bindings ) {
const map = this.get( binding );
if ( binding.isUniformsGroup || binding.isUniformBuffer ) {
const array = binding.buffer;
const bufferGPU = map.bufferGPU;
gl.bindBuffer( gl.UNIFORM_BUFFER, bufferGPU );
// update
const updateRanges = binding.updateRanges;
gl.bindBuffer( gl.UNIFORM_BUFFER, bufferGPU );
if ( updateRanges.length === 0 ) {
gl.bufferData( gl.UNIFORM_BUFFER, array, gl.DYNAMIC_DRAW );
} else {
const isTyped = isTypedArray( array );
const byteOffsetFactor = isTyped ? 1 : array.BYTES_PER_ELEMENT;
for ( let i = 0, l = updateRanges.length; i < l; i ++ ) {
const range = updateRanges[ i ];
const dataOffset = range.start * byteOffsetFactor;
const size = range.count * byteOffsetFactor;
const bufferOffset = dataOffset * ( isTyped ? array.BYTES_PER_ELEMENT : 1 ); // bufferOffset is always in bytes
gl.bufferSubData( gl.UNIFORM_BUFFER, bufferOffset, array, dataOffset, size );
}
}
this.set( binding, map );
} else if ( binding.isSampledTexture ) {
const { textureGPU, glTextureType } = this.get( binding.texture );
map.textureGPU = textureGPU;
map.glTextureType = glTextureType;
this.set( binding, map );
}
}
}
/**
* Updates a buffer binding.
*
* @param {Buffer} binding - The buffer binding to update.
*/
updateBinding( binding ) {
const gl = this.gl;
if ( binding.isUniformsGroup || binding.isUniformBuffer ) {
const bindingData = this.get( binding );
const bufferGPU = bindingData.bufferGPU;
const array = binding.buffer;
const updateRanges = binding.updateRanges;
gl.bindBuffer( gl.UNIFORM_BUFFER, bufferGPU );
if ( updateRanges.length === 0 ) {
gl.bufferData( gl.UNIFORM_BUFFER, array, gl.DYNAMIC_DRAW );
} else {
const isTyped = isTypedArray( array );
const byteOffsetFactor = isTyped ? 1 : array.BYTES_PER_ELEMENT;
// Update ranges arrive sorted and non-overlapping which makes
// it easy to merge contiguous ranges.
let start = updateRanges[ 0 ].start; // start of the current merged range
for ( let i = 0, l = updateRanges.length; i < l; i ++ ) {
const range = updateRanges[ i ];
const next = updateRanges[ i + 1 ];
const end = range.start + range.count; // exclusive end of the current range
// keep merging while the next range is contiguous
if ( next !== undefined && next.start === end ) continue;
// write the merged range
const dataOffset = start * byteOffsetFactor;
const size = ( end - start ) * byteOffsetFactor;
const bufferOffset = dataOffset * ( isTyped ? array.BYTES_PER_ELEMENT : 1 ); // bufferOffset is always in bytes
gl.bufferSubData( gl.UNIFORM_BUFFER, bufferOffset, array, dataOffset, size );
// start next if possible
if ( next !== undefined ) start = next.start;
}
}
}
}
// attributes
/**
* Creates a uniform buffer.
*
* @param {Buffer} uniformBuffer - The uniform buffer.
*/
createUniformBuffer( uniformBuffer ) {
const uniformBufferData = this.get( uniformBuffer );
if ( uniformBufferData.bufferGPU === undefined ) {
const gl = this.gl;
const array = uniformBuffer.buffer;
uniformBufferData.bufferGPU = gl.createBuffer();
gl.bindBuffer( gl.UNIFORM_BUFFER, uniformBufferData.bufferGPU );
gl.bufferData( gl.UNIFORM_BUFFER, array.byteLength, gl.DYNAMIC_DRAW );
}
}
/**
* Destroys the GPU data for the given uniform buffer.
*
* @param {Buffer} uniformBuffer - The uniform buffer.
*/
destroyUniformBuffer( uniformBuffer ) {
const uniformBufferData = this.get( uniformBuffer );
this.gl.deleteBuffer( uniformBufferData.bufferGPU );
this.delete( uniformBuffer );
}
/**
* Creates the GPU buffer of an indexed shader attribute.
*
* @param {BufferAttribute} attribute - The indexed buffer attribute.
*/
createIndexAttribute( attribute ) {
const gl = this.gl;
this.attributeUtils.createAttribute( attribute, gl.ELEMENT_ARRAY_BUFFER );
}
/**
* Creates the GPU buffer of a shader attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
createAttribute( attribute ) {
if ( this.has( attribute ) ) return;
const gl = this.gl;
this.attributeUtils.createAttribute( attribute, gl.ARRAY_BUFFER );
}
/**
* Creates the GPU buffer of a storage attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
createStorageAttribute( attribute ) {
if ( this.has( attribute ) ) return;
const gl = this.gl;
this.attributeUtils.createAttribute( attribute, gl.ARRAY_BUFFER );
}
/**
* Updates the GPU buffer of a shader attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute to update.
*/
updateAttribute( attribute ) {
this.attributeUtils.updateAttribute( attribute );
}
/**
* Destroys the GPU buffer of a shader attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute to destroy.
*/
destroyAttribute( attribute ) {
// VAOs keep the memory of attached buffers allocated
this.vertexArrayUtils.releaseBuffer( this.get( this.getBufferAttribute( attribute ) ) );
this.attributeUtils.destroyAttribute( attribute );
}
/**
* Checks if the given feature is supported by the backend.
*
* @param {string} name - The feature's name.
* @return {boolean} Whether the feature is supported or not.
*/
hasFeature( name ) {
const keysMatching = Object.keys( GLFeatureName ).filter( key => GLFeatureName[ key ] === name );
const extensions = this.extensions;
for ( let i = 0; i < keysMatching.length; i ++ ) {
if ( extensions.has( keysMatching[ i ] ) ) return true;
}
return false;
}
/**
* Copies data of the given source texture to the given destination texture.
*
* @param {Texture} srcTexture - The source texture.
* @param {Texture} dstTexture - The destination texture.
* @param {?(Box3|Box2)} [srcRegion=null] - The region of the source texture to copy.
* @param {?(Vector2|Vector3)} [dstPosition=null] - The destination position of the copy.
* @param {number} [srcLevel=0] - The source mip level to copy from.
* @param {number} [dstLevel=0] - The destination mip level to copy to.
*/
copyTextureToTexture( srcTexture, dstTexture, srcRegion = null, dstPosition = null, srcLevel = 0, dstLevel = 0 ) {
this.textureUtils.copyTextureToTexture( srcTexture, dstTexture, srcRegion, dstPosition, srcLevel, dstLevel );
}
/**
* Copies the current bound framebuffer to the given texture.
*
* @param {Texture} texture - The destination texture.
* @param {RenderContext} renderContext - The render context.
* @param {Vector4} rectangle - A four dimensional vector defining the origin and dimension of the copy.
*/
copyFramebufferToTexture( texture, renderContext, rectangle ) {
this.textureUtils.copyFramebufferToTexture( texture, renderContext, rectangle );
}
/**
* Checks if the given compatibility is supported by the backend.
*
* @param {string} name - The compatibility name.
* @return {boolean} Whether the compatibility is supported or not.
*/
hasCompatibility( name ) {
if ( name === Compatibility.TEXTURE_COMPARE ) return true;
return super.hasCompatibility( name );
}
/**
* Initializes the render target defined in the given render context.
*
* @param {RenderContext} renderContext - The render context.
*/
initRenderTarget( renderContext ) {
const { gl, state } = this;
this._setFramebuffer( renderContext );
state.bindFramebuffer( gl.FRAMEBUFFER, null );
}
/**
* Configures the active framebuffer from the given render context.
*
* @private
* @param {RenderContext} descriptor - The render context.
*/
_setFramebuffer( descriptor ) {
const { gl, state } = this;
let currentFrameBuffer = null;
if ( descriptor.textures !== null ) {
const renderTarget = descriptor.renderTarget;
const renderTargetContextData = this.get( renderTarget );
const { depthBuffer, stencilBuffer } = renderTarget;
const isCube = renderTarget.isCubeRenderTarget === true;
const isXRRenderTarget = renderTarget.isXRRenderTarget === true;
const _hasExternalTextures = ( isXRRenderTarget === true && renderTarget._hasExternalTextures === true );
let msaaFb = renderTargetContextData.msaaFrameBuffer;
let depthRenderbuffer = renderTargetContextData.depthRenderbuffer;
const multisampledRTTExt = this.extensions.get( 'WEBGL_multisampled_render_to_texture' );
const multiviewExt = this.extensions.get( 'OVR_multiview2' );
const useMultisampledRTT = this._useMultisampledExtension( renderTarget );
let samples = renderTarget.samples;
if ( descriptor.depthTexture !== null && descriptor.depthTexture.renderTarget !== renderTarget && useMultisampledRTT === false ) {
if ( samples > 0 ) {
warnOnce( 'WebGLBackend: Shared depth texture is not supported with MSAA when "WEBGL_multisampled_render_to_texture" is unsupported. Falling back to single-sample rendering.' );
samples = 0;
}
}
const cacheKey = getCacheKey( descriptor );
let fb;
if ( isCube ) {
renderTargetContextData.cubeFramebuffers || ( renderTargetContextData.cubeFramebuffers = {} );
fb = renderTargetContextData.cubeFramebuffers[ cacheKey ];
} else if ( isXRRenderTarget && _hasExternalTextures === false ) {
fb = this._xrFramebuffer;
} else {
renderTargetContextData.framebuffers || ( renderTargetContextData.framebuffers = {} );
fb = renderTargetContextData.framebuffers[ cacheKey ];
}
if ( fb === undefined ) {
fb = gl.createFramebuffer();
state.bindFramebuffer( gl.FRAMEBUFFER, fb );
const textures = descriptor.textures;
const depthInvalidationArray = [];
if ( isCube ) {
renderTargetContextData.cubeFramebuffers[ cacheKey ] = fb;
const { textureGPU } = this.get( textures[ 0 ] );
const cubeFace = descriptor.activeCubeFace;
const mipLevel = descriptor.activeMipmapLevel;
gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_CUBE_MAP_POSITIVE_X + cubeFace, textureGPU, mipLevel );
} else {
renderTargetContextData.framebuffers[ cacheKey ] = fb;
for ( let i = 0; i < textures.length; i ++ ) {
const texture = textures[ i ];
const textureData = this.get( texture );
const attachment = gl.COLOR_ATTACHMENT0 + i;
if ( renderTarget.multiview ) {
multiviewExt.framebufferTextureMultisampleMultiviewOVR( gl.FRAMEBUFFER, attachment, textureData.textureGPU, 0, samples, 0, 2 );
} else if ( textureData.glTextureType === gl.TEXTURE_2D_ARRAY || textureData.glTextureType === gl.TEXTURE_3D ) {
const layer = descriptor.activeCubeFace;
const mipLevel = descriptor.activeMipmapLevel;
gl.framebufferTextureLayer( gl.FRAMEBUFFER, attachment, textureData.textureGPU, mipLevel, layer );
} else {
if ( useMultisampledRTT ) {
multisampledRTTExt.framebufferTexture2DMultisampleEXT( gl.FRAMEBUFFER, attachment, gl.TEXTURE_2D, textureData.textureGPU, 0, samples );
} else {
const mipLevel = descriptor.activeMipmapLevel;
gl.framebufferTexture2D( gl.FRAMEBUFFER, attachment, gl.TEXTURE_2D, textureData.textureGPU, mipLevel );
}
}
}
}
const depthStyle = stencilBuffer ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT;
if ( renderTarget._autoAllocateDepthBuffer === true ) {
const renderbuffer = gl.createRenderbuffer();
this.textureUtils.setupRenderBufferStorage( renderbuffer, descriptor, 0, useMultisampledRTT );
renderTargetContextData.xrDepthRenderbuffer = renderbuffer;
depthInvalidationArray.push( stencilBuffer ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT );
gl.bindRenderbuffer( gl.RENDERBUFFER, renderbuffer );
gl.framebufferRenderbuffer( gl.FRAMEBUFFER, depthStyle, gl.RENDERBUFFER, renderbuffer );
} else {
if ( descriptor.depthTexture !== null ) {
depthInvalidationArray.push( stencilBuffer ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT );
const textureData = this.get( descriptor.depthTexture );
if ( renderTarget.multiview ) {
multiviewExt.framebufferTextureMultisampleMultiviewOVR( gl.FRAMEBUFFER, depthStyle, textureData.textureGPU, 0, samples, 0, 2 );
} else if ( _hasExternalTextures && useMultisampledRTT ) {
multisampledRTTExt.framebufferTexture2DMultisampleEXT( gl.FRAMEBUFFER, depthStyle, gl.TEXTURE_2D, textureData.textureGPU, 0, samples );
} else {
if ( descriptor.depthTexture.isArrayTexture ) {
const layer = descriptor.activeCubeFace;
gl.framebufferTextureLayer( gl.FRAMEBUFFER, depthStyle, textureData.textureGPU, 0, layer );
} else if ( descriptor.depthTexture.isCubeTexture ) {
const cubeFace = descriptor.activeCubeFace;
gl.framebufferTexture2D( gl.FRAMEBUFFER, depthStyle, gl.TEXTURE_CUBE_MAP_POSITIVE_X + cubeFace, textureData.textureGPU, 0 );
} else {
gl.framebufferTexture2D( gl.FRAMEBUFFER, depthStyle, gl.TEXTURE_2D, textureData.textureGPU, 0 );
}
}
}
}
renderTargetContextData.depthInvalidationArray = depthInvalidationArray;
} else {
const isRenderCameraDepthArray = this._isRenderCameraDepthArray( descriptor );
if ( isRenderCameraDepthArray ) {
state.bindFramebuffer( gl.FRAMEBUFFER, fb );
const layer = descriptor.activeCubeFace;
const depthData = this.get( descriptor.depthTexture );
const depthStyle = stencilBuffer ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT;
gl.framebufferTextureLayer(
gl.FRAMEBUFFER,
depthStyle,
depthData.textureGPU,
0,
layer
);
}
// rebind external XR textures
if ( ( isXRRenderTarget || useMultisampledRTT || renderTarget.multiview ) && ( renderTarget._isOpaqueFramebuffer !== true ) ) {
state.bindFramebuffer( gl.FRAMEBUFFER, fb );
// rebind color
if ( descriptor.textures.length > 0 ) {
const textureData = this.get( descriptor.textures[ 0 ] );
if ( renderTarget.multiview ) {
multiviewExt.framebufferTextureMultisampleMultiviewOVR( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, textureData.textureGPU, 0, samples, 0, 2 );
} else if ( useMultisampledRTT ) {
multisampledRTTExt.framebufferTexture2DMultisampleEXT( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, textureData.textureGPU, 0, samples );
} else {
gl.framebufferTexture2D( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, textureData.textureGPU, 0 );
}
}
// rebind depth
const depthStyle = stencilBuffer ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT;
if ( renderTarget._autoAllocateDepthBuffer === true ) {
const renderbuffer = renderTargetContextData.xrDepthRenderbuffer;
gl.bindRenderbuffer( gl.RENDERBUFFER, renderbuffer );
gl.framebufferRenderbuffer( gl.FRAMEBUFFER, depthStyle, gl.RENDERBUFFER, renderbuffer );
} else {
const textureData = this.get( descriptor.depthTexture );
if ( renderTarget.multiview ) {
multiviewExt.framebufferTextureMultisampleMultiviewOVR( gl.FRAMEBUFFER, depthStyle, textureData.textureGPU, 0, samples, 0, 2 );
} else if ( useMultisampledRTT ) {
multisampledRTTExt.framebufferTexture2DMultisampleEXT( gl.FRAMEBUFFER, depthStyle, gl.TEXTURE_2D, textureData.textureGPU, 0, samples );
} else {
gl.framebufferTexture2D( gl.FRAMEBUFFER, depthStyle, gl.TEXTURE_2D, textureData.textureGPU, 0 );
}
}
}
}
if ( samples > 0 && useMultisampledRTT === false && ! renderTarget.multiview ) {
if ( msaaFb === undefined ) {
msaaFb = gl.createFramebuffer();
state.bindFramebuffer( gl.FRAMEBUFFER, msaaFb );
const msaaRenderbuffers = [];
const textures = descriptor.textures;
for ( let i = 0; i < textures.length; i ++ ) {
msaaRenderbuffers[ i ] = gl.createRenderbuffer();
gl.bindRenderbuffer( gl.RENDERBUFFER, msaaRenderbuffers[ i ] );
const texture = descriptor.textures[ i ];
const textureData = this.get( texture );
gl.renderbufferStorageMultisample( gl.RENDERBUFFER, samples, textureData.glInternalFormat, descriptor.width, descriptor.height );
gl.framebufferRenderbuffer( gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.RENDERBUFFER, msaaRenderbuffers[ i ] );
}
gl.bindRenderbuffer( gl.RENDERBUFFER, null );
renderTargetContextData.msaaFrameBuffer = msaaFb;
renderTargetContextData.msaaRenderbuffers = msaaRenderbuffers;
if ( depthBuffer && depthRenderbuffer === undefined ) {
depthRenderbuffer = gl.createRenderbuffer();
this.textureUtils.setupRenderBufferStorage( depthRenderbuffer, descriptor, samples );
renderTargetContextData.depthRenderbuffer = depthRenderbuffer;
}
}
currentFrameBuffer = renderTargetContextData.msaaFrameBuffer;
} else {
currentFrameBuffer = fb;
}
state.drawBuffers( descriptor, fb );
}
state.bindFramebuffer( gl.FRAMEBUFFER, currentFrameBuffer );
}
/**
* Setups the given bindings.
*
* @private
* @param {Array<BindGroup>} bindings - The bindings.
* @param {WebGLProgram} programGPU - The WebGL program.
*/
_setupBindings( bindings, programGPU ) {
const gl = this.gl;
let uniformBuffers = 0;
let textures = 0;
for ( const bindGroup of bindings ) {
for ( const binding of bindGroup.bindings ) {
if ( binding.isUniformsGroup || binding.isUniformBuffer ) {
const index = uniformBuffers ++;
const location = gl.getUniformBlockIndex( programGPU, binding.name );
gl.uniformBlockBinding( programGPU, location, index );
} else if ( binding.isSampledTexture ) {
const index = textures ++;
const location = gl.getUniformLocation( programGPU, binding.name );
gl.uniform1i( location, index );
}
}
}
}
/**
* Binds the given uniforms.
*
* @private
* @param {Array<BindGroup>} bindings - The bindings.
*/
_bindUniforms( bindings ) {
const { gl, state } = this;
let uniformBuffers = 0;
let textures = 0;
for ( const bindGroup of bindings ) {
for ( const binding of bindGroup.bindings ) {
const bindingData = this.get( binding );
if ( binding.isUniformsGroup || binding.isUniformBuffer ) {
const index = uniformBuffers ++;
// TODO USE bindBufferRange to group multiple uniform buffers
state.bindBufferBase( gl.UNIFORM_BUFFER, index, bindingData.bufferGPU );
} else if ( binding.isSampledTexture ) {
const index = textures ++;
state.bindTexture( bindingData.glTextureType, bindingData.textureGPU, gl.TEXTURE0 + index );
}
}
}
}
/**
* The method ensures multisampled render targets are resolved.
*
* @private
* @param {RenderContext} renderContext - The render context.
*/
_resolveRenderTarget( renderContext ) {
const { gl, state } = this;
const renderTarget = renderContext.renderTarget;
if ( renderContext.textures !== null && renderTarget ) {
const renderTargetContextData = this.get( renderTarget );
if ( renderTarget.samples > 0 && renderTargetContextData.msaaFrameBuffer !== undefined && this._useMultisampledExtension( renderTarget ) === false ) {
const fb = renderTargetContextData.framebuffers[ renderContext.getCacheKey() ];
const textures = renderContext.textures;
let mask = renderTarget.resolveColorBuffer === false || textures.length === 0 ? 0 : gl.COLOR_BUFFER_BIT;
if ( renderTarget.resolveDepthBuffer ) {
if ( renderTarget.depthBuffer ) mask |= gl.DEPTH_BUFFER_BIT;
if ( renderTarget.stencilBuffer && renderTarget.resolveStencilBuffer ) mask |= gl.STENCIL_BUFFER_BIT;
}
const msaaFrameBuffer = renderTargetContextData.msaaFrameBuffer;
const msaaRenderbuffers = renderTargetContextData.msaaRenderbuffers;
const isMRT = textures.length > 1;
// Depth-only render targets have no color attachment but still resolve their depth.
const blitCount = Math.max( textures.length, 1 );
state.bindFramebuffer( gl.READ_FRAMEBUFFER, msaaFrameBuffer );
state.bindFramebuffer( gl.DRAW_FRAMEBUFFER, fb );
if ( isMRT ) {
// blitFramebuffer() can only copy/resolve the first color attachment of a framebuffer. When using MRT,
// the engine temporarily removes all attachments and then configures each attachment for the resolve.
for ( let i = 0; i < textures.length; i ++ ) {
gl.framebufferRenderbuffer( gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.RENDERBUFFER, null );
gl.framebufferTexture2D( gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, null, 0 );
}
}
for ( let i = 0; i < blitCount; i ++ ) {
if ( isMRT ) {
// configure attachment for resolve
const { textureGPU } = this.get( textures[ i ] );
gl.framebufferRenderbuffer( gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, msaaRenderbuffers[ i ] );
gl.framebufferTexture2D( gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, textureGPU, 0 );
}
if ( renderContext.scissor ) {
const { x, y, width, height } = renderContext.scissorValue;
const viewY = renderContext.height - height - y;
gl.blitFramebuffer( x, viewY, x + width, viewY + height, x, viewY, x + width, viewY + height, mask, gl.NEAREST );
} else {
gl.blitFramebuffer( 0, 0, renderContext.width, renderContext.height, 0, 0, renderContext.width, renderContext.height, mask, gl.NEAREST );
}
}
if ( isMRT ) {
// restore attachments
for ( let i = 0; i < textures.length; i ++ ) {
const { textureGPU } = this.get( textures[ i ] );
gl.framebufferRenderbuffer( gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.RENDERBUFFER, msaaRenderbuffers[ i ] );
gl.framebufferTexture2D( gl.DRAW_FRAMEBUFFER, gl.COLOR_ATTACHMENT0 + i, gl.TEXTURE_2D, textureGPU, 0 );
}
}
if ( this._supportsInvalidateFramebuffer === true ) {
if ( renderTarget.storeMultisampledColorBuffer === false ) {
for ( let i = 0; i < textures.length; i ++ ) {
_invalidationArray.push( gl.COLOR_ATTACHMENT0 + i );
}
}
if ( renderTarget.depthBuffer && renderTarget.storeMultisampledDepthBuffer === false ) {
_invalidationArray.push( renderTarget.stencilBuffer ? gl.DEPTH_STENCIL_ATTACHMENT : gl.DEPTH_ATTACHMENT );
}
if ( _invalidationArray.length > 0 ) {
gl.invalidateFramebuffer( gl.READ_FRAMEBUFFER, _invalidationArray );
_invalidationArray.length = 0;
}
}
} else if ( this._supportsInvalidateFramebuffer === true && renderTargetContextData.framebuffers &&
( renderTarget._autoAllocateDepthBuffer === true || ( renderTarget.samples > 0 && renderTarget.storeMultisampledDepthBuffer === false ) ) ) {
const fb = renderTargetContextData.framebuffers[ renderContext.getCacheKey() ];
state.bindFramebuffer( gl.DRAW_FRAMEBUFFER, fb );
gl.invalidateFramebuffer( gl.DRAW_FRAMEBUFFER, renderTargetContextData.depthInvalidationArray );
}
}
}
/**
* Returns `true` if the `WEBGL_multisampled_render_to_texture` extension
* should be used when MSAA is enabled.
*
* @private
* @param {RenderTarget} renderTarget - The render target that should be multisampled.
* @return {boolean} Whether to use the `WEBGL_multisampled_render_to_texture` extension for MSAA or not.
*/
_useMultisampledExtension( renderTarget ) {
if ( renderTarget.multiview === true ) {
return true;
}
return renderTarget.samples > 0 && this.extensions.has( 'WEBGL_multisampled_render_to_texture' ) === true && renderTarget._autoAllocateDepthBuffer !== false;
}
/**
* Frees internal resources.
*/
async dispose() {
await super.dispose();
if ( this.vertexArrayUtils !== null ) this.vertexArrayUtils.dispose();
if ( this.textureUtils !== null ) this.textureUtils.dispose();
if ( this.parameters.canvas === undefined ) {
const extension = this.extensions.get( 'WEBGL_lose_context' );
if ( extension ) extension.loseContext();
}
this.renderer.domElement.removeEventListener( 'webglcontextlost', this._onContextLost );
}
}
const GPUPrimitiveTopology = {
PointList: 'point-list',
LineList: 'line-list',
LineStrip: 'line-strip',
TriangleList: 'triangle-list'};
const GPUShaderStage = ( typeof self !== 'undefined' && self.GPUShaderStage ) ? self.GPUShaderStage : { VERTEX: 1, FRAGMENT: 2, COMPUTE: 4 };
const GPUCompareFunction = {
Never: 'never',
Less: 'less',
Equal: 'equal',
LessEqual: 'less-equal',
Greater: 'greater',
NotEqual: 'not-equal',
GreaterEqual: 'greater-equal',
Always: 'always'
};
const GPUStoreOp = {
Store: 'store',
Discard: 'discard'
};
const GPULoadOp = {
Load: 'load',
Clear: 'clear'
};
const GPUFrontFace = {
CCW: 'ccw',
CW: 'cw'
};
const GPUCullMode = {
None: 'none',
Back: 'back'
};
const GPUIndexFormat = {
Uint16: 'uint16',
Uint32: 'uint32'
};
const GPUTextureFormat = {
// 8-bit formats
R8Unorm: 'r8unorm',
R8Snorm: 'r8snorm',
R8Uint: 'r8uint',
R8Sint: 'r8sint',
// 16-bit formats
R16Uint: 'r16uint',
R16Sint: 'r16sint',
R16Float: 'r16float',
RG8Unorm: 'rg8unorm',
RG8Snorm: 'rg8snorm',
RG8Uint: 'rg8uint',
RG8Sint: 'rg8sint',
R16Unorm: 'r16unorm',
R16Snorm: 'r16snorm',
// 32-bit formats
R32Uint: 'r32uint',
R32Sint: 'r32sint',
R32Float: 'r32float',
RG16Uint: 'rg16uint',
RG16Sint: 'rg16sint',
RG16Float: 'rg16float',
RGBA8Unorm: 'rgba8unorm',
RGBA8UnormSRGB: 'rgba8unorm-srgb',
RGBA8Snorm: 'rgba8snorm',
RGBA8Uint: 'rgba8uint',
RGBA8Sint: 'rgba8sint',
BGRA8Unorm: 'bgra8unorm',
BGRA8UnormSRGB: 'bgra8unorm-srgb',
RG16Unorm: 'rg16unorm',
RG16Snorm: 'rg16snorm',
// Packed 32-bit formats
RGB9E5UFloat: 'rgb9e5ufloat',
RGB10A2Unorm: 'rgb10a2unorm',
RG11B10UFloat: 'rg11b10ufloat',
// 64-bit formats
RG32Uint: 'rg32uint',
RG32Sint: 'rg32sint',
RG32Float: 'rg32float',
RGBA16Uint: 'rgba16uint',
RGBA16Sint: 'rgba16sint',
RGBA16Float: 'rgba16float',
RGBA16Unorm: 'rgba16unorm',
RGBA16Snorm: 'rgba16snorm',
// 128-bit formats
RGBA32Uint: 'rgba32uint',
RGBA32Sint: 'rgba32sint',
RGBA32Float: 'rgba32float',
Depth16Unorm: 'depth16unorm',
Depth24Plus: 'depth24plus',
Depth24PlusStencil8: 'depth24plus-stencil8',
Depth32Float: 'depth32float',
// 'depth32float-stencil8' extension
Depth32FloatStencil8: 'depth32float-stencil8',
// BC compressed formats usable if 'texture-compression-bc' is both
// supported by the device/user agent and enabled in requestDevice.
BC1RGBAUnorm: 'bc1-rgba-unorm',
BC1RGBAUnormSRGB: 'bc1-rgba-unorm-srgb',
BC2RGBAUnorm: 'bc2-rgba-unorm',
BC2RGBAUnormSRGB: 'bc2-rgba-unorm-srgb',
BC3RGBAUnorm: 'bc3-rgba-unorm',
BC3RGBAUnormSRGB: 'bc3-rgba-unorm-srgb',
BC4RUnorm: 'bc4-r-unorm',
BC4RSnorm: 'bc4-r-snorm',
BC5RGUnorm: 'bc5-rg-unorm',
BC5RGSnorm: 'bc5-rg-snorm',
BC6HRGBUFloat: 'bc6h-rgb-ufloat',
BC6HRGBFloat: 'bc6h-rgb-float',
BC7RGBAUnorm: 'bc7-rgba-unorm',
BC7RGBAUnormSRGB: 'bc7-rgba-unorm-srgb',
// ETC2 compressed formats usable if 'texture-compression-etc2' is both
// supported by the device/user agent and enabled in requestDevice.
ETC2RGB8Unorm: 'etc2-rgb8unorm',
ETC2RGB8UnormSRGB: 'etc2-rgb8unorm-srgb',
ETC2RGB8A1Unorm: 'etc2-rgb8a1unorm',
ETC2RGB8A1UnormSRGB: 'etc2-rgb8a1unorm-srgb',
ETC2RGBA8Unorm: 'etc2-rgba8unorm',
ETC2RGBA8UnormSRGB: 'etc2-rgba8unorm-srgb',
EACR11Unorm: 'eac-r11unorm',
EACR11Snorm: 'eac-r11snorm',
EACRG11Unorm: 'eac-rg11unorm',
EACRG11Snorm: 'eac-rg11snorm',
// ASTC compressed formats usable if 'texture-compression-astc' is both
// supported by the device/user agent and enabled in requestDevice.
ASTC4x4Unorm: 'astc-4x4-unorm',
ASTC4x4UnormSRGB: 'astc-4x4-unorm-srgb',
ASTC5x4Unorm: 'astc-5x4-unorm',
ASTC5x4UnormSRGB: 'astc-5x4-unorm-srgb',
ASTC5x5Unorm: 'astc-5x5-unorm',
ASTC5x5UnormSRGB: 'astc-5x5-unorm-srgb',
ASTC6x5Unorm: 'astc-6x5-unorm',
ASTC6x5UnormSRGB: 'astc-6x5-unorm-srgb',
ASTC6x6Unorm: 'astc-6x6-unorm',
ASTC6x6UnormSRGB: 'astc-6x6-unorm-srgb',
ASTC8x5Unorm: 'astc-8x5-unorm',
ASTC8x5UnormSRGB: 'astc-8x5-unorm-srgb',
ASTC8x6Unorm: 'astc-8x6-unorm',
ASTC8x6UnormSRGB: 'astc-8x6-unorm-srgb',
ASTC8x8Unorm: 'astc-8x8-unorm',
ASTC8x8UnormSRGB: 'astc-8x8-unorm-srgb',
ASTC10x5Unorm: 'astc-10x5-unorm',
ASTC10x5UnormSRGB: 'astc-10x5-unorm-srgb',
ASTC10x6Unorm: 'astc-10x6-unorm',
ASTC10x6UnormSRGB: 'astc-10x6-unorm-srgb',
ASTC10x8Unorm: 'astc-10x8-unorm',
ASTC10x8UnormSRGB: 'astc-10x8-unorm-srgb',
ASTC10x10Unorm: 'astc-10x10-unorm',
ASTC10x10UnormSRGB: 'astc-10x10-unorm-srgb',
ASTC12x10Unorm: 'astc-12x10-unorm',
ASTC12x10UnormSRGB: 'astc-12x10-unorm-srgb',
ASTC12x12Unorm: 'astc-12x12-unorm',
ASTC12x12UnormSRGB: 'astc-12x12-unorm-srgb',
};
const GPUAddressMode = {
ClampToEdge: 'clamp-to-edge',
Repeat: 'repeat',
MirrorRepeat: 'mirror-repeat'
};
const GPUFilterMode = {
Linear: 'linear',
Nearest: 'nearest'
};
const GPUBlendFactor = {
Zero: 'zero',
One: 'one',
Src: 'src',
OneMinusSrc: 'one-minus-src',
SrcAlpha: 'src-alpha',
OneMinusSrcAlpha: 'one-minus-src-alpha',
Dst: 'dst',
OneMinusDst: 'one-minus-dst',
DstAlpha: 'dst-alpha',
OneMinusDstAlpha: 'one-minus-dst-alpha',
SrcAlphaSaturated: 'src-alpha-saturated',
Constant: 'constant',
OneMinusConstant: 'one-minus-constant'
};
const GPUBlendOperation = {
Add: 'add',
Subtract: 'subtract',
ReverseSubtract: 'reverse-subtract',
Min: 'min',
Max: 'max'
};
const GPUColorWriteFlags = {
None: 0,
All: 0xF
};
const GPUStencilOperation = {
Keep: 'keep',
Zero: 'zero',
Replace: 'replace',
Invert: 'invert',
IncrementClamp: 'increment-clamp',
DecrementClamp: 'decrement-clamp',
IncrementWrap: 'increment-wrap',
DecrementWrap: 'decrement-wrap'
};
const GPUBufferBindingType = {
Storage: 'storage',
ReadOnlyStorage: 'read-only-storage'
};
const GPUStorageTextureAccess = {
WriteOnly: 'write-only',
ReadOnly: 'read-only',
ReadWrite: 'read-write',
};
const GPUSamplerBindingType = {
NonFiltering: 'non-filtering',
Comparison: 'comparison'
};
const GPUTextureSampleType = {
Float: 'float',
UnfilterableFloat: 'unfilterable-float',
Depth: 'depth',
SInt: 'sint',
UInt: 'uint'
};
const GPUTextureDimension = {
TwoD: '2d',
ThreeD: '3d'
};
const GPUTextureViewDimension = {
TwoD: '2d',
TwoDArray: '2d-array',
Cube: 'cube',
ThreeD: '3d'
};
const GPUTextureAspect = {
All: 'all',
DepthOnly: 'depth-only'
};
const GPUInputStepMode = {
Vertex: 'vertex',
Instance: 'instance'
};
const GPUFeatureName = {
CoreFeaturesAndLimits: 'core-features-and-limits',
DepthClipControl: 'depth-clip-control',
Depth32FloatStencil8: 'depth32float-stencil8',
TextureCompressionBC: 'texture-compression-bc',
TextureCompressionBCSliced3D: 'texture-compression-bc-sliced-3d',
TextureCompressionETC2: 'texture-compression-etc2',
TextureCompressionASTC: 'texture-compression-astc',
TextureCompressionASTCSliced3D: 'texture-compression-astc-sliced-3d',
TimestampQuery: 'timestamp-query',
IndirectFirstInstance: 'indirect-first-instance',
ShaderF16: 'shader-f16',
RG11B10UFloat: 'rg11b10ufloat-renderable',
BGRA8UNormStorage: 'bgra8unorm-storage',
Float32Filterable: 'float32-filterable',
Float32Blendable: 'float32-blendable',
ClipDistances: 'clip-distances',
DualSourceBlending: 'dual-source-blending',
Subgroups: 'subgroups',
TextureFormatsTier1: 'texture-formats-tier1',
TextureFormatsTier2: 'texture-formats-tier2'
};
const GPUFeatureMap = {
'texture-compression-s3tc': 'texture-compression-bc',
'texture-compression-etc1': 'texture-compression-etc2'
};
/**
* A special form of sampler binding type.
* It's texture value is managed by a node object.
*
* @private
* @augments Sampler
*/
class NodeSampler extends Sampler {
/**
* Constructs a new node-based sampler.
*
* @param {string} name - The samplers's name.
* @param {TextureNode} textureNode - The texture node.
* @param {UniformGroupNode} groupNode - The uniform group node.
*/
constructor( name, textureNode, groupNode ) {
super( name, textureNode ? textureNode.value : null );
/**
* The texture node.
*
* @type {TextureNode}
*/
this.textureNode = textureNode;
/**
* The uniform group node.
*
* @type {UniformGroupNode}
*/
this.groupNode = groupNode;
}
/**
* Updates the texture value of this sampler.
*
* @return {boolean} Whether the sampler needs an update or not.
*/
update() {
const { textureNode } = this;
if ( this.texture !== textureNode.value ) {
this.texture = textureNode.value;
return true;
}
return super.update();
}
/**
* Clones the binding.
*
* @return {NodeSampler} The cloned binding.
*/
clone() {
const binding = super.clone();
binding.texture = this.textureNode.value; // copy current node value to avoid stale references
return binding;
}
}
/**
* Represents a storage buffer binding type.
*
* @private
* @augments Buffer
*/
class StorageBuffer extends Buffer {
/**
* Constructs a new uniform buffer.
*
* @param {string} name - The buffer's name.
* @param {BufferAttribute} attribute - The buffer attribute.
*/
constructor( name, attribute ) {
super( name, attribute ? attribute.array : null );
/**
* This flag can be used for type testing.
*
* @private
* @type {BufferAttribute}
*/
this._attribute = attribute;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStorageBuffer = true;
}
/**
* The storage buffer attribute.
*
* @type {BufferAttribute}
*/
get attribute() {
return this._attribute;
}
}
let _id = 0;
/**
* A special form of storage buffer binding type.
* It's buffer value is managed by a node object.
*
* @private
* @augments StorageBuffer
*/
class NodeStorageBuffer extends StorageBuffer {
/**
* Constructs a new node-based storage buffer.
*
* @param {StorageBufferNode} nodeUniform - The storage buffer node.
* @param {UniformGroupNode} groupNode - The uniform group node.
*/
constructor( nodeUniform, groupNode ) {
const label = nodeUniform && nodeUniform.name ? nodeUniform.name : _id ++;
super( 'StorageBuffer_' + label );
/**
* The node uniform.
*
* @type {StorageBufferNode}
*/
this.nodeUniform = nodeUniform;
/**
* The access type.
*
* @type {string}
*/
this.access = nodeUniform ? nodeUniform.access : NodeAccess.READ_WRITE;
/**
* The uniform group node.
*
* @type {UniformGroupNode}
*/
this.groupNode = groupNode;
}
/**
* The storage buffer attribute node.
*
* @type {StorageBufferAttribute}
*/
get attribute() {
return this.nodeUniform.value;
}
/**
* The storage buffer.
*
* @type {Float32Array}
*/
get buffer() {
return this.nodeUniform.value.array;
}
}
const _commandList = [ null ];
/**
* A WebGPU backend utility module with common helpers.
*
* @private
*/
class WebGPUUtils {
/**
* Constructs a new utility object.
*
* @param {WebGPUBackend} backend - The WebGPU backend.
*/
constructor( backend ) {
/**
* A reference to the WebGPU backend.
*
* @type {WebGPUBackend}
*/
this.backend = backend;
/**
* Caches the preferred canvas format.
*
* @private
* @type {?string}
* @default null
*/
this._preferredCanvasFormat = null;
}
/**
* Returns the depth/stencil GPU format for the given render context.
*
* @param {RenderContext} renderContext - The render context.
* @return {string} The depth/stencil GPU texture format.
*/
getCurrentDepthStencilFormat( renderContext ) {
let format;
if ( renderContext.depth ) {
if ( renderContext.depthTexture !== null ) {
format = this.getTextureFormatGPU( renderContext.depthTexture );
} else if ( renderContext.stencil ) {
if ( this.backend.renderer.reversedDepthBuffer === true ) {
format = GPUTextureFormat.Depth32FloatStencil8;
} else {
format = GPUTextureFormat.Depth24PlusStencil8;
}
} else {
if ( this.backend.renderer.reversedDepthBuffer === true ) {
format = GPUTextureFormat.Depth32Float;
} else {
format = GPUTextureFormat.Depth24Plus;
}
}
}
return format;
}
/**
* Returns the GPU format for the given texture.
*
* @param {Texture} texture - The texture.
* @return {string} The GPU texture format.
*/
getTextureFormatGPU( texture ) {
return this.backend.get( texture ).format;
}
/**
* Returns an object that defines the multi-sampling state of the given texture.
*
* @param {Texture} texture - The texture.
* @return {Object} The multi-sampling state.
*/
getTextureSampleData( texture ) {
let samples;
if ( texture.isFramebufferTexture ) {
samples = 1;
} else if ( texture.isDepthTexture && ! texture.renderTarget ) {
const textureData = this.backend.get( texture );
if ( textureData.texture !== undefined ) {
// use the effective sample count of the allocated texture
samples = textureData.texture.sampleCount;
} else {
// otherwise use the current samples of the renderer
samples = this.backend.renderer.currentSamples;
}
} else if ( texture.renderTarget ) {
samples = texture.renderTarget.samples;
}
samples = this.getSampleCount( samples || 1 );
const isMSAA = samples > 1 && texture.renderTarget !== null && ( texture.isDepthTexture !== true && texture.isFramebufferTexture !== true );
const isMSAAArrayDepthTexture = samples > 1 && texture.renderTarget !== null && texture.isDepthTexture === true && texture.isArrayTexture === true;
const primarySamples = isMSAA || isMSAAArrayDepthTexture ? 1 : samples;
return { samples, primarySamples, isMSAA };
}
/**
* Returns the default color attachment's GPU format of the current render context.
*
* @param {RenderContext} renderContext - The render context.
* @return {string} The GPU texture format of the default color attachment.
*/
getCurrentColorFormat( renderContext ) {
let format;
if ( renderContext.textures !== null ) {
format = renderContext.textures.length > 0 ? this.getTextureFormatGPU( renderContext.textures[ 0 ] ) : null;
} else {
format = this.getPreferredCanvasFormat(); // default context format
}
return format;
}
/**
* Returns the GPU formats of all color attachments of the current render context.
*
* @param {RenderContext} renderContext - The render context.
* @return {Array<string>} The GPU texture formats of all color attachments.
*/
getCurrentColorFormats( renderContext ) {
if ( renderContext.textures !== null ) {
return renderContext.textures.map( t => this.getTextureFormatGPU( t ) );
} else {
return [ this.getPreferredCanvasFormat() ]; // default context format
}
}
/**
* Returns the output color space of the current render context.
*
* @param {RenderContext} renderContext - The render context.
* @return {string} The output color space.
*/
getCurrentColorSpace( renderContext ) {
if ( renderContext.textures !== null ) {
return renderContext.textures[ 0 ]?.colorSpace;
}
return this.backend.renderer.outputColorSpace;
}
/**
* Returns GPU primitive topology for the given object and material.
*
* @param {Object3D} object - The 3D object.
* @param {Material} material - The material.
* @return {string} The GPU primitive topology.
*/
getPrimitiveTopology( object, material ) {
if ( object.isPoints ) return GPUPrimitiveTopology.PointList;
else if ( object.isLineSegments || ( object.isMesh && material.wireframe === true ) ) return GPUPrimitiveTopology.LineList;
else if ( object.isLine ) return GPUPrimitiveTopology.LineStrip;
else if ( object.isMesh ) return GPUPrimitiveTopology.TriangleList;
}
/**
* Returns the GPU strip index format for the given object and geometry.
*
* @param {Object3D} object - The 3D object.
* @param {BufferGeometry} geometry - The geometry.
* @return {string|undefined} The GPU strip index format. `undefined` if the object is not rendered as an indexed strip.
*/
getStripIndexFormat( object, geometry ) {
if ( geometry.index !== null && object.isLine === true && object.isLineSegments !== true ) {
return ( geometry.index.array instanceof Uint16Array ) ? GPUIndexFormat.Uint16 : GPUIndexFormat.Uint32;
}
}
/**
* Returns a modified sample count from the given sample count value.
*
* That is required since WebGPU only supports either 1 or 4.
*
* @param {number} sampleCount - The input sample count.
* @return {number} The (potentially updated) output sample count.
*/
getSampleCount( sampleCount ) {
return sampleCount >= 4 ? 4 : 1;
}
/**
* Returns the sample count of the given render context.
*
* @param {RenderContext} renderContext - The render context.
* @return {number} The sample count.
*/
getSampleCountRenderContext( renderContext ) {
if ( renderContext.textures !== null ) {
return this.getSampleCount( renderContext.sampleCount );
}
return this.getSampleCount( this.backend.renderer.currentSamples );
}
/**
* Returns the preferred canvas format.
*
* There is a separate method for this so it's possible to
* honor edge cases for specific devices.
*
* @return {string} The GPU texture format of the canvas.
*/
getPreferredCanvasFormat() {
const parameters = this.backend.parameters;
const bufferType = parameters.outputType;
if ( bufferType === undefined ) {
if ( this._preferredCanvasFormat === null ) {
this._preferredCanvasFormat = navigator.gpu.getPreferredCanvasFormat();
}
return this._preferredCanvasFormat;
} else if ( bufferType === UnsignedByteType ) {
return GPUTextureFormat.BGRA8Unorm;
} else if ( bufferType === HalfFloatType ) {
return GPUTextureFormat.RGBA16Float;
} else {
throw new Error( 'THREE.WebGPUUtils: Unsupported output buffer type.' );
}
}
}
/**
* Submits a single GPU command to the device queue using a shared, module-scoped
* array to avoid per-call array allocations.
*
* @private
* @param {GPUDevice} device - The GPU device.
* @param {GPUCommandBuffer} command - The command buffer to submit.
*/
function submit( device, command ) {
_commandList[ 0 ] = command;
device.queue.submit( _commandList );
_commandList[ 0 ] = null;
}
/**
* Reusable descriptor for `GPUDevice.createBindGroup()`.
*
* @private
*/
class GPUBindGroupDescriptor {
constructor() {
/**
* The label of the bind group.
*
* @type {string}
*/
this.label = '';
/**
* The bind group layout the bind group conforms to.
*
* @type {?GPUBindGroupLayout}
* @default null
*/
this.layout = null;
/**
* The bind group entries.
*
* @type {Array<Object>}
*/
this.entries = [];
}
/**
* Resets the descriptor to its default state. The internal `entries` array
* is emptied without releasing its backing storage.
*/
reset() {
this.label = '';
this.layout = null;
this.entries.length = 0;
}
}
/**
* Reusable descriptor for `GPUDevice.createBuffer()`.
*
* @private
*/
class GPUBufferDescriptor {
constructor() {
/**
* The label of the buffer.
*
* @type {string}
*/
this.label = '';
/**
* The size of the buffer in bytes.
*
* @type {number}
* @default 0
*/
this.size = 0;
/**
* The allowed usages for the buffer.
*
* @type {number}
* @default 0
*/
this.usage = 0;
/**
* Whether the buffer is in the mapped state at creation.
*
* @type {boolean}
* @default false
*/
this.mappedAtCreation = false;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.label = '';
this.size = 0;
this.usage = 0;
this.mappedAtCreation = false;
}
}
/**
* Reusable descriptor for `GPUDevice.createCommandEncoder()`.
*
* @private
*/
class GPUCommandEncoderDescriptor {
constructor() {
/**
* The label of the command encoder.
*
* @type {string}
*/
this.label = '';
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.label = '';
}
}
/**
* Reusable descriptor for `GPUDevice.createRenderBundleEncoder()`.
*
* @private
*/
class GPURenderBundleEncoderDescriptor {
constructor() {
/**
* The label of the render bundle encoder.
*
* @type {string}
*/
this.label = '';
/**
* The formats of the color attachments the bundle is compatible with.
*
* @type {?Array<?string>}
* @default null
*/
this.colorFormats = null;
/**
* The format of the depth/stencil attachment the bundle is compatible with.
*
* @type {string|undefined}
*/
this.depthStencilFormat = undefined;
/**
* The number of samples per pixel the bundle is compatible with.
*
* @type {number}
* @default 1
*/
this.sampleCount = 1;
/**
* Whether the depth attachment is read-only.
*
* @type {boolean}
* @default false
*/
this.depthReadOnly = false;
/**
* Whether the stencil attachment is read-only.
*
* @type {boolean}
* @default false
*/
this.stencilReadOnly = false;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.label = '';
this.colorFormats = null;
this.depthStencilFormat = undefined;
this.sampleCount = 1;
this.depthReadOnly = false;
this.stencilReadOnly = false;
}
}
/**
* Reusable descriptor for `GPURenderPassColorAttachment`, the type of each
* entry in `GPURenderPassDescriptor.colorAttachments`.
*
* @private
*/
class GPURenderPassColorAttachment {
constructor() {
/**
* The texture view the pass renders into.
*
* @type {?GPUTextureView}
* @default null
*/
this.view = null;
/**
* The depth slice the pass renders into.
*
* @type {number|undefined}
*/
this.depthSlice = undefined;
/**
* The texture view that receives the resolved output of multisampled rendering.
*
* @type {?GPUTextureView|undefined}
*/
this.resolveTarget = undefined;
/**
* The clear value used when `loadOp` is `'clear'`.
*
* @type {Object|undefined}
*/
this.clearValue = undefined;
/**
* The load operation performed at the start of the pass.
*
* @type {string|undefined}
*/
this.loadOp = undefined;
/**
* The store operation performed at the end of the pass.
*
* @type {string|undefined}
*/
this.storeOp = undefined;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.view = null;
this.depthSlice = undefined;
this.resolveTarget = undefined;
this.clearValue = undefined;
this.loadOp = undefined;
this.storeOp = undefined;
}
}
/**
* Reusable descriptor for `GPUCommandEncoder.beginRenderPass()`.
*
* @private
*/
class GPURenderPassDescriptor {
constructor() {
/**
* The label of the render pass.
*
* @type {string}
*/
this.label = '';
/**
* The color attachments of the render pass.
*
* @type {Array<?Object>}
*/
this.colorAttachments = [];
/**
* The depth-stencil attachment of the render pass.
*
* @type {Object|undefined}
*/
this.depthStencilAttachment = undefined;
/**
* The query set used for occlusion queries during the pass.
*
* @type {?GPUQuerySet|undefined}
*/
this.occlusionQuerySet = undefined;
/**
* Defines which timestamp values are written and where.
*
* @type {Object|undefined}
*/
this.timestampWrites = undefined;
/**
* The maximum number of draw calls that can be issued during the pass.
*
* @type {number}
* @default 50000000
*/
this.maxDrawCount = 50000000;
}
/**
* Resets the descriptor to its default state. The internal `colorAttachments`
* array is emptied without releasing its backing storage.
*/
reset() {
this.label = '';
this.colorAttachments.length = 0;
this.depthStencilAttachment = undefined;
this.occlusionQuerySet = undefined;
this.timestampWrites = undefined;
this.maxDrawCount = 50000000;
}
}
/**
* Reusable descriptor for `GPUDevice.createRenderPipeline()` and
* `createRenderPipelineAsync()`.
*
* @private
*/
class GPURenderPipelineDescriptor {
constructor() {
/**
* The label of the render pipeline.
*
* @type {string}
*/
this.label = '';
/**
* The pipeline layout the pipeline conforms to, or `'auto'`.
*
* @type {?GPUPipelineLayout|string}
* @default null
*/
this.layout = null;
/**
* The programmable vertex stage.
*
* @type {?Object}
* @default null
*/
this.vertex = null;
/**
* The primitive-assembly state.
*
* @type {Object}
*/
this.primitive = {};
/**
* The depth/stencil state, omitted when the pipeline has no depth or stencil aspect.
*
* @type {Object|undefined}
*/
this.depthStencil = undefined;
/**
* The multisample state.
*
* @type {GPUMultisampleState}
*/
this.multisample = new GPUMultisampleState();
/**
* The programmable fragment stage. Omitted for vertex-only pipelines.
*
* @type {?Object}
* @default null
*/
this.fragment = null;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.label = '';
this.layout = null;
this.vertex = null;
this.primitive = {};
this.depthStencil = undefined;
this.multisample.reset();
this.fragment = null;
}
}
/**
* Reusable nested state for `GPURenderPipelineDescriptor.multisample`.
*
* @private
*/
class GPUMultisampleState {
constructor() {
/**
* The number of samples per pixel.
*
* @type {number}
* @default 1
*/
this.count = 1;
/**
* A bitmask determining which samples are written to.
*
* @type {number}
* @default 0xFFFFFFFF
*/
this.mask = 0xFFFFFFFF;
/**
* Whether a fragment's alpha channel is used to generate a sample coverage mask.
*
* @type {boolean}
* @default false
*/
this.alphaToCoverageEnabled = false;
}
/**
* Resets the state to its default values.
*/
reset() {
this.count = 1;
this.mask = 0xFFFFFFFF;
this.alphaToCoverageEnabled = false;
}
}
/**
* Reusable descriptor for `GPUDevice.createShaderModule()`.
*
* @private
*/
class GPUShaderModuleDescriptor {
constructor() {
/**
* The label of the shader module.
*
* @type {string}
*/
this.label = '';
/**
* The WGSL source code of the shader module.
*
* @type {string}
*/
this.code = '';
/**
* Compilation hints that may help the implementation produce optimized code.
*
* @type {Array<Object>}
*/
this.compilationHints = [];
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.label = '';
this.code = '';
this.compilationHints.length = 0;
}
}
/**
* Reusable descriptor for `GPUDevice.createTexture()`.
*
* @private
*/
class GPUTextureDescriptor {
constructor() {
/**
* The label of the texture.
*
* @type {string}
*/
this.label = '';
/**
* The size of the texture.
*
* @type {{width: number, height: number, depthOrArrayLayers: number}}
*/
this.size = { width: 0, height: 1, depthOrArrayLayers: 1 };
/**
* The number of mip levels the texture will contain.
*
* @type {number}
* @default 1
*/
this.mipLevelCount = 1;
/**
* The sample count of the texture.
*
* @type {number}
* @default 1
*/
this.sampleCount = 1;
/**
* The dimension of the set of texel coordinates.
*
* @type {string}
* @default '2d'
*/
this.dimension = '2d';
/**
* The format of the texture.
*
* @type {string|undefined}
*/
this.format = undefined;
/**
* The allowed usages for the texture.
*
* @type {number|undefined}
*/
this.usage = undefined;
/**
* The formats that views of this texture may use.
*
* @type {Array<string>}
*/
this.viewFormats = [];
/**
* The view dimension to use when binding the texture (compatibility mode).
*
* @type {string|undefined}
*/
this.textureBindingViewDimension = undefined;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.label = '';
this.size.width = 0;
this.size.height = 1;
this.size.depthOrArrayLayers = 1;
this.mipLevelCount = 1;
this.sampleCount = 1;
this.dimension = '2d';
this.format = undefined;
this.usage = undefined;
this.viewFormats.length = 0;
this.textureBindingViewDimension = undefined;
}
}
/**
* Reusable descriptor for `GPUTexture.createView()`.
*
* @private
*/
class GPUTextureViewDescriptor {
constructor() {
/**
* The label of the texture view.
*
* @type {string}
*/
this.label = '';
/**
* The format of the texture view.
*
* @type {string|undefined}
*/
this.format = undefined;
/**
* The dimension of the texture view.
*
* @type {string|undefined}
*/
this.dimension = undefined;
/**
* The allowed usages for the texture view.
*
* @type {number}
* @default 0
*/
this.usage = 0;
/**
* Which aspect of the texture is referenced.
*
* @type {string}
* @default 'all'
*/
this.aspect = 'all';
/**
* The first mip level accessible to the texture view.
*
* @type {number}
* @default 0
*/
this.baseMipLevel = 0;
/**
* The number of mip levels accessible to the texture view.
*
* @type {number|undefined}
*/
this.mipLevelCount = undefined;
/**
* The first array layer accessible to the texture view.
*
* @type {number}
* @default 0
*/
this.baseArrayLayer = 0;
/**
* The number of array layers accessible to the texture view.
*
* @type {number|undefined}
*/
this.arrayLayerCount = undefined;
/**
* The component swizzle to apply when sampling the texture view.
* Requires the `'texture-component-swizzle'` feature; ignored otherwise.
*
* @type {string}
* @default 'rgba'
*/
this.swizzle = 'rgba';
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.label = '';
this.format = undefined;
this.dimension = undefined;
this.usage = 0;
this.aspect = 'all';
this.baseMipLevel = 0;
this.mipLevelCount = undefined;
this.baseArrayLayer = 0;
this.arrayLayerCount = undefined;
this.swizzle = 'rgba';
}
}
const _bindGroupDescriptor$1 = new GPUBindGroupDescriptor();
const _bufferDescriptor$5 = new GPUBufferDescriptor();
const _commandEncoderDescriptor$4 = new GPUCommandEncoderDescriptor();
const _renderBundleEncoderDescriptor$1 = new GPURenderBundleEncoderDescriptor();
const _renderPassDescriptor = new GPURenderPassDescriptor();
const _renderPipelineDescriptor$1 = new GPURenderPipelineDescriptor();
const _colorAttachment = new GPURenderPassColorAttachment();
const _shaderModuleDescriptor$1 = new GPUShaderModuleDescriptor();
const _textureDescriptor$2 = new GPUTextureDescriptor();
const _viewDescriptor$2 = new GPUTextureViewDescriptor();
/**
* A WebGPU backend utility module used by {@link WebGPUTextureUtils}.
*
* @private
*/
class WebGPUTexturePassUtils extends DataMap {
/**
* Constructs a new utility object.
*
* @param {GPUDevice} device - The WebGPU device.
*/
constructor( device ) {
super();
/**
* The WebGPU device.
*
* @type {GPUDevice}
*/
this.device = device;
const mipmapSource = `
struct VarysStruct {
@builtin( position ) Position: vec4f,
@location( 0 ) vTex : vec2f,
@location( 1 ) @interpolate(flat, either) vBaseArrayLayer: u32,
};
@group( 0 ) @binding ( 2 )
var<uniform> flipY: u32;
@vertex
fn mainVS(
@builtin( vertex_index ) vertexIndex : u32,
@builtin( instance_index ) instanceIndex : u32 ) -> VarysStruct {
var Varys : VarysStruct;
var pos = array(
vec2f( -1, -1 ),
vec2f( -1, 3 ),
vec2f( 3, -1 ),
);
let p = pos[ vertexIndex ];
let mult = select( vec2f( 0.5, -0.5 ), vec2f( 0.5, 0.5 ), flipY != 0 );
Varys.vTex = p * mult + vec2f( 0.5 );
Varys.Position = vec4f( p, 0, 1 );
Varys.vBaseArrayLayer = instanceIndex;
return Varys;
}
@group( 0 ) @binding( 0 )
var imgSampler : sampler;
@group( 0 ) @binding( 1 )
var img2d : texture_2d<f32>;
@fragment
fn main_2d( Varys: VarysStruct ) -> @location( 0 ) vec4<f32> {
return textureSample( img2d, imgSampler, Varys.vTex );
}
@group( 0 ) @binding( 1 )
var img2dArray : texture_2d_array<f32>;
@fragment
fn main_2d_array( Varys: VarysStruct ) -> @location( 0 ) vec4<f32> {
return textureSample( img2dArray, imgSampler, Varys.vTex, Varys.vBaseArrayLayer );
}
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
);
@group( 0 ) @binding( 1 )
var imgCube : texture_cube<f32>;
@fragment
fn main_cube( Varys: VarysStruct ) -> @location( 0 ) vec4<f32> {
return textureSample( imgCube, imgSampler, faceMat[ Varys.vBaseArrayLayer ] * vec3f( fract( Varys.vTex ), 1 ) );
}
`;
/**
* The mipmap GPU sampler.
*
* @type {GPUSampler}
*/
this.mipmapSampler = device.createSampler( { minFilter: GPUFilterMode.Linear } );
/**
* The flipY GPU sampler.
*
* @type {GPUSampler}
*/
this.flipYSampler = device.createSampler( { minFilter: GPUFilterMode.Nearest } ); //@TODO?: Consider using textureLoad()
/**
* flip uniform buffer
* @type {GPUBuffer}
*/
_bufferDescriptor$5.size = 4;
_bufferDescriptor$5.usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST;
this.flipUniformBuffer = device.createBuffer( _bufferDescriptor$5 );
_bufferDescriptor$5.reset();
device.queue.writeBuffer( this.flipUniformBuffer, 0, new Uint32Array( [ 1 ] ) );
/**
* no flip uniform buffer
* @type {GPUBuffer}
*/
_bufferDescriptor$5.size = 4;
_bufferDescriptor$5.usage = GPUBufferUsage.UNIFORM;
this.noFlipUniformBuffer = device.createBuffer( _bufferDescriptor$5 );
_bufferDescriptor$5.reset();
/**
* A cache for GPU render pipelines used for copy/transfer passes.
* Every texture format and textureBindingViewDimension combo requires a unique pipeline.
*
* @type {Object<string,GPURenderPipeline>}
*/
this.transferPipelines = {};
/**
* The mipmap shader module.
*
* @type {GPUShaderModule}
*/
_shaderModuleDescriptor$1.label = 'mipmap';
_shaderModuleDescriptor$1.code = mipmapSource;
this.mipmapShaderModule = device.createShaderModule( _shaderModuleDescriptor$1 );
_shaderModuleDescriptor$1.reset();
}
/**
* Returns a render pipeline for the internal copy render pass. The pass
* requires a unique render pipeline for each texture format.
*
* @param {string} format - The GPU texture format
* @param {string?} textureBindingViewDimension - The GPU texture binding view dimension
* @return {GPURenderPipeline} The GPU render pipeline.
*/
getTransferPipeline( format, textureBindingViewDimension ) {
textureBindingViewDimension = textureBindingViewDimension || '2d-array';
const key = `${ format }-${ textureBindingViewDimension }`;
let pipeline = this.transferPipelines[ key ];
if ( pipeline === undefined ) {
_renderPipelineDescriptor$1.label = `mipmap-${ format }-${ textureBindingViewDimension }`;
_renderPipelineDescriptor$1.vertex = { module: this.mipmapShaderModule };
_renderPipelineDescriptor$1.fragment = {
module: this.mipmapShaderModule,
entryPoint: `main_${ textureBindingViewDimension.replace( '-', '_' ) }`,
targets: [ { format } ]
};
_renderPipelineDescriptor$1.layout = 'auto';
pipeline = this.device.createRenderPipeline( _renderPipelineDescriptor$1 );
_renderPipelineDescriptor$1.reset();
this.transferPipelines[ key ] = pipeline;
}
return pipeline;
}
/**
* Flip the contents of the given GPU texture along its vertical axis.
*
* @param {GPUTexture} textureGPU - The GPU texture object.
* @param {Object} textureGPUDescriptor - The texture descriptor.
* @param {number} [baseArrayLayer=0] - The index of the first array layer accessible to the texture view.
*/
flipY( textureGPU, textureGPUDescriptor, baseArrayLayer = 0 ) {
const format = textureGPUDescriptor.format;
const { width, height } = textureGPUDescriptor.size;
_textureDescriptor$2.size.width = width;
_textureDescriptor$2.size.height = height;
_textureDescriptor$2.format = format;
_textureDescriptor$2.usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING;
const tempTexture = this.device.createTexture( _textureDescriptor$2 );
_textureDescriptor$2.reset();
const copyTransferPipeline = this.getTransferPipeline( format, textureGPU.textureBindingViewDimension );
const flipTransferPipeline = this.getTransferPipeline( format, tempTexture.textureBindingViewDimension );
const commandEncoder = this.device.createCommandEncoder( _commandEncoderDescriptor$4 );
const pass = ( pipeline, sourceTexture, sourceArrayLayer, destinationTexture, destinationArrayLayer, flipY ) => {
const bindGroupLayout = pipeline.getBindGroupLayout( 0 ); // @TODO: Consider making this static.
_viewDescriptor$2.dimension = sourceTexture.textureBindingViewDimension || '2d-array';
_viewDescriptor$2.mipLevelCount = 1;
const sourceView = sourceTexture.createView( _viewDescriptor$2 );
_viewDescriptor$2.reset();
_bindGroupDescriptor$1.layout = bindGroupLayout;
_bindGroupDescriptor$1.entries.push( {
binding: 0,
resource: this.flipYSampler
}, {
binding: 1,
resource: sourceView,
}, {
binding: 2,
resource: { buffer: flipY ? this.flipUniformBuffer : this.noFlipUniformBuffer }
} );
const bindGroup = this.device.createBindGroup( _bindGroupDescriptor$1 );
_bindGroupDescriptor$1.reset();
_viewDescriptor$2.dimension = '2d';
_viewDescriptor$2.mipLevelCount = 1;
_viewDescriptor$2.baseArrayLayer = destinationArrayLayer;
_viewDescriptor$2.arrayLayerCount = 1;
const destinationView = destinationTexture.createView( _viewDescriptor$2 );
_viewDescriptor$2.reset();
_colorAttachment.view = destinationView;
_colorAttachment.loadOp = GPULoadOp.Clear;
_colorAttachment.storeOp = GPUStoreOp.Store;
_renderPassDescriptor.colorAttachments.push( _colorAttachment );
const passEncoder = commandEncoder.beginRenderPass( _renderPassDescriptor );
_renderPassDescriptor.reset();
_colorAttachment.reset();
passEncoder.setPipeline( pipeline );
passEncoder.setBindGroup( 0, bindGroup );
passEncoder.draw( 3, 1, 0, sourceArrayLayer );
passEncoder.end();
};
pass( copyTransferPipeline, textureGPU, baseArrayLayer, tempTexture, 0, false );
pass( flipTransferPipeline, tempTexture, 0, textureGPU, baseArrayLayer, true );
submit( this.device, commandEncoder.finish() );
tempTexture.destroy();
}
/**
* Generates mipmaps for the given GPU texture.
*
* @param {GPUTexture} textureGPU - The GPU texture object.
* @param {?GPUCommandEncoder} [encoder=null] - An optional command encoder used to generate mipmaps.
*/
generateMipmaps( textureGPU, encoder = null ) {
const textureData = this.get( textureGPU );
const passes = textureData.layers || this._mipmapCreateBundles( textureGPU );
let commandEncoder = encoder;
if ( commandEncoder === null ) {
_commandEncoderDescriptor$4.label = 'mipmapEncoder';
commandEncoder = this.device.createCommandEncoder( _commandEncoderDescriptor$4 );
_commandEncoderDescriptor$4.reset();
}
this._mipmapRunBundles( commandEncoder, passes );
if ( encoder === null ) submit( this.device, commandEncoder.finish() );
textureData.layers = passes;
}
/**
* Since multiple copy render passes are required to generate mipmaps, the passes
* are managed as render bundles to improve performance.
*
* @param {GPUTexture} textureGPU - The GPU texture object.
* @return {Array<Object>} An array of render bundles.
*/
_mipmapCreateBundles( textureGPU ) {
const textureBindingViewDimension = textureGPU.textureBindingViewDimension || '2d-array';
const pipeline = this.getTransferPipeline( textureGPU.format, textureBindingViewDimension );
const bindGroupLayout = pipeline.getBindGroupLayout( 0 ); // @TODO: Consider making this static.
const passes = [];
for ( let baseMipLevel = 1; baseMipLevel < textureGPU.mipLevelCount; baseMipLevel ++ ) {
for ( let baseArrayLayer = 0; baseArrayLayer < textureGPU.depthOrArrayLayers; baseArrayLayer ++ ) {
_viewDescriptor$2.dimension = textureBindingViewDimension;
_viewDescriptor$2.baseMipLevel = baseMipLevel - 1;
_viewDescriptor$2.mipLevelCount = 1;
const sourceView = textureGPU.createView( _viewDescriptor$2 );
_viewDescriptor$2.reset();
_bindGroupDescriptor$1.layout = bindGroupLayout;
_bindGroupDescriptor$1.entries.push( {
binding: 0,
resource: this.mipmapSampler
}, {
binding: 1,
resource: sourceView,
}, {
binding: 2,
resource: { buffer: this.noFlipUniformBuffer }
} );
const bindGroup = this.device.createBindGroup( _bindGroupDescriptor$1 );
_bindGroupDescriptor$1.reset();
_viewDescriptor$2.dimension = '2d';
_viewDescriptor$2.baseMipLevel = baseMipLevel;
_viewDescriptor$2.mipLevelCount = 1;
_viewDescriptor$2.baseArrayLayer = baseArrayLayer;
_viewDescriptor$2.arrayLayerCount = 1;
const destinationView = textureGPU.createView( _viewDescriptor$2 );
_viewDescriptor$2.reset();
const passColorAttachment = new GPURenderPassColorAttachment();
passColorAttachment.view = destinationView;
passColorAttachment.loadOp = GPULoadOp.Clear;
passColorAttachment.storeOp = GPUStoreOp.Store;
const passDescriptor = new GPURenderPassDescriptor();
passDescriptor.colorAttachments.push( passColorAttachment );
_renderBundleEncoderDescriptor$1.colorFormats = [ textureGPU.format ];
const passEncoder = this.device.createRenderBundleEncoder( _renderBundleEncoderDescriptor$1 );
_renderBundleEncoderDescriptor$1.reset();
passEncoder.setPipeline( pipeline );
passEncoder.setBindGroup( 0, bindGroup );
passEncoder.draw( 3, 1, 0, baseArrayLayer );
passes.push( {
renderBundles: [ passEncoder.finish() ],
passDescriptor
} );
}
}
return passes;
}
/**
* Executes the render bundles.
*
* @param {GPUCommandEncoder} commandEncoder - The GPU command encoder.
* @param {Array<Object>} passes - An array of render bundles.
*/
_mipmapRunBundles( commandEncoder, passes ) {
const levels = passes.length;
for ( let i = 0; i < levels; i ++ ) {
const pass = passes[ i ];
const passEncoder = commandEncoder.beginRenderPass( pass.passDescriptor );
passEncoder.executeBundles( pass.renderBundles );
passEncoder.end();
}
}
}
/**
* Reusable descriptor for `GPUDevice.createSampler()`.
*
* @private
*/
class GPUSamplerDescriptor {
constructor() {
/**
* The label of the sampler.
*
* @type {string}
*/
this.label = '';
/**
* The address mode for the sampler's U coordinate.
*
* @type {string}
* @default 'clamp-to-edge'
*/
this.addressModeU = 'clamp-to-edge';
/**
* The address mode for the sampler's V coordinate.
*
* @type {string}
* @default 'clamp-to-edge'
*/
this.addressModeV = 'clamp-to-edge';
/**
* The address mode for the sampler's W coordinate.
*
* @type {string}
* @default 'clamp-to-edge'
*/
this.addressModeW = 'clamp-to-edge';
/**
* The magnification filter mode.
*
* @type {string}
* @default 'nearest'
*/
this.magFilter = 'nearest';
/**
* The minification filter mode.
*
* @type {string}
* @default 'nearest'
*/
this.minFilter = 'nearest';
/**
* The mipmap filter mode.
*
* @type {string}
* @default 'nearest'
*/
this.mipmapFilter = 'nearest';
/**
* The minimum level of detail used to sample.
*
* @type {number}
* @default 0
*/
this.lodMinClamp = 0;
/**
* The maximum level of detail used to sample.
*
* @type {number}
* @default 32
*/
this.lodMaxClamp = 32;
/**
* The compare function used by the sampler.
*
* @type {string|undefined}
*/
this.compare = undefined;
/**
* The maximum allowed anisotropic filtering.
*
* @type {number}
* @default 1
*/
this.maxAnisotropy = 1;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.label = '';
this.addressModeU = 'clamp-to-edge';
this.addressModeV = 'clamp-to-edge';
this.addressModeW = 'clamp-to-edge';
this.magFilter = 'nearest';
this.minFilter = 'nearest';
this.mipmapFilter = 'nearest';
this.lodMinClamp = 0;
this.lodMaxClamp = 32;
this.compare = undefined;
this.maxAnisotropy = 1;
}
}
/**
* Reusable descriptor for `GPUTexelCopyTextureInfo`, the texture side of
* `GPUCommandEncoder.copyTextureToTexture()`, `copyTextureToBuffer()` and
* `GPUQueue.writeTexture()`.
*
* @private
*/
class GPUTexelCopyTextureInfo {
constructor() {
/**
* The target texture.
*
* @type {?GPUTexture}
* @default null
*/
this.texture = null;
/**
* The mipmap level of the texture.
*
* @type {number}
* @default 0
*/
this.mipLevel = 0;
/**
* The origin offset within the texture.
*
* @type {{x: number, y: number, z: number}}
*/
this.origin = { x: 0, y: 0, z: 0 };
/**
* Which aspect of the texture is referenced.
*
* @type {string}
* @default 'all'
*/
this.aspect = 'all';
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.texture = null;
this.mipLevel = 0;
this.origin.x = 0;
this.origin.y = 0;
this.origin.z = 0;
this.aspect = 'all';
}
}
/**
* Reusable descriptor for `GPUTexelCopyBufferInfo`, the buffer side of
* `GPUCommandEncoder.copyTextureToBuffer()` and `copyBufferToTexture()`.
*
* @private
*/
class GPUTexelCopyBufferInfo {
constructor() {
/**
* The target buffer.
*
* @type {?GPUBuffer}
* @default null
*/
this.buffer = null;
/**
* The byte offset within the buffer where the texel data begins.
*
* @type {number}
* @default 0
*/
this.offset = 0;
/**
* The stride, in bytes, between rows of texel blocks.
*
* @type {number|undefined}
*/
this.bytesPerRow = undefined;
/**
* The number of texel block rows per single image of the texture.
*
* @type {number|undefined}
*/
this.rowsPerImage = undefined;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.buffer = null;
this.offset = 0;
this.bytesPerRow = undefined;
this.rowsPerImage = undefined;
}
}
/**
* Reusable descriptor for `GPUTexelCopyBufferLayout`, the data-layout argument
* to `GPUQueue.writeTexture()`.
*
* @private
*/
class GPUTexelCopyBufferLayout {
constructor() {
/**
* The byte offset within the source data where the texel data begins.
*
* @type {number}
* @default 0
*/
this.offset = 0;
/**
* The stride, in bytes, between rows of texel blocks.
*
* @type {number|undefined}
*/
this.bytesPerRow = undefined;
/**
* The number of texel block rows per single image of the texture.
*
* @type {number|undefined}
*/
this.rowsPerImage = undefined;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.offset = 0;
this.bytesPerRow = undefined;
this.rowsPerImage = undefined;
}
}
/**
* Reusable descriptor for `GPUCopyExternalImageSourceInfo`, the source argument
* to `GPUQueue.copyExternalImageToTexture()`.
*
* @private
*/
class GPUCopyExternalImageSourceInfo {
constructor() {
/**
* The image-like source.
*
* @type {?(ImageBitmap|ImageData|HTMLImageElement|HTMLVideoElement|VideoFrame|HTMLCanvasElement|OffscreenCanvas)}
* @default null
*/
this.source = null;
/**
* The origin offset within the source.
*
* @type {{x: number, y: number}}
*/
this.origin = { x: 0, y: 0 };
/**
* Whether the source is flipped vertically before copying.
*
* @type {boolean}
* @default false
*/
this.flipY = false;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.source = null;
this.origin.x = 0;
this.origin.y = 0;
this.flipY = false;
}
}
/**
* Reusable descriptor for `GPUCopyExternalImageDestInfo`, the destination
* argument to `GPUQueue.copyExternalImageToTexture()`.
*
* @private
* @augments GPUTexelCopyTextureInfo
*/
class GPUCopyExternalImageDestInfo extends GPUTexelCopyTextureInfo {
constructor() {
super();
/**
* The predefined color space the destination texture is interpreted in.
*
* @type {string}
* @default 'srgb'
*/
this.colorSpace = 'srgb';
/**
* Whether the destination texture has premultiplied alpha.
*
* @type {boolean}
* @default false
*/
this.premultipliedAlpha = false;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
super.reset();
this.colorSpace = 'srgb';
this.premultipliedAlpha = false;
}
}
/**
* Reusable descriptor for `GPUExtent3D` in its dictionary form, used by
* `GPUQueue.writeTexture()`, `GPUQueue.copyExternalImageToTexture()` and
* the various `GPUCommandEncoder` copy methods.
*
* @private
*/
class GPUExtent3D {
constructor() {
/**
* The width of the extent.
*
* @type {number}
* @default 0
*/
this.width = 0;
/**
* The height of the extent.
*
* @type {number}
* @default 1
*/
this.height = 1;
/**
* The depth (for 3D textures) or number of array layers.
*
* @type {number}
* @default 1
*/
this.depthOrArrayLayers = 1;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.width = 0;
this.height = 1;
this.depthOrArrayLayers = 1;
}
}
const _bufferDescriptor$4 = new GPUBufferDescriptor();
const _commandEncoderDescriptor$3 = new GPUCommandEncoderDescriptor();
const _samplerDescriptor = new GPUSamplerDescriptor();
const _texelCopyTextureInfo = new GPUTexelCopyTextureInfo();
const _texelCopyBufferInfo = new GPUTexelCopyBufferInfo();
const _texelCopyBufferLayout = new GPUTexelCopyBufferLayout();
const _copyExternalImageSourceInfo = new GPUCopyExternalImageSourceInfo();
const _copyExternalImageDestInfo = new GPUCopyExternalImageDestInfo();
const _textureDescriptor$1 = new GPUTextureDescriptor();
const _extent3D$1 = new GPUExtent3D();
const _compareToWebGPU = {
[ NeverCompare ]: 'never',
[ LessCompare ]: 'less',
[ EqualCompare ]: 'equal',
[ LessEqualCompare ]: 'less-equal',
[ GreaterCompare ]: 'greater',
[ GreaterEqualCompare ]: 'greater-equal',
[ AlwaysCompare ]: 'always',
[ NotEqualCompare ]: 'not-equal'
};
const _flipMap = [ 0, 1, 3, 2, 4, 5 ];
function writeTextureLayer( device, textureGPU, mipLevel, layerIndex, mipmap, dataOffset, bytesPerRow, rowsPerImage, textureWidth, textureHeight ) {
_texelCopyTextureInfo.texture = textureGPU;
_texelCopyTextureInfo.mipLevel = mipLevel;
_texelCopyTextureInfo.origin.z = layerIndex;
_texelCopyBufferLayout.offset = dataOffset;
_texelCopyBufferLayout.bytesPerRow = bytesPerRow;
_texelCopyBufferLayout.rowsPerImage = rowsPerImage;
_extent3D$1.width = textureWidth;
_extent3D$1.height = textureHeight;
device.queue.writeTexture(
_texelCopyTextureInfo,
mipmap.data,
_texelCopyBufferLayout,
_extent3D$1
);
_texelCopyTextureInfo.reset();
_texelCopyBufferLayout.reset();
_extent3D$1.reset();
}
/**
* A WebGPU backend utility module for managing textures.
*
* @private
*/
class WebGPUTextureUtils {
/**
* Constructs a new utility object.
*
* @param {WebGPUBackend} backend - The WebGPU backend.
*/
constructor( backend ) {
/**
* A reference to the WebGPU backend.
*
* @type {WebGPUBackend}
*/
this.backend = backend;
/**
* A reference to the pass utils.
*
* @type {?WebGPUTexturePassUtils}
* @default null
*/
this._passUtils = null;
/**
* A dictionary for managing default textures. The key
* is the texture format, the value the texture object.
*
* @type {Object<string,Texture>}
*/
this.defaultTexture = {};
/**
* A dictionary for managing default cube textures. The key
* is the texture format, the value the texture object.
*
* @type {Object<string,CubeTexture>}
*/
this.defaultCubeTexture = {};
/**
* A default video frame.
*
* @type {?VideoFrame}
* @default null
*/
this.defaultVideoFrame = null;
/**
* A cache of shared texture samplers.
*
* @type {Map<string, Object>}
*/
this._samplerCache = new Map();
}
/**
* Creates a GPU sampler for the given texture.
*
* @param {Sampler} binding - The sampler binding to update.
* @return {string} The current sampler key.
*/
updateSampler( binding ) {
const backend = this.backend;
const texture = binding.texture;
const textureNode = binding.textureNode;
const isComparison = texture.isDepthTexture === true && texture.compareFunction !== null && textureNode.isSampleCompare() && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE );
const samplerKey = texture.minFilter + '-' + texture.magFilter + '-' +
texture.wrapS + '-' + texture.wrapT + '-' + ( texture.wrapR || '0' ) + '-' +
texture.anisotropy + '-' + ( texture.isDepthTexture === true ? 1 : 0 ) + '-' +
( isComparison ? texture.compareFunction : 0 );
let samplerData = this._samplerCache.get( samplerKey );
if ( samplerData === undefined ) {
_samplerDescriptor.addressModeU = this._convertAddressMode( texture.wrapS );
_samplerDescriptor.addressModeV = this._convertAddressMode( texture.wrapT );
_samplerDescriptor.addressModeW = this._convertAddressMode( texture.wrapR );
_samplerDescriptor.magFilter = this._convertFilterMode( texture.magFilter );
_samplerDescriptor.minFilter = this._convertFilterMode( texture.minFilter );
_samplerDescriptor.mipmapFilter = this._convertMipmapFilterMode( texture.minFilter );
// Depth textures without compare function must use non-filtering (nearest) sampling
if ( texture.isDepthTexture && isComparison === false ) {
_samplerDescriptor.magFilter = GPUFilterMode.Nearest;
_samplerDescriptor.minFilter = GPUFilterMode.Nearest;
_samplerDescriptor.mipmapFilter = GPUFilterMode.Nearest;
}
// anisotropy can only be used when all filter modes are set to linear.
if ( _samplerDescriptor.magFilter === GPUFilterMode.Linear && _samplerDescriptor.minFilter === GPUFilterMode.Linear && _samplerDescriptor.mipmapFilter === GPUFilterMode.Linear ) {
_samplerDescriptor.maxAnisotropy = texture.anisotropy;
}
if ( isComparison ) {
_samplerDescriptor.compare = _compareToWebGPU[ texture.compareFunction ];
}
const sampler = backend.device.createSampler( _samplerDescriptor );
_samplerDescriptor.reset();
samplerData = { sampler, usedTimes: 0 };
this._samplerCache.set( samplerKey, samplerData );
}
const bindingData = backend.get( binding );
if ( bindingData.sampler !== samplerData.sampler ) {
// release the previous sampler (if any) so it can be deleted when unused
this._releaseSampler( bindingData );
// update to new sampler data
bindingData.samplerKey = samplerKey;
bindingData.sampler = samplerData.sampler;
samplerData.usedTimes ++;
}
return samplerKey;
}
/**
* Frees the GPU sampler referenced by the given sampler binding.
*
* @param {Sampler} binding - The sampler binding to free.
*/
destroySampler( binding ) {
this._releaseSampler( this.backend.get( binding ) );
}
/**
* Releases the pooled sampler referenced by the given binding data and
* removes it from the cache when no binding references it anymore.
*
* @private
* @param {Object} bindingData - The binding data holding the sampler reference.
*/
_releaseSampler( bindingData ) {
if ( bindingData.sampler !== undefined ) {
const samplerData = this._samplerCache.get( bindingData.samplerKey );
samplerData.usedTimes --;
if ( samplerData.usedTimes === 0 ) {
this._samplerCache.delete( bindingData.samplerKey );
}
bindingData.sampler = undefined;
bindingData.samplerKey = undefined;
}
}
/**
* Creates a default texture for the given texture that can be used
* as a placeholder until the actual texture is ready for usage.
*
* @param {Texture} texture - The texture to create a default texture for.
*/
createDefaultTexture( texture ) {
let textureGPU;
const format = getFormat( texture, this.backend.device );
if ( texture.isCubeTexture ) {
textureGPU = this._getDefaultCubeTextureGPU( format );
} else {
textureGPU = this._getDefaultTextureGPU( format );
}
this.backend.get( texture ).texture = textureGPU;
}
/**
* Defines a texture on the GPU for the given texture object.
*
* @param {Texture} texture - The texture.
* @param {Object} [options={}] - Optional configuration parameter.
*/
createTexture( texture, options = {} ) {
const backend = this.backend;
const textureData = backend.get( texture );
if ( textureData.initialized ) {
// Skip creation for external XR textures - they are already set up
if ( textureData.externalTexture === true ) {
return;
}
throw new Error( 'THREE.WebGPUTextureUtils: Texture already initialized.' );
}
if ( texture.isExternalTexture ) {
textureData.texture = texture.sourceTexture;
textureData.initialized = true;
return;
}
if ( options.needsMipmaps === undefined ) options.needsMipmaps = false;
if ( options.levels === undefined ) options.levels = 1;
if ( options.depth === undefined ) options.depth = 1;
const { width, height, depth, levels } = options;
if ( texture.isFramebufferTexture ) {
if ( options.renderTarget ) {
options.format = this.backend.utils.getCurrentColorFormat( options.renderTarget );
} else {
options.format = this.backend.utils.getPreferredCanvasFormat();
}
}
const dimension = this._getDimension( texture );
const format = texture.internalFormat || options.format || getFormat( texture, backend.device );
textureData.format = format;
const { samples, primarySamples, isMSAA } = backend.utils.getTextureSampleData( texture );
const renderTarget = texture.renderTarget;
// WebGPU multisampled 2D textures can only have a single array layer.
const useSeparateMSAATextures = samples > 1 && renderTarget !== null && depth > 1 && dimension === GPUTextureDimension.TwoD;
const supportsTransientAttachments = GPUTextureUsage.TRANSIENT_ATTACHMENT !== undefined;
// Layered rendering can resume after a framebuffer copy, so its attachments must support loading.
const useTransientAttachments = supportsTransientAttachments && useSeparateMSAATextures === false;
const useTransientDepthAttachment = texture.isDepthTexture === true &&
useTransientAttachments &&
renderTarget?.storeMultisampledDepthBuffer === false &&
( renderTarget.stencilBuffer === false || renderTarget.storeMultisampledStencilBuffer === false );
const useTransientColorAttachment = texture.isDepthTexture !== true &&
useTransientAttachments &&
renderTarget?.storeMultisampledColorBuffer === false;
let usage = GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.COPY_SRC;
if ( texture.isStorageTexture === true ) {
usage |= GPUTextureUsage.STORAGE_BINDING;
}
if ( texture.isCompressedTexture !== true && texture.isCompressedArrayTexture !== true && format !== GPUTextureFormat.RGB9E5UFloat ) {
usage |= GPUTextureUsage.RENDER_ATTACHMENT;
}
// when the multisampled data are discarded, try to use a transient attachment if possible
if ( primarySamples > 1 && useTransientDepthAttachment ) {
usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TRANSIENT_ATTACHMENT;
}
const textureDescriptorGPU = new GPUTextureDescriptor();
textureDescriptorGPU.label = texture.name;
textureDescriptorGPU.size.width = width;
textureDescriptorGPU.size.height = height;
textureDescriptorGPU.size.depthOrArrayLayers = depth;
textureDescriptorGPU.mipLevelCount = levels;
textureDescriptorGPU.sampleCount = primarySamples;
textureDescriptorGPU.dimension = dimension;
textureDescriptorGPU.format = format;
textureDescriptorGPU.usage = usage;
// texture creation
if ( format === undefined ) {
warn( 'WebGPURenderer: Texture format not supported.' );
this.createDefaultTexture( texture );
return;
}
if ( texture.isCubeTexture ) {
textureDescriptorGPU.textureBindingViewDimension = GPUTextureViewDimension.Cube;
}
try {
textureData.texture = backend.device.createTexture( textureDescriptorGPU );
} catch ( e ) {
warn( 'WebGPURenderer: Failed to create texture with descriptor:', textureDescriptorGPU );
this.createDefaultTexture( texture );
return;
}
if ( isMSAA || useSeparateMSAATextures ) {
const msaaTextureDescriptorGPU = Object.assign( {}, textureDescriptorGPU );
msaaTextureDescriptorGPU.label = msaaTextureDescriptorGPU.label + '-msaa';
msaaTextureDescriptorGPU.sampleCount = samples;
msaaTextureDescriptorGPU.mipLevelCount = 1; // See https://www.w3.org/TR/webgpu/#texture-creation
// when the multisampled data are discarded, try to use a transient attachment if possible
if ( useTransientDepthAttachment || useTransientColorAttachment ) {
msaaTextureDescriptorGPU.usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TRANSIENT_ATTACHMENT;
}
if ( useSeparateMSAATextures ) {
msaaTextureDescriptorGPU.size = Object.assign( {}, msaaTextureDescriptorGPU.size, { depthOrArrayLayers: 1 } );
textureData.msaaTextures = [];
for ( let i = 0; i < depth; i ++ ) {
msaaTextureDescriptorGPU.label = textureDescriptorGPU.label + '-msaa-' + i;
textureData.msaaTextures.push( backend.device.createTexture( msaaTextureDescriptorGPU ) );
}
} else {
textureData.msaaTexture = backend.device.createTexture( msaaTextureDescriptorGPU );
}
}
textureData.initialized = true;
textureData.textureDescriptorGPU = textureDescriptorGPU;
}
/**
* Destroys the GPU data for the given texture object.
*
* @param {Texture} texture - The texture.
* @param {boolean} [isDefaultTexture=false] - Whether the texture uses a default GPU texture or not.
*/
destroyTexture( texture, isDefaultTexture = false ) {
const backend = this.backend;
const textureData = backend.get( texture );
if ( textureData.texture !== undefined && isDefaultTexture === false && texture.isExternalTexture !== true && textureData.externalTexture !== true ) textureData.texture.destroy();
if ( textureData.msaaTexture !== undefined ) textureData.msaaTexture.destroy();
if ( textureData.msaaTextures !== undefined ) {
for ( const msaaTexture of textureData.msaaTextures ) msaaTexture.destroy();
}
backend.delete( texture );
}
/**
* Generates mipmaps for the given texture.
*
* @param {Texture} texture - The texture.
* @param {?GPUCommandEncoder} [encoder=null] - An optional command encoder used to generate mipmaps.
*/
generateMipmaps( texture, encoder = null ) {
const textureData = this.backend.get( texture );
this._generateMipmaps( textureData.texture, encoder );
}
/**
* Returns the color buffer representing the color
* attachment of the default framebuffer.
*
* @return {GPUTexture} The color buffer.
*/
getColorBuffer() {
const backend = this.backend;
const canvasTarget = backend.renderer.getCanvasTarget();
const { width, height } = backend.getDrawingBufferSize();
const samples = backend.renderer.currentSamples;
const colorTexture = canvasTarget.colorTexture;
const colorTextureData = backend.get( colorTexture );
if ( colorTextureData.texture !== undefined && colorTexture.width === width && colorTexture.height === height && colorTexture.samples === samples ) {
return colorTextureData.texture;
}
// recreate
let colorBuffer = colorTextureData.texture;
if ( colorBuffer ) colorBuffer.destroy();
_textureDescriptor$1.label = 'colorBuffer';
_textureDescriptor$1.size.width = width;
_textureDescriptor$1.size.height = height;
_textureDescriptor$1.sampleCount = backend.utils.getSampleCount( backend.renderer.currentSamples );
_textureDescriptor$1.format = backend.utils.getPreferredCanvasFormat();
_textureDescriptor$1.usage = GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC;
colorBuffer = backend.device.createTexture( _textureDescriptor$1 );
_textureDescriptor$1.reset();
//
colorTexture.image.width = width;
colorTexture.image.height = height;
colorTexture.samples = samples;
colorTextureData.texture = colorBuffer;
return colorBuffer;
}
/**
* Returns the depth buffer representing the depth
* attachment of the default framebuffer.
*
* @param {boolean} [depth=true] - Whether depth is enabled or not.
* @param {boolean} [stencil=false] - Whether stencil is enabled or not.
* @return {GPUTexture} The depth buffer.
*/
getDepthBuffer( depth = true, stencil = false ) {
const backend = this.backend;
const canvasTarget = backend.renderer.getCanvasTarget();
const { width, height } = backend.getDrawingBufferSize();
const samples = backend.renderer.currentSamples;
const depthTexture = canvasTarget.depthTexture;
if ( depthTexture.width === width &&
depthTexture.height === height &&
depthTexture.samples === samples &&
depthTexture.depth === depth &&
depthTexture.stencil === stencil ) {
return backend.get( depthTexture ).texture;
}
//
const depthTextureGPU = backend.get( depthTexture ).texture;
let format, type;
if ( stencil ) {
format = DepthStencilFormat;
type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedInt248Type;
} else if ( depth ) {
format = DepthFormat;
type = backend.renderer.reversedDepthBuffer === true ? FloatType : UnsignedIntType;
}
if ( depthTextureGPU !== undefined ) {
if ( depthTexture.image.width === width && depthTexture.image.height === height && depthTexture.format === format && depthTexture.type === type && depthTexture.samples === samples ) {
return depthTextureGPU;
}
this.destroyTexture( depthTexture );
}
// recreate
depthTexture.name = 'depthBuffer';
depthTexture.format = format;
depthTexture.type = type;
depthTexture.image.width = width;
depthTexture.image.height = height;
depthTexture.samples = samples;
this.createTexture( depthTexture, { width, height } );
return backend.get( depthTexture ).texture;
}
/**
* Uploads the updated texture data to the GPU.
*
* @param {Texture} texture - The texture.
* @param {Object} [options={}] - Optional configuration parameter.
*/
updateTexture( texture, options ) {
const textureData = this.backend.get( texture );
const mipmaps = texture.mipmaps;
const { textureDescriptorGPU } = textureData;
if ( texture.isRenderTargetTexture || ( textureDescriptorGPU === undefined /* unsupported texture format */ ) )
return;
// transfer texture data
if ( texture.isDataTexture ) {
if ( mipmaps.length > 0 ) {
for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {
const mipmap = mipmaps[ i ];
this._copyBufferToTexture( mipmap, textureData.texture, textureDescriptorGPU, 0, texture.flipY, 0, i );
}
} else {
this._copyBufferToTexture( options.image, textureData.texture, textureDescriptorGPU, 0, texture.flipY );
}
} else if ( texture.isArrayTexture || texture.isDataArrayTexture || texture.isData3DTexture ) {
if ( texture.layerUpdates && texture.layerUpdates.size > 0 ) {
for ( const layerIndex of texture.layerUpdates ) {
this._copyBufferToTexture( options.image, textureData.texture, textureDescriptorGPU, layerIndex, texture.flipY, layerIndex );
}
texture.clearLayerUpdates();
} else {
for ( let i = 0; i < options.image.depth; i ++ ) {
this._copyBufferToTexture( options.image, textureData.texture, textureDescriptorGPU, i, texture.flipY, i );
}
}
} else if ( texture.isCompressedTexture || texture.isCompressedArrayTexture ) {
if ( texture.isCompressedCubeTexture ) {
for ( let i = 0; i < 6; i ++ ) {
this._copyCompressedBufferToTexture( texture.image[ i ].mipmaps, textureData.texture, textureDescriptorGPU, null, i );
}
} else if ( texture.isCompressedArrayTexture && texture.layerUpdates.size > 0 ) {
this._copyCompressedBufferToTexture( texture.mipmaps, textureData.texture, textureDescriptorGPU, texture.layerUpdates );
texture.clearLayerUpdates();
} else {
this._copyCompressedBufferToTexture( texture.mipmaps, textureData.texture, textureDescriptorGPU );
}
} else if ( texture.isCubeTexture ) {
this._copyCubeMapToTexture( texture, textureData.texture, textureDescriptorGPU );
} else if ( texture.isHTMLTexture ) {
const device = this.backend.device;
const canvas = this.backend.renderer.domElement;
const image = texture.image;
if ( typeof device.queue.copyElementImageToTexture !== 'function' ) return;
// Skip the first frame — the element needs a paint record first.
if ( ! textureData.hasPaintCallback ) {
textureData.hasPaintCallback = true;
canvas.requestPaint();
return;
}
const width = textureDescriptorGPU.size.width;
const height = textureDescriptorGPU.size.height;
if ( device.queue.copyElementImageToTexture.length === 2 ) {
// Chrome 150+
device.queue.copyElementImageToTexture(
{ source: image },
{
destination: { texture: textureData.texture },
width: width,
height: height
}
);
} else {
// Chrome 138 - 149
device.queue.copyElementImageToTexture(
image, width, height,
{ texture: textureData.texture }
);
}
if ( texture.flipY ) {
this._flipY( textureData.texture, textureDescriptorGPU );
}
} else {
if ( mipmaps.length > 0 ) {
for ( let i = 0, il = mipmaps.length; i < il; i ++ ) {
const mipmap = mipmaps[ i ];
this._copyImageToTexture( mipmap, textureData.texture, textureDescriptorGPU, 0, texture.flipY, texture.premultiplyAlpha, i );
}
} else {
this._copyImageToTexture( options.image, textureData.texture, textureDescriptorGPU, 0, texture.flipY, texture.premultiplyAlpha );
}
}
//
textureData.version = texture.version;
}
/**
* Returns texture data as a typed array.
*
* @async
* @param {Texture} texture - The texture to copy.
* @param {number} x - The x coordinate of the copy origin.
* @param {number} y - The y coordinate of the copy origin.
* @param {number} width - The width of the copy.
* @param {number} height - The height of the copy.
* @param {number} faceIndex - The cube face, depth slice or array layer index.
* @return {Promise<TypedArray>} A Promise that resolves with a typed array when the copy operation has finished.
*/
async copyTextureToBuffer( texture, x, y, width, height, faceIndex ) {
const device = this.backend.device;
const textureData = this.backend.get( texture );
const textureGPU = textureData.texture;
const format = textureData.textureDescriptorGPU.format;
const bytesPerTexel = this._getBytesPerTexel( format );
const unpaddedBytesPerRow = width * bytesPerTexel;
const bytesPerRow = Math.ceil( unpaddedBytesPerRow / 256 ) * 256; // copyTextureToBuffer() requires a multiple of 256
_bufferDescriptor$4.size = height * bytesPerRow; // multiple of 256, so it also meets the 4-byte size alignment required by mapAsync()
_bufferDescriptor$4.usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ;
const readBuffer = device.createBuffer( _bufferDescriptor$4 );
_bufferDescriptor$4.reset();
const encoder = device.createCommandEncoder( _commandEncoderDescriptor$3 );
_texelCopyTextureInfo.texture = textureGPU;
_texelCopyTextureInfo.origin.x = x;
_texelCopyTextureInfo.origin.y = y;
_texelCopyTextureInfo.origin.z = faceIndex;
_texelCopyBufferInfo.buffer = readBuffer;
_texelCopyBufferInfo.bytesPerRow = bytesPerRow;
_extent3D$1.width = width;
_extent3D$1.height = height;
encoder.copyTextureToBuffer(
_texelCopyTextureInfo,
_texelCopyBufferInfo,
_extent3D$1
);
_texelCopyTextureInfo.reset();
_texelCopyBufferInfo.reset();
_extent3D$1.reset();
const typedArrayType = this._getTypedArrayType( format );
submit( device, encoder.finish() );
await readBuffer.mapAsync( GPUMapMode.READ );
// remove the row padding so the result is tightly packed
const paddedData = new Uint8Array( readBuffer.getMappedRange() );
const data = new Uint8Array( unpaddedBytesPerRow * height );
for ( let row = 0; row < height; row ++ ) {
const offset = row * bytesPerRow;
data.set( paddedData.subarray( offset, offset + unpaddedBytesPerRow ), row * unpaddedBytesPerRow );
}
readBuffer.destroy();
return new typedArrayType( data.buffer );
}
/**
* Frees all internal resources.
*/
dispose() {
this._samplerCache.clear();
}
/**
* Returns the default GPU texture for the given format.
*
* @private
* @param {string} format - The GPU format.
* @return {GPUTexture} The GPU texture.
*/
_getDefaultTextureGPU( format ) {
let defaultTexture = this.defaultTexture[ format ];
if ( defaultTexture === undefined ) {
const texture = new Texture();
texture.minFilter = NearestFilter;
texture.magFilter = NearestFilter;
this.createTexture( texture, { width: 1, height: 1, format } );
this.defaultTexture[ format ] = defaultTexture = texture;
}
return this.backend.get( defaultTexture ).texture;
}
/**
* Returns the default GPU cube texture for the given format.
*
* @private
* @param {string} format - The GPU format.
* @return {GPUTexture} The GPU texture.
*/
_getDefaultCubeTextureGPU( format ) {
let defaultCubeTexture = this.defaultCubeTexture[ format ];
if ( defaultCubeTexture === undefined ) {
const texture = new CubeTexture();
texture.minFilter = NearestFilter;
texture.magFilter = NearestFilter;
this.createTexture( texture, { width: 1, height: 1, depth: 6 } );
this.defaultCubeTexture[ format ] = defaultCubeTexture = texture;
}
return this.backend.get( defaultCubeTexture ).texture;
}
/**
* Uploads cube texture image data to the GPU memory.
*
* @private
* @param {CubeTexture} texture - The cube texture.
* @param {GPUTexture} textureGPU - The GPU texture.
* @param {Object} textureDescriptorGPU - The GPU texture descriptor.
*/
_copyCubeMapToTexture( texture, textureGPU, textureDescriptorGPU ) {
const images = texture.images;
const mipmaps = texture.mipmaps;
for ( let i = 0; i < 6; i ++ ) {
const image = images[ i ];
const flipIndex = texture.flipY === true ? _flipMap[ i ] : i;
if ( image.isDataTexture ) {
this._copyBufferToTexture( image.image, textureGPU, textureDescriptorGPU, flipIndex, texture.flipY );
} else {
this._copyImageToTexture( image, textureGPU, textureDescriptorGPU, flipIndex, texture.flipY, texture.premultiplyAlpha );
}
for ( let j = 0; j < mipmaps.length; j ++ ) {
const mipmap = mipmaps[ j ];
const image = mipmap.images[ i ];
if ( image.isDataTexture ) {
this._copyBufferToTexture( image.image, textureGPU, textureDescriptorGPU, flipIndex, texture.flipY, 0, j + 1 );
} else {
this._copyImageToTexture( image, textureGPU, textureDescriptorGPU, flipIndex, texture.flipY, texture.premultiplyAlpha, j + 1 );
}
}
}
}
/**
* Uploads texture image data to the GPU memory.
*
* @private
* @param {HTMLImageElement|ImageBitmap|HTMLCanvasElement} image - The image data.
* @param {GPUTexture} textureGPU - The GPU texture.
* @param {Object} textureDescriptorGPU - The GPU texture descriptor.
* @param {number} originDepth - The origin depth.
* @param {boolean} flipY - Whether to flip texture data along their vertical axis or not.
* @param {boolean} premultiplyAlpha - Whether the texture should have its RGB channels premultiplied by the alpha channel or not.
* @param {number} [mipLevel=0] - The mip level where the data should be copied to.
*/
_copyImageToTexture( image, textureGPU, textureDescriptorGPU, originDepth, flipY, premultiplyAlpha, mipLevel = 0 ) {
const device = this.backend.device;
const width = ( mipLevel > 0 ) ? image.width : textureDescriptorGPU.size.width;
const height = ( mipLevel > 0 ) ? image.height : textureDescriptorGPU.size.height;
_copyExternalImageSourceInfo.source = image;
_copyExternalImageSourceInfo.flipY = flipY;
_copyExternalImageDestInfo.texture = textureGPU;
_copyExternalImageDestInfo.mipLevel = mipLevel;
_copyExternalImageDestInfo.origin.z = originDepth;
_copyExternalImageDestInfo.premultipliedAlpha = premultiplyAlpha;
_extent3D$1.width = width;
_extent3D$1.height = height;
try {
device.queue.copyExternalImageToTexture(
_copyExternalImageSourceInfo,
_copyExternalImageDestInfo,
_extent3D$1
);
// try/catch has been added to fix bad video frame data on certain devices, see #32391
} catch ( _ ) {
} finally {
_copyExternalImageSourceInfo.reset();
_copyExternalImageDestInfo.reset();
_extent3D$1.reset();
}
}
/**
* Returns the pass utils singleton.
*
* @private
* @return {WebGPUTexturePassUtils} The utils instance.
*/
_getPassUtils() {
let passUtils = this._passUtils;
if ( passUtils === null ) {
this._passUtils = passUtils = new WebGPUTexturePassUtils( this.backend.device );
}
return passUtils;
}
/**
* Generates mipmaps for the given GPU texture.
*
* @private
* @param {GPUTexture} textureGPU - The GPU texture object.
* @param {?GPUCommandEncoder} [encoder=null] - An optional command encoder used to generate mipmaps.
*/
_generateMipmaps( textureGPU, encoder = null ) {
this._getPassUtils().generateMipmaps( textureGPU, encoder );
}
/**
* Flip the contents of the given GPU texture along its vertical axis.
*
* @private
* @param {GPUTexture} textureGPU - The GPU texture object.
* @param {Object} textureDescriptorGPU - The texture descriptor.
* @param {number} [originDepth=0] - The origin depth.
*/
_flipY( textureGPU, textureDescriptorGPU, originDepth = 0 ) {
this._getPassUtils().flipY( textureGPU, textureDescriptorGPU, originDepth );
}
/**
* Uploads texture buffer data to the GPU memory.
*
* @private
* @param {Object} image - An object defining the image buffer data.
* @param {GPUTexture} textureGPU - The GPU texture.
* @param {Object} textureDescriptorGPU - The GPU texture descriptor.
* @param {number} originDepth - The origin depth.
* @param {boolean} flipY - Whether to flip texture data along their vertical axis or not.
* @param {number} [depth=0] - The depth offset when copying array or 3D texture data.
* @param {number} [mipLevel=0] - The mip level where the data should be copied to.
*/
_copyBufferToTexture( image, textureGPU, textureDescriptorGPU, originDepth, flipY, depth = 0, mipLevel = 0 ) {
// @TODO: Consider to use GPUCommandEncoder.copyBufferToTexture()
// @TODO: Consider to support valid buffer layouts with other formats like RGB
const device = this.backend.device;
const data = image.data;
const bytesPerTexel = this._getBytesPerTexel( textureDescriptorGPU.format );
const bytesPerRow = image.width * bytesPerTexel;
_texelCopyTextureInfo.texture = textureGPU;
_texelCopyTextureInfo.mipLevel = mipLevel;
_texelCopyTextureInfo.origin.z = originDepth;
_texelCopyBufferLayout.offset = image.width * image.height * bytesPerTexel * depth;
_texelCopyBufferLayout.bytesPerRow = bytesPerRow;
_extent3D$1.width = image.width;
_extent3D$1.height = image.height;
device.queue.writeTexture(
_texelCopyTextureInfo,
data,
_texelCopyBufferLayout,
_extent3D$1
);
_texelCopyTextureInfo.reset();
_texelCopyBufferLayout.reset();
_extent3D$1.reset();
if ( flipY === true ) {
this._flipY( textureGPU, textureDescriptorGPU, originDepth );
}
}
/**
* Uploads compressed texture data to the GPU memory.
*
* @private
* @param {Array<Object>} mipmaps - An array with mipmap data.
* @param {GPUTexture} textureGPU - The GPU texture.
* @param {Object} textureDescriptorGPU - The GPU texture descriptor.
* @param {?Set<number>} [layerUpdates=null] - The layer indices to update.
* @param {number} [faceIndex=-1] - The cube face the mipmaps belong to. Only relevant for compressed cube maps.
*/
_copyCompressedBufferToTexture( mipmaps, textureGPU, textureDescriptorGPU, layerUpdates = null, faceIndex = -1 ) {
// @TODO: Consider to use GPUCommandEncoder.copyBufferToTexture()
const device = this.backend.device;
const blockData = this._getBlockData( textureDescriptorGPU.format );
const isArrayTexture = textureDescriptorGPU.size.depthOrArrayLayers > 1;
const activeLayerUpdates = layerUpdates && layerUpdates.size > 0 ? layerUpdates : null;
for ( let i = 0; i < mipmaps.length; i ++ ) {
const mipmap = mipmaps[ i ];
const width = mipmap.width;
const height = mipmap.height;
const depth = isArrayTexture ? textureDescriptorGPU.size.depthOrArrayLayers : 1;
const bytesPerRow = Math.ceil( width / blockData.width ) * blockData.byteLength;
const rowsPerImage = Math.ceil( height / blockData.height );
const bytesPerImage = bytesPerRow * rowsPerImage;
const textureWidth = Math.ceil( width / blockData.width ) * blockData.width;
const textureHeight = rowsPerImage * blockData.height;
if ( faceIndex !== -1 ) {
writeTextureLayer( device, textureGPU, i, faceIndex, mipmap, 0, bytesPerRow, rowsPerImage, textureWidth, textureHeight );
} else if ( activeLayerUpdates !== null ) {
for ( const layerIndex of activeLayerUpdates ) {
writeTextureLayer( device, textureGPU, i, layerIndex, mipmap, layerIndex * bytesPerImage, bytesPerRow, rowsPerImage, textureWidth, textureHeight );
}
} else {
for ( let layerIndex = 0; layerIndex < depth; layerIndex ++ ) {
writeTextureLayer( device, textureGPU, i, layerIndex, mipmap, layerIndex * bytesPerImage, bytesPerRow, rowsPerImage, textureWidth, textureHeight );
}
}
}
}
/**
* This method is only relevant for compressed textures. It returns a block
* data descriptor for the given GPU texture format.
*
* @private
* @param {string} format - The GPU texture format.
* @return {Object} The block data descriptor.
*/
_getBlockData( format ) {
if ( format === GPUTextureFormat.BC1RGBAUnorm || format === GPUTextureFormat.BC1RGBAUnormSRGB ) return { byteLength: 8, width: 4, height: 4 }; // DXT1
if ( format === GPUTextureFormat.BC2RGBAUnorm || format === GPUTextureFormat.BC2RGBAUnormSRGB ) return { byteLength: 16, width: 4, height: 4 }; // DXT3
if ( format === GPUTextureFormat.BC3RGBAUnorm || format === GPUTextureFormat.BC3RGBAUnormSRGB ) return { byteLength: 16, width: 4, height: 4 }; // DXT5
if ( format === GPUTextureFormat.BC4RUnorm || format === GPUTextureFormat.BC4RSnorm ) return { byteLength: 8, width: 4, height: 4 }; // RGTC1
if ( format === GPUTextureFormat.BC5RGUnorm || format === GPUTextureFormat.BC5RGSnorm ) return { byteLength: 16, width: 4, height: 4 }; // RGTC2
if ( format === GPUTextureFormat.BC6HRGBUFloat || format === GPUTextureFormat.BC6HRGBFloat ) return { byteLength: 16, width: 4, height: 4 }; // BPTC (float)
if ( format === GPUTextureFormat.BC7RGBAUnorm || format === GPUTextureFormat.BC7RGBAUnormSRGB ) return { byteLength: 16, width: 4, height: 4 }; // BPTC (unorm)
if ( format === GPUTextureFormat.ETC2RGB8Unorm || format === GPUTextureFormat.ETC2RGB8UnormSRGB ) return { byteLength: 8, width: 4, height: 4 };
if ( format === GPUTextureFormat.ETC2RGB8A1Unorm || format === GPUTextureFormat.ETC2RGB8A1UnormSRGB ) return { byteLength: 8, width: 4, height: 4 };
if ( format === GPUTextureFormat.ETC2RGBA8Unorm || format === GPUTextureFormat.ETC2RGBA8UnormSRGB ) return { byteLength: 16, width: 4, height: 4 };
if ( format === GPUTextureFormat.EACR11Unorm ) return { byteLength: 8, width: 4, height: 4 };
if ( format === GPUTextureFormat.EACR11Snorm ) return { byteLength: 8, width: 4, height: 4 };
if ( format === GPUTextureFormat.EACRG11Unorm ) return { byteLength: 16, width: 4, height: 4 };
if ( format === GPUTextureFormat.EACRG11Snorm ) return { byteLength: 16, width: 4, height: 4 };
if ( format === GPUTextureFormat.ASTC4x4Unorm || format === GPUTextureFormat.ASTC4x4UnormSRGB ) return { byteLength: 16, width: 4, height: 4 };
if ( format === GPUTextureFormat.ASTC5x4Unorm || format === GPUTextureFormat.ASTC5x4UnormSRGB ) return { byteLength: 16, width: 5, height: 4 };
if ( format === GPUTextureFormat.ASTC5x5Unorm || format === GPUTextureFormat.ASTC5x5UnormSRGB ) return { byteLength: 16, width: 5, height: 5 };
if ( format === GPUTextureFormat.ASTC6x5Unorm || format === GPUTextureFormat.ASTC6x5UnormSRGB ) return { byteLength: 16, width: 6, height: 5 };
if ( format === GPUTextureFormat.ASTC6x6Unorm || format === GPUTextureFormat.ASTC6x6UnormSRGB ) return { byteLength: 16, width: 6, height: 6 };
if ( format === GPUTextureFormat.ASTC8x5Unorm || format === GPUTextureFormat.ASTC8x5UnormSRGB ) return { byteLength: 16, width: 8, height: 5 };
if ( format === GPUTextureFormat.ASTC8x6Unorm || format === GPUTextureFormat.ASTC8x6UnormSRGB ) return { byteLength: 16, width: 8, height: 6 };
if ( format === GPUTextureFormat.ASTC8x8Unorm || format === GPUTextureFormat.ASTC8x8UnormSRGB ) return { byteLength: 16, width: 8, height: 8 };
if ( format === GPUTextureFormat.ASTC10x5Unorm || format === GPUTextureFormat.ASTC10x5UnormSRGB ) return { byteLength: 16, width: 10, height: 5 };
if ( format === GPUTextureFormat.ASTC10x6Unorm || format === GPUTextureFormat.ASTC10x6UnormSRGB ) return { byteLength: 16, width: 10, height: 6 };
if ( format === GPUTextureFormat.ASTC10x8Unorm || format === GPUTextureFormat.ASTC10x8UnormSRGB ) return { byteLength: 16, width: 10, height: 8 };
if ( format === GPUTextureFormat.ASTC10x10Unorm || format === GPUTextureFormat.ASTC10x10UnormSRGB ) return { byteLength: 16, width: 10, height: 10 };
if ( format === GPUTextureFormat.ASTC12x10Unorm || format === GPUTextureFormat.ASTC12x10UnormSRGB ) return { byteLength: 16, width: 12, height: 10 };
if ( format === GPUTextureFormat.ASTC12x12Unorm || format === GPUTextureFormat.ASTC12x12UnormSRGB ) return { byteLength: 16, width: 12, height: 12 };
return { byteLength: this._getBytesPerTexel( format ), width: 1, height: 1 }; // uncompressed data
}
/**
* Converts the three.js uv wrapping constants to GPU address mode constants.
*
* @private
* @param {number} value - The three.js constant defining a uv wrapping mode.
* @return {string} The GPU address mode.
*/
_convertAddressMode( value ) {
let addressMode = GPUAddressMode.ClampToEdge;
if ( value === RepeatWrapping ) {
addressMode = GPUAddressMode.Repeat;
} else if ( value === MirroredRepeatWrapping ) {
addressMode = GPUAddressMode.MirrorRepeat;
}
return addressMode;
}
/**
* Converts the three.js filter constants to GPU filter constants.
*
* @private
* @param {number} value - The three.js constant defining a filter mode.
* @return {string} The GPU filter mode.
*/
_convertFilterMode( value ) {
let filterMode = GPUFilterMode.Linear;
if ( value === NearestFilter || value === NearestMipmapNearestFilter || value === NearestMipmapLinearFilter ) {
filterMode = GPUFilterMode.Nearest;
}
return filterMode;
}
/**
* Converts the three.js filter constants to a GPU mipmap filter constant.
* Unlike `_convertFilterMode`, this extracts the between-mip-level filtering
* axis from the combined three.js constant rather than the within-level axis.
*
* @private
* @param {number} value - The three.js constant defining a filter mode.
* @return {string} The GPU mipmap filter mode.
*/
_convertMipmapFilterMode( value ) {
if ( value === NearestMipmapLinearFilter || value === LinearMipmapLinearFilter ) {
return GPUFilterMode.Linear;
}
return GPUFilterMode.Nearest;
}
/**
* Returns the bytes-per-texel value for the given GPU texture format.
*
* @private
* @param {string} format - The GPU texture format.
* @return {number} The bytes-per-texel.
*/
_getBytesPerTexel( format ) {
// 8-bit formats
if ( format === GPUTextureFormat.R8Unorm ||
format === GPUTextureFormat.R8Snorm ||
format === GPUTextureFormat.R8Uint ||
format === GPUTextureFormat.R8Sint ) return 1;
// 16-bit formats
if ( format === GPUTextureFormat.R16Uint ||
format === GPUTextureFormat.R16Sint ||
format === GPUTextureFormat.R16Float ||
format === GPUTextureFormat.RG8Unorm ||
format === GPUTextureFormat.RG8Snorm ||
format === GPUTextureFormat.RG8Uint ||
format === GPUTextureFormat.RG8Sint ||
format === GPUTextureFormat.R16Unorm ||
format === GPUTextureFormat.R16Snorm ) return 2;
// 32-bit formats
if ( format === GPUTextureFormat.R32Uint ||
format === GPUTextureFormat.R32Sint ||
format === GPUTextureFormat.R32Float ||
format === GPUTextureFormat.RG16Uint ||
format === GPUTextureFormat.RG16Sint ||
format === GPUTextureFormat.RG16Float ||
format === GPUTextureFormat.RGBA8Unorm ||
format === GPUTextureFormat.RGBA8UnormSRGB ||
format === GPUTextureFormat.RGBA8Snorm ||
format === GPUTextureFormat.RGBA8Uint ||
format === GPUTextureFormat.RGBA8Sint ||
format === GPUTextureFormat.BGRA8Unorm ||
format === GPUTextureFormat.BGRA8UnormSRGB ||
format === GPUTextureFormat.RG16Unorm ||
format === GPUTextureFormat.RG16Snorm ||
// Packed 32-bit formats
format === GPUTextureFormat.RGB9E5UFloat ||
format === GPUTextureFormat.RGB10A2Unorm ||
format === GPUTextureFormat.RG11B10UFloat ||
format === GPUTextureFormat.Depth32Float ||
format === GPUTextureFormat.Depth24Plus ||
format === GPUTextureFormat.Depth24PlusStencil8 ||
format === GPUTextureFormat.Depth32FloatStencil8 ) return 4;
// 64-bit formats
if ( format === GPUTextureFormat.RG32Uint ||
format === GPUTextureFormat.RG32Sint ||
format === GPUTextureFormat.RG32Float ||
format === GPUTextureFormat.RGBA16Uint ||
format === GPUTextureFormat.RGBA16Sint ||
format === GPUTextureFormat.RGBA16Float ||
format === GPUTextureFormat.RGBA16Unorm ||
format === GPUTextureFormat.RGBA16Snorm ) return 8;
// 128-bit formats
if ( format === GPUTextureFormat.RGBA32Uint ||
format === GPUTextureFormat.RGBA32Sint ||
format === GPUTextureFormat.RGBA32Float ) return 16;
}
/**
* Returns the corresponding typed array type for the given GPU texture format.
*
* @private
* @param {string} format - The GPU texture format.
* @return {TypedArray.constructor} The typed array type.
*/
_getTypedArrayType( format ) {
if ( format === GPUTextureFormat.R8Uint ) return Uint8Array;
if ( format === GPUTextureFormat.R8Sint ) return Int8Array;
if ( format === GPUTextureFormat.R8Unorm ) return Uint8Array;
if ( format === GPUTextureFormat.R8Snorm ) return Int8Array;
if ( format === GPUTextureFormat.RG8Uint ) return Uint8Array;
if ( format === GPUTextureFormat.RG8Sint ) return Int8Array;
if ( format === GPUTextureFormat.RG8Unorm ) return Uint8Array;
if ( format === GPUTextureFormat.RG8Snorm ) return Int8Array;
if ( format === GPUTextureFormat.RGBA8Uint ) return Uint8Array;
if ( format === GPUTextureFormat.RGBA8Sint ) return Int8Array;
if ( format === GPUTextureFormat.RGBA8Unorm || format === GPUTextureFormat.RGBA8UnormSRGB ) return Uint8Array;
if ( format === GPUTextureFormat.RGBA8Snorm ) return Int8Array;
if ( format === GPUTextureFormat.R16Uint ) return Uint16Array;
if ( format === GPUTextureFormat.R16Sint ) return Int16Array;
if ( format === GPUTextureFormat.RG16Uint ) return Uint16Array;
if ( format === GPUTextureFormat.RG16Sint ) return Int16Array;
if ( format === GPUTextureFormat.RGBA16Uint ) return Uint16Array;
if ( format === GPUTextureFormat.RGBA16Sint ) return Int16Array;
if ( format === GPUTextureFormat.R16Float ) return Uint16Array;
if ( format === GPUTextureFormat.RG16Float ) return Uint16Array;
if ( format === GPUTextureFormat.RGBA16Float ) return Uint16Array;
if ( format === GPUTextureFormat.R16Unorm ) return Uint16Array;
if ( format === GPUTextureFormat.R16Snorm ) return Int16Array;
if ( format === GPUTextureFormat.RG16Unorm ) return Uint16Array;
if ( format === GPUTextureFormat.RG16Snorm ) return Int16Array;
if ( format === GPUTextureFormat.RGBA16Unorm ) return Uint16Array;
if ( format === GPUTextureFormat.RGBA16Snorm ) return Int16Array;
if ( format === GPUTextureFormat.R32Uint ) return Uint32Array;
if ( format === GPUTextureFormat.R32Sint ) return Int32Array;
if ( format === GPUTextureFormat.R32Float ) return Float32Array;
if ( format === GPUTextureFormat.RG32Uint ) return Uint32Array;
if ( format === GPUTextureFormat.RG32Sint ) return Int32Array;
if ( format === GPUTextureFormat.RG32Float ) return Float32Array;
if ( format === GPUTextureFormat.RGBA32Uint ) return Uint32Array;
if ( format === GPUTextureFormat.RGBA32Sint ) return Int32Array;
if ( format === GPUTextureFormat.RGBA32Float ) return Float32Array;
if ( format === GPUTextureFormat.BGRA8Unorm || format === GPUTextureFormat.BGRA8UnormSRGB ) return Uint8Array;
if ( format === GPUTextureFormat.RGB10A2Unorm ) return Uint32Array;
if ( format === GPUTextureFormat.RGB9E5UFloat ) return Uint32Array;
if ( format === GPUTextureFormat.RG11B10UFloat ) return Uint32Array;
if ( format === GPUTextureFormat.Depth32Float ) return Float32Array;
if ( format === GPUTextureFormat.Depth24Plus ) return Uint32Array;
if ( format === GPUTextureFormat.Depth24PlusStencil8 ) return Uint32Array;
if ( format === GPUTextureFormat.Depth32FloatStencil8 ) return Float32Array;
}
/**
* Returns the GPU dimensions for the given texture.
*
* @private
* @param {Texture} texture - The texture.
* @return {string} The GPU dimension.
*/
_getDimension( texture ) {
let dimension;
if ( texture.is3DTexture || texture.isData3DTexture ) {
dimension = GPUTextureDimension.ThreeD;
} else {
dimension = GPUTextureDimension.TwoD;
}
return dimension;
}
}
/**
* Returns the GPU format for the given texture.
*
* @param {Texture} texture - The texture.
* @param {GPUDevice} [device] - The GPU device which is used for feature detection.
* @return {string} The GPU format.
*/
function getFormat( texture, device ) {
const format = texture.format;
const type = texture.type;
const normalized = texture.normalized;
const colorSpace = texture.colorSpace;
const transfer = ColorManagement.getTransfer( colorSpace );
let formatGPU;
let textureFormatsTier1 = false;
if ( normalized ) {
textureFormatsTier1 = device.features.has( GPUFeatureName.TextureFormatsTier1 );
if ( textureFormatsTier1 === false ) {
warn( 'WebGPURenderer: Unable to use normalized textures without texture-formats-tier1 feature.' );
}
}
if ( texture.isCompressedTexture === true || texture.isCompressedArrayTexture === true ) {
switch ( format ) {
case RGB_S3TC_DXT1_Format:
case RGBA_S3TC_DXT1_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.BC1RGBAUnormSRGB : GPUTextureFormat.BC1RGBAUnorm;
break;
case RGBA_S3TC_DXT3_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.BC2RGBAUnormSRGB : GPUTextureFormat.BC2RGBAUnorm;
break;
case RGBA_S3TC_DXT5_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.BC3RGBAUnormSRGB : GPUTextureFormat.BC3RGBAUnorm;
break;
case RED_RGTC1_Format:
formatGPU = GPUTextureFormat.BC4RUnorm;
break;
case SIGNED_RED_RGTC1_Format:
formatGPU = GPUTextureFormat.BC4RSnorm;
break;
case RED_GREEN_RGTC2_Format:
formatGPU = GPUTextureFormat.BC5RGUnorm;
break;
case SIGNED_RED_GREEN_RGTC2_Format:
formatGPU = GPUTextureFormat.BC5RGSnorm;
break;
case RGBA_BPTC_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.BC7RGBAUnormSRGB : GPUTextureFormat.BC7RGBAUnorm;
break;
case RGB_BPTC_SIGNED_Format:
formatGPU = GPUTextureFormat.BC6HRGBFloat;
break;
case RGB_BPTC_UNSIGNED_Format:
formatGPU = GPUTextureFormat.BC6HRGBUFloat;
break;
case RGB_ETC2_Format:
case RGB_ETC1_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ETC2RGB8UnormSRGB : GPUTextureFormat.ETC2RGB8Unorm;
break;
case RGBA_ETC2_EAC_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ETC2RGBA8UnormSRGB : GPUTextureFormat.ETC2RGBA8Unorm;
break;
case R11_EAC_Format:
formatGPU = GPUTextureFormat.EACR11Unorm;
break;
case SIGNED_R11_EAC_Format:
formatGPU = GPUTextureFormat.EACR11Snorm;
break;
case RG11_EAC_Format:
formatGPU = GPUTextureFormat.EACRG11Unorm;
break;
case SIGNED_RG11_EAC_Format:
formatGPU = GPUTextureFormat.EACRG11Snorm;
break;
case RGBA_ASTC_4x4_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC4x4UnormSRGB : GPUTextureFormat.ASTC4x4Unorm;
break;
case RGBA_ASTC_5x4_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC5x4UnormSRGB : GPUTextureFormat.ASTC5x4Unorm;
break;
case RGBA_ASTC_5x5_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC5x5UnormSRGB : GPUTextureFormat.ASTC5x5Unorm;
break;
case RGBA_ASTC_6x5_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC6x5UnormSRGB : GPUTextureFormat.ASTC6x5Unorm;
break;
case RGBA_ASTC_6x6_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC6x6UnormSRGB : GPUTextureFormat.ASTC6x6Unorm;
break;
case RGBA_ASTC_8x5_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC8x5UnormSRGB : GPUTextureFormat.ASTC8x5Unorm;
break;
case RGBA_ASTC_8x6_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC8x6UnormSRGB : GPUTextureFormat.ASTC8x6Unorm;
break;
case RGBA_ASTC_8x8_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC8x8UnormSRGB : GPUTextureFormat.ASTC8x8Unorm;
break;
case RGBA_ASTC_10x5_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC10x5UnormSRGB : GPUTextureFormat.ASTC10x5Unorm;
break;
case RGBA_ASTC_10x6_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC10x6UnormSRGB : GPUTextureFormat.ASTC10x6Unorm;
break;
case RGBA_ASTC_10x8_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC10x8UnormSRGB : GPUTextureFormat.ASTC10x8Unorm;
break;
case RGBA_ASTC_10x10_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC10x10UnormSRGB : GPUTextureFormat.ASTC10x10Unorm;
break;
case RGBA_ASTC_12x10_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC12x10UnormSRGB : GPUTextureFormat.ASTC12x10Unorm;
break;
case RGBA_ASTC_12x12_Format:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.ASTC12x12UnormSRGB : GPUTextureFormat.ASTC12x12Unorm;
break;
case RGBAFormat:
if ( type === HalfFloatType ) {
formatGPU = GPUTextureFormat.RGBA16Float;
} else {
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.RGBA8UnormSRGB : GPUTextureFormat.RGBA8Unorm;
}
break;
default:
error( 'WebGPURenderer: Unsupported texture format.', format );
}
} else {
switch ( format ) {
case RGBAFormat:
switch ( type ) {
case ByteType:
formatGPU = GPUTextureFormat.RGBA8Snorm;
break;
case ShortType:
formatGPU = textureFormatsTier1 ? GPUTextureFormat.RGBA16Snorm : GPUTextureFormat.RGBA16Sint;
break;
case UnsignedShortType:
formatGPU = textureFormatsTier1 ? GPUTextureFormat.RGBA16Unorm : GPUTextureFormat.RGBA16Uint;
break;
case UnsignedIntType:
formatGPU = GPUTextureFormat.RGBA32Uint;
break;
case IntType:
formatGPU = GPUTextureFormat.RGBA32Sint;
break;
case UnsignedByteType:
formatGPU = ( transfer === SRGBTransfer ) ? GPUTextureFormat.RGBA8UnormSRGB : GPUTextureFormat.RGBA8Unorm;
break;
case HalfFloatType:
formatGPU = GPUTextureFormat.RGBA16Float;
break;
case FloatType:
formatGPU = GPUTextureFormat.RGBA32Float;
break;
default:
error( 'WebGPURenderer: Unsupported texture type with RGBAFormat.', type );
}
break;
case RGBFormat:
switch ( type ) {
case UnsignedInt5999Type:
formatGPU = GPUTextureFormat.RGB9E5UFloat;
break;
case UnsignedInt101111Type:
formatGPU = GPUTextureFormat.RG11B10UFloat;
break;
default:
error( 'WebGPURenderer: Unsupported texture type with RGBFormat.', type );
}
break;
case RedFormat:
switch ( type ) {
case ByteType:
formatGPU = GPUTextureFormat.R8Snorm;
break;
case ShortType:
formatGPU = textureFormatsTier1 ? GPUTextureFormat.R16Snorm : GPUTextureFormat.R16Sint;
break;
case UnsignedShortType:
formatGPU = textureFormatsTier1 ? GPUTextureFormat.R16Unorm : GPUTextureFormat.R16Uint;
break;
case UnsignedIntType:
formatGPU = GPUTextureFormat.R32Uint;
break;
case IntType:
formatGPU = GPUTextureFormat.R32Sint;
break;
case UnsignedByteType:
formatGPU = GPUTextureFormat.R8Unorm;
break;
case HalfFloatType:
formatGPU = GPUTextureFormat.R16Float;
break;
case FloatType:
formatGPU = GPUTextureFormat.R32Float;
break;
default:
error( 'WebGPURenderer: Unsupported texture type with RedFormat.', type );
}
break;
case RGFormat:
switch ( type ) {
case ByteType:
formatGPU = GPUTextureFormat.RG8Snorm;
break;
case ShortType:
formatGPU = textureFormatsTier1 ? GPUTextureFormat.RG16Snorm : GPUTextureFormat.RG16Sint;
break;
case UnsignedShortType:
formatGPU = textureFormatsTier1 ? GPUTextureFormat.RG16Unorm : GPUTextureFormat.RG16Uint;
break;
case UnsignedIntType:
formatGPU = GPUTextureFormat.RG32Uint;
break;
case IntType:
formatGPU = GPUTextureFormat.RG32Sint;
break;
case UnsignedByteType:
formatGPU = GPUTextureFormat.RG8Unorm;
break;
case HalfFloatType:
formatGPU = GPUTextureFormat.RG16Float;
break;
case FloatType:
formatGPU = GPUTextureFormat.RG32Float;
break;
default:
error( 'WebGPURenderer: Unsupported texture type with RGFormat.', type );
}
break;
case DepthFormat:
switch ( type ) {
case UnsignedShortType:
formatGPU = GPUTextureFormat.Depth16Unorm;
break;
case UnsignedIntType:
formatGPU = GPUTextureFormat.Depth24Plus;
break;
case FloatType:
formatGPU = GPUTextureFormat.Depth32Float;
break;
default:
error( 'WebGPURenderer: Unsupported texture type with DepthFormat.', type );
}
break;
case DepthStencilFormat:
switch ( type ) {
case UnsignedInt248Type:
formatGPU = GPUTextureFormat.Depth24PlusStencil8;
break;
case FloatType:
if ( device && device.features.has( GPUFeatureName.Depth32FloatStencil8 ) === false ) {
error( 'WebGPURenderer: Depth textures with DepthStencilFormat + FloatType can only be used with the "depth32float-stencil8" GPU feature.' );
}
formatGPU = GPUTextureFormat.Depth32FloatStencil8;
break;
default:
error( 'WebGPURenderer: Unsupported texture type with DepthStencilFormat.', type );
}
break;
case RedIntegerFormat:
switch ( type ) {
case IntType:
formatGPU = GPUTextureFormat.R32Sint;
break;
case UnsignedIntType:
formatGPU = GPUTextureFormat.R32Uint;
break;
default:
error( 'WebGPURenderer: Unsupported texture type with RedIntegerFormat.', type );
}
break;
case RGIntegerFormat:
switch ( type ) {
case IntType:
formatGPU = GPUTextureFormat.RG32Sint;
break;
case UnsignedIntType:
formatGPU = GPUTextureFormat.RG32Uint;
break;
default:
error( 'WebGPURenderer: Unsupported texture type with RGIntegerFormat.', type );
}
break;
case RGBAIntegerFormat:
switch ( type ) {
case IntType:
formatGPU = GPUTextureFormat.RGBA32Sint;
break;
case UnsignedIntType:
formatGPU = GPUTextureFormat.RGBA32Uint;
break;
default:
error( 'WebGPURenderer: Unsupported texture type with RGBAIntegerFormat.', type );
}
break;
default:
error( 'WebGPURenderer: Unsupported texture format.', format );
}
}
return formatGPU;
}
const declarationRegexp = /^[fn]*\s*([a-z_0-9]+)?\s*\(([\s\S]*?)\)\s*[\-\>]*\s*([a-z_0-9]+(?:<[\s\S]+?>)?)/i;
const propertiesRegexp = /([a-z_0-9]+)\s*:\s*([a-z_0-9]+(?:<[\s\S]+?>)?)/ig;
const wgslTypeLib$1 = {
'f32': 'float',
'i32': 'int',
'u32': 'uint',
'bool': 'bool',
'vec2<f32>': 'vec2',
'vec2<i32>': 'ivec2',
'vec2<u32>': 'uvec2',
'vec2<bool>': 'bvec2',
'vec2f': 'vec2',
'vec2i': 'ivec2',
'vec2u': 'uvec2',
'vec2b': 'bvec2',
'vec3<f32>': 'vec3',
'vec3<i32>': 'ivec3',
'vec3<u32>': 'uvec3',
'vec3<bool>': 'bvec3',
'vec3f': 'vec3',
'vec3i': 'ivec3',
'vec3u': 'uvec3',
'vec3b': 'bvec3',
'vec4<f32>': 'vec4',
'vec4<i32>': 'ivec4',
'vec4<u32>': 'uvec4',
'vec4<bool>': 'bvec4',
'vec4f': 'vec4',
'vec4i': 'ivec4',
'vec4u': 'uvec4',
'vec4b': 'bvec4',
'mat2x2<f32>': 'mat2',
'mat2x2f': 'mat2',
'mat3x3<f32>': 'mat3',
'mat3x3f': 'mat3',
'mat4x4<f32>': 'mat4',
'mat4x4f': 'mat4',
'sampler': 'sampler',
'sampler_comparison': 'samplerComparison',
'texture_1d': 'texture',
'texture_2d': 'texture',
'texture_2d_array': 'texture',
'texture_multisampled_2d': 'cubeTexture',
'texture_depth_2d': 'depthTexture',
'texture_depth_2d_array': 'depthTexture',
'texture_depth_multisampled_2d': 'depthTexture',
'texture_depth_cube': 'depthTexture',
'texture_depth_cube_array': 'depthTexture',
'texture_3d': 'texture3D',
'texture_cube': 'cubeTexture',
'texture_cube_array': 'cubeTexture',
'texture_storage_1d': 'storageTexture',
'texture_storage_2d': 'storageTexture',
'texture_storage_2d_array': 'storageTexture',
'texture_storage_3d': 'storageTexture'
};
const parse = ( source ) => {
source = source.replace( /^(?:\s*\/\/[^\r\n]*|\s*\/\*[\s\S]*?\*\/|\s*)+/, '' );
const declaration = source.match( declarationRegexp );
if ( declaration !== null && declaration.length === 4 ) {
const inputsCode = declaration[ 2 ];
const propsMatches = [];
let match = null;
while ( ( match = propertiesRegexp.exec( inputsCode ) ) !== null ) {
propsMatches.push( { name: match[ 1 ], type: match[ 2 ] } );
}
// Process matches to correctly pair names and types
const inputs = [];
for ( let i = 0; i < propsMatches.length; i ++ ) {
const { name, type } = propsMatches[ i ];
let resolvedType = type;
if ( resolvedType.startsWith( 'ptr' ) ) {
resolvedType = 'pointer';
} else {
if ( resolvedType.startsWith( 'texture' ) ) {
resolvedType = type.split( '<' )[ 0 ];
}
resolvedType = wgslTypeLib$1[ resolvedType ];
}
inputs.push( new NodeFunctionInput( resolvedType, name ) );
}
const blockCode = source.substring( declaration[ 0 ].length );
const outputType = declaration[ 3 ] || 'void';
const name = declaration[ 1 ] !== undefined ? declaration[ 1 ] : '';
const type = wgslTypeLib$1[ outputType ] || outputType;
return {
type,
inputs,
name,
inputsCode,
blockCode,
outputType
};
} else {
throw new Error( 'THREE.WGSLNodeFunction: Function is not a WGSL code.' );
}
};
/**
* This class represents a WSL node function.
*
* @augments NodeFunction
*/
class WGSLNodeFunction extends NodeFunction {
/**
* Constructs a new WGSL node function.
*
* @param {string} source - The WGSL source.
*/
constructor( source ) {
const { type, inputs, name, inputsCode, blockCode, outputType } = parse( source );
super( type, inputs, name );
this.inputsCode = inputsCode;
this.blockCode = blockCode;
this.outputType = outputType;
}
/**
* This method returns the WGSL code of the node function.
*
* @param {string} [name=this.name] - The function's name.
* @return {string} The shader code.
*/
getCode( name = this.name ) {
const outputType = this.outputType !== 'void' ? '-> ' + this.outputType : '';
return `fn ${ name } ( ${ this.inputsCode.trim() } ) ${ outputType }` + this.blockCode;
}
}
/**
* A WGSL node parser.
*
* @augments NodeParser
*/
class WGSLNodeParser extends NodeParser {
/**
* The method parses the given WGSL code an returns a node function.
*
* @param {string} source - The WGSL code.
* @return {WGSLNodeFunction} A node function.
*/
parseFunction( source ) {
return new WGSLNodeFunction( source );
}
}
const accessNames = {
[ NodeAccess.READ_ONLY ]: 'read',
[ NodeAccess.WRITE_ONLY ]: 'write',
[ NodeAccess.READ_WRITE ]: 'read_write'
};
const wrapNames = {
[ RepeatWrapping ]: 'repeat',
[ ClampToEdgeWrapping ]: 'clamp',
[ MirroredRepeatWrapping ]: 'mirror'
};
const gpuShaderStageLib = {
'vertex': GPUShaderStage.VERTEX,
'fragment': GPUShaderStage.FRAGMENT,
'compute': GPUShaderStage.COMPUTE
};
const supports = {
instance: true,
swizzleAssign: false,
storageBuffer: true
};
const wgslFnOpLib = {
'^^': 'tsl_xor'
};
const wgslTypeLib = {
float: 'f32',
int: 'i32',
uint: 'u32',
bool: 'bool',
color: 'vec3<f32>',
vec2: 'vec2<f32>',
ivec2: 'vec2<i32>',
uvec2: 'vec2<u32>',
bvec2: 'vec2<bool>',
vec3: 'vec3<f32>',
ivec3: 'vec3<i32>',
uvec3: 'vec3<u32>',
bvec3: 'vec3<bool>',
vec4: 'vec4<f32>',
ivec4: 'vec4<i32>',
uvec4: 'vec4<u32>',
bvec4: 'vec4<bool>',
mat2: 'mat2x2<f32>',
mat3: 'mat3x3<f32>',
mat4: 'mat4x4<f32>'
};
const wgslCodeCache = {};
const wgslPolyfill = {
tsl_xor: new CodeNode( 'fn tsl_xor( a : bool, b : bool ) -> bool { return ( a || b ) && !( a && b ); }' ),
mod_float: new CodeNode( 'fn tsl_mod_float( x : f32, y : f32 ) -> f32 { return x - y * floor( x / y ); }' ),
mod_vec2: new CodeNode( 'fn tsl_mod_vec2( x : vec2f, y : vec2f ) -> vec2f { return x - y * floor( x / y ); }' ),
mod_vec3: new CodeNode( 'fn tsl_mod_vec3( x : vec3f, y : vec3f ) -> vec3f { return x - y * floor( x / y ); }' ),
mod_vec4: new CodeNode( 'fn tsl_mod_vec4( x : vec4f, y : vec4f ) -> vec4f { return x - y * floor( x / y ); }' ),
equals_bool: new CodeNode( 'fn tsl_equals_bool( a : bool, b : bool ) -> bool { return a == b; }' ),
equals_bvec2: new CodeNode( 'fn tsl_equals_bvec2( a : vec2f, b : vec2f ) -> vec2<bool> { return vec2<bool>( a.x == b.x, a.y == b.y ); }' ),
equals_bvec3: new CodeNode( 'fn tsl_equals_bvec3( a : vec3f, b : vec3f ) -> vec3<bool> { return vec3<bool>( a.x == b.x, a.y == b.y, a.z == b.z ); }' ),
equals_bvec4: new CodeNode( 'fn tsl_equals_bvec4( a : vec4f, b : vec4f ) -> vec4<bool> { return vec4<bool>( a.x == b.x, a.y == b.y, a.z == b.z, a.w == b.w ); }' ),
repeatWrapping_float: new CodeNode( 'fn tsl_repeatWrapping_float( coord: f32 ) -> f32 { return fract( coord ); }' ),
mirrorWrapping_float: new CodeNode( 'fn tsl_mirrorWrapping_float( coord: f32 ) -> f32 { let mirrored = fract( coord * 0.5 ) * 2.0; return 1.0 - abs( 1.0 - mirrored ); }' ),
clampWrapping_float: new CodeNode( 'fn tsl_clampWrapping_float( coord: f32 ) -> f32 { return clamp( coord, 0.0, 1.0 ); }' ),
inverse_mat2: new CodeNode( /* wgsl */`
fn tsl_inverse_mat2( m : mat2x2<f32> ) -> mat2x2<f32> {
let det = m[ 0 ][ 0 ] * m[ 1 ][ 1 ] - m[ 0 ][ 1 ] * m[ 1 ][ 0 ];
return mat2x2<f32>(
m[ 1 ][ 1 ], - m[ 0 ][ 1 ],
- m[ 1 ][ 0 ], m[ 0 ][ 0 ]
) * ( 1.0 / det );
}
` ),
inverse_mat3: new CodeNode( /* wgsl */`
fn tsl_inverse_mat3( m : mat3x3<f32> ) -> mat3x3<f32> {
let a00 = m[ 0 ][ 0 ]; let a01 = m[ 0 ][ 1 ]; let a02 = m[ 0 ][ 2 ];
let a10 = m[ 1 ][ 0 ]; let a11 = m[ 1 ][ 1 ]; let a12 = m[ 1 ][ 2 ];
let a20 = m[ 2 ][ 0 ]; let a21 = m[ 2 ][ 1 ]; let a22 = m[ 2 ][ 2 ];
let b01 = a22 * a11 - a12 * a21;
let b11 = - a22 * a10 + a12 * a20;
let b21 = a21 * a10 - a11 * a20;
let det = a00 * b01 + a01 * b11 + a02 * b21;
return mat3x3<f32>(
b01, ( - a22 * a01 + a02 * a21 ), ( a12 * a01 - a02 * a11 ),
b11, ( a22 * a00 - a02 * a20 ), ( - a12 * a00 + a02 * a10 ),
b21, ( - a21 * a00 + a01 * a20 ), ( a11 * a00 - a01 * a10 )
) * ( 1.0 / det );
}
` ),
inverse_mat4: new CodeNode( /* wgsl */`
fn tsl_inverse_mat4( m : mat4x4<f32> ) -> mat4x4<f32> {
let a00 = m[ 0 ][ 0 ]; let a01 = m[ 0 ][ 1 ]; let a02 = m[ 0 ][ 2 ]; let a03 = m[ 0 ][ 3 ];
let a10 = m[ 1 ][ 0 ]; let a11 = m[ 1 ][ 1 ]; let a12 = m[ 1 ][ 2 ]; let a13 = m[ 1 ][ 3 ];
let a20 = m[ 2 ][ 0 ]; let a21 = m[ 2 ][ 1 ]; let a22 = m[ 2 ][ 2 ]; let a23 = m[ 2 ][ 3 ];
let a30 = m[ 3 ][ 0 ]; let a31 = m[ 3 ][ 1 ]; let a32 = m[ 3 ][ 2 ]; let a33 = m[ 3 ][ 3 ];
let b00 = a00 * a11 - a01 * a10;
let b01 = a00 * a12 - a02 * a10;
let b02 = a00 * a13 - a03 * a10;
let b03 = a01 * a12 - a02 * a11;
let b04 = a01 * a13 - a03 * a11;
let b05 = a02 * a13 - a03 * a12;
let b06 = a20 * a31 - a21 * a30;
let b07 = a20 * a32 - a22 * a30;
let b08 = a20 * a33 - a23 * a30;
let b09 = a21 * a32 - a22 * a31;
let b10 = a21 * a33 - a23 * a31;
let b11 = a22 * a33 - a23 * a32;
let det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
return mat4x4<f32>(
a11 * b11 - a12 * b10 + a13 * b09,
a02 * b10 - a01 * b11 - a03 * b09,
a31 * b05 - a32 * b04 + a33 * b03,
a22 * b04 - a21 * b05 - a23 * b03,
a12 * b08 - a10 * b11 - a13 * b07,
a00 * b11 - a02 * b08 + a03 * b07,
a32 * b02 - a30 * b05 - a33 * b01,
a20 * b05 - a22 * b02 + a23 * b01,
a10 * b10 - a11 * b08 + a13 * b06,
a01 * b08 - a00 * b10 - a03 * b06,
a30 * b04 - a31 * b02 + a33 * b00,
a21 * b02 - a20 * b04 - a23 * b00,
a11 * b07 - a10 * b09 - a12 * b06,
a00 * b09 - a01 * b07 + a02 * b06,
a31 * b01 - a30 * b03 - a32 * b00,
a20 * b03 - a21 * b01 + a22 * b00
) * ( 1.0 / det );
}
` ),
biquadraticTexture: new CodeNode( /* wgsl */`
fn tsl_biquadraticTexture( map : texture_2d<f32>, coord : vec2f, iRes : vec2u, level : u32 ) -> vec4f {
let res = vec2f( iRes );
let uvScaled = coord * res;
let uvWrapping = ( ( uvScaled % res ) + res ) % res;
// https://www.shadertoy.com/view/WtyXRy
let uv = uvWrapping - 0.5;
let iuv = floor( uv );
let f = fract( uv );
let rg1 = textureLoad( map, vec2u( iuv + vec2( 0.5, 0.5 ) ) % iRes, level );
let rg2 = textureLoad( map, vec2u( iuv + vec2( 1.5, 0.5 ) ) % iRes, level );
let rg3 = textureLoad( map, vec2u( iuv + vec2( 0.5, 1.5 ) ) % iRes, level );
let rg4 = textureLoad( map, vec2u( iuv + vec2( 1.5, 1.5 ) ) % iRes, level );
return mix( mix( rg1, rg2, f.x ), mix( rg3, rg4, f.x ), f.y );
}
` ),
biquadraticTextureArray: new CodeNode( /* wgsl */`
fn tsl_biquadraticTexture_array( map : texture_2d_array<f32>, coord : vec2f, iRes : vec2u, layer : u32, level : u32 ) -> vec4f {
let res = vec2f( iRes );
let uvScaled = coord * res;
let uvWrapping = ( ( uvScaled % res ) + res ) % res;
// https://www.shadertoy.com/view/WtyXRy
let uv = uvWrapping - 0.5;
let iuv = floor( uv );
let f = fract( uv );
let rg1 = textureLoad( map, vec2u( iuv + vec2( 0.5, 0.5 ) ) % iRes, layer, level );
let rg2 = textureLoad( map, vec2u( iuv + vec2( 1.5, 0.5 ) ) % iRes, layer, level );
let rg3 = textureLoad( map, vec2u( iuv + vec2( 0.5, 1.5 ) ) % iRes, layer, level );
let rg4 = textureLoad( map, vec2u( iuv + vec2( 1.5, 1.5 ) ) % iRes, layer, level );
return mix( mix( rg1, rg2, f.x ), mix( rg3, rg4, f.x ), f.y );
}
` )
};
const wgslMethods = {
dFdx: 'dpdx',
dFdy: '- dpdy',
mod_float: 'tsl_mod_float',
mod_vec2: 'tsl_mod_vec2',
mod_vec3: 'tsl_mod_vec3',
mod_vec4: 'tsl_mod_vec4',
equals_bool: 'tsl_equals_bool',
equals_bvec2: 'tsl_equals_bvec2',
equals_bvec3: 'tsl_equals_bvec3',
equals_bvec4: 'tsl_equals_bvec4',
inverse_mat2: 'tsl_inverse_mat2',
inverse_mat3: 'tsl_inverse_mat3',
inverse_mat4: 'tsl_inverse_mat4',
inversesqrt: 'inverseSqrt',
faceforward: 'faceForward',
bitcast: 'bitcast<f32>',
floatpack_snorm_2x16: 'pack2x16snorm',
floatpack_unorm_2x16: 'pack2x16unorm',
floatpack_float16_2x16: 'pack2x16float',
floatpack_snorm_4x8: 'pack4x8snorm',
floatpack_unorm_4x8: 'pack4x8unorm',
floatunpack_snorm_2x16: 'unpack2x16snorm',
floatunpack_unorm_2x16: 'unpack2x16unorm',
floatunpack_float16_2x16: 'unpack2x16float',
floatunpack_snorm_4x8: 'unpack4x8snorm',
floatunpack_unorm_4x8: 'unpack4x8unorm'
};
// See: https://www.w3.org/TR/WGSL/#keyword-summary and #reserved-words-section
const wgslReservedKeywords = new Set( [
// keywords
'alias', 'break', 'case', 'const', 'const_assert', 'continue', 'continuing', 'default', 'diagnostic',
'discard', 'else', 'enable', 'false', 'fn', 'for', 'if', 'let', 'loop', 'override', 'requires',
'return', 'struct', 'switch', 'true', 'var', 'while',
// reserved words
'NULL', 'Self', 'abstract', 'active', 'alignas', 'alignof', 'as', 'asm', 'asm_fragment', 'async',
'attribute', 'auto', 'await', 'become', 'binding_array', 'cast', 'catch', 'class', 'co_await',
'co_return', 'co_yield', 'coherent', 'column_major', 'common', 'compile', 'compile_fragment',
'concept', 'const_cast', 'consteval', 'constexpr', 'constinit', 'crate', 'debugger', 'decltype',
'delete', 'demote', 'demote_to_helper', 'do', 'dynamic_cast', 'enum', 'explicit', 'export',
'extends', 'extern', 'external', 'fallthrough', 'filter', 'final', 'finally', 'friend', 'from',
'fxgroup', 'get', 'goto', 'groupshared', 'highp', 'impl', 'implements', 'import', 'inline',
'instanceof', 'interface', 'layout', 'lowp', 'macro', 'macro_rules', 'match', 'mediump', 'meta',
'mod', 'module', 'move', 'mut', 'mutable', 'namespace', 'new', 'nil', 'noexcept', 'noinline',
'nointerpolation', 'non_coherent', 'noncoherent', 'noperspective', 'null', 'nullptr', 'of',
'operator', 'package', 'packoffset', 'partition', 'pass', 'patch', 'pixelfragment', 'precise',
'precision', 'premerge', 'priv', 'protected', 'pub', 'public', 'readonly', 'ref', 'regardless',
'register', 'reinterpret_cast', 'require', 'resource', 'restrict', 'self', 'set', 'shared',
'sizeof', 'smooth', 'snorm', 'static', 'static_assert', 'static_cast', 'std', 'subroutine',
'super', 'target', 'template', 'this', 'thread_local', 'throw', 'trait', 'try', 'type', 'typedef',
'typeid', 'typename', 'typeof', 'union', 'unless', 'unorm', 'unsafe', 'unsized', 'use', 'using',
'varying', 'virtual', 'volatile', 'wgsl', 'where', 'with', 'writeonly', 'yield',
// generated entry points
'main'
] );
//
let diagnostics = '';
if ( ( typeof navigator !== 'undefined' && /Firefox|Deno/g.test( navigator.userAgent ) ) !== true ) {
diagnostics += 'diagnostic( off, derivative_uniformity );\n';
}
/**
* A node builder targeting WGSL.
*
* This module generates WGSL shader code from node materials and also
* generates the respective bindings and vertex buffer definitions. These
* data are later used by the renderer to create render and compute pipelines
* for render objects.
*
* @augments NodeBuilder
*/
class WGSLNodeBuilder extends NodeBuilder {
/**
* Constructs a new WGSL node builder renderer.
*
* @param {Object3D} object - The 3D object.
* @param {Renderer} renderer - The renderer.
*/
constructor( object, renderer ) {
super( object, renderer, new WGSLNodeParser() );
/**
* A dictionary that holds for each shader stage ('vertex', 'fragment', 'compute')
* another dictionary which manages UBOs per group ('render','frame','object').
*
* @type {Object<string,Object<string,NodeUniformsGroup>>}
*/
this.uniformGroups = {};
/**
* A dictionary that holds the assigned binding indices for each uniform group.
* This ensures the same binding index is used across all shader stages.
*
* @type {Object<string,{index: number, id: number}>}
*/
this.uniformGroupsBindings = {};
/**
* A dictionary that holds for each shader stage a Map of builtins.
*
* @type {Object<string,Map<string,Object>>}
*/
this.builtins = {};
/**
* A dictionary that holds for each shader stage a Set of directives.
*
* @type {Object<string,Set<string>>}
*/
this.directives = {};
/**
* A map for managing scope arrays. Only relevant for when using
* {@link WorkgroupInfoNode} in context of compute shaders.
*
* @type {Map<string,Object>}
*/
this.scopedArrays = new Map();
/**
* A flag that indicates that early returns are allowed.
*
* @type {boolean}
* @default true
*/
this.allowEarlyReturns = true;
/**
* A flag that indicates that global variables are allowed.
*
* @type {boolean}
* @default true
*/
this.allowGlobalVariables = true;
}
/**
* Generates the WGSL snippet for sampled textures.
*
* @private
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @param {string} [shaderStage=this.shaderStage] - The shader stage this code snippet is generated for.
* @return {string} The WGSL snippet.
*/
_generateTextureSample( texture, textureProperty, uvSnippet, depthSnippet, offsetSnippet, shaderStage = this.shaderStage ) {
if ( shaderStage === 'fragment' ) {
if ( depthSnippet ) {
if ( offsetSnippet ) {
return `textureSample( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ offsetSnippet } )`;
}
return `textureSample( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet } )`;
} else {
if ( offsetSnippet ) {
return `textureSample( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ offsetSnippet } )`;
}
return `textureSample( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet } )`;
}
} else {
return this.generateTextureSampleLevel( texture, textureProperty, uvSnippet, '0', depthSnippet );
}
}
/**
* Generates the WGSL snippet when sampling textures with explicit mip level.
*
* @private
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {string} levelSnippet - A WGSL snippet that represents the mip level, with level 0 containing a full size version of the texture.
* @param {string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @return {string} The WGSL snippet.
*/
generateTextureSampleLevel( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ) {
if ( this.isUnfilterable( texture ) === false ) {
if ( depthSnippet ) {
if ( offsetSnippet ) {
return `textureSampleLevel( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ levelSnippet }, ${ offsetSnippet } )`;
}
return `textureSampleLevel( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ levelSnippet } )`;
} else {
if ( offsetSnippet ) {
return `textureSampleLevel( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ levelSnippet }, ${ offsetSnippet } )`;
}
return `textureSampleLevel( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ levelSnippet } )`;
}
} else if ( this.isFilteredTexture( texture ) ) {
return this.generateFilteredTexture( texture, textureProperty, uvSnippet, offsetSnippet, levelSnippet, depthSnippet );
} else {
return this.generateTextureLod( texture, textureProperty, uvSnippet, depthSnippet, offsetSnippet, levelSnippet );
}
}
/**
* Generates a wrap function used in context of textures.
*
* @param {Texture} texture - The texture to generate the function for.
* @return {string} The name of the generated function.
*/
generateWrapFunction( texture ) {
const functionName = `tsl_coord_${ wrapNames[ texture.wrapS ] }S_${ wrapNames[ texture.wrapT ] }T_${ texture.is3DTexture || texture.isData3DTexture ? '3d' : '2d' }`;
let nodeCode = wgslCodeCache[ functionName ];
if ( nodeCode === undefined ) {
const includes = [];
// For 3D textures, use vec3f; for texture arrays, keep vec2f since array index is separate
const coordType = texture.is3DTexture || texture.isData3DTexture ? 'vec3f' : 'vec2f';
let code = `fn ${ functionName }( coord : ${ coordType } ) -> ${ coordType } {\n\n\treturn ${ coordType }(\n`;
const addWrapSnippet = ( wrap, axis ) => {
if ( wrap === RepeatWrapping ) {
includes.push( wgslPolyfill.repeatWrapping_float );
code += `\t\ttsl_repeatWrapping_float( coord.${ axis } )`;
} else if ( wrap === ClampToEdgeWrapping ) {
includes.push( wgslPolyfill.clampWrapping_float );
code += `\t\ttsl_clampWrapping_float( coord.${ axis } )`;
} else if ( wrap === MirroredRepeatWrapping ) {
includes.push( wgslPolyfill.mirrorWrapping_float );
code += `\t\ttsl_mirrorWrapping_float( coord.${ axis } )`;
} else {
code += `\t\tcoord.${ axis }`;
warn( `WebGPURenderer: Unsupported texture wrap type "${ wrap }" for vertex shader.` );
}
};
addWrapSnippet( texture.wrapS, 'x' );
code += ',\n';
addWrapSnippet( texture.wrapT, 'y' );
if ( texture.is3DTexture || texture.isData3DTexture ) {
code += ',\n';
addWrapSnippet( texture.wrapR, 'z' );
}
code += '\n\t);\n\n}\n';
wgslCodeCache[ functionName ] = nodeCode = new CodeNode( code, includes );
}
nodeCode.build( this );
return functionName;
}
/**
* Generates the array declaration string.
*
* @param {string} type - The type.
* @param {?number} [count] - The count.
* @return {string} The generated value as a shader string.
*/
generateArrayDeclaration( type, count ) {
return `array< ${ this.getType( type ) }, ${ count } >`;
}
/**
* Generates a WGSL variable that holds the texture dimension of the given texture.
* It also returns information about the number of layers (elements) of an arrayed
* texture as well as the cube face count of cube textures.
*
* @param {Texture} texture - The texture to generate the function for.
* @param {string} textureProperty - The name of the video texture uniform in the shader.
* @param {string} levelSnippet - A WGSL snippet that represents the mip level, with level 0 containing a full size version of the texture.
* @return {string} The name of the dimension variable.
*/
generateTextureDimension( texture, textureProperty, levelSnippet ) {
const textureData = this.getDataFromNode( texture, this.shaderStage, this.cache );
if ( textureData.dimensionsSnippet === undefined ) textureData.dimensionsSnippet = {};
let textureDimensionNode = textureData.dimensionsSnippet[ levelSnippet ];
if ( textureData.dimensionsSnippet[ levelSnippet ] === undefined ) {
let textureDimensionsParams;
let dimensionType;
const { primarySamples } = this.renderer.backend.utils.getTextureSampleData( texture );
const isMultisampled = primarySamples > 1;
if ( texture.is3DTexture || texture.isData3DTexture ) {
dimensionType = 'vec3<u32>';
} else {
// Regular 2D textures, depth textures, etc.
dimensionType = 'vec2<u32>';
}
// Build parameters string based on texture type and multisampling
if ( isMultisampled || texture.isStorageTexture ) {
textureDimensionsParams = textureProperty;
} else {
textureDimensionsParams = `${textureProperty}${levelSnippet ? `, u32( ${ levelSnippet } )` : ''}`;
}
textureDimensionNode = new VarNode( new ExpressionNode( `textureDimensions( ${ textureDimensionsParams } )`, dimensionType ) );
textureData.dimensionsSnippet[ levelSnippet ] = textureDimensionNode;
if ( texture.isArrayTexture || texture.isDataArrayTexture || texture.is3DTexture || texture.isData3DTexture ) {
textureData.arrayLayerCount = new VarNode(
new ExpressionNode(
`textureNumLayers(${textureProperty})`,
'u32'
)
);
}
// For cube textures, we know it's always 6 faces
if ( texture.isTextureCube ) {
textureData.cubeFaceCount = new VarNode(
new ExpressionNode( '6u', 'u32' )
);
}
}
return textureDimensionNode.build( this );
}
/**
* Generates the WGSL snippet that resolves the dimensions of the given texture.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} levelSnippet - A WGSL snippet that represents the mip level.
* @return {string} The WGSL snippet.
*/
generateTextureSize( texture, textureProperty, levelSnippet ) {
const { primarySamples } = this.renderer.backend.utils.getTextureSampleData( texture );
const isMultisampled = primarySamples > 1;
const params = ( isMultisampled || texture.isStorageTexture ) ? textureProperty : `${ textureProperty }, ${ levelSnippet }`;
return `textureDimensions( ${ params } )`;
}
/**
* Generates the WGSL snippet for a manual filtered texture.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @param {string} [levelSnippet='0u'] - A WGSL snippet that represents the mip level, with level 0 containing a full size version of the texture.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @return {string} The WGSL snippet.
*/
generateFilteredTexture( texture, textureProperty, uvSnippet, offsetSnippet, levelSnippet = '0u', depthSnippet ) {
const wrapFunction = this.generateWrapFunction( texture );
const textureDimension = this.generateTextureDimension( texture, textureProperty, levelSnippet );
if ( offsetSnippet ) {
uvSnippet = `${ uvSnippet } + vec2<f32>(${ offsetSnippet }) / ${ textureDimension }`;
}
if ( depthSnippet ) {
this._include( 'biquadraticTextureArray' );
return `tsl_biquadraticTexture_array( ${ textureProperty }, ${ wrapFunction }( ${ uvSnippet } ), ${ textureDimension }, u32( ${ depthSnippet } ), u32( ${ levelSnippet } ) )`;
}
this._include( 'biquadraticTexture' );
return `tsl_biquadraticTexture( ${ textureProperty }, ${ wrapFunction }( ${ uvSnippet } ), ${ textureDimension }, u32( ${ levelSnippet } ) )`;
}
/**
* Generates the WGSL snippet for a texture lookup with explicit level-of-detail.
* Since it's a lookup, no sampling or filtering is applied.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @param {string} [levelSnippet='0u'] - A WGSL snippet that represents the mip level, with level 0 containing a full size version of the texture.
* @return {string} The WGSL snippet.
*/
generateTextureLod( texture, textureProperty, uvSnippet, depthSnippet, offsetSnippet, levelSnippet = '0u' ) {
// Cube textures cannot use textureLoad in WGSL, must use textureSampleLevel
if ( texture.isCubeTexture === true ) {
if ( offsetSnippet ) {
uvSnippet = `${ uvSnippet } + vec3<f32>(${ offsetSnippet })`;
}
// Depth textures require integer level, regular textures use float
const levelType = texture.isDepthTexture ? 'u32' : 'f32';
return `textureSampleLevel( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ levelType }( ${ levelSnippet } ) )`;
}
const wrapFunction = this.generateWrapFunction( texture );
const textureDimension = this.generateTextureDimension( texture, textureProperty, levelSnippet );
const vecType = texture.is3DTexture || texture.isData3DTexture ? 'vec3' : 'vec2';
const textureDimensionMargin = ( vecType === 'vec3' ) ? 'vec3<u32>( 1, 1, 1 )' : 'vec2<u32>( 1, 1 )';
if ( offsetSnippet ) {
uvSnippet = `${ uvSnippet } + ${ vecType }<f32>(${ offsetSnippet }) / ${ vecType }<f32>( ${ textureDimension } )`;
}
const clampMin = `${ vecType }<f32>( 0 )`;
const clampMax = `${ vecType }<f32>( ${ textureDimension } - ${ textureDimensionMargin } )`;
uvSnippet = `${ vecType }<u32>( clamp( floor( ${ wrapFunction }( ${ uvSnippet } ) * ${ vecType }<f32>( ${ textureDimension } ) ), ${ clampMin }, ${ clampMax } ) )`;
return this.generateTextureLoad( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, null );
}
/**
* Generates the WGSL snippet that reads a single texel from a storage texture.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvIndexSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {?string} levelSnippet - A WGSL snippet that represents the mip level, with level 0 containing a full size version of the texture.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @return {string} The WGSL snippet.
*/
generateStorageTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) {
if ( offsetSnippet ) {
uvIndexSnippet = `${ uvIndexSnippet } + ${ offsetSnippet }`;
}
let snippet;
if ( depthSnippet ) {
snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet } )`;
} else {
snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet } )`;
}
return snippet;
}
/**
* Generates the WGSL snippet that reads a single texel from a texture without sampling or filtering.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvIndexSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {?string} levelSnippet - A WGSL snippet that represents the mip level, with level 0 containing a full size version of the texture.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @return {string} The WGSL snippet.
*/
generateTextureLoad( texture, textureProperty, uvIndexSnippet, levelSnippet, depthSnippet, offsetSnippet ) {
if ( levelSnippet === null ) levelSnippet = '0u';
if ( offsetSnippet ) {
uvIndexSnippet = `${ uvIndexSnippet } + ${ offsetSnippet }`;
}
let snippet;
if ( depthSnippet ) {
snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet }, u32( ${ levelSnippet } ) )`;
} else {
snippet = `textureLoad( ${ textureProperty }, ${ uvIndexSnippet }, u32( ${ levelSnippet } ) )`;
if ( this.renderer.backend.compatibilityMode && texture.isDepthTexture ) {
snippet += '.x';
}
}
return snippet;
}
/**
* Generates the WGSL snippet that writes a single texel to a texture.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvIndexSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {string} valueSnippet - A WGSL snippet that represent the new texel value.
* @return {string} The WGSL snippet.
*/
generateTextureStore( texture, textureProperty, uvIndexSnippet, depthSnippet, valueSnippet ) {
let snippet;
if ( depthSnippet ) {
snippet = `textureStore( ${ textureProperty }, ${ uvIndexSnippet }, ${ depthSnippet }, ${ valueSnippet } )`;
} else {
snippet = `textureStore( ${ textureProperty }, ${ uvIndexSnippet }, ${ valueSnippet } )`;
}
return snippet;
}
/**
* Returns `true` if the sampled values of the given texture should be compared against a reference value.
*
* @param {Texture} texture - The texture.
* @return {boolean} Whether the sampled values of the given texture should be compared against a reference value or not.
*/
isSampleCompare( texture ) {
return texture.isDepthTexture === true && texture.compareFunction !== null && this.renderer.hasCompatibility( Compatibility.TEXTURE_COMPARE );
}
/**
* Returns `true` if the given texture is unfilterable.
*
* @param {Texture} texture - The texture.
* @return {boolean} Whether the given texture is unfilterable or not.
*/
isUnfilterable( texture ) {
return this.getComponentTypeFromTexture( texture ) !== 'float' ||
( ! this.isAvailable( 'float32Filterable' ) && texture.type === FloatType ) ||
( this.isSampleCompare( texture ) === false && texture.minFilter === NearestFilter && texture.magFilter === NearestFilter ) ||
this.renderer.backend.utils.getTextureSampleData( texture ).primarySamples > 1 ||
texture.normalized === true && ( texture.type === ShortType || texture.type === UnsignedShortType );
}
/**
* Generates the WGSL snippet for sampling/loading the given texture.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @param {string} [shaderStage=this.shaderStage] - The shader stage this code snippet is generated for.
* @return {string} The WGSL snippet.
*/
generateTexture( texture, textureProperty, uvSnippet, depthSnippet, offsetSnippet, shaderStage = this.shaderStage ) {
let snippet = null;
if ( this.isUnfilterable( texture ) ) {
snippet = this.generateTextureLod( texture, textureProperty, uvSnippet, depthSnippet, offsetSnippet, '0', shaderStage );
} else {
snippet = this._generateTextureSample( texture, textureProperty, uvSnippet, depthSnippet, offsetSnippet, shaderStage );
}
return snippet;
}
/**
* Generates the WGSL snippet for sampling/loading the given texture using explicit gradients.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {Array<string>} gradSnippet - An array holding both gradient WGSL snippets.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @param {string} [shaderStage=this.shaderStage] - The shader stage this code snippet is generated for.
* @return {string} The WGSL snippet.
*/
generateTextureGrad( texture, textureProperty, uvSnippet, gradSnippet, depthSnippet, offsetSnippet, shaderStage = this.shaderStage ) {
if ( shaderStage === 'fragment' ) {
// Unfilterable textures have no sampler binding. Use the same texel lookup as regular
// sampling, preserving nearest filtering, wrapping and offsets.
if ( this.isUnfilterable( texture ) ) return this.generateTexture( texture, textureProperty, uvSnippet, depthSnippet, offsetSnippet, shaderStage );
if ( depthSnippet ) {
if ( offsetSnippet ) {
return `textureSampleGrad( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ gradSnippet[ 0 ] }, ${ gradSnippet[ 1 ] }, ${ offsetSnippet } )`;
}
return `textureSampleGrad( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ gradSnippet[ 0 ] }, ${ gradSnippet[ 1 ] } )`;
} else {
if ( offsetSnippet ) {
return `textureSampleGrad( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ gradSnippet[ 0 ] }, ${ gradSnippet[ 1 ] }, ${ offsetSnippet } )`;
}
return `textureSampleGrad( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ gradSnippet[ 0 ] }, ${ gradSnippet[ 1 ] } )`;
}
} else {
error( `WebGPURenderer: THREE.TextureNode.gradient() does not support ${ shaderStage } shader.` );
}
}
/**
* Generates the WGSL snippet for sampling a depth texture and comparing the sampled depth values
* against a reference value.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {string} compareSnippet - A WGSL snippet that represents the reference value.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @param {string} [shaderStage=this.shaderStage] - The shader stage this code snippet is generated for.
* @return {string} The WGSL snippet.
*/
generateTextureCompare( texture, textureProperty, uvSnippet, compareSnippet, depthSnippet, offsetSnippet, shaderStage = this.shaderStage ) {
if ( shaderStage === 'fragment' ) {
if ( texture.isDepthTexture === true && texture.isArrayTexture === true ) {
if ( offsetSnippet ) {
return `textureSampleCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ compareSnippet }, ${ offsetSnippet } )`;
}
return `textureSampleCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ compareSnippet } )`;
}
if ( offsetSnippet ) {
return `textureSampleCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ compareSnippet }, ${ offsetSnippet } )`;
}
return `textureSampleCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ compareSnippet } )`;
} else {
error( `WebGPURenderer: THREE.DepthTexture.compareFunction() does not support ${ shaderStage } shader.` );
}
}
/**
* Generates the WGSL snippet for gathering four texels from the given texture.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {string} gatherSnippet - A WGSL snippet that represents the index of the channel to read.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @return {string} The WGSL snippet.
*/
generateTextureGather( texture, textureProperty, uvSnippet, gatherSnippet, depthSnippet, offsetSnippet ) {
const { primarySamples } = this.renderer.backend.utils.getTextureSampleData( texture );
if ( primarySamples > 1 ) {
// textureGather() has no overload for multisampled textures (e.g. the depth
// of a MSAA render target), so the four texels are fetched with textureLoad()
const textureDimension = this.generateTextureDimension( texture, textureProperty, '0u' );
let coordSnippet = `vec2<i32>( floor( ${ uvSnippet } * vec2<f32>( ${ textureDimension } ) - 0.5 ) )`;
if ( offsetSnippet ) {
coordSnippet = `${ coordSnippet } + ${ offsetSnippet }`;
}
const coord = new VarNode( new ExpressionNode( coordSnippet, 'ivec2' ) ).build( this );
const coordMax = `vec2<i32>( ${ textureDimension } ) - 1`;
const load = ( x, y ) => {
const snippet = this.generateTextureLoad( texture, textureProperty, `clamp( ${ coord } + vec2<i32>( ${ x }, ${ y } ), vec2<i32>( 0 ), ${ coordMax } )`, null, null, null );
return texture.isDepthTexture === true ? snippet : `${ snippet }[ ${ gatherSnippet } ]`;
};
// same texel order as textureGather()
const componentPrefix = this.getComponentTypeFromTexture( texture ).charAt( 0 );
return `vec4<${ componentPrefix }32>( ${ load( 0, 1 ) }, ${ load( 1, 1 ) }, ${ load( 1, 0 ) }, ${ load( 0, 0 ) } )`;
}
const componentSnippet = texture.isDepthTexture === true ? '' : `${gatherSnippet}, `;
if ( depthSnippet ) {
if ( offsetSnippet ) {
return `textureGather( ${componentSnippet}${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ offsetSnippet } )`;
}
return `textureGather( ${componentSnippet}${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet } )`;
}
if ( offsetSnippet ) {
return `textureGather( ${componentSnippet}${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ offsetSnippet } )`;
}
return `textureGather( ${componentSnippet}${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet })`;
}
/**
* Generates the WGSL snippet for performing a depth comparison on four texels in the given depth texture.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {string} compareSnippet - A WGSL snippet that represents the reference value.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @return {string} The WGSL snippet.
*/
generateTextureGatherCompare( texture, textureProperty, uvSnippet, compareSnippet, depthSnippet, offsetSnippet ) {
if ( depthSnippet ) {
if ( offsetSnippet ) {
return `textureGatherCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ compareSnippet }, ${ offsetSnippet } )`;
}
return `textureGatherCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ compareSnippet })`;
}
if ( offsetSnippet ) {
return `textureGatherCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ compareSnippet }, ${ offsetSnippet } )`;
}
return `textureGatherCompare( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ compareSnippet })`;
}
/**
* Generates the WGSL snippet when sampling textures with explicit mip level.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {string} levelSnippet - A WGSL snippet that represents the mip level, with level 0 containing a full size version of the texture.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @return {string} The WGSL snippet.
*/
generateTextureLevel( texture, textureProperty, uvSnippet, levelSnippet, depthSnippet, offsetSnippet ) {
if ( this.isUnfilterable( texture ) === false ) {
if ( depthSnippet ) {
if ( offsetSnippet ) {
return `textureSampleLevel( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ levelSnippet }, ${ offsetSnippet } )`;
}
return `textureSampleLevel( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ levelSnippet } )`;
} else {
if ( offsetSnippet ) {
return `textureSampleLevel( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ levelSnippet }, ${ offsetSnippet } )`;
}
return `textureSampleLevel( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ levelSnippet } )`;
}
} else if ( this.isFilteredTexture( texture ) ) {
return this.generateFilteredTexture( texture, textureProperty, uvSnippet, offsetSnippet, levelSnippet, depthSnippet );
} else {
return this.generateTextureLod( texture, textureProperty, uvSnippet, depthSnippet, offsetSnippet, levelSnippet );
}
}
/**
* Generates the WGSL snippet when sampling textures with a bias to the mip level.
*
* @param {Texture} texture - The texture.
* @param {string} textureProperty - The name of the texture uniform in the shader.
* @param {string} uvSnippet - A WGSL snippet that represents texture coordinates used for sampling.
* @param {string} biasSnippet - A WGSL snippet that represents the bias to apply to the mip level before sampling.
* @param {?string} depthSnippet - A WGSL snippet that represents 0-based texture array index to sample.
* @param {?string} offsetSnippet - A WGSL snippet that represents the offset that will be applied to the unnormalized texture coordinate before sampling the texture.
* @param {string} [shaderStage=this.shaderStage] - The shader stage this code snippet is generated for.
* @return {string} The WGSL snippet.
*/
generateTextureBias( texture, textureProperty, uvSnippet, biasSnippet, depthSnippet, offsetSnippet, shaderStage = this.shaderStage ) {
if ( shaderStage === 'fragment' ) {
if ( depthSnippet ) {
if ( offsetSnippet ) {
return `textureSampleBias( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ biasSnippet }, ${ offsetSnippet } )`;
}
return `textureSampleBias( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ depthSnippet }, ${ biasSnippet } )`;
} else {
if ( offsetSnippet ) {
return `textureSampleBias( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ biasSnippet }, ${ offsetSnippet } )`;
}
return `textureSampleBias( ${ textureProperty }, ${ textureProperty }_sampler, ${ uvSnippet }, ${ biasSnippet } )`;
}
} else {
error( `WebGPURenderer: THREE.TextureNode.biasNode does not support ${ shaderStage } shader.` );
}
}
/**
* Returns a WGSL snippet that represents the property name of the given node.
*
* @param {Node} node - The node.
* @param {string} [shaderStage=this.shaderStage] - The shader stage this code snippet is generated for.
* @return {string} The property name.
*/
getPropertyName( node, shaderStage = this.shaderStage ) {
if ( node.isNodeVarying === true && node.needsInterpolation === true ) {
if ( shaderStage === 'vertex' ) {
return `varyings.${ node.name }`;
}
} else if ( node.isNodeUniform === true ) {
const name = node.name;
const type = node.type;
if ( type === 'texture' || type === 'cubeTexture' || type === 'cubeDepthTexture' || type === 'storageTexture' || type === 'texture3D' ) {
return name;
} else if ( type === 'buffer' || type === 'storageBuffer' || type === 'indirectStorageBuffer' ) {
return name + '.value';
} else {
return node.groupNode.name + '.' + name;
}
}
return super.getPropertyName( node );
}
/**
* Returns the identifier the given node declares in the shader. Buffers are
* accessed via `name.value` but declare `name` in the module scope.
*
* @param {Object} node - The node.
* @return {string} The declaration name.
*/
getDeclarationName( node ) {
if ( node.isNodeUniform === true ) {
const type = node.type;
if ( type === 'buffer' || type === 'storageBuffer' || type === 'indirectStorageBuffer' ) {
return node.name;
}
}
return super.getDeclarationName( node );
}
/**
* Returns whether the given name is a reserved keyword of WGSL.
*
* @param {string} name - The name to test.
* @return {boolean} Whether the name is a reserved keyword or not.
*/
isReservedKeyword( name ) {
return wgslReservedKeywords.has( name );
}
/**
* Returns the output struct name.
*
* @return {string} The name of the output struct.
*/
getOutputStructName() {
return 'output';
}
/**
* Returns the native shader operator name for a given generic name.
*
* @param {string} op - The operator name to resolve.
* @return {?string} The resolved operator name.
*/
getFunctionOperator( op ) {
const fnOp = wgslFnOpLib[ op ];
if ( fnOp !== undefined ) {
this._include( fnOp );
return fnOp;
}
return null;
}
/**
* Returns the node access for the given node and shader stage.
*
* @param {StorageTextureNode|StorageBufferNode} node - The storage node.
* @param {string} shaderStage - The shader stage.
* @return {string} The node access.
*/
getNodeAccess( node, shaderStage ) {
if ( shaderStage !== 'compute' ) {
if ( node.isAtomic === true ) {
// Per the WGSL spec, atomic built-in functions are only allowed in
// the fragment and compute stages, not in the vertex stage.
if ( shaderStage === 'vertex' ) {
warn( 'WebGPURenderer: Atomic operations are not supported in the vertex stage. They are only available in the fragment and compute stages.' );
return NodeAccess.READ_ONLY;
}
return NodeAccess.READ_WRITE;
}
// Honor read-write for storage texture in the fragment stage.
if ( shaderStage === 'fragment' && node.isStorageTextureNode === true && node.access === NodeAccess.READ_WRITE ) {
return NodeAccess.READ_WRITE;
}
return NodeAccess.READ_ONLY;
}
return node.access;
}
/**
* Returns A WGSL snippet representing the storage access.
*
* @param {StorageTextureNode|StorageBufferNode} node - The storage node.
* @param {string} shaderStage - The shader stage.
* @return {string} The WGSL snippet representing the storage access.
*/
getStorageAccess( node, shaderStage ) {
return accessNames[ this.getNodeAccess( node, shaderStage ) ];
}
/**
* This method is one of the more important ones since it's responsible
* for generating a matching binding instance for the given uniform node.
*
* These bindings are later used in the renderer to create bind groups
* and layouts.
*
* @param {UniformNode} node - The uniform node.
* @param {string} type - The node data type.
* @param {string} shaderStage - The shader stage.
* @param {?string} [name=null] - An optional uniform name.
* @return {NodeUniform} The node uniform object.
*/
getUniformFromNode( node, type, shaderStage, name = null ) {
const uniformNode = super.getUniformFromNode( node, type, shaderStage, name );
const nodeData = this.getDataFromNode( node, shaderStage, this.globalCache );
if ( nodeData.uniformGPU === undefined ) {
let uniformGPU;
const group = node.groupNode;
const groupName = group.name;
const bindings = this.getBindGroupArray( groupName, shaderStage );
if ( type === 'texture' || type === 'cubeTexture' || type === 'cubeDepthTexture' || type === 'storageTexture' || type === 'texture3D' ) {
let texture = null;
const access = this.getNodeAccess( node, shaderStage );
if ( type === 'texture' || type === 'storageTexture' ) {
if ( node.value.is3DTexture === true ) {
texture = new NodeSampledTexture3D( uniformNode.name, uniformNode.node, group, access );
} else {
texture = new NodeSampledTexture( uniformNode.name, uniformNode.node, group, access );
}
} else if ( type === 'cubeTexture' || type === 'cubeDepthTexture' ) {
texture = new NodeSampledCubeTexture( uniformNode.name, uniformNode.node, group, access );
} else if ( type === 'texture3D' ) {
texture = new NodeSampledTexture3D( uniformNode.name, uniformNode.node, group, access );
}
texture.store = node.isStorageTextureNode === true;
texture.mipLevel = texture.store ? node.mipLevel : 0;
texture.setVisibility( gpuShaderStageLib[ shaderStage ] );
// Cube textures always need samplers (they use textureSampleLevel, not textureLoad)
// Also textureGather always need sampler.
const needsSampler = node.value.isCubeTexture === true || ( this.isUnfilterable( node.value ) === false && texture.store === false ) || node.gatherNode !== null;
if ( needsSampler ) {
const sampler = new NodeSampler( `${ uniformNode.name }_sampler`, uniformNode.node, group );
sampler.setVisibility( gpuShaderStageLib[ shaderStage ] );
bindings.push( sampler, texture );
uniformGPU = [ sampler, texture ];
} else {
bindings.push( texture );
uniformGPU = [ texture ];
}
} else if ( type === 'buffer' || type === 'storageBuffer' || type === 'indirectStorageBuffer' ) {
const bufferClass = type === 'buffer' ? NodeUniformBuffer : NodeStorageBuffer;
const buffer = new bufferClass( node, group );
buffer.setVisibility( gpuShaderStageLib[ shaderStage ] );
bindings.push( buffer );
uniformGPU = buffer;
} else {
let uniformsGroup = this.uniformGroups[ groupName ];
if ( uniformsGroup === undefined ) {
uniformsGroup = new NodeUniformsGroup( groupName, group );
uniformsGroup.setVisibility( GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT | GPUShaderStage.COMPUTE );
this.uniformGroups[ groupName ] = uniformsGroup;
}
// Add to bindings for this stage if not already present
if ( bindings.indexOf( uniformsGroup ) === -1 ) {
bindings.push( uniformsGroup );
}
uniformGPU = this.getNodeUniform( uniformNode, type );
// Only add uniform if not already present in the group (check by name to avoid duplicates across stages)
const uniformName = uniformGPU.name;
const alreadyExists = uniformsGroup.uniforms.some( u => u.name === uniformName );
if ( ! alreadyExists ) {
uniformsGroup.addUniform( uniformGPU );
}
}
nodeData.uniformGPU = uniformGPU;
}
return uniformNode;
}
/**
* This method should be used whenever builtins are required in nodes.
* The internal builtins data structure will make sure builtins are
* defined in the WGSL source.
*
* @param {string} name - The builtin name.
* @param {string} property - The property name.
* @param {string} type - The node data type.
* @param {string} [shaderStage=this.shaderStage] - The shader stage this code snippet is generated for.
* @return {string} The property name.
*/
getBuiltin( name, property, type, shaderStage = this.shaderStage ) {
const map = this.builtins[ shaderStage ] || ( this.builtins[ shaderStage ] = new Map() );
if ( map.has( name ) === false ) {
map.set( name, {
name,
property,
type
} );
}
return property;
}
/**
* Returns `true` if the given builtin is defined in the given shader stage.
*
* @param {string} name - The builtin name.
* @param {string} [shaderStage=this.shaderStage] - The shader stage this code snippet is generated for.
* @return {boolean} Whether the given builtin is defined in the given shader stage or not.
*/
hasBuiltin( name, shaderStage = this.shaderStage ) {
return ( this.builtins[ shaderStage ] !== undefined && this.builtins[ shaderStage ].has( name ) );
}
/**
* Returns the vertex index builtin.
*
* @return {string} The vertex index.
*/
getVertexIndex() {
if ( this.shaderStage === 'vertex' ) {
return this.getBuiltin( 'vertex_index', 'vertexIndex', 'u32', 'attribute' );
}
return 'vertexIndex';
}
/**
* Builds the given shader node.
*
* @param {ShaderNodeInternal} shaderNode - The shader node.
* @return {string} The WGSL function code.
*/
buildFunctionCode( shaderNode ) {
const layout = shaderNode.layout;
const flowData = this.flowShaderNode( shaderNode );
const parameters = [];
for ( const input of layout.inputs ) {
parameters.push( input.name + ' : ' + this.getType( input.type ) );
}
//
let code = `fn ${ layout.name }( ${ parameters.join( ', ' ) } ) -> ${ this.getType( layout.type ) } {
${ flowData.vars }
${ flowData.code }
`;
if ( flowData.result ) {
code += `\treturn ${ flowData.result };\n`;
}
code += '\n}\n';
//
return code;
}
/**
* Contextually returns either the vertex stage instance index builtin
* or the linearized index of an compute invocation within a grid of workgroups.
*
* @return {string} The instance index.
*/
getInstanceIndex() {
if ( this.shaderStage === 'vertex' ) {
return this.getBuiltin( 'instance_index', 'instanceIndex', 'u32', 'attribute' );
}
return 'instanceIndex';
}
/**
* Returns a builtin representing the index of a compute invocation within the scope of a workgroup load.
*
* @return {string} The invocation local index.
*/
getInvocationLocalIndex() {
return this.getBuiltin( 'local_invocation_index', 'invocationLocalIndex', 'u32', 'attribute' );
}
/**
* Returns a builtin representing the size of a subgroup within the current shader.
*
* @return {string} The subgroup size.
*/
getSubgroupSize() {
this.enableSubGroups();
return this.getBuiltin( 'subgroup_size', 'subgroupSize', 'u32', 'attribute' );
}
/**
* Returns a builtin representing the index of a compute invocation within the scope of a subgroup.
*
* @return {string} The invocation subgroup index.
*/
getInvocationSubgroupIndex() {
this.enableSubGroups();
return this.getBuiltin( 'subgroup_invocation_id', 'invocationSubgroupIndex', 'u32', 'attribute' );
}
/**
* Returns a builtin representing the index of a compute invocation's subgroup within its workgroup.
*
* @return {string} The subgroup index.
*/
getSubgroupIndex() {
this.enableSubGroups();
return this.getBuiltin( 'subgroup_id', 'subgroupIndex', 'u32', 'attribute' );
}
/**
* Overwritten as a NOP since this method is intended for the WebGL 2 backend.
*
* @return {null} Null.
*/
getDrawIndex() {
return null;
}
/**
* Returns the front facing builtin.
*
* @return {string} The front facing builtin.
*/
getFrontFacing() {
return this.getBuiltin( 'front_facing', 'isFront', 'bool' );
}
/**
* Returns the frag coord builtin.
*
* @return {string} The frag coord builtin.
*/
getFragCoord() {
return this.getBuiltin( 'position', 'fragCoord', 'vec4<f32>' ) + '.xy';
}
/**
* Returns the frag depth builtin.
*
* @return {string} The frag depth builtin.
*/
getFragDepth() {
return 'output.' + this.getBuiltin( 'frag_depth', 'depth', 'f32', 'output' );
}
/**
* Returns the clip distances builtin.
*
* @return {string} The clip distances builtin.
*/
getClipDistance() {
return 'varyings.hw_clip_distances';
}
/**
* Whether to flip texture data along its vertical axis or not.
*
* @return {boolean} Returns always `false` in context of WGSL.
*/
isFlipY() {
return false;
}
/**
* Enables the given directive for the given shader stage.
*
* @param {string} name - The directive name.
* @param {string} [shaderStage=this.shaderStage] - The shader stage to enable the directive for.
*/
enableDirective( name, shaderStage = this.shaderStage ) {
const stage = this.directives[ shaderStage ] || ( this.directives[ shaderStage ] = new Set() );
stage.add( name );
}
/**
* Returns the directives of the given shader stage as a WGSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} A WGSL snippet that enables the directives of the given stage.
*/
getDirectives( shaderStage ) {
const snippets = [];
const directives = this.directives[ shaderStage ];
if ( directives !== undefined ) {
for ( const directive of directives ) {
snippets.push( `enable ${directive};` );
}
}
return snippets.join( '\n' );
}
/**
* Enables subgroups.
*/
enableSubGroups() {
if ( this.renderer.hasFeature( 'subgroups' ) === false ) {
error( 'WGSLNodeBuilder: The \'subgroups\' feature is not supported by the current device.' );
}
this.enableDirective( 'subgroups' );
}
/**
* Enables the 'clip_distances' directive.
*/
enableClipDistances() {
this.enableDirective( 'clip_distances' );
}
/**
* Enables 16 bit floats.
*/
enableShaderF16() {
this.enableDirective( 'f16' );
}
/**
* Enables dual source blending.
*/
enableDualSourceBlending() {
this.enableDirective( 'dual_source_blending' );
}
/**
* Enables hardware clipping.
*
* @param {string} planeCount - The clipping plane count.
*/
enableHardwareClipping( planeCount ) {
this.enableClipDistances();
this.getBuiltin( 'clip_distances', 'hw_clip_distances', `array<f32, ${ planeCount } >`, 'vertex' );
}
/**
* Returns the builtins of the given shader stage as a WGSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} A WGSL snippet that represents the builtins of the given stage.
*/
getBuiltins( shaderStage ) {
const snippets = [];
const builtins = this.builtins[ shaderStage ];
if ( builtins !== undefined ) {
for ( const { name, property, type } of builtins.values() ) {
const invariant = this.context.invariant === true && shaderStage === 'vertex' && name === 'position' ? '@invariant ' : '';
snippets.push( `${ invariant }@builtin( ${name} ) ${property} : ${type}` );
}
}
return snippets.join( ',\n\t' );
}
/**
* This method should be used when a new scoped buffer is used in context of
* compute shaders. It adds the array to the internal data structure which is
* later used to generate the respective WGSL.
*
* @param {string} name - The array name.
* @param {string} scope - The scope.
* @param {string} bufferType - The buffer type.
* @param {string} bufferCount - The buffer count.
* @param {boolean} isAtomic - Whether the array elements are atomic or not.
* @return {string} The array name.
*/
getScopedArray( name, scope, bufferType, bufferCount, isAtomic ) {
if ( this.scopedArrays.has( name ) === false ) {
this.scopedArrays.set( name, {
name,
scope,
bufferType,
bufferCount,
isAtomic
} );
}
return name;
}
/**
* Returns the scoped arrays of the given shader stage as a WGSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string|undefined} The WGSL snippet that defines the scoped arrays.
* Returns `undefined` when used in the vertex or fragment stage.
*/
getScopedArrays( shaderStage ) {
if ( shaderStage !== 'compute' ) {
return;
}
const snippets = [];
for ( const { name, scope, bufferType, bufferCount, isAtomic } of this.scopedArrays.values() ) {
let type = this.getType( bufferType );
if ( isAtomic === true ) type = `atomic<${type}>`;
snippets.push( `var<${scope}> ${name}: array< ${type}, ${bufferCount} >;` );
}
return snippets.join( '\n' );
}
/**
* Returns the shader attributes of the given shader stage as a WGSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} The WGSL snippet that defines the shader attributes.
*/
getAttributes( shaderStage ) {
const snippets = [];
if ( shaderStage === 'compute' ) {
this.getBuiltin( 'global_invocation_id', 'globalId', 'vec3<u32>', 'attribute' );
this.getBuiltin( 'workgroup_id', 'workgroupId', 'vec3<u32>', 'attribute' );
this.getBuiltin( 'local_invocation_id', 'localId', 'vec3<u32>', 'attribute' );
this.getBuiltin( 'num_workgroups', 'numWorkgroups', 'vec3<u32>', 'attribute' );
if ( this.renderer.hasFeature( 'subgroups' ) ) {
this.enableDirective( 'subgroups', shaderStage );
this.getBuiltin( 'subgroup_size', 'subgroupSize', 'u32', 'attribute' );
}
}
if ( shaderStage === 'vertex' || shaderStage === 'compute' ) {
const builtins = this.getBuiltins( 'attribute' );
if ( builtins ) snippets.push( builtins );
const attributes = this.getAttributesArray();
for ( let index = 0, length = attributes.length; index < length; index ++ ) {
const attribute = attributes[ index ];
const name = attribute.name;
const type = this.getType( attribute.type );
snippets.push( `@location( ${index} ) ${ name } : ${ type }` );
}
}
return snippets.join( ',\n\t' );
}
/**
* Returns the members of the given struct type node as a WGSL string.
*
* @param {StructTypeNode} struct - The struct type node.
* @return {string} The WGSL snippet that defines the struct members.
*/
getStructMembers( struct ) {
const snippets = [];
for ( const member of struct.members ) {
const prefix = struct.output ? '@location( ' + member.index + ' ) ' : '';
let type = this.getType( member.type );
if ( member.atomic ) {
type = 'atomic< ' + type + ' >';
}
snippets.push( `\t${ prefix + member.name } : ${ type }` );
}
if ( struct.output ) {
snippets.push( `\t${ this.getBuiltins( 'output' ) }` );
}
return snippets.join( ',\n' );
}
/**
* Returns the structs of the given shader stage as a WGSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} The WGSL snippet that defines the structs.
*/
getStructs( shaderStage ) {
let result = '';
const structs = this.structs[ shaderStage ];
if ( structs.length > 0 ) {
const snippets = [];
for ( const struct of structs ) {
let snippet = `struct ${ struct.name } {\n`;
snippet += this.getStructMembers( struct );
snippet += '\n};';
snippets.push( snippet );
}
result = '\n' + snippets.join( '\n\n' ) + '\n';
}
return result;
}
/**
* Returns a WGSL string representing a variable.
*
* @param {string} type - The variable's type.
* @param {string} name - The variable's name.
* @param {?number} [count=null] - The array length.
* @param {string} [qualifier=''] - The variable's qualifier.
* @return {string} The WGSL snippet that defines a variable.
*/
getVar( type, name, count = null, qualifier = '' ) {
let snippet = `var${ qualifier } ${ name } : `;
if ( count !== null ) {
snippet += this.generateArrayDeclaration( type, count );
} else {
snippet += this.getType( type );
}
return snippet;
}
/**
* Returns a single const variable statement as a WGSL string for the given variable type and name.
*
* @param {string} type - The variable's type.
* @param {string} name - The variable's name.
* @param {?number} [count=null] - The array length.
* @return {string} The WGSL snippet that defines a const variable.
*/
generateConstStatement( type, name/*, count = null*/ ) {
return `const ${ name }`;
}
/**
* Returns a single variable statement as a WGSL string for the given variable type and name.
*
* @param {string} type - The variable's type.
* @param {string} name - The variable's name.
* @param {?number} [count=null] - The array length.
* @return {string} The WGSL snippet that defines a variable.
*/
generateVarStatement( type, name, count = null ) {
return this.getVar( type, name, count );
}
/**
* Returns a runtime read-only variable statement as a WGSL string.
*
* @param {string} type - The variable's type.
* @param {string} name - The variable's name.
* @param {?number} [count=null] - The array length.
* @return {string} The WGSL snippet that defines a let variable.
*/
generateLetStatement( type, name/*, count = null*/ ) {
return `let ${ name }`;
}
/**
* Returns the variables of the given shader stage as a WGSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} The WGSL snippet that defines the variables.
*/
getVars( shaderStage, global = false ) {
let qualifier = '';
if ( global ) {
qualifier = '<private>';
}
const snippets = [];
const vars = this.vars[ shaderStage ];
if ( vars !== undefined ) {
for ( const variable of vars ) {
snippets.push( `${ this.getVar( variable.type, variable.name, variable.count, qualifier ) };` );
}
}
return global ? snippets.join( '\n' ) : `\n\t${ snippets.join( '\n\t' ) }\n`;
}
/**
* Returns the varyings of the given shader stage as a WGSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} The WGSL snippet that defines the varyings.
*/
getVaryings( shaderStage ) {
const snippets = [];
if ( shaderStage === 'vertex' ) {
this.getBuiltin( 'position', 'builtinClipSpace', 'vec4<f32>', 'vertex' );
}
if ( shaderStage === 'vertex' || shaderStage === 'fragment' ) {
const varyings = this.varyings;
const vars = this.vars[ shaderStage ];
let varyingIndex = 0;
for ( let index = 0; index < varyings.length; index ++ ) {
const varying = varyings[ index ];
if ( varying.needsInterpolation ) {
let attributesSnippet = `@location( ${ varyingIndex ++ } )`;
if ( varying.interpolationType ) {
const samplingSnippet = varying.interpolationSampling !== null ? `, ${ varying.interpolationSampling } )` : ' )';
attributesSnippet += ` @interpolate( ${ varying.interpolationType }${ samplingSnippet }`;
// Otherwise, optimize interpolation when sensible
} else if ( /^(int|uint|ivec|uvec)/.test( varying.type ) ) {
attributesSnippet += ' @interpolate(flat, either)';
}
snippets.push( `${ attributesSnippet } ${ varying.name } : ${ this.getType( varying.type ) }` );
} else if ( shaderStage === 'vertex' && vars.includes( varying ) === false ) {
vars.push( varying );
}
}
}
const builtins = this.getBuiltins( shaderStage );
if ( builtins ) snippets.push( builtins );
const code = snippets.join( ',\n\t' );
return shaderStage === 'vertex' ? this._getWGSLStruct( 'VaryingsStruct', '\t' + code ) : code;
}
/**
* Returns the uniforms of the given shader stage as a WGSL string.
*
* @param {string} shaderStage - The shader stage.
* @return {string} The WGSL snippet that defines the uniforms.
*/
getUniforms( shaderStage ) {
const backend = this.renderer.backend;
const uniforms = this.uniforms[ shaderStage ];
const bindingSnippets = [];
const bufferSnippets = [];
const structSnippets = [];
const uniformGroups = {};
for ( const uniform of uniforms ) {
const groupName = uniform.groupNode.name;
const uniformIndexes = this.bindingsIndexes[ groupName ];
if ( uniform.type === 'texture' || uniform.type === 'cubeTexture' || uniform.type === 'cubeDepthTexture' || uniform.type === 'storageTexture' || uniform.type === 'texture3D' ) {
const textureNode = uniform.node;
const texture = textureNode.value;
// Cube textures always need samplers (they use textureSampleLevel, not textureLoad)
// Also textureGather always need sampler.
const needsSampler = texture.isCubeTexture === true || ( this.isUnfilterable( texture ) === false && textureNode.isStorageTextureNode !== true ) || textureNode.gatherNode !== null;
if ( needsSampler ) {
if ( this.isSampleCompare( texture ) && textureNode.isSampleCompare() ) {
bindingSnippets.push( `@binding( ${ uniformIndexes.binding ++ } ) @group( ${ uniformIndexes.group } ) var ${ uniform.name }_sampler : sampler_comparison;` );
} else {
bindingSnippets.push( `@binding( ${ uniformIndexes.binding ++ } ) @group( ${ uniformIndexes.group } ) var ${ uniform.name }_sampler : sampler;` );
}
}
let textureType;
let multisampled = '';
const { primarySamples } = backend.utils.getTextureSampleData( texture );
if ( primarySamples > 1 ) {
multisampled = '_multisampled';
}
if ( texture.isCubeTexture === true && texture.isDepthTexture === true ) {
textureType = 'texture_depth_cube';
} else if ( texture.isCubeTexture === true ) {
textureType = 'texture_cube<f32>';
} else if ( texture.isDepthTexture === true ) {
if ( backend.compatibilityMode && texture.compareFunction === null ) {
textureType = `texture${ multisampled }_2d<f32>`;
} else {
textureType = `texture_depth${ multisampled }_2d${ texture.isArrayTexture === true ? '_array' : '' }`;
}
} else if ( uniform.node.isStorageTextureNode === true ) {
const format = getFormat( texture, backend.device );
const access = this.getStorageAccess( uniform.node, shaderStage );
const is3D = uniform.node.value.is3DTexture;
const isArrayTexture = uniform.node.value.isArrayTexture;
const dimension = is3D ? '3d' : `2d${ isArrayTexture ? '_array' : '' }`;
textureType = `texture_storage_${ dimension }<${ format }, ${ access }>`;
} else if ( texture.isArrayTexture === true || texture.isDataArrayTexture === true || texture.isCompressedArrayTexture === true ) {
const componentPrefix = this.getComponentTypeFromTexture( texture ).charAt( 0 );
textureType = `texture_2d_array<${ componentPrefix }32>`;
} else if ( texture.is3DTexture === true || texture.isData3DTexture === true ) {
const componentPrefix = this.getComponentTypeFromTexture( texture ).charAt( 0 );
textureType = `texture_3d<${ componentPrefix }32>`;
} else {
const componentPrefix = this.getComponentTypeFromTexture( texture ).charAt( 0 );
textureType = `texture${ multisampled }_2d<${ componentPrefix }32>`;
}
bindingSnippets.push( `@binding( ${ uniformIndexes.binding ++ } ) @group( ${ uniformIndexes.group } ) var ${ uniform.name } : ${ textureType };` );
} else if ( uniform.type === 'buffer' || uniform.type === 'storageBuffer' || uniform.type === 'indirectStorageBuffer' ) {
const bufferNode = uniform.node;
const bufferType = this.getType( bufferNode.getNodeType( this ) );
const bufferCount = bufferNode.bufferCount;
const bufferCountSnippet = bufferCount > 0 && uniform.type === 'buffer' ? ', ' + bufferCount : '';
const bufferAccessMode = bufferNode.isStorageBufferNode ? `storage, ${ this.getStorageAccess( bufferNode, shaderStage ) }` : 'uniform';
const bufferTypeSnippet = bufferNode.isAtomic ? `atomic<${ bufferType }>` : `${ bufferType }`;
const bufferSnippet = `\tvalue : array< ${ bufferTypeSnippet }${ bufferCountSnippet } >`;
bufferSnippets.push( this._getWGSLStructBinding( uniform.name, bufferSnippet, bufferAccessMode, uniformIndexes.binding ++, uniformIndexes.group ) );
} else {
const groupName = uniform.groupNode.name;
// Check if this group has already been processed in this shader stage
if ( uniformGroups[ groupName ] === undefined ) {
// Get the shared uniform group that contains uniforms from all stages
const sharedUniformGroup = this.uniformGroups[ groupName ];
if ( sharedUniformGroup !== undefined ) {
// Generate snippets for ALL uniforms in this shared group
const snippets = [];
for ( const sharedUniform of sharedUniformGroup.uniforms ) {
const type = sharedUniform.getType();
const vectorType = this.getType( this.getVectorType( type ) );
snippets.push( `\t${ sharedUniform.name } : ${ vectorType }` );
}
// Check if this group already has an assigned binding index (from another shader stage)
let groupBinding = this.uniformGroupsBindings[ groupName ];
if ( groupBinding === undefined ) {
// First time processing this group - assign a new binding index
groupBinding = {
index: uniformIndexes.binding ++,
id: uniformIndexes.group
};
this.uniformGroupsBindings[ groupName ] = groupBinding;
}
uniformGroups[ groupName ] = {
index: groupBinding.index,
id: groupBinding.id,
snippets: snippets
};
}
}
}
}
for ( const name in uniformGroups ) {
const group = uniformGroups[ name ];
structSnippets.push( this._getWGSLStructBinding( name, group.snippets.join( ',\n' ), 'uniform', group.index, group.id ) );
}
const code = [ ...bindingSnippets, ...bufferSnippets, ...structSnippets ].join( '\n' );
return code;
}
/**
* Controls the code build of the shader stages.
*/
buildCode() {
const shadersData = this.material !== null ? { fragment: {}, vertex: {} } : { compute: {} };
this.sortBindingGroups();
for ( const shaderStage in shadersData ) {
this.shaderStage = shaderStage;
const allowGlobal = this.allowGlobalVariables;
const stageData = shadersData[ shaderStage ];
stageData.uniforms = this.getUniforms( shaderStage );
stageData.attributes = this.getAttributes( shaderStage );
stageData.varyings = this.getVaryings( shaderStage );
stageData.structs = this.getStructs( shaderStage );
stageData.vars = this.getVars( shaderStage, allowGlobal );
stageData.codes = this.getCodes( shaderStage );
stageData.directives = this.getDirectives( shaderStage );
stageData.scopedArrays = this.getScopedArrays( shaderStage );
//
let flow = '// code\n\n';
flow += this.flowCode[ shaderStage ];
const flowNodes = this.flowNodes[ shaderStage ];
const mainNode = flowNodes[ flowNodes.length - 1 ];
const outputNode = mainNode.outputNode;
const isOutputStruct = ( outputNode !== undefined && outputNode.isOutputStructNode === true );
for ( const node of flowNodes ) {
const flowSlotData = this.getFlowData( node/*, shaderStage*/ );
const slotName = node.name;
if ( slotName ) {
if ( flow.length > 0 ) flow += '\n';
flow += `\t// flow -> ${ slotName }\n`;
}
flow += `${ flowSlotData.code }\n\t`;
if ( node === mainNode && shaderStage !== 'compute' ) {
flow += '// result\n\n\t';
if ( shaderStage === 'vertex' ) {
flow += `varyings.builtinClipSpace = ${ flowSlotData.result };`;
} else if ( shaderStage === 'fragment' ) {
if ( isOutputStruct ) {
stageData.returnType = outputNode.getNodeType( this );
stageData.structs += 'var<private> output : ' + stageData.returnType + ';';
flow += `return ${ flowSlotData.result };`;
} else {
let structSnippet = `\t@location( 0 ) color: ${ this.getType( this.getOutputType() ) }`;
const builtins = this.getBuiltins( 'output' );
if ( builtins ) structSnippet += ',\n\t' + builtins;
stageData.returnType = 'OutputStruct';
stageData.structs += this._getWGSLStruct( 'OutputStruct', structSnippet );
stageData.structs += '\nvar<private> output : OutputStruct;';
flow += `output.color = ${ this.format( flowSlotData.result, mainNode.getNodeType( this ), this.getOutputType() ) };\n\n\treturn output;`;
}
}
}
}
stageData.flow = flow;
}
this.shaderStage = null;
if ( this.material !== null ) {
this.vertexShader = this._getWGSLVertexCode( shadersData.vertex );
this.fragmentShader = this._getWGSLFragmentCode( shadersData.fragment );
} else {
// Early strictly validated in computeNode
const workgroupSize = this.compute.workgroupSize;
this.computeShader = this._getWGSLComputeCode( shadersData.compute, workgroupSize );
}
}
/**
* Returns the native shader method name for a given generic name.
*
* @param {string} method - The method name to resolve.
* @param {?string} [output=null] - An optional output.
* @return {string} The resolved WGSL method name.
*/
getMethod( method, output = null ) {
let wgslMethod;
if ( output !== null ) {
wgslMethod = this._getWGSLMethod( method + '_' + output );
}
if ( wgslMethod === undefined ) {
wgslMethod = this._getWGSLMethod( method );
}
return wgslMethod || method;
}
/**
* Returns the bitcast method name for a given input and outputType.
*
* @param {string} type - The output type to bitcast to.
* @return {string} The resolved WGSL bitcast invocation.
*/
getBitcastMethod( type ) {
const dataType = this.getType( type );
return `bitcast<${ dataType }>`;
}
/**
* Returns the float packing method name for a given numeric encoding.
*
* @param {string} encoding - The numeric encoding that describes how the float values are mapped to the integer range.
* @param {string} [layout='2x16'] - The component layout of the packed integer.
* @returns {string} The resolve WGSL float packing method name.
*/
getFloatPackingMethod( encoding, layout = '2x16' ) {
return this.getMethod( `floatpack_${ encoding }_${ layout }` );
}
/**
* Returns the float unpacking method name for a given numeric encoding.
*
* @param {string} encoding - The numeric encoding that describes how the integer values are mapped to the float range.
* @param {string} [layout='2x16'] - The component layout of the packed integer.
* @returns {string} The resolve WGSL float unpacking method name.
*/
getFloatUnpackingMethod( encoding, layout = '2x16' ) {
return this.getMethod( `floatunpack_${ encoding }_${ layout }` );
}
/**
* Returns the native snippet for a ternary operation.
*
* @param {string} condSnippet - The condition determining which expression gets resolved.
* @param {string} ifSnippet - The expression to resolve to if the condition is true.
* @param {string} elseSnippet - The expression to resolve to if the condition is false.
* @return {string} The resolved method name.
*/
getTernary( condSnippet, ifSnippet, elseSnippet ) {
return `select( ${elseSnippet}, ${ifSnippet}, ${condSnippet} )`;
}
/**
* Returns the WGSL type of the given node data type.
*
* @param {string} type - The node data type.
* @return {string} The WGSL type.
*/
getType( type ) {
return wgslTypeLib[ type ] || type;
}
/**
* Whether the requested feature is available or not.
*
* @param {string} name - The requested feature.
* @return {boolean} Whether the requested feature is supported or not.
*/
isAvailable( name ) {
let result = supports[ name ];
if ( result === undefined ) {
if ( name === 'float32Filterable' ) {
result = this.renderer.hasFeature( 'float32-filterable' );
} else if ( name === 'clipDistance' ) {
result = this.renderer.hasFeature( 'clip-distances' );
}
supports[ name ] = result;
}
return result;
}
/**
* Returns the native shader method name for a given generic name.
*
* @private
* @param {string} method - The method name to resolve.
* @return {string} The resolved WGSL method name.
*/
_getWGSLMethod( method ) {
if ( wgslPolyfill[ method ] !== undefined ) {
this._include( method );
}
return wgslMethods[ method ];
}
/**
* Includes the given method name into the current
* function node.
*
* @private
* @param {string} name - The method name to include.
* @return {CodeNode} The respective code node.
*/
_include( name ) {
const codeNode = wgslPolyfill[ name ];
codeNode.build( this );
this.addInclude( codeNode );
return codeNode;
}
/**
* Returns a WGSL vertex shader based on the given shader data.
*
* @private
* @param {Object} shaderData - The shader data.
* @return {string} The vertex shader.
*/
_getWGSLVertexCode( shaderData ) {
return `${ this.getSignature() }
// directives
${shaderData.directives}
// structs
${shaderData.structs}
// uniforms
${shaderData.uniforms}
// varyings
${shaderData.varyings}
var<private> varyings : VaryingsStruct;
// vars
${shaderData.vars}
// codes
${shaderData.codes}
@vertex
fn main( ${shaderData.attributes} ) -> VaryingsStruct {
// flow
${shaderData.flow}
return varyings;
}
`;
}
/**
* Returns a WGSL fragment shader based on the given shader data.
*
* @private
* @param {Object} shaderData - The shader data.
* @return {string} The vertex shader.
*/
_getWGSLFragmentCode( shaderData ) {
return `${ this.getSignature() }
// global
${ diagnostics }
// directives
${shaderData.directives}
// structs
${shaderData.structs}
// uniforms
${shaderData.uniforms}
// vars
${shaderData.vars}
// codes
${shaderData.codes}
@fragment
fn main( ${shaderData.varyings} ) -> ${shaderData.returnType} {
// flow
${shaderData.flow}
}
`;
}
/**
* Returns a WGSL compute shader based on the given shader data.
*
* @private
* @param {Object} shaderData - The shader data.
* @param {string} workgroupSize - The workgroup size.
* @return {string} The vertex shader.
*/
_getWGSLComputeCode( shaderData, workgroupSize ) {
const [ workgroupSizeX, workgroupSizeY, workgroupSizeZ ] = workgroupSize;
return `${ this.getSignature() }
// directives
${ shaderData.directives }
// system
var<private> instanceIndex : u32;
// locals
${ shaderData.scopedArrays }
// structs
${ shaderData.structs }
// uniforms
${ shaderData.uniforms }
// vars
${ this.allowGlobalVariables ? shaderData.vars : '' }
// codes
${ shaderData.codes }
@compute @workgroup_size( ${ workgroupSizeX }, ${ workgroupSizeY }, ${ workgroupSizeZ } )
fn main( ${ shaderData.attributes } ) {
// local vars
${ this.allowGlobalVariables ? '' : shaderData.vars }
// system
instanceIndex = globalId.x
+ globalId.y * ( ${ workgroupSizeX } * numWorkgroups.x )
+ globalId.z * ( ${ workgroupSizeX } * numWorkgroups.x ) * ( ${ workgroupSizeY } * numWorkgroups.y );
// flow
${ shaderData.flow }
}
`;
}
/**
* Returns a WGSL struct based on the given name and variables.
*
* @private
* @param {string} name - The struct name.
* @param {string} vars - The struct variables.
* @return {string} The WGSL snippet representing a struct.
*/
_getWGSLStruct( name, vars ) {
return `
struct ${name} {
${vars}
};`;
}
/**
* Returns a WGSL struct binding.
*
* @private
* @param {string} name - The struct name.
* @param {string} vars - The struct variables.
* @param {string} access - The access.
* @param {number} [binding=0] - The binding index.
* @param {number} [group=0] - The group index.
* @return {string} The WGSL snippet representing a struct binding.
*/
_getWGSLStructBinding( name, vars, access, binding = 0, group = 0 ) {
const structName = name + 'Struct';
const structSnippet = this._getWGSLStruct( structName, vars );
return `${structSnippet}
@binding( ${ binding } ) @group( ${ group } )
var<${access}> ${ name } : ${ structName };`;
}
}
const _bufferDescriptor$3 = new GPUBufferDescriptor();
const _commandEncoderDescriptor$2 = new GPUCommandEncoderDescriptor();
const typedArraysToVertexFormatPrefix = new Map( [
[ Int8Array, [ 'sint8', 'snorm8' ]],
[ Uint8Array, [ 'uint8', 'unorm8' ]],
[ Int16Array, [ 'sint16', 'snorm16' ]],
[ Uint16Array, [ 'uint16', 'unorm16' ]],
[ Int32Array, [ 'sint32' ]],
[ Uint32Array, [ 'uint32' ]],
[ Float32Array, [ 'float32', ]],
] );
if ( typeof Float16Array !== 'undefined' ) {
typedArraysToVertexFormatPrefix.set( Float16Array, [ 'float16' ] );
}
const typedAttributeToVertexFormatPrefix = new Map( [
[ Float16BufferAttribute, [ 'float16', ]],
] );
const typeArraysToVertexFormatPrefixForItemSize1 = new Map( [
[ Int32Array, 'sint32' ],
[ Uint32Array, 'uint32' ],
[ Float32Array, 'float32' ]
] );
const _tail = new Uint8Array( 4 );
/**
* Writes a range of a typed array into the given GPU buffer. `writeBuffer()` requires
* the offset and size to be multiples of 4 bytes, so the range is expanded to the
* element boundaries of the surrounding 4-byte word. The function assumes
* that the buffer is already properly sized to a multiple of 4 bytes.
*
* @private
* @function
* @param {GPUDevice} device - The GPU device calling `writeBuffer()`.
* @param {GPUBuffer} buffer - The GPU buffer, properly sized to a multiple of 4 bytes.
* @param {TypedArray} array - The source array.
* @param {number} start - The index of the first element to write.
* @param {number} count - The number of elements to write.
*/
function writeBufferAligned( device, buffer, array, start, count ) {
const bytes = new Uint8Array( array.buffer, array.byteOffset, array.byteLength );
const byteStart = Math.floor( start * array.BYTES_PER_ELEMENT / 4 ) * 4;
const byteEnd = ( start + count ) * array.BYTES_PER_ELEMENT;
const alignedEnd = Math.floor( byteEnd / 4 ) * 4;
if ( alignedEnd > byteStart ) device.queue.writeBuffer( buffer, byteStart, bytes, byteStart, alignedEnd - byteStart );
if ( byteEnd > alignedEnd ) {
_tail.fill( 0 );
_tail.set( bytes.subarray( alignedEnd, alignedEnd + 4 ) );
device.queue.writeBuffer( buffer, alignedEnd, _tail );
}
}
/**
* A WebGPU backend utility module for managing shader attributes.
*
* @private
*/
class WebGPUAttributeUtils {
/**
* Constructs a new utility object.
*
* @param {WebGPUBackend} backend - The WebGPU backend.
*/
constructor( backend ) {
/**
* A reference to the WebGPU backend.
*
* @type {WebGPUBackend}
*/
this.backend = backend;
}
/**
* Creates the GPU buffer for the given buffer attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
* @param {GPUBufferUsage} usage - A flag that indicates how the buffer may be used after its creation.
*/
createAttribute( attribute, usage ) {
const backend = this.backend;
const bufferAttribute = backend.getBufferAttribute( attribute );
const bufferData = backend.get( bufferAttribute );
let buffer = bufferData.buffer;
if ( buffer === undefined ) {
const device = backend.device;
let array = bufferAttribute.array;
// WGSL has no 8 or 16 bit integer types and vertex strides must be a multiple of 4 bytes,
// so widen these integers to 32 bit. index buffers only need uint16 unless used as storage
if ( attribute.normalized === false && attribute.isInterleavedBufferAttribute !== true ) {
if ( array.constructor === Int16Array || array.constructor === Int8Array ) {
array = new Int32Array( array );
} else if ( array.constructor === Uint16Array || array.constructor === Uint8Array ) {
const isIndexBuffer = ( usage & GPUBufferUsage.INDEX );
const isStorageBuffer = ( usage & GPUBufferUsage.STORAGE );
const UintConstructor = ( isIndexBuffer && ! isStorageBuffer ) ? Uint16Array : Uint32Array;
if ( array.constructor !== UintConstructor ) {
array = new UintConstructor( array );
}
// widened indices need the uint32 primitive restart value
if ( isIndexBuffer && isStorageBuffer ) {
for ( let i = 0; i < array.length; i ++ ) {
if ( array[ i ] === 0xffff ) array[ i ] = 0xffffffff;
}
}
}
}
bufferAttribute.array = array;
let paddedItemSize;
if ( ( bufferAttribute.isStorageBufferAttribute || bufferAttribute.isStorageInstancedBufferAttribute ) && bufferAttribute.itemSize === 3 ) {
// WGSL does not support packed vec3 data in storage buffers, pad to vec4
paddedItemSize = 4;
} else if ( bufferAttribute.itemSize > 1 && ( bufferAttribute.itemSize * array.BYTES_PER_ELEMENT ) % 4 !== 0 ) {
// arrayStride must be a multiple of 4
const byteStride = bufferAttribute.itemSize * array.BYTES_PER_ELEMENT;
paddedItemSize = ( Math.floor( ( byteStride + 3 ) / 4 ) * 4 ) / array.BYTES_PER_ELEMENT;
}
if ( paddedItemSize !== undefined ) {
const itemSize = bufferAttribute.itemSize;
const paddedArray = new array.constructor( bufferAttribute.count * paddedItemSize );
for ( let i = 0; i < bufferAttribute.count; i ++ ) {
paddedArray.set( array.subarray( i * itemSize, i * itemSize + itemSize ), i * paddedItemSize );
}
if ( bufferAttribute.isStorageBufferAttribute || bufferAttribute.isStorageInstancedBufferAttribute ) {
// update the storage attribute so storage bindings access the padded layout
bufferAttribute.itemSize = paddedItemSize;
bufferAttribute.array = paddedArray;
}
array = paddedArray;
// save the original and padded item size so buffer updates can apply the same padding
bufferData._itemSize = itemSize;
bufferData._paddedItemSize = paddedItemSize;
}
// total buffer size must be a multiple of 4
const byteLength = array.byteLength;
const size = byteLength + ( ( 4 - ( byteLength % 4 ) ) % 4 );
_bufferDescriptor$3.label = bufferAttribute.name;
_bufferDescriptor$3.size = size;
_bufferDescriptor$3.usage = usage;
_bufferDescriptor$3.mappedAtCreation = true;
buffer = device.createBuffer( _bufferDescriptor$3 );
_bufferDescriptor$3.reset();
new array.constructor( buffer.getMappedRange() ).set( array );
buffer.unmap();
bufferData.buffer = buffer;
}
}
/**
* Updates the GPU buffer of the given buffer attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
updateAttribute( attribute ) {
const backend = this.backend;
const device = backend.device;
const bufferAttribute = backend.getBufferAttribute( attribute );
const bufferData = backend.get( bufferAttribute );
const buffer = backend.get( bufferAttribute ).buffer;
let array = bufferAttribute.array;
const itemSize = bufferData._itemSize;
const paddedItemSize = bufferData._paddedItemSize;
if ( paddedItemSize !== undefined ) {
// if the attribute data were padded on upload, apply the same padding on updates.
array = new array.constructor( bufferAttribute.count * paddedItemSize );
for ( let i = 0; i < bufferAttribute.count; i ++ ) {
array.set( bufferAttribute.array.subarray( i * itemSize, i * itemSize + itemSize ), i * paddedItemSize );
}
if ( bufferAttribute.isStorageBufferAttribute || bufferAttribute.isStorageInstancedBufferAttribute ) {
// keep the storage attribute in sync with the padded layout
bufferAttribute.array = array;
}
}
const updateRanges = bufferAttribute.updateRanges;
const needsAlignment = isTypedArray( array ) && array.BYTES_PER_ELEMENT < 4;
if ( updateRanges.length === 0 ) {
// Not using update ranges
if ( needsAlignment ) {
writeBufferAligned( device, buffer, array, 0, array.length );
} else {
device.queue.writeBuffer( buffer, 0, array, 0 );
}
} else {
const isTyped = isTypedArray( array );
const byteOffsetFactor = isTyped ? 1 : array.BYTES_PER_ELEMENT;
for ( let i = 0, l = updateRanges.length; i < l; i ++ ) {
const range = updateRanges[ i ];
let dataOffset, size;
if ( paddedItemSize !== undefined ) {
const vertexStart = Math.floor( range.start / itemSize );
const vertexCount = Math.ceil( ( range.start + range.count ) / itemSize ) - vertexStart;
dataOffset = vertexStart * paddedItemSize * byteOffsetFactor;
size = vertexCount * paddedItemSize * byteOffsetFactor;
} else {
dataOffset = range.start * byteOffsetFactor;
size = range.count * byteOffsetFactor;
}
if ( needsAlignment ) {
writeBufferAligned( device, buffer, array, dataOffset, size );
} else {
const bufferOffset = dataOffset * ( isTyped ? array.BYTES_PER_ELEMENT : 1 ); // bufferOffset is always in bytes
device.queue.writeBuffer( buffer, bufferOffset, array, dataOffset, size );
}
}
bufferAttribute.clearUpdateRanges();
}
}
/**
* Returns the vertex buffer layout data which are required
* when creating a render pipeline for the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @return {Array<Object>} An array holding objects which describe the vertex buffer layout.
*/
getVertexBufferLayout( renderObject ) {
const attributes = renderObject.getAttributes();
const vertexBuffers = new Map();
for ( let slot = 0; slot < attributes.length; slot ++ ) {
const geometryAttribute = attributes[ slot ];
const bytesPerElement = geometryAttribute.array.BYTES_PER_ELEMENT;
const bufferAttribute = this.backend.getBufferAttribute( geometryAttribute );
let vertexBufferLayout = vertexBuffers.get( bufferAttribute );
if ( vertexBufferLayout === undefined ) {
let arrayStride, stepMode;
// the layout is defined by the buffer, interleaved attributes can also view a buffer attribute
if ( bufferAttribute.isInterleavedBuffer === true ) {
arrayStride = bufferAttribute.stride * bytesPerElement;
stepMode = bufferAttribute.isInstancedInterleavedBuffer ? GPUInputStepMode.Instance : GPUInputStepMode.Vertex;
} else {
arrayStride = bufferAttribute.itemSize * bytesPerElement;
stepMode = bufferAttribute.isInstancedBufferAttribute ? GPUInputStepMode.Instance : GPUInputStepMode.Vertex;
if ( bufferAttribute.itemSize > 1 && arrayStride % 4 !== 0 ) {
// packed attribute data are padded per vertex on upload
arrayStride = Math.floor( ( arrayStride + 3 ) / 4 ) * 4;
}
}
vertexBufferLayout = {
arrayStride,
attributes: [],
stepMode
};
vertexBuffers.set( bufferAttribute, vertexBufferLayout );
}
const format = this._getVertexFormat( geometryAttribute );
const offset = ( geometryAttribute.isInterleavedBufferAttribute === true ) ? geometryAttribute.offset * bytesPerElement : 0;
vertexBufferLayout.attributes.push( {
shaderLocation: slot,
offset,
format
} );
}
return Array.from( vertexBuffers.values() );
}
/**
* Returns a cache key that represents the vertex buffer layout data
* of the given render object.
*
* @param {RenderObject} renderObject - The render object.
* @return {string} The cache key.
*/
getVertexBufferLayoutCacheKey( renderObject ) {
const vertexBufferLayout = this.getVertexBufferLayout( renderObject );
let cacheKey = '';
for ( const entry of vertexBufferLayout ) {
cacheKey += entry.arrayStride + ',' + entry.stepMode + ',';
for ( const attribute of entry.attributes ) {
cacheKey += attribute.shaderLocation + ',' + attribute.offset + ',' + attribute.format + ',';
}
}
return cacheKey;
}
/**
* Destroys the GPU buffer of the given buffer attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
destroyAttribute( attribute ) {
const backend = this.backend;
const bufferAttribute = backend.getBufferAttribute( attribute );
const data = backend.get( bufferAttribute );
data.buffer.destroy();
backend.delete( bufferAttribute );
}
/**
* This method performs a readback operation by moving buffer data from
* a storage buffer attribute from the GPU to the CPU. ReadbackBuffer can
* be used to retain and reuse handles to the intermediate buffers and prevent
* new allocation.
*
* @async
* @param {BufferAttribute} attribute - The storage buffer attribute to read from.
* @param {?(ReadbackBuffer|ArrayBuffer)} [target=null] - The readback buffer or array buffer that receives the data. If `null`, a new array buffer is returned.
* @param {number} [offset=0] - The byte offset to start reading from. Must be a multiple of 4.
* @param {number} [count=-1] - The number of bytes to read. Must be a multiple of 4, or `-1` to read to the end of the buffer.
* @return {Promise<ArrayBuffer|ReadbackBuffer>} A promise that resolves with the buffer data when the data are ready.
*/
async getArrayBufferAsync( attribute, target = null, offset = 0, count = -1 ) {
const backend = this.backend;
const device = backend.device;
const data = backend.get( backend.getBufferAttribute( attribute ) );
const bufferGPU = data.buffer;
const byteLength = count === -1 ? bufferGPU.size - offset : count;
let readBufferGPU;
if ( target !== null && target.isReadbackBuffer ) {
const readbackInfo = backend.get( target );
if ( target._mapped === true ) {
throw new Error( 'THREE.WebGPUAttributeUtils: ReadbackBuffer must be released before being used again.' );
}
target._mapped = true;
// initialize the GPU-side read copy buffer if it is not present
if ( readbackInfo.readBufferGPU === undefined ) {
_bufferDescriptor$3.label = `${ target.name }_readback`;
_bufferDescriptor$3.size = target.maxByteLength;
_bufferDescriptor$3.usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ;
readBufferGPU = device.createBuffer( _bufferDescriptor$3 );
_bufferDescriptor$3.reset();
// release / dispose
const releaseCallback = () => {
target.buffer = null;
target._mapped = false;
readBufferGPU.unmap();
};
const disposeCallback = () => {
target.buffer = null;
target._mapped = false;
readBufferGPU.destroy();
backend.delete( target );
target.removeEventListener( 'release', releaseCallback );
target.removeEventListener( 'dispose', disposeCallback );
};
target.addEventListener( 'release', releaseCallback );
target.addEventListener( 'dispose', disposeCallback );
// register
readbackInfo.readBufferGPU = readBufferGPU;
} else {
readBufferGPU = readbackInfo.readBufferGPU;
}
} else {
// create a new temp buffer for array buffers otherwise
_bufferDescriptor$3.label = `${ attribute.name }_readback`;
_bufferDescriptor$3.size = byteLength;
_bufferDescriptor$3.usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ;
readBufferGPU = device.createBuffer( _bufferDescriptor$3 );
_bufferDescriptor$3.reset();
}
// copy the data
_commandEncoderDescriptor$2.label = `readback_encoder_${ attribute.name }`;
const cmdEncoder = device.createCommandEncoder( _commandEncoderDescriptor$2 );
_commandEncoderDescriptor$2.reset();
cmdEncoder.copyBufferToBuffer(
bufferGPU,
offset,
readBufferGPU,
0,
byteLength,
);
const gpuCommands = cmdEncoder.finish();
submit( device, gpuCommands );
// map the data to the CPU
await readBufferGPU.mapAsync( GPUMapMode.READ, 0, byteLength );
if ( target === null ) {
// return a new array buffer and clean up the gpu handles
const arrayBuffer = readBufferGPU.getMappedRange( 0, byteLength );
const result = arrayBuffer.slice();
readBufferGPU.destroy();
return result;
} else if ( target.isReadbackBuffer ) {
// assign the data to the read back handle
target.buffer = readBufferGPU.getMappedRange( 0, byteLength );
return target;
} else {
// copy the data into the target array buffer
const arrayBuffer = readBufferGPU.getMappedRange( 0, byteLength );
new Uint8Array( target ).set( new Uint8Array( arrayBuffer ) );
readBufferGPU.destroy();
return target;
}
}
/**
* Returns the vertex format of the given buffer attribute.
*
* @private
* @param {BufferAttribute} geometryAttribute - The buffer attribute.
* @return {string|undefined} The vertex format (e.g. 'float32x3').
*/
_getVertexFormat( geometryAttribute ) {
const { itemSize, normalized } = geometryAttribute;
const ArrayType = geometryAttribute.array.constructor;
const AttributeType = geometryAttribute.constructor;
let format;
if ( itemSize === 1 ) {
format = typeArraysToVertexFormatPrefixForItemSize1.get( ArrayType );
} else {
const prefixOptions = typedAttributeToVertexFormatPrefix.get( AttributeType ) || typedArraysToVertexFormatPrefix.get( ArrayType );
const prefix = prefixOptions[ normalized ? 1 : 0 ];
if ( prefix ) {
const bytesPerUnit = ArrayType.BYTES_PER_ELEMENT * itemSize;
const paddedBytesPerUnit = Math.floor( ( bytesPerUnit + 3 ) / 4 ) * 4;
const paddedItemSize = paddedBytesPerUnit / ArrayType.BYTES_PER_ELEMENT;
if ( paddedItemSize % 1 ) {
throw new Error( 'THREE.WebGPUAttributeUtils: Bad vertex format item size.' );
}
format = `${prefix}x${paddedItemSize}`;
}
}
if ( ! format ) {
error( 'WebGPUAttributeUtils: Vertex format not supported yet.' );
}
return format;
}
}
const _bindGroupDescriptor = new GPUBindGroupDescriptor();
const _bufferDescriptor$2 = new GPUBufferDescriptor();
const _viewDescriptor$1 = new GPUTextureViewDescriptor();
/**
* Class representing a WebGPU bind group layout.
*
* @private
*/
class BindGroupLayout {
/**
* Constructs a new layout.
*
* @param {GPUBindGroupLayout} layoutGPU - A GPU Bind Group Layout.
*/
constructor( layoutGPU ) {
/**
* The current GPUBindGroupLayout.
*
* @type {GPUBindGroupLayout}
*/
this.layoutGPU = layoutGPU;
/**
* The number of bind groups that use this layout.
*
* @type {number}
*/
this.usedTimes = 0;
}
}
/**
* A WebGPU backend utility module for managing bindings.
*
* When reading the documentation it's helpful to keep in mind that
* all class definitions starting with 'GPU*' are modules from the
* WebGPU API. So for example `BindGroup` is a class from the engine
* whereas `GPUBindGroup` is a class from WebGPU.
*
* @private
*/
class WebGPUBindingUtils {
/**
* Constructs a new utility object.
*
* @param {WebGPUBackend} backend - The WebGPU backend.
*/
constructor( backend ) {
/**
* A reference to the WebGPU backend.
*
* @type {WebGPUBackend}
*/
this.backend = backend;
/**
* A cache that maps combinations of layout entries to existing bind group layouts.
*
* @private
* @type {Map<string, BindGroupLayout>}
*/
this._bindGroupLayoutCache = new Map();
}
/**
* Creates a GPU bind group layout for the given bind group.
*
* @param {BindGroup} bindGroup - The bind group.
* @return {GPUBindGroupLayout} The GPU bind group layout.
*/
createBindingsLayout( bindGroup ) {
const backend = this.backend;
const device = backend.device;
const bindingsData = backend.get( bindGroup );
// check if the the bind group already has a layout
if ( bindingsData.layout ) {
return bindingsData.layout.layoutGPU;
}
// if not, assing one
const entries = this._createLayoutEntries( bindGroup );
const bindGroupLayoutKey = hashString( JSON.stringify( entries ) );
// try to find an existing layout in the cache
let bindGroupLayout = this._bindGroupLayoutCache.get( bindGroupLayoutKey );
// if not create a new one
if ( bindGroupLayout === undefined ) {
bindGroupLayout = new BindGroupLayout( device.createBindGroupLayout( { entries } ) );
this._bindGroupLayoutCache.set( bindGroupLayoutKey, bindGroupLayout );
}
bindGroupLayout.usedTimes ++;
bindingsData.layout = bindGroupLayout;
bindingsData.layoutKey = bindGroupLayoutKey;
return bindGroupLayout.layoutGPU;
}
/**
* Creates bindings from the given bind group definition.
*
* @param {BindGroup} bindGroup - The bind group.
* @param {Array<BindGroup>} bindings - Array of bind groups.
* @param {string} cacheKey - The cache key.
* @param {number} version - The version.
*/
createBindings( bindGroup, bindings, cacheKey, version = 0 ) {
const { backend } = this;
const bindingsData = backend.get( bindGroup );
// setup (static) binding layout and (dynamic) binding group
const bindLayoutGPU = this.createBindingsLayout( bindGroup );
let bindGroupGPU;
if ( cacheKey !== '' ) {
if ( bindingsData.groups === undefined ) {
bindingsData.groups = {};
bindingsData.versions = {};
}
if ( bindingsData.versions[ cacheKey ] === version ) {
bindGroupGPU = bindingsData.groups[ cacheKey ];
}
}
if ( bindGroupGPU === undefined ) {
bindGroupGPU = this.createBindGroup( bindGroup, bindLayoutGPU );
if ( cacheKey !== '' ) {
bindingsData.groups[ cacheKey ] = bindGroupGPU;
bindingsData.versions[ cacheKey ] = version;
}
}
bindingsData.group = bindGroupGPU;
}
/**
* Updates a buffer binding.
*
* @param {Buffer} binding - The buffer binding to update.
*/
updateBinding( binding ) {
const backend = this.backend;
const device = backend.device;
const array = binding.buffer; // cpu
const buffer = backend.get( binding ).buffer; // gpu
const updateRanges = binding.updateRanges;
if ( updateRanges.length === 0 ) {
device.queue.writeBuffer(
buffer,
0,
array,
0
);
} else {
const isTyped = isTypedArray( array );
const byteOffsetFactor = isTyped ? 1 : array.BYTES_PER_ELEMENT;
// Update ranges arrive sorted and non-overlapping which makes
// it easy to merge contiguous ranges.
let start = updateRanges[ 0 ].start; // start of the current merged range
for ( let i = 0, l = updateRanges.length; i < l; i ++ ) {
const range = updateRanges[ i ];
const next = updateRanges[ i + 1 ];
const end = range.start + range.count; // exclusive end of the current range
// keep merging while the next range is contiguous
if ( next !== undefined && next.start === end ) continue;
// write the merged range
const dataOffset = start * byteOffsetFactor;
const size = ( end - start ) * byteOffsetFactor;
const bufferOffset = dataOffset * ( isTyped ? array.BYTES_PER_ELEMENT : 1 ); // bufferOffset is always in bytes
device.queue.writeBuffer(
buffer,
bufferOffset,
array,
dataOffset,
size
);
// start next if possible
if ( next !== undefined ) start = next.start;
}
}
}
/**
* Creates a GPU bind group for the camera index.
*
* @param {Uint32Array} data - The index data.
* @param {GPUBindGroupLayout} layoutGPU - The GPU bind group layout.
* @return {GPUBindGroup} The GPU bind group.
*/
createBindGroupIndex( data, layoutGPU ) {
const backend = this.backend;
const device = backend.device;
const usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST;
const index = data[ 0 ];
_bufferDescriptor$2.label = 'bindingCameraIndex_' + index;
_bufferDescriptor$2.size = 16; // uint(4) * 4
_bufferDescriptor$2.usage = usage;
const buffer = device.createBuffer( _bufferDescriptor$2 );
_bufferDescriptor$2.reset();
device.queue.writeBuffer( buffer, 0, data, 0 );
_bindGroupDescriptor.label = 'bindGroupCameraIndex_' + index;
_bindGroupDescriptor.layout = layoutGPU;
_bindGroupDescriptor.entries.push( { binding: 0, resource: { buffer } } );
const bindGroup = device.createBindGroup( _bindGroupDescriptor );
_bindGroupDescriptor.reset();
return bindGroup;
}
/**
* Creates a GPU bind group for the given bind group and GPU layout.
*
* @param {BindGroup} bindGroup - The bind group.
* @param {GPUBindGroupLayout} layoutGPU - The GPU bind group layout.
* @return {GPUBindGroup} The GPU bind group.
*/
createBindGroup( bindGroup, layoutGPU ) {
const backend = this.backend;
const device = backend.device;
let bindingPoint = 0;
_bindGroupDescriptor.label = 'bindGroup_' + bindGroup.name;
_bindGroupDescriptor.layout = layoutGPU;
for ( const binding of bindGroup.bindings ) {
if ( binding.isUniformBuffer ) {
const bindingData = backend.get( binding );
_bindGroupDescriptor.entries.push( { binding: bindingPoint, resource: { buffer: bindingData.buffer } } );
} else if ( binding.isStorageBuffer ) {
const buffer = backend.get( binding.attribute ).buffer;
_bindGroupDescriptor.entries.push( { binding: bindingPoint, resource: { buffer: buffer } } );
} else if ( binding.isSampledTexture ) {
const textureData = backend.get( binding.texture );
let resourceGPU;
if ( textureData.externalTexture !== undefined ) {
resourceGPU = device.importExternalTexture( { source: textureData.externalTexture } );
} else {
const mipLevelCount = binding.store ? 1 : textureData.texture.mipLevelCount;
const baseMipLevel = binding.store ? binding.mipLevel : 0;
let propertyName = `view-${ textureData.texture.width }-${ textureData.texture.height }`;
if ( textureData.texture.depthOrArrayLayers > 1 ) {
propertyName += `-${ textureData.texture.depthOrArrayLayers }`;
}
propertyName += `-${ mipLevelCount }-${ baseMipLevel }`;
resourceGPU = textureData[ propertyName ];
if ( resourceGPU === undefined ) {
const aspectGPU = binding.texture.isDepthTexture ? GPUTextureAspect.DepthOnly : GPUTextureAspect.All;
let dimensionViewGPU;
if ( binding.isSampledCubeTexture ) {
dimensionViewGPU = GPUTextureViewDimension.Cube;
} else if ( binding.texture.isArrayTexture || binding.texture.isDataArrayTexture || binding.texture.isCompressedArrayTexture ) {
// Prefer the texture's actual array flag over the cached 3D binding type.
// Layered render targets can become array textures after shader compilation.
dimensionViewGPU = GPUTextureViewDimension.TwoDArray;
} else if ( binding.isSampledTexture3D ) {
dimensionViewGPU = GPUTextureViewDimension.ThreeD;
} else {
dimensionViewGPU = GPUTextureViewDimension.TwoD;
}
_viewDescriptor$1.aspect = aspectGPU;
_viewDescriptor$1.dimension = dimensionViewGPU;
_viewDescriptor$1.mipLevelCount = mipLevelCount;
_viewDescriptor$1.baseMipLevel = baseMipLevel;
resourceGPU = textureData[ propertyName ] = textureData.texture.createView( _viewDescriptor$1 );
_viewDescriptor$1.reset();
}
}
_bindGroupDescriptor.entries.push( { binding: bindingPoint, resource: resourceGPU } );
} else if ( binding.isSampler ) {
const bindingData = backend.get( binding );
_bindGroupDescriptor.entries.push( { binding: bindingPoint, resource: bindingData.sampler } );
}
bindingPoint ++;
}
const bindGroupGPU = device.createBindGroup( _bindGroupDescriptor );
_bindGroupDescriptor.reset();
return bindGroupGPU;
}
/**
* Creates a GPU bind group layout entries for the given bind group.
*
* @private
* @param {BindGroup} bindGroup - The bind group.
* @return {Array<GPUBindGroupLayoutEntry>} The GPU bind group layout entries.
*/
_createLayoutEntries( bindGroup ) {
const entries = [];
let index = 0;
for ( const binding of bindGroup.bindings ) {
const backend = this.backend;
const bindingGPU = {
binding: index,
visibility: binding.visibility
};
if ( binding.isUniformBuffer || binding.isStorageBuffer ) {
const buffer = {}; // GPUBufferBindingLayout
if ( binding.isStorageBuffer ) {
if ( binding.visibility & GPUShaderStage.COMPUTE ) {
if ( binding.access === NodeAccess.READ_WRITE || binding.access === NodeAccess.WRITE_ONLY ) {
buffer.type = GPUBufferBindingType.Storage;
} else {
buffer.type = GPUBufferBindingType.ReadOnlyStorage;
}
} else if ( binding.nodeUniform && binding.nodeUniform.isAtomic && ( binding.visibility & GPUShaderStage.FRAGMENT ) ) {
// Atomic buffers require a read_write storage binding. Per the WGSL
// spec this is only valid in the fragment stage (compute is handled
// above), so the vertex stage falls back to read-only storage.
buffer.type = GPUBufferBindingType.Storage;
} else {
buffer.type = GPUBufferBindingType.ReadOnlyStorage;
}
}
bindingGPU.buffer = buffer;
} else if ( binding.isSampledTexture && binding.store ) {
const storageTexture = {}; // GPUStorageTextureBindingLayout
storageTexture.format = this.backend.get( binding.texture ).texture.format;
const access = binding.access;
if ( access === NodeAccess.READ_WRITE ) {
storageTexture.access = GPUStorageTextureAccess.ReadWrite;
} else if ( access === NodeAccess.WRITE_ONLY ) {
storageTexture.access = GPUStorageTextureAccess.WriteOnly;
} else {
storageTexture.access = GPUStorageTextureAccess.ReadOnly;
}
if ( binding.texture.isArrayTexture ) {
storageTexture.viewDimension = GPUTextureViewDimension.TwoDArray;
} else if ( binding.texture.is3DTexture ) {
storageTexture.viewDimension = GPUTextureViewDimension.ThreeD;
}
bindingGPU.storageTexture = storageTexture;
} else if ( binding.isSampledTexture ) {
const texture = {}; // GPUTextureBindingLayout
const { primarySamples } = backend.utils.getTextureSampleData( binding.texture );
if ( primarySamples > 1 ) {
texture.multisampled = true;
if ( ! binding.texture.isDepthTexture ) {
texture.sampleType = GPUTextureSampleType.UnfilterableFloat;
}
}
if ( binding.texture.isDepthTexture ) {
if ( backend.compatibilityMode && binding.texture.compareFunction === null ) {
texture.sampleType = GPUTextureSampleType.UnfilterableFloat;
} else {
texture.sampleType = GPUTextureSampleType.Depth;
}
} else {
const type = binding.texture.type;
if ( type === IntType ) {
texture.sampleType = GPUTextureSampleType.SInt;
} else if ( type === UnsignedIntType ) {
texture.sampleType = GPUTextureSampleType.UInt;
} else if ( binding.texture.normalized === true && ( type === ShortType || type === UnsignedShortType ) ) {
texture.sampleType = GPUTextureSampleType.UnfilterableFloat;
} else if ( type === FloatType ) {
if ( this.backend.hasFeature( 'float32-filterable' ) ) {
texture.sampleType = GPUTextureSampleType.Float;
} else {
texture.sampleType = GPUTextureSampleType.UnfilterableFloat;
}
}
}
if ( binding.isSampledCubeTexture ) {
texture.viewDimension = GPUTextureViewDimension.Cube;
} else if ( binding.texture.isArrayTexture || binding.texture.isDataArrayTexture || binding.texture.isCompressedArrayTexture ) {
texture.viewDimension = GPUTextureViewDimension.TwoDArray;
} else if ( binding.isSampledTexture3D ) {
texture.viewDimension = GPUTextureViewDimension.ThreeD;
}
bindingGPU.texture = texture;
} else if ( binding.isSampler ) {
const sampler = {}; // GPUSamplerBindingLayout
if ( binding.texture.isDepthTexture ) {
if ( binding.texture.compareFunction !== null && binding.textureNode.isSampleCompare() && backend.hasCompatibility( Compatibility.TEXTURE_COMPARE ) ) {
sampler.type = GPUSamplerBindingType.Comparison;
} else {
// Depth textures without compare must use non-filtering sampler
sampler.type = GPUSamplerBindingType.NonFiltering;
}
}
bindingGPU.sampler = sampler;
} else {
error( `WebGPUBindingUtils: Unsupported binding "${ binding }".` );
}
entries.push( bindingGPU );
index ++;
}
return entries;
}
/**
* Delete the data associated with a bind group.
*
* @param {BindGroup} bindGroup - The bind group.
*/
deleteBindGroupData( bindGroup ) {
const { backend } = this;
const bindingsData = backend.get( bindGroup );
if ( bindingsData.layout ) {
bindingsData.layout.usedTimes --;
if ( bindingsData.layout.usedTimes === 0 ) {
this._bindGroupLayoutCache.delete( bindingsData.layoutKey );
}
bindingsData.layout = undefined;
bindingsData.layoutKey = undefined;
}
}
/**
* Frees internal resources.
*/
dispose() {
this._bindGroupLayoutCache.clear();
}
}
/**
* A WebGPU backend utility module for managing the device's capabilities.
*
* @private
*/
class WebGPUCapabilities {
/**
* 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 maximum anisotropy texture filtering value.
*
* @return {number} The maximum anisotropy texture filtering value.
*/
getMaxAnisotropy() {
return 16;
}
/**
* Returns the maximum number of bytes available for uniform buffers.
*
* @return {number} The maximum number of bytes available for uniform buffers.
*/
getUniformBufferLimit() {
return this.backend.device.limits.maxUniformBufferBindingSize;
}
}
/**
* Reusable descriptor for `GPUDevice.createComputePipeline()`.
*
* @private
*/
class GPUComputePipelineDescriptor {
constructor() {
/**
* The label of the compute pipeline.
*
* @type {string}
*/
this.label = '';
/**
* The pipeline layout the pipeline conforms to, or `'auto'`.
*
* @type {?GPUPipelineLayout|string}
* @default null
*/
this.layout = null;
/**
* The programmable compute stage.
*
* @type {?Object}
* @default null
*/
this.compute = null;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.label = '';
this.layout = null;
this.compute = null;
}
}
/**
* Reusable descriptor for `GPUDevice.createPipelineLayout()`.
*
* @private
*/
class GPUPipelineLayoutDescriptor {
constructor() {
/**
* The label of the pipeline layout.
*
* @type {string}
*/
this.label = '';
/**
* The set of bind group layouts the pipeline layout describes.
*
* @type {?Array<?GPUBindGroupLayout>}
* @default null
*/
this.bindGroupLayouts = null;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.label = '';
this.bindGroupLayouts = null;
}
}
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.getVertexBufferLayout( 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 = ( this.backend.parameters.reversedDepthBuffer ) ? - material.polygonOffsetUnits : material.polygonOffsetUnits;
depthStencil.depthBiasSlopeScale = ( this.backend.parameters.reversedDepthBuffer ) ? - material.polygonOffsetFactor : 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();
// Pop the error scope before awaiting the pipeline, so several pipelines
// can be created at the same time without mixing their error scopes.
const errorScopePromise = device.popErrorScope();
if ( pipelinePromise !== null ) {
try {
pipelineData.pipeline = await pipelinePromise;
} catch ( err ) {
asyncError = err;
}
}
const errorScope = await errorScopePromise;
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.
* @param {?Array<Promise>} [promises=null] - Optional compilation promises.
*/
createComputePipeline( pipeline, bindings, promises = null ) {
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;
if ( promises === null ) {
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 );
}
} );
} else {
const promise = new Promise( async ( resolve /*, reject*/ ) => {
try {
let asyncError = null;
let pipelinePromise = null;
try {
pipelinePromise = device.createComputePipelineAsync( _computePipelineDescriptor );
} catch ( err ) {
asyncError = err;
}
_computePipelineDescriptor.reset();
// Pop the error scope before awaiting the pipeline, so several pipelines
// can be created at the same time without mixing their error scopes.
const errorScopePromise = device.popErrorScope();
if ( pipelinePromise !== null ) {
try {
pipelineGPU.pipeline = await pipelinePromise;
} catch ( err ) {
asyncError = err;
}
}
const errorScope = await errorScopePromise;
if ( errorScope !== null || asyncError !== null ) {
pipelineGPU.error = true;
const reason = ( errorScope && errorScope.message ) || ( asyncError && asyncError.message ) || 'unknown';
error( `WebGPURenderer: Async compute pipeline creation failed (${ pipelineLabel }): ${ reason }` );
await this._reportShaderDiagnostics( [ { program: computeStage, module: computeProgram.module } ], pipelineLabel );
}
} finally {
// Guarantee resolution so `compileComputeAsync`'s Promise.all cannot hang on an
// unexpected throw from any await above.
resolve();
}
} );
promises.push( promise );
}
}
/**
* 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 ConstantColorFactor:
case ConstantAlphaFactor: // WebGPU has no dedicated constant alpha blend factors
blendFactor = GPUBlendFactor.Constant;
break;
case OneMinusConstantColorFactor:
case OneMinusConstantAlphaFactor: // WebGPU has no dedicated constant alpha blend factors
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 );
descriptor.stripIndexFormat = utils.getStripIndexFormat( object, geometry );
//
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;
}
}
/**
* Reusable descriptor for `GPUDevice.createQuerySet()`.
*
* @private
*/
class GPUQuerySetDescriptor {
constructor() {
/**
* The label of the query set.
*
* @type {string}
*/
this.label = '';
/**
* The type of queries managed by the set.
*
* @type {string|undefined}
*/
this.type = undefined;
/**
* The number of queries managed by the set.
*
* @type {number}
* @default 0
*/
this.count = 0;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.label = '';
this.type = undefined;
this.count = 0;
}
}
const _bufferDescriptor$1 = new GPUBufferDescriptor();
const _commandEncoderDescriptor$1 = new GPUCommandEncoderDescriptor();
const _querySetDescriptor$1 = new GPUQuerySetDescriptor();
/**
* Manages a pool of WebGPU timestamp queries for performance measurement.
* Extends the base TimestampQueryPool to provide WebGPU-specific implementation.
*
* @augments TimestampQueryPool
*/
class WebGPUTimestampQueryPool extends TimestampQueryPool {
/**
* Creates a new WebGPU timestamp query pool.
*
* @param {GPUDevice} device - The WebGPU device to create queries on.
* @param {string} type - The type identifier for this query pool.
* @param {number} [maxQueries=2048] - Maximum number of queries this pool can hold.
*/
constructor( device, type, maxQueries = 2048 ) {
super( maxQueries );
this.device = device;
this.type = type;
_querySetDescriptor$1.label = `queryset_global_timestamp_${type}`;
_querySetDescriptor$1.type = 'timestamp';
_querySetDescriptor$1.count = this.maxQueries;
this.querySet = this.device.createQuerySet( _querySetDescriptor$1 );
_querySetDescriptor$1.reset();
const bufferSize = this.maxQueries * 8;
_bufferDescriptor$1.label = `buffer_timestamp_resolve_${type}`;
_bufferDescriptor$1.size = bufferSize;
_bufferDescriptor$1.usage = GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC;
this.resolveBuffer = this.device.createBuffer( _bufferDescriptor$1 );
_bufferDescriptor$1.reset();
_bufferDescriptor$1.label = `buffer_timestamp_result_${type}`;
_bufferDescriptor$1.size = bufferSize;
_bufferDescriptor$1.usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ;
this.resultBuffer = this.device.createBuffer( _bufferDescriptor$1 );
_bufferDescriptor$1.reset();
}
/**
* Allocates a pair of queries for a given render context.
*
* @param {string} uid - A unique identifier for the render context.
* @returns {?number} The base offset for the allocated queries, or null if allocation failed.
*/
allocateQueriesForContext( uid ) {
if ( ! this.trackTimestamp || this.isDisposed ) return null;
if ( this.currentQueryIndex + 2 > this.maxQueries ) {
this.resolveQueriesAsync();
this.currentQueryIndex = 0;
this.queryOffsets.clear();
}
const baseOffset = this.currentQueryIndex;
this.currentQueryIndex += 2;
this.queryOffsets.set( uid, baseOffset );
return baseOffset;
}
/**
* Asynchronously resolves all pending queries and returns the total duration.
* If there's already a pending resolve operation, returns that promise instead.
*
* @async
* @returns {Promise<number>} The total duration in milliseconds, or the last valid value if resolution fails.
*/
async resolveQueriesAsync() {
if ( ! this.trackTimestamp || this.currentQueryIndex === 0 || this.isDisposed ) {
return this.lastValue;
}
if ( this.pendingResolve ) {
return this.pendingResolve;
}
this.pendingResolve = this._resolveQueries();
try {
const result = await this.pendingResolve;
return result;
} finally {
this.pendingResolve = null;
}
}
/**
* Internal method to resolve queries and calculate total duration.
*
* @async
* @private
* @returns {Promise<number>} The total duration in milliseconds.
*/
async _resolveQueries() {
if ( this.isDisposed ) {
return this.lastValue;
}
try {
if ( this.resultBuffer.mapState !== 'unmapped' ) {
return this.lastValue;
}
const currentOffsets = new Map( this.queryOffsets );
const queryCount = this.currentQueryIndex;
const bytesUsed = queryCount * 8;
// Reset state before GPU work
this.currentQueryIndex = 0;
this.queryOffsets.clear();
const commandEncoder = this.device.createCommandEncoder( _commandEncoderDescriptor$1 );
commandEncoder.resolveQuerySet(
this.querySet,
0,
queryCount,
this.resolveBuffer,
0
);
commandEncoder.copyBufferToBuffer(
this.resolveBuffer,
0,
this.resultBuffer,
0,
bytesUsed
);
const commandBuffer = commandEncoder.finish();
submit( this.device, commandBuffer );
if ( this.resultBuffer.mapState !== 'unmapped' ) {
return this.lastValue;
}
// Create and track the mapping operation
await this.resultBuffer.mapAsync( GPUMapMode.READ, 0, bytesUsed );
if ( this.isDisposed ) {
if ( this.resultBuffer.mapState === 'mapped' ) {
this.resultBuffer.unmap();
}
return this.lastValue;
}
//
const times = new BigUint64Array( this.resultBuffer.getMappedRange( 0, bytesUsed ) );
const framesDuration = {};
const frames = [];
this.timestamps.clear();
for ( const [ uid, baseOffset ] of currentOffsets ) {
const match = uid.match( /^(.*):f(\d+)$/ );
const frame = parseInt( match[ 2 ] );
if ( frames.includes( frame ) === false ) {
frames.push( frame );
}
if ( framesDuration[ frame ] === undefined ) framesDuration[ frame ] = 0;
const startTime = times[ baseOffset ];
const endTime = times[ baseOffset + 1 ];
const duration = Number( endTime - startTime ) / 1e6;
this.timestamps.set( uid, duration );
framesDuration[ frame ] += duration;
}
// Return the total duration of the last frame
const totalDuration = framesDuration[ frames[ frames.length - 1 ] ];
this.resultBuffer.unmap();
this.lastValue = totalDuration;
this.frames = frames;
return totalDuration;
} catch ( e ) {
error( 'Error resolving queries:', e );
if ( this.resultBuffer.mapState === 'mapped' ) {
this.resultBuffer.unmap();
}
return this.lastValue;
}
}
/**
* Dispose of the query pool.
*
* @async
* @returns {Promise} A Promise that resolves when the dispose has been executed.
*/
async dispose() {
if ( this.isDisposed ) {
return;
}
this.isDisposed = true;
// Wait for pending resolve operation
if ( this.pendingResolve ) {
try {
await this.pendingResolve;
} catch ( e ) {
error( 'Error waiting for pending resolve:', e );
}
}
// Ensure buffer is unmapped before destroying
if ( this.resultBuffer && this.resultBuffer.mapState === 'mapped' ) {
try {
this.resultBuffer.unmap();
} catch ( e ) {
error( 'Error unmapping buffer:', e );
}
}
// Destroy resources
if ( this.querySet ) {
this.querySet.destroy();
this.querySet = null;
}
if ( this.resolveBuffer ) {
this.resolveBuffer.destroy();
this.resolveBuffer = null;
}
if ( this.resultBuffer ) {
this.resultBuffer.destroy();
this.resultBuffer = null;
}
this.queryOffsets.clear();
this.timestamps.clear();
this.frames = [];
this.pendingResolve = null;
}
}
/**
* Reusable descriptor for `GPUCommandEncoder.beginComputePass()`.
*
* @private
*/
class GPUComputePassDescriptor {
constructor() {
/**
* The label of the compute pass.
*
* @type {string}
*/
this.label = '';
/**
* Defines which timestamp values are written and where.
*
* @type {Object|undefined}
*/
this.timestampWrites = undefined;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.label = '';
this.timestampWrites = undefined;
}
}
/**
* Reusable descriptor for `GPURenderPassDepthStencilAttachment`, the
* `depthStencilAttachment` field of `GPURenderPassDescriptor`.
*
* @private
*/
class GPURenderPassDepthStencilAttachment {
constructor() {
/**
* The depth/stencil texture view the pass renders into.
*
* @type {?GPUTextureView}
* @default null
*/
this.view = null;
/**
* The load operation applied to the depth aspect at the start of the pass.
*
* @type {string|undefined}
*/
this.depthLoadOp = undefined;
/**
* The store operation applied to the depth aspect at the end of the pass.
*
* @type {string|undefined}
*/
this.depthStoreOp = undefined;
/**
* The clear value used when `depthLoadOp` is `'clear'`.
*
* @type {number|undefined}
*/
this.depthClearValue = undefined;
/**
* Whether the depth aspect is read-only.
*
* @type {boolean}
* @default false
*/
this.depthReadOnly = false;
/**
* The load operation applied to the stencil aspect at the start of the pass.
*
* @type {string|undefined}
*/
this.stencilLoadOp = undefined;
/**
* The store operation applied to the stencil aspect at the end of the pass.
*
* @type {string|undefined}
*/
this.stencilStoreOp = undefined;
/**
* The clear value used when `stencilLoadOp` is `'clear'`.
*
* @type {number}
* @default 0
*/
this.stencilClearValue = 0;
/**
* Whether the stencil aspect is read-only.
*
* @type {boolean}
* @default false
*/
this.stencilReadOnly = false;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.view = null;
this.depthLoadOp = undefined;
this.depthStoreOp = undefined;
this.depthClearValue = undefined;
this.depthReadOnly = false;
this.stencilLoadOp = undefined;
this.stencilStoreOp = undefined;
this.stencilClearValue = 0;
this.stencilReadOnly = false;
}
}
/**
* Reusable descriptor for `GPURenderPassTimestampWrites`, the
* `timestampWrites` field of `GPURenderPassDescriptor`. The same shape is
* also accepted as `GPUComputePassTimestampWrites`.
*
* @private
*/
class GPURenderPassTimestampWrites {
constructor() {
/**
* The query set the timestamps are written to.
*
* @type {?GPUQuerySet}
* @default null
*/
this.querySet = null;
/**
* The index in the query set the beginning timestamp is written to.
*
* @type {number|undefined}
*/
this.beginningOfPassWriteIndex = undefined;
/**
* The index in the query set the ending timestamp is written to.
*
* @type {number|undefined}
*/
this.endOfPassWriteIndex = undefined;
}
/**
* Resets the descriptor to its default state.
*/
reset() {
this.querySet = null;
this.beginningOfPassWriteIndex = undefined;
this.endOfPassWriteIndex = undefined;
}
}
// debugger tools
// import 'https://greggman.github.io/webgpu-avoid-redundant-state-setting/webgpu-check-redundant-state-setting.js';
const _clearValue = { r: 0, g: 0, b: 0, a: 1 };
const _blendConstant = { r: 0, g: 0, b: 0, a: 0 };
const _bufferDescriptor = new GPUBufferDescriptor();
const _commandEncoderDescriptor = new GPUCommandEncoderDescriptor();
const _computePassDescriptor = new GPUComputePassDescriptor();
const _querySetDescriptor = new GPUQuerySetDescriptor();
const _shaderModuleDescriptor = new GPUShaderModuleDescriptor();
const _renderPassTimestampWrites = new GPURenderPassTimestampWrites();
const _texelCopyTextureInfoSrc = new GPUTexelCopyTextureInfo();
const _texelCopyTextureInfoDst = new GPUTexelCopyTextureInfo();
const _textureDescriptor = new GPUTextureDescriptor();
const _viewDescriptor = new GPUTextureViewDescriptor();
const _extent3D = new GPUExtent3D();
/**
* A backend implementation targeting WebGPU.
*
* @private
* @augments Backend
*/
class WebGPUBackend extends Backend {
/**
* WebGPUBackend options.
*
* @typedef {Object} WebGPUBackend~Options
* @property {boolean} [logarithmicDepthBuffer=false] - Whether logarithmic depth buffer is enabled or not.
* @property {boolean} [reversedDepthBuffer=false] - Whether reversed depth buffer is enabled or not.
* @property {boolean} [alpha=true] - Whether the default framebuffer (which represents the final contents of the canvas) should be transparent or opaque.
* @property {boolean} [depth=true] - Whether the default framebuffer should have a depth buffer or not.
* @property {boolean} [stencil=false] - Whether the default framebuffer should have a stencil buffer or not.
* @property {boolean} [antialias=false] - Whether MSAA as the default anti-aliasing should be enabled or not.
* @property {number} [samples=0] - When `antialias` is `true`, `4` samples are used by default. Set this parameter to any other integer value than 0 to overwrite the default.
* @property {boolean} [forceWebGL=false] - If set to `true`, the renderer uses a WebGL 2 backend no matter if WebGPU is supported or not.
* @property {boolean} [trackTimestamp=false] - Whether to track timestamps with a Timestamp Query API or not.
* @property {string} [powerPreference=undefined] - The power preference.
* @property {Object} [requiredLimits=undefined] - Specifies the limits that are required by the device request. The request will fail if the adapter cannot provide these limits.
* @property {GPUDevice} [device=undefined] - If there is an existing GPU device on app level, it can be passed to the renderer as a parameter.
* @property {number} [outputType=undefined] - Texture type for output to canvas. By default, device's preferred format is used; other formats may incur overhead.
*/
/**
* Constructs a new WebGPU backend.
*
* @param {WebGPUBackend~Options} [parameters] - The configuration parameter.
*/
constructor( parameters = {} ) {
super( parameters );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isWebGPUBackend = true;
// some parameters require default values other than "undefined"
this.parameters.alpha = ( parameters.alpha === undefined ) ? true : parameters.alpha;
this.parameters.requiredLimits = ( parameters.requiredLimits === undefined ) ? {} : parameters.requiredLimits;
/**
* Indicates whether the backend is in WebGPU compatibility mode or not.
* The backend must be initialized before the property can be evaluated.
*
* @type {?boolean}
* @readonly
* @default null
*/
this.compatibilityMode = null;
/**
* A reference to the device.
*
* @type {?GPUDevice}
* @default null
*/
this.device = null;
/**
* A reference to the default render pass descriptor.
*
* @type {?Object}
* @default null
*/
this.defaultRenderPassdescriptor = null;
/**
* A reference to a backend module holding common utility functions.
*
* @type {WebGPUUtils}
*/
this.utils = new WebGPUUtils( this );
/**
* A reference to a backend module holding shader attribute-related
* utility functions.
*
* @type {WebGPUAttributeUtils}
*/
this.attributeUtils = new WebGPUAttributeUtils( this );
/**
* A reference to a backend module holding shader binding-related
* utility functions.
*
* @type {WebGPUBindingUtils}
*/
this.bindingUtils = new WebGPUBindingUtils( this );
/**
* A reference to a backend module holding device capability related
* utility functions.
*
* @type {WebGPUCapabilities}
*/
this.capabilities = new WebGPUCapabilities( this );
/**
* A reference to a backend module holding shader pipeline-related
* utility functions.
*
* @type {WebGPUPipelineUtils}
*/
this.pipelineUtils = new WebGPUPipelineUtils( this );
/**
* A reference to a backend module holding shader texture-related
* utility functions.
*
* @type {WebGPUTextureUtils}
*/
this.textureUtils = new WebGPUTextureUtils( this );
/**
* A map that manages the resolve buffers for occlusion queries.
*
* @type {Map<number,GPUBuffer>}
*/
this.occludedResolveCache = new Map();
// compatibility checks
const compatibilityTextureCompare = typeof navigator === 'undefined' ? true : /Android/.test( navigator.userAgent ) === false;
/**
* A map of compatibility checks.
*
* @type {Object}
*/
this._compatibility = {
[ Compatibility.TEXTURE_COMPARE ]: compatibilityTextureCompare
};
}
/**
* Initializes the backend so it is ready for usage.
*
* @async
* @param {Renderer} renderer - The renderer.
* @return {Promise} A Promise that resolves when the backend has been initialized.
*/
async init( renderer ) {
await super.init( renderer );
//
const parameters = this.parameters;
// create the device if it is not passed with parameters
let device;
if ( parameters.device === undefined ) {
const adapterOptions = {
powerPreference: parameters.powerPreference,
featureLevel: 'compatibility',
xrCompatible: renderer.xr.enabled
};
const adapter = ( typeof navigator !== 'undefined' ) ? await navigator.gpu.requestAdapter( adapterOptions ) : null;
if ( adapter === null ) {
throw new Error( 'THREE.WebGPUBackend: Unable to create WebGPU adapter.' );
}
// feature support
const features = Object.values( GPUFeatureName );
const supportedFeatures = [];
for ( const name of features ) {
if ( adapter.features.has( name ) ) {
supportedFeatures.push( name );
}
}
const deviceDescriptor = {
requiredFeatures: supportedFeatures,
requiredLimits: parameters.requiredLimits
};
device = await adapter.requestDevice( deviceDescriptor );
} else {
device = parameters.device;
}
this.compatibilityMode = ! device.features.has( 'core-features-and-limits' );
if ( this.compatibilityMode ) {
renderer._samples = 0;
}
device.lost.then( ( info ) => {
if ( info.reason === 'destroyed' ) return;
const deviceLossInfo = {
api: 'WebGPU',
message: info.message || 'Unknown reason',
reason: info.reason || null,
originalEvent: info
};
renderer.onDeviceLost( deviceLossInfo );
} );
device.onuncapturederror = ( event ) => {
const gpuError = event.error;
const type = gpuError && gpuError.constructor ? gpuError.constructor.name : 'GPUError';
const message = ( gpuError && gpuError.message ) || 'Unknown uncaptured GPU error';
renderer.onError( {
api: 'WebGPU',
type,
message,
originalEvent: event
} );
};
this.device = device;
this.trackTimestamp = this.trackTimestamp && this.hasFeature( GPUFeatureName.TimestampQuery );
this.updateSize();
}
/**
* Registers external GPU textures from `XRGPUBinding` for use in rendering.
*
* @param {RenderTarget} renderTarget - The render target to register the textures for.
* @param {GPUTexture} colorTexture - The shared XR color GPUTexture.
* @param {?Array<Object>} [viewDescriptors=null] - Optional view descriptors, one per XR view.
*/
setXRRenderTargetTextures( renderTarget, colorTexture, viewDescriptors = null ) {
// Update the external XR texture without replacing the cached MSAA attachments.
const textureData = this.get( renderTarget.texture );
textureData.texture = colorTexture;
textureData.format = colorTexture.format;
textureData.externalTexture = true;
textureData.xrViewDescriptors = viewDescriptors;
textureData.initialized = true;
}
/**
* A reference to the context.
*
* @type {?GPUCanvasContext}
* @default null
*/
get context() {
const canvasTarget = this.renderer.getCanvasTarget();
const canvasData = this.get( canvasTarget );
let context = canvasData.context;
if ( context === undefined ) {
const parameters = this.parameters;
if ( canvasTarget.isDefaultCanvasTarget === true && parameters.context !== undefined ) {
context = parameters.context;
} else {
context = canvasTarget.domElement.getContext( 'webgpu' );
}
// OffscreenCanvas does not have setAttribute, see #22811
if ( 'setAttribute' in canvasTarget.domElement ) canvasTarget.domElement.setAttribute( 'data-engine', `three.js r${ REVISION } webgpu` );
const alphaMode = parameters.alpha ? 'premultiplied' : 'opaque';
const toneMappingMode = parameters.outputType === HalfFloatType ? 'extended' : 'standard';
context.configure( {
device: this.device,
format: this.utils.getPreferredCanvasFormat(),
usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC,
alphaMode: alphaMode,
toneMapping: {
mode: toneMappingMode
}
} );
canvasData.context = context;
}
return context;
}
/**
* The coordinate system of the backend.
*
* @type {number}
* @readonly
*/
get coordinateSystem() {
return WebGPUCoordinateSystem;
}
/**
* Whether the backend supports query timestamps or not.
*
* @type {boolean}
* @readonly
*/
get hasTimestamp() {
return true;
}
/**
* This method performs a readback operation by moving buffer data from
* a storage buffer attribute from the GPU to the CPU. ReadbackBuffer can
* be used to retain and reuse handles to the intermediate buffers and prevent
* new allocation.
*
* @async
* @param {BufferAttribute} attribute - The storage buffer attribute to read from.
* @param {?(ReadbackBuffer|ArrayBuffer)} [target=null] - The readback buffer or array buffer that receives the data. If `null`, a new array buffer is returned.
* @param {number} [offset=0] - The byte offset to start reading from. Must be a multiple of 4.
* @param {number} [count=-1] - The number of bytes to read. Must be a multiple of 4, or `-1` to read to the end of the buffer.
* @return {Promise<ArrayBuffer|ReadbackBuffer>} A promise that resolves with the buffer data when the data are ready.
*/
async getArrayBufferAsync( attribute, target = null, offset = 0, count = -1 ) {
return await this.attributeUtils.getArrayBufferAsync( attribute, target, offset, count );
}
/**
* Returns the backend's rendering context.
*
* @return {GPUCanvasContext} The rendering context.
*/
getContext() {
return this.context;
}
/**
* Returns the default render pass descriptor.
*
* In WebGPU, the default framebuffer must be configured
* like custom framebuffers so the backend needs a render
* pass descriptor even when rendering directly to screen.
*
* @private
* @return {GPURenderPassDescriptor} The render pass descriptor.
*/
_getDefaultRenderPassDescriptor() {
const renderer = this.renderer;
const canvasTarget = renderer.getCanvasTarget();
const canvasData = this.get( canvasTarget );
const samples = renderer.currentSamples;
let descriptor = canvasData.descriptor;
if ( descriptor === undefined || canvasData.samples !== samples ) {
descriptor = new GPURenderPassDescriptor();
descriptor.colorAttachments.push( new GPURenderPassColorAttachment() );
if ( renderer.depth === true || renderer.stencil === true ) {
const depthStencilAttachment = new GPURenderPassDepthStencilAttachment();
depthStencilAttachment.view = this.textureUtils.getDepthBuffer( renderer.depth, renderer.stencil ).createView();
descriptor.depthStencilAttachment = depthStencilAttachment;
}
const colorAttachment = descriptor.colorAttachments[ 0 ];
if ( samples > 0 ) {
colorAttachment.view = this.textureUtils.getColorBuffer().createView();
} else {
colorAttachment.resolveTarget = undefined;
}
canvasData.descriptor = descriptor;
canvasData.samples = samples;
}
const colorAttachment = descriptor.colorAttachments[ 0 ];
if ( samples > 0 ) {
colorAttachment.resolveTarget = this.context.getCurrentTexture().createView();
} else {
colorAttachment.view = this.context.getCurrentTexture().createView();
}
return descriptor;
}
/**
* Returns whether the render target is a render target array with depth 2D array texture.
*
* @param {RenderContext} renderContext - The render context.
* @return {boolean} Whether the render target is a render target array with depth 2D array texture.
*
* @private
*/
_isRenderCameraDepthArray( renderContext ) {
const camera = renderContext.camera;
return renderContext.depthTexture && renderContext.depthTexture.isArrayTexture === true && camera !== null && camera.isArrayCamera === true;
}
/**
* Returns whether the current render context references external textures.
*
* External textures can change every frame, so their descriptors must not be cached.
*
* @private
* @param {RenderContext} renderContext - The render context.
* @return {boolean} Whether the render context uses external textures.
*/
_hasExternalTexture( renderContext ) {
const textures = renderContext.textures;
if ( textures === null ) return false;
for ( let i = 0; i < textures.length; i ++ ) {
if ( this.get( textures[ i ] ).externalTexture === true ) return true;
}
return false;
}
/**
* Returns multisampled color textures for an external render target.
*
* @private
* @param {RenderContext} renderContext - The render context.
* @param {Object} textureData - The backend data for the external texture.
* @param {number} count - The number of textures to create.
* @return {?Array<GPUTexture>} The multisampled textures.
*/
_getExternalMSAATextures( renderContext, textureData, count ) {
const samples = this.utils.getSampleCount( renderContext.sampleCount );
if ( samples === 1 ) {
if ( textureData.msaaTextures !== undefined ) {
for ( const texture of textureData.msaaTextures ) texture.destroy();
textureData.msaaTextures = undefined;
}
return null;
}
const renderTarget = renderContext.renderTarget;
const width = renderTarget.width;
const height = renderTarget.height;
const format = textureData.format;
if ( textureData.msaaTextures === undefined ||
textureData.msaaTextures.length !== count ||
textureData.msaaWidth !== width ||
textureData.msaaHeight !== height ||
textureData.msaaSamples !== samples ||
textureData.msaaFormat !== format ) {
if ( textureData.msaaTextures !== undefined ) {
for ( const texture of textureData.msaaTextures ) texture.destroy();
}
_textureDescriptor.size.width = width;
_textureDescriptor.size.height = height;
_textureDescriptor.sampleCount = samples;
_textureDescriptor.format = format;
// Layered rendering can resume after a framebuffer copy,
// so these attachments must support loading.
_textureDescriptor.usage = GPUTextureUsage.RENDER_ATTACHMENT;
textureData.msaaTextures = [];
for ( let i = 0; i < count; i ++ ) {
_textureDescriptor.label = renderTarget.texture.name + '-msaa-' + i;
textureData.msaaTextures.push( this.device.createTexture( _textureDescriptor ) );
}
_textureDescriptor.reset();
textureData.msaaWidth = width;
textureData.msaaHeight = height;
textureData.msaaSamples = samples;
textureData.msaaFormat = format;
}
return textureData.msaaTextures;
}
/**
* Creates attachment views for an external texture render target.
*
* @private
* @param {RenderContext} renderContext - The render context.
* @param {Object} textureData - The backend data for the texture.
* @return {Array<Object>} The attachment view descriptors.
*/
_createExternalTextureViews( renderContext, textureData ) {
const textureViews = [];
const camera = renderContext.camera;
const viewDescriptors = textureData.xrViewDescriptors;
const viewCount = Math.max( viewDescriptors?.length || 0, renderContext.activeCubeFace + 1, 1 );
const msaaTextures = this._getExternalMSAATextures( renderContext, textureData, viewCount );
if ( viewDescriptors && camera !== null && camera.isArrayCamera === true ) {
for ( let i = 0; i < viewDescriptors.length; i ++ ) {
const textureView = textureData.texture.createView( viewDescriptors[ i ] );
textureViews.push( {
view: msaaTextures !== null ? msaaTextures[ i ].createView() : textureView,
resolveTarget: msaaTextures !== null && renderContext.renderTarget.resolveColorBuffer === true ? textureView : undefined,
depthSlice: undefined
} );
}
} else {
const layer = renderContext.activeCubeFace;
const textureView = textureData.texture.createView( {
dimension: GPUTextureViewDimension.TwoD,
baseArrayLayer: layer,
arrayLayerCount: 1
} );
textureViews.push( {
view: msaaTextures !== null ? msaaTextures[ layer ].createView() : textureView,
resolveTarget: msaaTextures !== null && renderContext.renderTarget.resolveColorBuffer === true ? textureView : undefined,
depthSlice: undefined
} );
}
return textureViews;
}
/**
* Returns the render pass descriptor for the given render context.
*
* @private
* @param {RenderContext} renderContext - The render context.
* @param {Object} colorAttachmentsConfig - Configuration object for the color attachments.
* @return {Object} The render pass descriptor.
*/
_getRenderPassDescriptor( renderContext, colorAttachmentsConfig = {} ) {
const renderTarget = renderContext.renderTarget;
const renderTargetData = this.get( renderTarget );
const hasExternalTexture = this._hasExternalTexture( renderContext );
let descriptors = renderTargetData.descriptors;
if ( descriptors === undefined ||
renderTargetData.width !== renderTarget.width ||
renderTargetData.height !== renderTarget.height ||
renderTargetData.samples !== renderTarget.samples ||
hasExternalTexture
) {
descriptors = {};
renderTargetData.descriptors = descriptors;
}
const cacheKey = renderContext.getCacheKey();
let descriptorBase = descriptors[ cacheKey ];
if ( descriptorBase === undefined || hasExternalTexture ) {
const textures = renderContext.textures;
const textureViews = [];
let sliceIndex;
const isRenderCameraDepthArray = this._isRenderCameraDepthArray( renderContext );
for ( let i = 0; i < textures.length; i ++ ) {
const textureData = this.get( textures[ i ] );
if ( textureData.externalTexture === true ) {
textureViews.push( ...this._createExternalTextureViews( renderContext, textureData ) );
continue;
}
_viewDescriptor.label = `colorAttachment_${ i }`;
_viewDescriptor.baseMipLevel = renderContext.activeMipmapLevel;
_viewDescriptor.mipLevelCount = 1;
_viewDescriptor.baseArrayLayer = renderContext.activeCubeFace;
_viewDescriptor.arrayLayerCount = 1;
_viewDescriptor.dimension = GPUTextureViewDimension.TwoD;
if ( renderTarget.isRenderTarget3D ) {
sliceIndex = renderContext.activeCubeFace;
_viewDescriptor.baseArrayLayer = 0;
_viewDescriptor.dimension = GPUTextureViewDimension.ThreeD;
} else if ( renderTarget.isRenderTarget && textures[ i ].image.depth > 1 ) {
if ( isRenderCameraDepthArray === true ) {
const cameras = renderContext.camera.cameras;
for ( let layer = 0; layer < cameras.length; layer ++ ) {
_viewDescriptor.baseArrayLayer = layer;
_viewDescriptor.arrayLayerCount = 1;
_viewDescriptor.dimension = GPUTextureViewDimension.TwoD;
const textureView = textureData.texture.createView( _viewDescriptor );
const msaaTexture = textureData.msaaTextures?.[ layer ];
textureViews.push( {
view: msaaTexture !== undefined ? msaaTexture.createView() : textureView,
resolveTarget: msaaTexture !== undefined && renderTarget.resolveColorBuffer === true ? textureView : undefined,
depthSlice: undefined
} );
}
} else {
_viewDescriptor.dimension = GPUTextureViewDimension.TwoDArray;
}
}
if ( isRenderCameraDepthArray !== true ) {
const textureView = textureData.texture.createView( _viewDescriptor );
let view, resolveTarget;
if ( textureData.msaaTexture !== undefined ) {
view = textureData.msaaTexture.createView();
resolveTarget = renderTarget.resolveColorBuffer === true ? textureView : undefined;
} else if ( textureData.msaaTextures !== undefined ) {
view = textureData.msaaTextures[ renderContext.activeCubeFace ].createView();
resolveTarget = renderTarget.resolveColorBuffer === true ? textureView : undefined;
} else {
view = textureView;
resolveTarget = undefined;
}
textureViews.push( {
view,
resolveTarget,
depthSlice: sliceIndex
} );
}
_viewDescriptor.reset();
}
const colorAttachments = [];
for ( let i = 0; i < textureViews.length; i ++ ) {
const viewInfo = textureViews[ i ];
const attachment = new GPURenderPassColorAttachment();
attachment.view = viewInfo.view;
attachment.depthSlice = viewInfo.depthSlice;
attachment.resolveTarget = viewInfo.resolveTarget;
colorAttachments.push( attachment );
}
descriptorBase = {
textureViews,
colorAttachments,
descriptor: new GPURenderPassDescriptor()
};
if ( renderContext.depth ) {
const depthTextureData = this.get( renderContext.depthTexture );
if ( renderContext.depthTexture.isArrayTexture || renderContext.depthTexture.isCubeTexture ) {
_viewDescriptor.dimension = GPUTextureViewDimension.TwoD;
_viewDescriptor.arrayLayerCount = 1;
_viewDescriptor.baseArrayLayer = renderContext.activeCubeFace;
}
const depthStencilAttachment = new GPURenderPassDepthStencilAttachment();
const msaaDepthTexture = depthTextureData.msaaTextures?.[ renderContext.activeCubeFace ];
depthStencilAttachment.view = msaaDepthTexture !== undefined ? msaaDepthTexture.createView() : depthTextureData.texture.createView( _viewDescriptor );
descriptorBase.depthStencilAttachment = depthStencilAttachment;
_viewDescriptor.reset();
}
descriptors[ cacheKey ] = descriptorBase;
renderTargetData.width = renderTarget.width;
renderTargetData.height = renderTarget.height;
renderTargetData.samples = renderTarget.samples;
renderTargetData.activeMipmapLevel = renderContext.activeMipmapLevel;
renderTargetData.activeCubeFace = renderContext.activeCubeFace;
}
const descriptor = descriptorBase.descriptor;
descriptor.reset();
// Apply dynamic properties to cached attachments
for ( let i = 0; i < descriptorBase.colorAttachments.length; i ++ ) {
const attachment = descriptorBase.colorAttachments[ i ];
let clearValue = { r: 0, g: 0, b: 0, a: 1 };
if ( i === 0 && colorAttachmentsConfig.clearValue ) {
clearValue = colorAttachmentsConfig.clearValue;
}
attachment.loadOp = colorAttachmentsConfig.loadOp || GPULoadOp.Load;
attachment.storeOp = colorAttachmentsConfig.storeOp || GPUStoreOp.Store;
attachment.clearValue = clearValue;
descriptor.colorAttachments.push( attachment );
}
if ( descriptorBase.depthStencilAttachment ) {
descriptor.depthStencilAttachment = descriptorBase.depthStencilAttachment;
}
return descriptor;
}
/**
* This method is executed at the beginning of a render call and prepares
* the WebGPU state for upcoming render calls
*
* @param {RenderContext} renderContext - The render context.
*/
beginRender( renderContext ) {
const renderContextData = this.get( renderContext );
//
const device = this.device;
const occlusionQueryCount = renderContext.occlusionQueryCount;
const occlusionData = this.getOcclusionData( renderContext );
let occlusionQuerySet;
if ( occlusionQueryCount > 0 ) {
if ( occlusionData.currentOcclusionQuerySet ) occlusionData.currentOcclusionQuerySet.destroy();
if ( occlusionData.currentOcclusionQueryBuffer ) occlusionData.currentOcclusionQueryBuffer.destroy();
// Get a reference to the array of objects with queries. The occlusionData property
// can be changed by another render pass before the buffer.mapAsyc() completes.
occlusionData.currentOcclusionQuerySet = occlusionData.occlusionQuerySet;
occlusionData.currentOcclusionQueryBuffer = occlusionData.occlusionQueryBuffer;
occlusionData.currentOcclusionQueryObjects = occlusionData.occlusionQueryObjects;
//
_querySetDescriptor.label = `occlusionQuerySet_${ renderContext.id }`;
_querySetDescriptor.type = 'occlusion';
_querySetDescriptor.count = occlusionQueryCount;
occlusionQuerySet = device.createQuerySet( _querySetDescriptor );
_querySetDescriptor.reset();
occlusionData.occlusionQuerySet = occlusionQuerySet;
occlusionData.occlusionQueryIndex = 0;
occlusionData.occlusionQueryObjects = new Array( occlusionQueryCount );
occlusionData.lastOcclusionObject = null;
} else if ( occlusionData.lastOcclusionObject !== undefined ) {
// invalidate if there is a stale query
occlusionData.lastOcclusionObject = undefined;
occlusionData.occlusionQuerySet.destroy();
occlusionData.occlusionQuerySet = undefined;
}
let descriptor;
if ( renderContext.textures === null ) {
descriptor = this._getDefaultRenderPassDescriptor();
} else {
descriptor = this._getRenderPassDescriptor( renderContext, { loadOp: GPULoadOp.Load } );
}
this.initTimestampQuery( TimestampQuery.RENDER, this.getTimestampUID( renderContext ), descriptor );
descriptor.occlusionQuerySet = occlusionQuerySet;
const depthStencilAttachment = descriptor.depthStencilAttachment;
const renderTarget = renderContext.renderTarget;
const discardColor = renderContext.sampleCount > 1 && renderTarget?.storeMultisampledColorBuffer === false;
const discardDepth = renderContext.sampleCount > 1 && renderTarget?.storeMultisampledDepthBuffer === false;
const discardStencil = renderContext.sampleCount > 1 && renderTarget?.storeMultisampledStencilBuffer === false;
// A fullscreen pass overwrites the external texture, so its previous contents do not need to be loaded.
const clearExternalColor = renderContext.fullscreenPass === true && this._hasExternalTexture( renderContext );
if ( renderContext.textures !== null ) {
const colorAttachments = descriptor.colorAttachments;
for ( let i = 0; i < colorAttachments.length; i ++ ) {
const colorAttachment = colorAttachments[ i ];
if ( renderContext.clearColor || discardColor || clearExternalColor ) {
const clearColor = renderContext.mrt ? renderContext.mrt.getClearColor( renderContext.textures[ i ].name ) : null;
if ( clearColor !== null ) {
colorAttachment.clearValue = clearColor;
} else if ( i === 0 ) {
colorAttachment.clearValue = renderContext.clearColorValue;
} else {
_clearValue.r = 0;
_clearValue.g = 0;
_clearValue.b = 0;
_clearValue.a = 1;
colorAttachment.clearValue = _clearValue;
}
colorAttachment.loadOp = GPULoadOp.Clear;
} else {
colorAttachment.loadOp = GPULoadOp.Load;
}
if ( discardColor ) {
colorAttachment.storeOp = GPUStoreOp.Discard;
} else {
colorAttachment.storeOp = GPUStoreOp.Store;
}
}
} else {
const colorAttachment = descriptor.colorAttachments[ 0 ];
if ( renderContext.clearColor ) {
colorAttachment.clearValue = renderContext.clearColorValue;
colorAttachment.loadOp = GPULoadOp.Clear;
} else {
colorAttachment.loadOp = GPULoadOp.Load;
}
colorAttachment.storeOp = GPUStoreOp.Store;
}
//
if ( renderContext.depth ) {
if ( renderContext.clearDepth || discardDepth ) {
depthStencilAttachment.depthClearValue = renderContext.clearDepthValue;
depthStencilAttachment.depthLoadOp = GPULoadOp.Clear;
} else {
depthStencilAttachment.depthLoadOp = GPULoadOp.Load;
}
if ( discardDepth ) {
depthStencilAttachment.depthStoreOp = GPUStoreOp.Discard;
} else {
depthStencilAttachment.depthStoreOp = GPUStoreOp.Store;
}
}
if ( renderContext.stencil ) {
if ( renderContext.clearStencil || discardStencil ) {
depthStencilAttachment.stencilClearValue = renderContext.clearStencilValue;
depthStencilAttachment.stencilLoadOp = GPULoadOp.Clear;
} else {
depthStencilAttachment.stencilLoadOp = GPULoadOp.Load;
}
if ( discardStencil ) {
depthStencilAttachment.stencilStoreOp = GPUStoreOp.Discard;
} else {
depthStencilAttachment.stencilStoreOp = GPUStoreOp.Store;
}
}
//
_commandEncoderDescriptor.label = 'renderContext_' + renderContext.id;
const encoder = device.createCommandEncoder( _commandEncoderDescriptor );
_commandEncoderDescriptor.reset();
// Layered render targets: prepare bundle encoders for each camera in the array camera.
if ( this._isRenderCameraDepthArray( renderContext ) === true ) {
const cameras = renderContext.camera.cameras;
// The layer descriptors hold views of the depth texture, which can be recreated (dispose, resize)
// or belong to another render target, since render contexts are shared by attachment state.
const depthTextureGPU = this.get( renderContext.depthTexture ).texture;
if ( ! renderContextData.layerDescriptors || renderContextData.layerDescriptors.length !== cameras.length || renderContextData.layerDepthTexture !== depthTextureGPU ) {
this._createArrayCameraLayerDescriptors( renderContext, renderContextData, descriptor, cameras );
renderContextData.layerDepthTexture = depthTextureGPU;
} else {
this._updateArrayCameraLayerDescriptors( renderContext, renderContextData, descriptor, cameras );
}
this._createArrayCameraBundleEncoders( renderContext, renderContextData );
renderContextData.arrayCameraRenderStages = [];
// We'll complete the bundles in finishRender
renderContextData.currentPass = null;
} else {
renderContextData.bundleEncoders = undefined;
renderContextData.bundleSets = undefined;
renderContextData.arrayCameraRenderStages = undefined;
const currentPass = encoder.beginRenderPass( descriptor );
renderContextData.currentPass = currentPass;
if ( renderContext.viewport ) {
this.updateViewport( renderContext );
}
if ( renderContext.scissor ) {
this.updateScissor( renderContext );
}
}
//
renderContextData.descriptor = descriptor;
renderContextData.encoder = encoder;
renderContextData.renderBundles = [];
this._resetRenderContextData( renderContextData );
}
/**
* Resets the state cache of the given render context data.
*
* A new render pass encoder starts with a blend constant and a stencil reference
* of zero so the cached values must be reset as well.
*
* @private
* @param {Object} renderContextData - The render context data.
*/
_resetRenderContextData( renderContextData ) {
renderContextData.currentSets = { attributes: {}, bindingGroups: [], pipeline: null, index: null };
if ( renderContextData.currentBlendColor === undefined ) renderContextData.currentBlendColor = new Color();
renderContextData.currentBlendColor.setRGB( 0, 0, 0 );
renderContextData.currentBlendAlpha = 0;
renderContextData.currentStencilRef = 0;
}
/**
* Creates a render bundle encoder and state cache for each camera layer.
*
* @param {RenderContext} renderContext - The render context.
* @param {Object} renderContextData - The render context data.
* @private
*/
_createArrayCameraBundleEncoders( renderContext, renderContextData ) {
const cameras = renderContext.camera.cameras;
renderContextData.bundleEncoders = [];
renderContextData.bundleSets = [];
for ( let i = 0; i < cameras.length; i ++ ) {
const bundleEncoder = this.pipelineUtils.createBundleEncoder(
renderContext,
'renderBundleArrayCamera_' + i
);
const bundleSets = {
attributes: {},
bindingGroups: [],
pipeline: null,
index: null
};
renderContextData.bundleEncoders.push( bundleEncoder );
renderContextData.bundleSets.push( bundleSets );
}
}
/**
* Finishes the render bundle encoders for all camera layers.
*
* @param {Object} renderContextData - The render context data.
* @return {Array<GPURenderBundle>} The completed render bundles.
* @private
*/
_finishArrayCameraBundleEncoders( renderContextData ) {
const bundles = [];
for ( const bundleEncoder of renderContextData.bundleEncoders ) {
bundles.push( bundleEncoder.finish() );
}
return bundles;
}
/**
* Creates render pass descriptors for each camera in an array camera.
*
* @param {RenderContext} renderContext - The render context.
* @param {Object} renderContextData - The render context data.
* @param {Object} descriptor - The render pass descriptor.
* @param {ArrayCamera} cameras - The array camera.
*
* @private
*/
_createArrayCameraLayerDescriptors( renderContext, renderContextData, descriptor, cameras ) {
const depthStencilAttachment = descriptor.depthStencilAttachment;
renderContextData.layerDescriptors = [];
const depthTextureData = this.get( renderContext.depthTexture );
if ( ! depthTextureData.viewCache ) {
depthTextureData.viewCache = [];
}
for ( let i = 0; i < cameras.length; i ++ ) {
const layerDescriptor = new GPURenderPassDescriptor();
layerDescriptor.label = descriptor.label;
layerDescriptor.occlusionQuerySet = descriptor.occlusionQuerySet;
layerDescriptor.timestampWrites = descriptor.timestampWrites;
if ( descriptor.colorAttachments.length > 0 ) {
const sourceAttachment = descriptor.colorAttachments[ 0 ];
const layerAttachment = descriptor.colorAttachments[ i ];
const layerColorAttachment = new GPURenderPassColorAttachment();
layerColorAttachment.view = layerAttachment.view;
layerColorAttachment.depthSlice = layerAttachment.depthSlice;
layerColorAttachment.resolveTarget = layerAttachment.resolveTarget;
layerColorAttachment.loadOp = sourceAttachment.loadOp;
layerColorAttachment.storeOp = sourceAttachment.storeOp;
layerColorAttachment.clearValue = sourceAttachment.clearValue;
layerDescriptor.colorAttachments.push( layerColorAttachment );
}
if ( descriptor.depthStencilAttachment ) {
const layerIndex = i;
if ( ! depthTextureData.viewCache[ layerIndex ] ) {
const msaaTexture = depthTextureData.msaaTextures?.[ layerIndex ];
if ( msaaTexture !== undefined ) {
depthTextureData.viewCache[ layerIndex ] = msaaTexture.createView();
} else {
_viewDescriptor.dimension = GPUTextureViewDimension.TwoD;
_viewDescriptor.baseArrayLayer = i;
_viewDescriptor.arrayLayerCount = 1;
depthTextureData.viewCache[ layerIndex ] = depthTextureData.texture.createView( _viewDescriptor );
}
_viewDescriptor.reset();
}
const layerDepthStencilAttachment = new GPURenderPassDepthStencilAttachment();
layerDepthStencilAttachment.view = depthTextureData.viewCache[ layerIndex ];
layerDepthStencilAttachment.depthLoadOp = depthStencilAttachment.depthLoadOp || GPULoadOp.Clear;
layerDepthStencilAttachment.depthStoreOp = depthStencilAttachment.depthStoreOp || GPUStoreOp.Store;
layerDepthStencilAttachment.depthClearValue = depthStencilAttachment.depthClearValue ?? 1.0;
if ( renderContext.stencil ) {
layerDepthStencilAttachment.stencilLoadOp = depthStencilAttachment.stencilLoadOp;
layerDepthStencilAttachment.stencilStoreOp = depthStencilAttachment.stencilStoreOp;
layerDepthStencilAttachment.stencilClearValue = depthStencilAttachment.stencilClearValue;
}
layerDescriptor.depthStencilAttachment = layerDepthStencilAttachment;
} else {
const layerDepthStencilAttachment = new GPURenderPassDepthStencilAttachment();
layerDepthStencilAttachment.view = depthStencilAttachment.view;
layerDepthStencilAttachment.depthLoadOp = depthStencilAttachment.depthLoadOp;
layerDepthStencilAttachment.depthStoreOp = depthStencilAttachment.depthStoreOp;
layerDepthStencilAttachment.depthClearValue = depthStencilAttachment.depthClearValue;
layerDepthStencilAttachment.depthReadOnly = depthStencilAttachment.depthReadOnly;
layerDepthStencilAttachment.stencilLoadOp = depthStencilAttachment.stencilLoadOp;
layerDepthStencilAttachment.stencilStoreOp = depthStencilAttachment.stencilStoreOp;
layerDepthStencilAttachment.stencilClearValue = depthStencilAttachment.stencilClearValue;
layerDepthStencilAttachment.stencilReadOnly = depthStencilAttachment.stencilReadOnly;
layerDescriptor.depthStencilAttachment = layerDepthStencilAttachment;
}
renderContextData.layerDescriptors.push( layerDescriptor );
}
}
/**
* Updates render pass descriptors for each camera in an array camera.
*
* @param {RenderContext} renderContext - The render context.
* @param {Object} renderContextData - The render context data.
* @param {Object} descriptor - The render pass descriptor.
* @param {ArrayCamera} cameras - The array camera.
*
*/
_updateArrayCameraLayerDescriptors( renderContext, renderContextData, descriptor, cameras ) {
const renderTarget = renderContext.renderTarget;
const discardDepth = renderContext.sampleCount > 1 && renderTarget?.storeMultisampledDepthBuffer === false;
const discardStencil = renderContext.sampleCount > 1 && renderTarget?.storeMultisampledStencilBuffer === false;
for ( let i = 0; i < cameras.length; i ++ ) {
const layerDescriptor = renderContextData.layerDescriptors[ i ];
layerDescriptor.timestampWrites = descriptor.timestampWrites;
if ( descriptor.colorAttachments.length > 0 ) {
const sourceColorAttachment = descriptor.colorAttachments[ 0 ];
const layerColorAttachment = descriptor.colorAttachments[ i ];
const colorAttachment = layerDescriptor.colorAttachments[ 0 ];
colorAttachment.view = layerColorAttachment.view;
colorAttachment.resolveTarget = layerColorAttachment.resolveTarget;
colorAttachment.depthSlice = layerColorAttachment.depthSlice;
colorAttachment.loadOp = sourceColorAttachment.loadOp;
colorAttachment.storeOp = sourceColorAttachment.storeOp;
colorAttachment.clearValue = sourceColorAttachment.clearValue;
}
if ( layerDescriptor.depthStencilAttachment ) {
const depthAttachment = layerDescriptor.depthStencilAttachment;
if ( renderContext.depth ) {
if ( renderContext.clearDepth || discardDepth ) {
depthAttachment.depthClearValue = renderContext.clearDepthValue;
depthAttachment.depthLoadOp = GPULoadOp.Clear;
} else {
depthAttachment.depthLoadOp = GPULoadOp.Load;
}
}
if ( renderContext.stencil ) {
if ( renderContext.clearStencil || discardStencil ) {
depthAttachment.stencilClearValue = renderContext.clearStencilValue;
depthAttachment.stencilLoadOp = GPULoadOp.Clear;
} else {
depthAttachment.stencilLoadOp = GPULoadOp.Load;
}
}
}
}
}
/**
* This method is executed at the end of a render call and finalizes work
* after draw calls.
*
* @param {RenderContext} renderContext - The render context.
*/
finishRender( renderContext ) {
const renderContextData = this.get( renderContext );
const occlusionQueryCount = renderContext.occlusionQueryCount;
const occlusionData = this.getOcclusionData( renderContext );
if ( renderContextData.renderBundles.length > 0 ) {
renderContextData.currentPass.executeBundles( renderContextData.renderBundles );
}
const lastOcclusionObject = occlusionData.lastOcclusionObject;
if ( lastOcclusionObject && lastOcclusionObject.occlusionTest === true ) {
renderContextData.currentPass.endOcclusionQuery();
}
// Layered render targets: execute the bundle for each layer.
const encoder = renderContextData.encoder;
if ( this._isRenderCameraDepthArray( renderContext ) === true ) {
const bundles = this._finishArrayCameraBundleEncoders( renderContextData );
const renderStages = renderContextData.arrayCameraRenderStages;
renderStages.push( { bundles } );
// A viewport texture is 2D, so each layer must be captured immediately before that layer samples it.
for ( let i = 0; i < renderContextData.layerDescriptors.length; i ++ ) {
const layerDescriptor = renderContextData.layerDescriptors[ i ];
const colorLoadOps = layerDescriptor.colorAttachments.map( attachment => attachment.loadOp );
const colorStoreOps = layerDescriptor.colorAttachments.map( attachment => attachment.storeOp );
const depthLoadOp = layerDescriptor.depthStencilAttachment?.depthLoadOp;
const depthStoreOp = layerDescriptor.depthStencilAttachment?.depthStoreOp;
const stencilLoadOp = layerDescriptor.depthStencilAttachment?.stencilLoadOp;
const stencilStoreOp = layerDescriptor.depthStencilAttachment?.stencilStoreOp;
for ( let stageIndex = 0; stageIndex < renderStages.length; stageIndex ++ ) {
const renderStage = renderStages[ stageIndex ];
const bundle = renderStage.bundles[ i ];
const isLastStage = stageIndex === renderStages.length - 1;
for ( let j = 0; j < layerDescriptor.colorAttachments.length; j ++ ) {
const attachment = layerDescriptor.colorAttachments[ j ];
attachment.loadOp = stageIndex === 0 ? colorLoadOps[ j ] : GPULoadOp.Load;
attachment.storeOp = isLastStage ? colorStoreOps[ j ] : GPUStoreOp.Store;
}
if ( renderContext.depth ) {
layerDescriptor.depthStencilAttachment.depthLoadOp = stageIndex === 0 ? depthLoadOp : GPULoadOp.Load;
layerDescriptor.depthStencilAttachment.depthStoreOp = isLastStage ? depthStoreOp : GPUStoreOp.Store;
}
if ( renderContext.stencil ) {
layerDescriptor.depthStencilAttachment.stencilLoadOp = stageIndex === 0 ? stencilLoadOp : GPULoadOp.Load;
layerDescriptor.depthStencilAttachment.stencilStoreOp = isLastStage ? stencilStoreOp : GPUStoreOp.Store;
}
const renderPass = encoder.beginRenderPass( layerDescriptor );
if ( renderContext.viewport ) {
const { x, y, width, height, minDepth, maxDepth } = renderContext.viewportValue;
renderPass.setViewport( x, y, width, height, minDepth, maxDepth );
}
if ( renderContext.scissor ) {
const { x, y, width, height } = renderContext.scissorValue;
renderPass.setScissorRect( x, y, width, height );
}
renderPass.executeBundles( [ bundle ] );
renderPass.end();
if ( renderStage.framebufferCopy !== undefined ) {
const { texture, sourceGPU, destinationGPU, rectangle, generateMipmaps } = renderStage.framebufferCopy;
this._copyFramebufferToTexture( encoder, texture, sourceGPU, destinationGPU, rectangle, i, generateMipmaps );
}
}
for ( let j = 0; j < layerDescriptor.colorAttachments.length; j ++ ) {
layerDescriptor.colorAttachments[ j ].loadOp = colorLoadOps[ j ];
layerDescriptor.colorAttachments[ j ].storeOp = colorStoreOps[ j ];
}
if ( renderContext.depth ) {
layerDescriptor.depthStencilAttachment.depthLoadOp = depthLoadOp;
layerDescriptor.depthStencilAttachment.depthStoreOp = depthStoreOp;
}
if ( renderContext.stencil ) {
layerDescriptor.depthStencilAttachment.stencilLoadOp = stencilLoadOp;
layerDescriptor.depthStencilAttachment.stencilStoreOp = stencilStoreOp;
}
}
} else if ( renderContextData.currentPass ) {
renderContextData.currentPass.end();
}
if ( occlusionQueryCount > 0 ) {
const bufferSize = occlusionQueryCount * 8; // 8 byte entries for query results
//
let queryResolveBuffer = this.occludedResolveCache.get( bufferSize );
if ( queryResolveBuffer === undefined ) {
_bufferDescriptor.size = bufferSize;
_bufferDescriptor.usage = GPUBufferUsage.QUERY_RESOLVE | GPUBufferUsage.COPY_SRC;
queryResolveBuffer = this.device.createBuffer( _bufferDescriptor );
_bufferDescriptor.reset();
this.occludedResolveCache.set( bufferSize, queryResolveBuffer );
}
//
_bufferDescriptor.size = bufferSize;
_bufferDescriptor.usage = GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ;
const readBuffer = this.device.createBuffer( _bufferDescriptor );
_bufferDescriptor.reset();
// two buffers required here - WebGPU doesn't allow usage of QUERY_RESOLVE & MAP_READ to be combined
renderContextData.encoder.resolveQuerySet( occlusionData.occlusionQuerySet, 0, occlusionQueryCount, queryResolveBuffer, 0 );
renderContextData.encoder.copyBufferToBuffer( queryResolveBuffer, 0, readBuffer, 0, bufferSize );
occlusionData.occlusionQueryBuffer = readBuffer;
//
this.resolveOccludedAsync( renderContext );
}
submit( this.device, renderContextData.encoder.finish() );
// pass and encoder are only valid between beginRender() and finishRender()
renderContextData.currentPass = null;
renderContextData.encoder = null;
//
if ( renderContext.textures !== null ) {
const textures = renderContext.textures;
for ( let i = 0; i < textures.length; i ++ ) {
const texture = textures[ i ];
if ( texture.generateMipmaps === true && texture.mipmapsAutoUpdate === true ) {
this.textureUtils.generateMipmaps( texture );
}
}
}
}
/**
* Returns `true` if the given 3D object is fully occluded by other
* 3D objects in the scene.
*
* @param {RenderContext} renderContext - The render context.
* @param {Object3D} object - The 3D object to test.
* @return {boolean} Whether the 3D object is fully occluded or not.
*/
isOccluded( renderContext, object ) {
const occlusionData = this.getOcclusionData( renderContext );
return occlusionData.occluded && occlusionData.occluded.has( object );
}
/**
* This method processes the result of occlusion queries and writes it
* into the occlusion data of the render context.
*
* @async
* @param {RenderContext} renderContext - The render context.
* @return {Promise} A Promise that resolves when the occlusion query results have been processed.
*/
async resolveOccludedAsync( renderContext ) {
const occlusionData = this.getOcclusionData( renderContext );
// handle occlusion query results
const { currentOcclusionQueryBuffer, currentOcclusionQueryObjects } = occlusionData;
if ( currentOcclusionQueryBuffer && currentOcclusionQueryObjects ) {
const occluded = new WeakSet();
occlusionData.currentOcclusionQueryObjects = null;
occlusionData.currentOcclusionQueryBuffer = null;
await currentOcclusionQueryBuffer.mapAsync( GPUMapMode.READ );
const buffer = currentOcclusionQueryBuffer.getMappedRange();
const results = new BigUint64Array( buffer );
for ( let i = 0; i < currentOcclusionQueryObjects.length; i ++ ) {
const object = currentOcclusionQueryObjects[ i ];
if ( object !== undefined && results[ i ] === BigInt( 0 ) ) {
occluded.add( object );
}
}
currentOcclusionQueryBuffer.destroy();
occlusionData.occluded = occluded;
}
}
/**
* Updates the viewport with the values from the given render context.
*
* @param {RenderContext} renderContext - The render context.
*/
updateViewport( renderContext ) {
const { currentPass } = this.get( renderContext );
const { x, y, width, height, minDepth, maxDepth } = renderContext.viewportValue;
currentPass.setViewport( x, y, width, height, minDepth, maxDepth );
}
/**
* Updates the scissor with the values from the given render context.
*
* @param {RenderContext} renderContext - The render context.
*/
updateScissor( renderContext ) {
const { currentPass } = this.get( renderContext );
const { x, y, width, height } = renderContext.scissorValue;
currentPass.setScissorRect( x, y, width, height );
}
/**
* Returns the clear color and alpha into a single
* color object.
*
* @return {Color4} The clear color.
*/
getClearColor() {
const clearColor = super.getClearColor();
// only premultiply alpha when alphaMode is "premultiplied"
if ( this.renderer.alpha === true ) {
clearColor.r *= clearColor.a;
clearColor.g *= clearColor.a;
clearColor.b *= clearColor.a;
}
return clearColor;
}
/**
* Performs a clear operation.
*
* @param {boolean} color - Whether the color buffer should be cleared or not.
* @param {boolean} depth - Whether the depth buffer should be cleared or not.
* @param {boolean} stencil - Whether the stencil buffer should be cleared or not.
* @param {?RenderContext} [renderTargetContext=null] - The render context of the current set render target.
*/
clear( color, depth, stencil, renderTargetContext = null ) {
const device = this.device;
const renderer = this.renderer;
let colorAttachments = [];
let depthStencilAttachment;
let supportsDepth;
let supportsStencil;
if ( color ) {
const clearColor = this.getClearColor();
_clearValue.r = clearColor.r;
_clearValue.g = clearColor.g;
_clearValue.b = clearColor.b;
_clearValue.a = clearColor.a;
}
if ( renderTargetContext === null ) {
supportsDepth = renderer.depth;
supportsStencil = renderer.stencil;
const descriptor = this._getDefaultRenderPassDescriptor();
if ( color ) {
colorAttachments = descriptor.colorAttachments;
const colorAttachment = colorAttachments[ 0 ];
colorAttachment.clearValue = _clearValue;
colorAttachment.loadOp = GPULoadOp.Clear;
colorAttachment.storeOp = GPUStoreOp.Store;
}
if ( supportsDepth || supportsStencil ) {
depthStencilAttachment = descriptor.depthStencilAttachment;
}
} else {
supportsDepth = renderTargetContext.depth;
supportsStencil = renderTargetContext.stencil;
const clearConfig = {
loadOp: color ? GPULoadOp.Clear : GPULoadOp.Load,
clearValue: color ? _clearValue : undefined
};
if ( supportsDepth ) {
clearConfig.depthLoadOp = depth ? GPULoadOp.Clear : GPULoadOp.Load;
clearConfig.depthClearValue = depth ? renderer.getClearDepth() : undefined;
clearConfig.depthStoreOp = GPUStoreOp.Store;
}
if ( supportsStencil ) {
clearConfig.stencilLoadOp = stencil ? GPULoadOp.Clear : GPULoadOp.Load;
clearConfig.stencilClearValue = stencil ? renderer.getClearStencil() : undefined;
clearConfig.stencilStoreOp = GPUStoreOp.Store;
}
const descriptor = this._getRenderPassDescriptor( renderTargetContext, clearConfig );
colorAttachments = descriptor.colorAttachments;
depthStencilAttachment = descriptor.depthStencilAttachment;
}
if ( supportsDepth && depthStencilAttachment ) {
if ( depth ) {
depthStencilAttachment.depthLoadOp = GPULoadOp.Clear;
depthStencilAttachment.depthClearValue = renderer.getClearDepth();
depthStencilAttachment.depthStoreOp = GPUStoreOp.Store;
} else {
depthStencilAttachment.depthLoadOp = GPULoadOp.Load;
depthStencilAttachment.depthStoreOp = GPUStoreOp.Store;
}
}
//
if ( supportsStencil && depthStencilAttachment ) {
if ( stencil ) {
depthStencilAttachment.stencilLoadOp = GPULoadOp.Clear;
depthStencilAttachment.stencilClearValue = renderer.getClearStencil();
depthStencilAttachment.stencilStoreOp = GPUStoreOp.Store;
} else {
depthStencilAttachment.stencilLoadOp = GPULoadOp.Load;
depthStencilAttachment.stencilStoreOp = GPUStoreOp.Store;
}
}
//
_commandEncoderDescriptor.label = 'clear';
const encoder = device.createCommandEncoder( _commandEncoderDescriptor );
_commandEncoderDescriptor.reset();
const currentPass = encoder.beginRenderPass( {
colorAttachments,
depthStencilAttachment
} );
currentPass.end();
submit( device, encoder.finish() );
}
// compute
/**
* This method is executed at the beginning of a compute call and
* prepares the state for upcoming compute tasks.
*
* @param {Node|Array<Node>} computeGroup - The compute node(s).
*/
beginCompute( computeGroup ) {
const groupGPU = this.get( computeGroup );
//
const label = 'computeGroup_' + computeGroup.id;
_computePassDescriptor.label = label;
_commandEncoderDescriptor.label = label;
this.initTimestampQuery( TimestampQuery.COMPUTE, this.getTimestampUID( computeGroup ), _computePassDescriptor );
groupGPU.cmdEncoderGPU = this.device.createCommandEncoder( _commandEncoderDescriptor );
groupGPU.passEncoderGPU = groupGPU.cmdEncoderGPU.beginComputePass( _computePassDescriptor );
groupGPU.currentPipeline = null;
_commandEncoderDescriptor.reset();
_computePassDescriptor.reset();
}
/**
* Executes a compute command for the given compute node.
*
* @param {Node|Array<Node>} computeGroup - The group of compute nodes of a compute call. Can be a single compute node.
* @param {Node} computeNode - The compute node.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {ComputePipeline} pipeline - The compute pipeline.
* @param {number|Array<number>|IndirectStorageBufferAttribute} [dispatchSize=null]
* - A single number representing count, or
* - An array [x, y, z] representing dispatch size, or
* - A IndirectStorageBufferAttribute for indirect dispatch size.
*/
compute( computeGroup, computeNode, bindings, pipeline, dispatchSize = null ) {
const computeNodeData = this.get( computeNode );
const groupGPU = this.get( computeGroup );
const { passEncoderGPU } = groupGPU;
// pipeline
const pipelineGPU = this.get( pipeline ).pipeline;
if ( groupGPU.currentPipeline !== pipelineGPU ) {
passEncoderGPU.setPipeline( pipelineGPU );
groupGPU.currentPipeline = pipelineGPU;
}
// bind groups
for ( let i = 0, l = bindings.length; i < l; i ++ ) {
const bindGroup = bindings[ i ];
const bindingsData = this.get( bindGroup );
passEncoderGPU.setBindGroup( i, bindingsData.group );
}
if ( dispatchSize === null ) {
dispatchSize = computeNode.dispatchSize || computeNode.count;
}
// When the dispatchSize is set with a StorageBuffer from the GPU.
if ( dispatchSize && dispatchSize.isIndirectStorageBufferAttribute ) {
const dispatchBuffer = this.get( dispatchSize ).buffer;
passEncoderGPU.dispatchWorkgroupsIndirect( dispatchBuffer, 0 );
return;
}
if ( typeof dispatchSize === 'number' ) {
// If a single number is given, we calculate the dispatch size based on the workgroup size
const count = dispatchSize;
if ( computeNodeData.dispatchSize === undefined || computeNodeData.count !== count ) {
// cache dispatch size to avoid recalculating it every time
computeNodeData.dispatchSize = [ 0, 1, 1 ];
computeNodeData.count = count;
const workgroupSize = computeNode.workgroupSize;
let size = workgroupSize[ 0 ];
for ( let i = 1; i < workgroupSize.length; i ++ )
size *= workgroupSize[ i ];
const dispatchCount = Math.ceil( count / size );
//
const maxComputeWorkgroupsPerDimension = this.device.limits.maxComputeWorkgroupsPerDimension;
dispatchSize = [ dispatchCount, 1, 1 ];
if ( dispatchCount > maxComputeWorkgroupsPerDimension ) {
dispatchSize[ 0 ] = Math.min( dispatchCount, maxComputeWorkgroupsPerDimension );
dispatchSize[ 1 ] = Math.ceil( dispatchCount / maxComputeWorkgroupsPerDimension );
}
computeNodeData.dispatchSize = dispatchSize;
}
dispatchSize = computeNodeData.dispatchSize;
}
//
passEncoderGPU.dispatchWorkgroups(
dispatchSize[ 0 ],
dispatchSize[ 1 ] || 1,
dispatchSize[ 2 ] || 1
);
}
/**
* This method is executed at the end of a compute call and
* finalizes work after compute tasks.
*
* @param {Node|Array<Node>} computeGroup - The compute node(s).
*/
finishCompute( computeGroup ) {
const groupData = this.get( computeGroup );
groupData.passEncoderGPU.end();
submit( this.device, groupData.cmdEncoderGPU.finish() );
}
/**
* Internal draw function that performs the draw with the given pass encoder.
*
* @private
* @param {RenderObject} renderObject - The render object.
* @param {Info} info - Holds a series of statistical information about the GPU memory and the rendering process.
* @param {Object} renderContextData - The render context data object, holding current pass state and occlusion query tracking.
* @param {GPURenderPipeline} pipelineGPU - The GPU render pipeline.
* @param {Array<BindGroup>} bindings - The bind groups.
* @param {Array<BufferAttribute>} vertexBuffers - The vertex buffers.
* @param {{vertexCount: number, firstVertex: number, instanceCount: number, firstInstance: number}} drawParams - The draw parameters.
* @param {GPURenderPassEncoder|GPURenderBundleEncoder} passEncoderGPU - The GPU pass encoder used for recording draw commands.
* @param {Object} currentSets - Tracking object for currently set pipeline, attributes, bind groups, and index state.
* @param {number} [cameraIndexSlot=-1] - Index of the binding slot holding the camera index bind group.
*/
_draw( renderObject, info, renderContextData, pipelineGPU, bindings, vertexBuffers, drawParams, passEncoderGPU, currentSets, cameraIndexSlot = -1 ) {
const { object, material, context } = renderObject;
const index = renderObject.getIndex();
const hasIndex = ( index !== null );
// pipeline
if ( currentSets.pipeline !== pipelineGPU ) {
passEncoderGPU.setPipeline( pipelineGPU );
currentSets.pipeline = pipelineGPU;
}
// bind groups
const currentBindingGroups = currentSets.bindingGroups;
for ( let i = 0, l = bindings.length; i < l; i ++ ) {
// the camera index slot is bound per sub-camera in draw()
if ( i === cameraIndexSlot ) continue;
// compare native bind groups
const bindGroup = bindings[ i ];
const bindGroupGPU = this.get( bindGroup ).group;
if ( currentBindingGroups[ i ] !== bindGroupGPU ) {
passEncoderGPU.setBindGroup( i, bindGroupGPU );
currentBindingGroups[ i ] = bindGroupGPU;
}
}
// attributes
// index
if ( hasIndex === true ) {
if ( currentSets.index !== index ) {
const buffer = this.get( index ).buffer;
const indexFormat = ( index.array instanceof Uint16Array ) ? GPUIndexFormat.Uint16 : GPUIndexFormat.Uint32;
passEncoderGPU.setIndexBuffer( buffer, indexFormat );
currentSets.index = index;
}
}
for ( let i = 0, l = vertexBuffers.length; i < l; i ++ ) {
const vertexBuffer = vertexBuffers[ i ];
if ( currentSets.attributes[ i ] !== vertexBuffer ) {
const buffer = this.get( vertexBuffer ).buffer;
passEncoderGPU.setVertexBuffer( i, buffer );
currentSets.attributes[ i ] = vertexBuffer;
}
}
// stencil
if ( context.stencil === true && material.stencilWrite === true && renderContextData.currentStencilRef !== material.stencilRef ) {
passEncoderGPU.setStencilReference( material.stencilRef );
renderContextData.currentStencilRef = material.stencilRef;
}
// blend constant
if ( material.blending === CustomBlending && passEncoderGPU.setBlendConstant !== undefined ) {
const blendColor = material.blendColor;
const blendAlpha = material.blendAlpha;
if ( blendColor.equals( renderContextData.currentBlendColor ) === false || blendAlpha !== renderContextData.currentBlendAlpha ) {
_blendConstant.r = blendColor.r;
_blendConstant.g = blendColor.g;
_blendConstant.b = blendColor.b;
_blendConstant.a = blendAlpha;
passEncoderGPU.setBlendConstant( _blendConstant );
renderContextData.currentBlendColor.copy( blendColor );
renderContextData.currentBlendAlpha = blendAlpha;
}
}
if ( object.isBatchedMesh === true ) {
const starts = object._mergedDrawStarts;
const counts = object._mergedDrawCounts;
const instanceCounts = object._mergedDrawInstanceCounts;
const drawCount = object._mergedDrawCount;
const bytesPerElement = object._multiDrawBytesPerElement;
let firstInstance = 0;
for ( let i = 0; i < drawCount; i ++ ) {
const instanceCount = instanceCounts[ i ];
if ( hasIndex === true ) {
passEncoderGPU.drawIndexed( counts[ i ], instanceCount, starts[ i ] / bytesPerElement, 0, firstInstance );
} else {
passEncoderGPU.draw( counts[ i ], instanceCount, starts[ i ], firstInstance );
}
info.update( object, counts[ i ], instanceCount );
firstInstance += instanceCount;
}
} else if ( hasIndex === true ) {
const { vertexCount: indexCount, instanceCount, firstVertex: firstIndex } = drawParams;
const indirect = renderObject.getIndirect();
if ( indirect !== null ) {
const buffer = this.get( indirect ).buffer;
const indirectOffset = renderObject.getIndirectOffset();
const indirectOffsets = Array.isArray( indirectOffset ) ? indirectOffset : [ indirectOffset ];
for ( let i = 0; i < indirectOffsets.length; i ++ ) {
passEncoderGPU.drawIndexedIndirect( buffer, indirectOffsets[ i ] );
}
} else {
passEncoderGPU.drawIndexed( indexCount, instanceCount, firstIndex, 0, 0 );
info.update( object, indexCount, instanceCount );
}
} else {
const { vertexCount, instanceCount, firstVertex } = drawParams;
const indirect = renderObject.getIndirect();
if ( indirect !== null ) {
const buffer = this.get( indirect ).buffer;
const indirectOffset = renderObject.getIndirectOffset();
const indirectOffsets = Array.isArray( indirectOffset ) ? indirectOffset : [ indirectOffset ];
for ( let i = 0; i < indirectOffsets.length; i ++ ) {
passEncoderGPU.drawIndirect( buffer, indirectOffsets[ i ] );
}
} else {
passEncoderGPU.draw( vertexCount, instanceCount, firstVertex, 0 );
info.update( object, vertexCount, instanceCount );
}
}
}
// render object
/**
* Executes a draw command for the given render object.
*
* @param {RenderObject} renderObject - The render object to draw.
* @param {Info} info - Holds a series of statistical information about the GPU memory and the rendering process.
*/
draw( renderObject, info ) {
const { object, context, pipeline } = renderObject;
const renderContextData = this.get( context );
const pipelineData = this.get( pipeline );
const pipelineGPU = pipelineData.pipeline;
// Skip if pipeline has error
if ( pipelineData.error === true ) return;
const drawParams = renderObject.getDrawParameters();
if ( drawParams === null ) return;
const bindings = renderObject.getBindings();
// vertex buffers
const vertexBuffers = renderObject.getVertexBuffers();
if ( renderObject.camera.isArrayCamera && renderObject.camera.cameras.length > 0 ) {
const cameraData = this.get( renderObject.camera );
const cameras = renderObject.camera.cameras;
const cameraIndex = renderObject.getBindingGroup( 'cameraIndex' );
if ( cameraData.indexesGPU === undefined || cameraData.indexesGPU.length !== cameras.length ) {
const bindingsData = this.get( cameraIndex );
const indexesGPU = [];
const data = new Uint32Array( [ 0, 0, 0, 0 ] );
for ( let i = 0, len = cameras.length; i < len; i ++ ) {
data[ 0 ] = i;
const { layoutGPU } = bindingsData.layout;
const bindGroupIndex = this.bindingUtils.createBindGroupIndex( data, layoutGPU );
indexesGPU.push( bindGroupIndex );
}
cameraData.indexesGPU = indexesGPU; // TODO: Create a global library for this
}
const renderTarget = context.renderTarget;
const pixelRatio = ( renderTarget === null || renderTarget.isPostProcessingRenderTarget === true ) ? this.renderer.getPixelRatio() : 1;
const indexPos = cameraIndex ? bindings.indexOf( cameraIndex ) : -1;
const frustumArray = context.frustumArray;
for ( let i = 0, len = cameras.length; i < len; i ++ ) {
const subCamera = cameras[ i ];
if ( object.layers.test( subCamera.layers ) && ( renderObject.camera.perCameraFrustumCulled === false || object.frustumCulled === false || object.intersectsFrustum( frustumArray.getFrustum( i ) ) ) ) {
const vp = subCamera.viewport;
let pass = renderContextData.currentPass;
let sets = renderContextData.currentSets;
const isBundleEncoder = renderContextData.bundleEncoders !== undefined;
if ( isBundleEncoder ) {
const bundleEncoder = renderContextData.bundleEncoders[ i ];
const bundleSets = renderContextData.bundleSets[ i ];
pass = bundleEncoder;
sets = bundleSets;
}
// GPURenderBundleEncoder does not support setViewport, only GPURenderPassEncoder does
if ( vp && ! isBundleEncoder ) {
pass.setViewport(
Math.floor( vp.x * pixelRatio ),
Math.floor( vp.y * pixelRatio ),
Math.floor( vp.width * pixelRatio ),
Math.floor( vp.height * pixelRatio ),
context.viewportValue.minDepth,
context.viewportValue.maxDepth
);
}
// Set camera index binding for this layer
if ( indexPos !== -1 && cameraData.indexesGPU ) {
const cameraIndexGPU = cameraData.indexesGPU[ i ];
if ( sets.bindingGroups[ indexPos ] !== cameraIndexGPU ) {
pass.setBindGroup( indexPos, cameraIndexGPU );
sets.bindingGroups[ indexPos ] = cameraIndexGPU;
}
}
this._draw( renderObject, info, renderContextData, pipelineGPU, bindings, vertexBuffers, drawParams, pass, sets, indexPos );
}
}
} else {
// Regular single camera rendering
if ( renderContextData.currentPass ) {
// Handle occlusion queries
if ( renderContextData.currentPass instanceof GPURenderBundleEncoder ) {
if ( object.occlusionTest === true ) {
warnOnce( 'WebGPUBackend: Occlusion queries can not be recorded into render bundles.' );
}
} else if ( context.occlusionQueryCount > 0 ) {
const occlusionData = this.getOcclusionData( context );
const lastObject = occlusionData.lastOcclusionObject;
if ( lastObject !== object ) {
if ( lastObject !== null && lastObject.occlusionTest === true ) {
renderContextData.currentPass.endOcclusionQuery();
occlusionData.occlusionQueryIndex ++;
}
if ( object.occlusionTest === true ) {
renderContextData.currentPass.beginOcclusionQuery( occlusionData.occlusionQueryIndex );
occlusionData.occlusionQueryObjects[ occlusionData.occlusionQueryIndex ] = object;
}
occlusionData.lastOcclusionObject = object;
}
}
this._draw( renderObject, info, renderContextData, pipelineGPU, bindings, vertexBuffers, drawParams, renderContextData.currentPass, renderContextData.currentSets );
}
}
}
// cache key
/**
* Returns `true` if the render pipeline requires an update.
*
* @param {RenderObject} renderObject - The render object.
* @return {boolean} Whether the render pipeline requires an update or not.
*/
needsRenderUpdate( renderObject ) {
const data = this.get( renderObject );
const { object, material, geometry } = renderObject;
const utils = this.utils;
const sampleCount = utils.getSampleCountRenderContext( renderObject.context );
const colorSpace = utils.getCurrentColorSpace( renderObject.context );
const colorFormat = utils.getCurrentColorFormat( renderObject.context );
const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context );
const primitiveTopology = utils.getPrimitiveTopology( object, material );
const stripIndexFormat = utils.getStripIndexFormat( object, geometry );
const frontFaceCW = ( object.isMesh && object.matrixWorld.determinantAffine() < 0 );
let needsUpdate = false;
if ( data.material !== material || data.materialVersion !== material.version ||
data.transparent !== material.transparent || data.blending !== material.blending || data.premultipliedAlpha !== material.premultipliedAlpha ||
data.blendSrc !== material.blendSrc || data.blendDst !== material.blendDst || data.blendEquation !== material.blendEquation ||
data.blendSrcAlpha !== material.blendSrcAlpha || data.blendDstAlpha !== material.blendDstAlpha || data.blendEquationAlpha !== material.blendEquationAlpha ||
data.colorWrite !== material.colorWrite || data.depthWrite !== material.depthWrite || data.depthTest !== material.depthTest || data.depthFunc !== material.depthFunc ||
data.stencilWrite !== material.stencilWrite || data.stencilFunc !== material.stencilFunc ||
data.stencilFail !== material.stencilFail || data.stencilZFail !== material.stencilZFail || data.stencilZPass !== material.stencilZPass ||
data.stencilFuncMask !== material.stencilFuncMask || data.stencilWriteMask !== material.stencilWriteMask ||
data.polygonOffset !== material.polygonOffset || data.polygonOffsetFactor !== material.polygonOffsetFactor || data.polygonOffsetUnits !== material.polygonOffsetUnits ||
data.side !== material.side || data.alphaToCoverage !== material.alphaToCoverage ||
data.sampleCount !== sampleCount || data.colorSpace !== colorSpace ||
data.colorFormat !== colorFormat || data.depthStencilFormat !== depthStencilFormat ||
data.primitiveTopology !== primitiveTopology || data.stripIndexFormat !== stripIndexFormat ||
data.frontFaceCW !== frontFaceCW ||
data.geometryVersion !== renderObject.geometryVersion ||
data.clippingContextCacheKey !== renderObject.clippingContextCacheKey
) {
data.material = material; data.materialVersion = material.version;
data.transparent = material.transparent; data.blending = material.blending; data.premultipliedAlpha = material.premultipliedAlpha;
data.blendSrc = material.blendSrc; data.blendDst = material.blendDst; data.blendEquation = material.blendEquation;
data.blendSrcAlpha = material.blendSrcAlpha; data.blendDstAlpha = material.blendDstAlpha; data.blendEquationAlpha = material.blendEquationAlpha;
data.colorWrite = material.colorWrite;
data.depthWrite = material.depthWrite; data.depthTest = material.depthTest; data.depthFunc = material.depthFunc;
data.stencilWrite = material.stencilWrite; data.stencilFunc = material.stencilFunc;
data.stencilFail = material.stencilFail; data.stencilZFail = material.stencilZFail; data.stencilZPass = material.stencilZPass;
data.stencilFuncMask = material.stencilFuncMask; data.stencilWriteMask = material.stencilWriteMask;
data.polygonOffset = material.polygonOffset; data.polygonOffsetFactor = material.polygonOffsetFactor; data.polygonOffsetUnits = material.polygonOffsetUnits;
data.side = material.side; data.alphaToCoverage = material.alphaToCoverage;
data.sampleCount = sampleCount;
data.colorSpace = colorSpace;
data.colorFormat = colorFormat;
data.depthStencilFormat = depthStencilFormat;
data.primitiveTopology = primitiveTopology;
data.stripIndexFormat = stripIndexFormat;
data.frontFaceCW = frontFaceCW;
data.geometryVersion = renderObject.geometryVersion;
data.clippingContextCacheKey = renderObject.clippingContextCacheKey;
needsUpdate = true;
}
return needsUpdate;
}
/**
* Returns a cache key that is used to identify render pipelines.
*
* @param {RenderObject} renderObject - The render object.
* @return {string} The cache key.
*/
getRenderCacheKey( renderObject ) {
const { object, material, geometry } = renderObject;
const utils = this.utils;
const renderContext = renderObject.context;
// meshes with negative scale have a different frontFace render pipeline
// descriptor value so the following must be honored in the cache key
const frontFaceCW = ( object.isMesh && object.matrixWorld.determinantAffine() < 0 );
return [
material.transparent, material.blending, material.premultipliedAlpha,
material.blendSrc, material.blendDst, material.blendEquation,
material.blendSrcAlpha, material.blendDstAlpha, material.blendEquationAlpha,
material.colorWrite,
material.depthWrite, material.depthTest, material.depthFunc,
material.stencilWrite, material.stencilFunc,
material.stencilFail, material.stencilZFail, material.stencilZPass,
material.stencilFuncMask, material.stencilWriteMask,
material.polygonOffset, material.polygonOffsetFactor, material.polygonOffsetUnits,
material.side, material.alphaToCoverage,
frontFaceCW,
utils.getSampleCountRenderContext( renderContext ),
utils.getCurrentColorSpace( renderContext ), utils.getCurrentColorFormat( renderContext ), utils.getCurrentDepthStencilFormat( renderContext ),
utils.getPrimitiveTopology( object, material ), utils.getStripIndexFormat( object, geometry ),
this.attributeUtils.getVertexBufferLayoutCacheKey( renderObject ),
renderObject.clippingContextCacheKey
].join();
}
// textures
/**
* Updates a GPU sampler for the given texture.
*
* @param {Sampler} binding - The sampler binding to update.
* @return {string} The current sampler key.
*/
updateSampler( binding ) {
return this.textureUtils.updateSampler( binding );
}
/**
* Frees the GPU sampler for the given sampler binding.
*
* @param {Sampler} binding - The sampler binding to free.
*/
destroySampler( binding ) {
this.textureUtils.destroySampler( binding );
}
/**
* Creates a default texture for the given texture that can be used
* as a placeholder until the actual texture is ready for usage.
*
* @param {Texture} texture - The texture to create a default texture for.
* @return {boolean} Whether the sampler has been updated or not.
*/
createDefaultTexture( texture ) {
return this.textureUtils.createDefaultTexture( texture );
}
/**
* Defines a texture on the GPU for the given texture object.
*
* @param {Texture} texture - The texture.
* @param {Object} [options={}] - Optional configuration parameter.
*/
createTexture( texture, options ) {
this.textureUtils.createTexture( texture, options );
}
/**
* Uploads the updated texture data to the GPU.
*
* @param {Texture} texture - The texture.
* @param {Object} [options={}] - Optional configuration parameter.
*/
updateTexture( texture, options ) {
this.textureUtils.updateTexture( texture, options );
}
/**
* Generates mipmaps for the given texture.
*
* @param {Texture} texture - The texture.
*/
generateMipmaps( texture ) {
this.textureUtils.generateMipmaps( texture );
}
/**
* Destroys the GPU data for the given texture object.
*
* @param {Texture} texture - The texture.
* @param {boolean} [isDefaultTexture=false] - Whether the texture uses a default GPU texture or not.
*/
destroyTexture( texture, isDefaultTexture = false ) {
this.textureUtils.destroyTexture( texture, isDefaultTexture );
}
/**
* Returns texture data as a typed array.
*
* @async
* @param {Texture} texture - The texture to copy.
* @param {number} x - The x coordinate of the copy origin.
* @param {number} y - The y coordinate of the copy origin.
* @param {number} width - The width of the copy.
* @param {number} height - The height of the copy.
* @param {number} faceIndex - The cube face, depth slice or array layer index.
* @return {Promise<TypedArray>} A Promise that resolves with a typed array when the copy operation has finished.
*/
async copyTextureToBuffer( texture, x, y, width, height, faceIndex ) {
return this.textureUtils.copyTextureToBuffer( texture, x, y, width, height, faceIndex );
}
/**
* Inits a time stamp query for the given render context.
*
* @param {string} type - The type of the timestamp query (e.g. 'render', 'compute').
* @param {number} uid - Unique id for the context (e.g. render context id).
* @param {Object} descriptor - The query descriptor.
*/
initTimestampQuery( type, uid, descriptor ) {
if ( ! this.trackTimestamp ) {
descriptor.timestampWrites = undefined;
return;
}
if ( ! this.timestampQueryPool[ type ] ) {
// TODO: Variable maxQueries?
this.timestampQueryPool[ type ] = new WebGPUTimestampQueryPool( this.device, type, 2048 );
}
const timestampQueryPool = this.timestampQueryPool[ type ];
const baseOffset = timestampQueryPool.allocateQueriesForContext( uid );
_renderPassTimestampWrites.querySet = timestampQueryPool.querySet;
_renderPassTimestampWrites.beginningOfPassWriteIndex = baseOffset;
_renderPassTimestampWrites.endOfPassWriteIndex = baseOffset + 1;
descriptor.timestampWrites = _renderPassTimestampWrites;
}
// node builder
/**
* Returns a node builder for the given render object.
*
* @param {RenderObject} object - The render object.
* @param {Renderer} renderer - The renderer.
* @return {WGSLNodeBuilder} The node builder.
*/
createNodeBuilder( object, renderer ) {
return new WGSLNodeBuilder( object, renderer );
}
// program
/**
* Creates a shader program from the given programmable stage.
*
* @param {ProgrammableStage} program - The programmable stage.
*/
createProgram( program ) {
const programGPU = this.get( program );
_shaderModuleDescriptor.label = program.stage + ( program.name !== '' ? `_${ program.name }` : '' );
_shaderModuleDescriptor.code = program.code;
programGPU.module = {
module: this.device.createShaderModule( _shaderModuleDescriptor ),
entryPoint: 'main'
};
_shaderModuleDescriptor.reset();
}
/**
* Destroys the shader program of the given programmable stage.
*
* @param {ProgrammableStage} program - The programmable stage.
*/
destroyProgram( program ) {
this.delete( program );
}
// pipelines
/**
* 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 ) {
this.pipelineUtils.createRenderPipeline( renderObject, promises );
}
/**
* Creates a compute pipeline for the given compute node.
*
* @param {ComputePipeline} computePipeline - The compute pipeline.
* @param {Array<BindGroup>} bindings - The bindings.
* @param {?Array<Promise>} [promises=null] - Optional compilation promises.
*/
createComputePipeline( computePipeline, bindings, promises = null ) {
this.pipelineUtils.createComputePipeline( computePipeline, bindings, promises );
}
/**
* Prepares the state for encoding render bundles.
*
* @param {RenderContext} renderContext - The render context.
*/
beginBundle( renderContext ) {
const renderContextData = this.get( renderContext );
renderContextData._currentPass = renderContextData.currentPass;
renderContextData._currentSets = renderContextData.currentSets;
renderContextData.currentSets = { attributes: {}, bindingGroups: [], pipeline: null, index: null };
renderContextData.currentPass = this.pipelineUtils.createBundleEncoder( renderContext );
}
/**
* After processing render bundles this method finalizes related work.
*
* @param {RenderContext} renderContext - The render context.
* @param {RenderBundle} bundle - The render bundle.
*/
finishBundle( renderContext, bundle ) {
const renderContextData = this.get( renderContext );
const bundleEncoder = renderContextData.currentPass;
const bundleGPU = bundleEncoder.finish();
this.get( bundle ).bundleGPU = bundleGPU;
// restore render pass state
renderContextData.currentSets = renderContextData._currentSets;
renderContextData.currentPass = renderContextData._currentPass;
renderContextData._currentPass = null;
renderContextData._currentSets = null;
}
/**
* Adds a render bundle to the render context data.
*
* @param {RenderContext} renderContext - The render context.
* @param {RenderBundle} bundle - The render bundle to add.
*/
addBundle( renderContext, bundle ) {
const renderContextData = this.get( renderContext );
renderContextData.renderBundles.push( this.get( bundle ).bundleGPU );
}
// bindings
/**
* Creates a uniform buffer.
*
* @param {Buffer} uniformBuffer - The uniform buffer.
*/
createUniformBuffer( uniformBuffer ) {
const uniformBufferData = this.get( uniformBuffer );
if ( uniformBufferData.buffer === undefined ) {
const byteLength = uniformBuffer.byteLength;
const usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST;
const visibilities = [];
if ( uniformBuffer.visibility & GPUShaderStage.VERTEX ) {
visibilities.push( 'vertex' );
}
if ( uniformBuffer.visibility & GPUShaderStage.FRAGMENT ) {
visibilities.push( 'fragment' );
}
if ( uniformBuffer.visibility & GPUShaderStage.COMPUTE ) {
visibilities.push( 'compute' );
}
const bufferVisibility = `(${visibilities.join( ',' )})`;
_bufferDescriptor.label = `bindingBuffer${uniformBuffer.id}_${uniformBuffer.name}_${bufferVisibility}`;
_bufferDescriptor.size = byteLength;
_bufferDescriptor.usage = usage;
const bufferGPU = this.device.createBuffer( _bufferDescriptor );
_bufferDescriptor.reset();
uniformBufferData.buffer = bufferGPU;
}
}
/**
* Destroys the GPU data for the given uniform buffer.
*
* @param {Buffer} uniformBuffer - The uniform buffer.
*/
destroyUniformBuffer( uniformBuffer ) {
const uniformBufferData = this.get( uniformBuffer );
uniformBufferData.buffer.destroy();
this.delete( uniformBuffer );
}
/**
* Creates bindings from the given bind group definition.
*
* @param {BindGroup} bindGroup - The bind group.
* @param {Array<BindGroup>} bindings - Array of bind groups.
* @param {string} cacheKey - The cache key.
* @param {number} version - The version.
*/
createBindings( bindGroup, bindings, cacheKey, version ) {
this.bindingUtils.createBindings( bindGroup, bindings, cacheKey, version );
}
/**
* Updates the given bind group definition.
*
* @param {BindGroup} bindGroup - The bind group.
* @param {Array<BindGroup>} bindings - Array of bind groups.
* @param {string} cacheKey - The cache key.
* @param {number} version - The version.
*/
updateBindings( bindGroup, bindings, cacheKey, version ) {
this.bindingUtils.createBindings( bindGroup, bindings, cacheKey, version );
}
/**
* Updates a buffer binding.
*
* @param {Buffer} binding - The buffer binding to update.
*/
updateBinding( binding ) {
this.bindingUtils.updateBinding( binding );
}
/**
* Delete data associated with the current bind group.
*
* @param {BindGroup} bindGroup - The bind group.
*/
deleteBindGroupData( bindGroup ) {
this.bindingUtils.deleteBindGroupData( bindGroup );
}
// attributes
/**
* Creates the buffer of an indexed shader attribute.
*
* @param {BufferAttribute} attribute - The indexed buffer attribute.
*/
createIndexAttribute( attribute ) {
let usage = GPUBufferUsage.INDEX | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST;
if ( attribute.isStorageBufferAttribute || attribute.isStorageInstancedBufferAttribute ) {
usage |= GPUBufferUsage.STORAGE;
}
this.attributeUtils.createAttribute( attribute, usage );
}
/**
* Creates the GPU buffer of a shader attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
createAttribute( attribute ) {
this.attributeUtils.createAttribute( attribute, GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST );
}
/**
* Creates the GPU buffer of a storage attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
createStorageAttribute( attribute ) {
this.attributeUtils.createAttribute( attribute, GPUBufferUsage.STORAGE | GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST );
}
/**
* Creates the GPU buffer of an indirect storage attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute.
*/
createIndirectStorageAttribute( attribute ) {
this.attributeUtils.createAttribute( attribute, GPUBufferUsage.STORAGE | GPUBufferUsage.INDIRECT | GPUBufferUsage.COPY_SRC | GPUBufferUsage.COPY_DST );
}
/**
* Updates the GPU buffer of a shader attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute to update.
*/
updateAttribute( attribute ) {
this.attributeUtils.updateAttribute( attribute );
}
/**
* Destroys the GPU buffer of a shader attribute.
*
* @param {BufferAttribute} attribute - The buffer attribute to destroy.
*/
destroyAttribute( attribute ) {
this.attributeUtils.destroyAttribute( attribute );
}
// canvas
/**
* Triggers an update of the default render pass descriptor.
*/
updateSize() {
this.delete( this.renderer.getCanvasTarget() );
}
/**
* Frees the GPU resources of the given canvas target.
*
* @param {CanvasTarget} canvasTarget - The canvas target.
*/
destroyCanvasTarget( canvasTarget ) {
this.destroyTexture( canvasTarget.colorTexture );
this.destroyTexture( canvasTarget.depthTexture );
this.delete( canvasTarget );
}
// utils public
/**
* Checks if the given feature is supported by the backend.
*
* @param {string} name - The feature's name.
* @return {boolean} Whether the feature is supported or not.
*/
hasFeature( name ) {
if ( GPUFeatureMap[ name ] !== undefined ) name = GPUFeatureMap[ name ];
return this.device.features.has( name );
}
/**
* Copies data of the given source texture to the given destination texture.
*
* @param {Texture} srcTexture - The source texture.
* @param {Texture} dstTexture - The destination texture.
* @param {?(Box3|Box2)} [srcRegion=null] - The region of the source texture to copy.
* @param {?(Vector2|Vector3)} [dstPosition=null] - The destination position of the copy.
* @param {number} [srcLevel=0] - The mipmap level to copy.
* @param {number} [dstLevel=0] - The destination mip level to copy to.
*/
copyTextureToTexture( srcTexture, dstTexture, srcRegion = null, dstPosition = null, srcLevel = 0, dstLevel = 0 ) {
let dstX = 0;
let dstY = 0;
let dstZ = 0;
let srcX = 0;
let srcY = 0;
let srcZ = 0;
let srcWidth = srcTexture.image.width;
let srcHeight = srcTexture.image.height;
let srcDepth = 1;
if ( srcRegion !== null ) {
if ( srcRegion.isBox3 === true ) {
srcX = srcRegion.min.x;
srcY = srcRegion.min.y;
srcZ = srcRegion.min.z;
srcWidth = srcRegion.max.x - srcRegion.min.x;
srcHeight = srcRegion.max.y - srcRegion.min.y;
srcDepth = srcRegion.max.z - srcRegion.min.z;
} else {
// Assume it's a Box2
srcX = srcRegion.min.x;
srcY = srcRegion.min.y;
srcWidth = srcRegion.max.x - srcRegion.min.x;
srcHeight = srcRegion.max.y - srcRegion.min.y;
srcDepth = 1;
}
}
if ( dstPosition !== null ) {
dstX = dstPosition.x;
dstY = dstPosition.y;
dstZ = dstPosition.z || 0;
}
_commandEncoderDescriptor.label = 'copyTextureToTexture_' + srcTexture.id + '_' + dstTexture.id;
const encoder = this.device.createCommandEncoder( _commandEncoderDescriptor );
_commandEncoderDescriptor.reset();
const sourceGPU = this.get( srcTexture ).texture;
const destinationGPU = this.get( dstTexture ).texture;
_texelCopyTextureInfoSrc.texture = sourceGPU;
_texelCopyTextureInfoSrc.mipLevel = srcLevel;
_texelCopyTextureInfoSrc.origin.x = srcX;
_texelCopyTextureInfoSrc.origin.y = srcY;
_texelCopyTextureInfoSrc.origin.z = srcZ;
_texelCopyTextureInfoDst.texture = destinationGPU;
_texelCopyTextureInfoDst.mipLevel = dstLevel;
_texelCopyTextureInfoDst.origin.x = dstX;
_texelCopyTextureInfoDst.origin.y = dstY;
_texelCopyTextureInfoDst.origin.z = dstZ;
_extent3D.width = srcWidth;
_extent3D.height = srcHeight;
_extent3D.depthOrArrayLayers = srcDepth;
encoder.copyTextureToTexture(
_texelCopyTextureInfoSrc,
_texelCopyTextureInfoDst,
_extent3D
);
_texelCopyTextureInfoSrc.reset();
_texelCopyTextureInfoDst.reset();
_extent3D.reset();
submit( this.device, encoder.finish() );
if ( dstLevel === 0 && dstTexture.generateMipmaps === true && dstTexture.mipmapsAutoUpdate === true ) {
this.textureUtils.generateMipmaps( dstTexture );
}
}
/**
* Copies the current bound framebuffer to the given texture.
*
* @param {Texture} texture - The destination texture.
* @param {RenderContext} renderContext - The render context.
* @param {Vector4} rectangle - A four dimensional vector defining the origin and dimension of the copy.
*/
copyFramebufferToTexture( texture, renderContext, rectangle ) {
const renderContextData = this.get( renderContext );
let sourceGPU = null;
if ( renderContext.renderTarget ) {
if ( texture.isDepthTexture ) {
sourceGPU = this.get( renderContext.depthTexture ).texture;
} else {
sourceGPU = this.get( renderContext.textures[ 0 ] ).texture;
}
} else {
if ( texture.isDepthTexture ) {
sourceGPU = this.textureUtils.getDepthBuffer( renderContext.depth, renderContext.stencil );
} else {
sourceGPU = this.context.getCurrentTexture();
}
}
const destinationGPU = this.get( texture ).texture;
if ( sourceGPU.format !== destinationGPU.format ) {
error( 'WebGPUBackend: copyFramebufferToTexture: Source and destination formats do not match.', sourceGPU.format, destinationGPU.format );
return;
}
const generateMipmaps = texture.generateMipmaps === true && texture.mipmapsAutoUpdate === true && destinationGPU.mipLevelCount > 1;
if ( renderContextData.encoder && this._isRenderCameraDepthArray( renderContext ) === true ) {
// Layered draws are only executed in finishRender(), so preserve this copy as a render-stage boundary.
const bundles = this._finishArrayCameraBundleEncoders( renderContextData );
renderContextData.arrayCameraRenderStages.push( {
bundles,
framebufferCopy: {
texture,
sourceGPU,
destinationGPU,
rectangle: { x: rectangle.x, y: rectangle.y, z: rectangle.z, w: rectangle.w },
// ViewportTextureNode restores this flag before the deferred copy executes.
generateMipmaps
}
} );
this._createArrayCameraBundleEncoders( renderContext, renderContextData );
return;
}
let encoder;
if ( renderContextData.currentPass ) {
renderContextData.currentPass.end();
encoder = renderContextData.encoder;
} else {
_commandEncoderDescriptor.label = 'copyFramebufferToTexture_' + texture.id;
encoder = this.device.createCommandEncoder( _commandEncoderDescriptor );
_commandEncoderDescriptor.reset();
}
// mipmaps must be genereated with the same encoder otherwise the copied texture data
// might be out-of-sync, see #31768
this._copyFramebufferToTexture( encoder, texture, sourceGPU, destinationGPU, rectangle, 0, generateMipmaps );
if ( renderContextData.currentPass ) {
const { descriptor } = renderContextData;
for ( let i = 0; i < descriptor.colorAttachments.length; i ++ ) {
descriptor.colorAttachments[ i ].loadOp = GPULoadOp.Load;
}
if ( renderContext.depth ) descriptor.depthStencilAttachment.depthLoadOp = GPULoadOp.Load;
if ( renderContext.stencil ) descriptor.depthStencilAttachment.stencilLoadOp = GPULoadOp.Load;
renderContextData.currentPass = encoder.beginRenderPass( descriptor );
this._resetRenderContextData( renderContextData );
if ( renderContext.viewport ) {
this.updateViewport( renderContext );
}
if ( renderContext.scissor ) {
this.updateScissor( renderContext );
}
} else {
submit( this.device, encoder.finish() );
}
}
/**
* Encodes a framebuffer copy from a specific texture-array layer.
*
* @param {GPUCommandEncoder} encoder - The command encoder.
* @param {Texture} texture - The destination texture.
* @param {GPUTexture} sourceGPU - The source GPU texture.
* @param {GPUTexture} destinationGPU - The destination GPU texture.
* @param {Object} rectangle - The source rectangle.
* @param {number} [sourceLayer=0] - The source array layer.
* @param {boolean} [generateMipmaps=texture.generateMipmaps] - Whether mipmaps should be generated.
* @private
*/
_copyFramebufferToTexture( encoder, texture, sourceGPU, destinationGPU, rectangle, sourceLayer = 0, generateMipmaps = texture.generateMipmaps ) {
_texelCopyTextureInfoSrc.texture = sourceGPU;
_texelCopyTextureInfoSrc.origin.x = rectangle.x;
_texelCopyTextureInfoSrc.origin.y = rectangle.y;
_texelCopyTextureInfoSrc.origin.z = sourceLayer;
_texelCopyTextureInfoDst.texture = destinationGPU;
_extent3D.width = rectangle.z;
_extent3D.height = rectangle.w;
encoder.copyTextureToTexture(
_texelCopyTextureInfoSrc,
_texelCopyTextureInfoDst,
_extent3D
);
_texelCopyTextureInfoSrc.reset();
_texelCopyTextureInfoDst.reset();
_extent3D.reset();
if ( generateMipmaps === true && destinationGPU.mipLevelCount > 1 ) {
this.textureUtils.generateMipmaps( texture, encoder );
}
}
/**
* Checks if the given compatibility is supported by the backend.
*
* @param {string} name - The compatibility name.
* @return {boolean} Whether the compatibility is supported or not.
*/
hasCompatibility( name ) {
if ( this._compatibility[ name ] !== undefined ) {
return this._compatibility[ name ];
}
return super.hasCompatibility( name );
}
async dispose() {
await super.dispose();
this.bindingUtils.dispose();
this.textureUtils.dispose();
if ( this.occludedResolveCache ) {
for ( const buffer of this.occludedResolveCache.values() ) {
buffer.destroy();
}
this.occludedResolveCache.clear();
}
if ( this.parameters.device === undefined && this.device !== null ) {
this.device.destroy();
}
}
}
/**
* A IES version of {@link SpotLight}. Can only be used with {@link WebGPURenderer}.
*
* @augments SpotLight
*/
class IESSpotLight extends SpotLight {
/**
* Constructs a new IES spot light.
*
* @param {(number|Color|string)} [color=0xffffff] - The light's color.
* @param {number} [intensity=1] - The light's strength/intensity measured in candela (cd).
* @param {number} [distance=0] - Maximum range of the light. `0` means no limit.
* @param {number} [angle=Math.PI/3] - Maximum angle of light dispersion from its direction whose upper bound is `Math.PI/2`.
* @param {number} [penumbra=0] - Percent of the spotlight cone that is attenuated due to penumbra. Value range is `[0,1]`.
* @param {number} [decay=2] - The amount the light dims along the distance of the light.
*/
constructor( color, intensity, distance, angle, penumbra, decay ) {
super( color, intensity, distance, angle, penumbra, decay );
/**
* The IES map. It's a lookup table that stores normalized attenuation factors
* (0.0 to 1.0) that represent the light's intensity at a specific angle.
*
* @type {?Texture}
* @default null
*/
this.iesMap = null;
}
copy( source, recursive ) {
super.copy( source, recursive );
this.iesMap = source.iesMap;
return this;
}
}
/**
* A projector light version of {@link SpotLight}. Can only be used with {@link WebGPURenderer}.
*
* @augments SpotLight
*/
class ProjectorLight extends SpotLight {
/**
* Constructs a new projector light.
*
* @param {(number|Color|string)} [color=0xffffff] - The light's color.
* @param {number} [intensity=1] - The light's strength/intensity measured in candela (cd).
* @param {number} [distance=0] - Maximum range of the light. `0` means no limit.
* @param {number} [angle=Math.PI/3] - Maximum angle of light dispersion from its direction whose upper bound is `Math.PI/2`.
* @param {number} [penumbra=0] - Percent of the spotlight cone that is attenuated due to penumbra. Value range is `[0,1]`.
* @param {number} [decay=2] - The amount the light dims along the distance of the light.
*/
constructor( color, intensity, distance, angle, penumbra, decay ) {
super( color, intensity, distance, angle, penumbra, decay );
/**
* Aspect ratio of the light. Set to `null` to use the texture aspect ratio.
*
* @type {?number}
* @default null
*/
this.aspect = null;
}
copy( source, recursive ) {
super.copy( source, recursive );
this.aspect = source.aspect;
return this;
}
}
PointLight.registerNode( PointLightNode );
DirectionalLight.registerNode( DirectionalLightNode );
RectAreaLight.registerNode( RectAreaLightNode );
SpotLight.registerNode( SpotLightNode );
AmbientLight.registerNode( AmbientLightNode );
HemisphereLight.registerNode( HemisphereLightNode );
LightProbe.registerNode( LightProbeNode );
IESSpotLight.registerNode( IESSpotLightNode );
ProjectorLight.registerNode( ProjectorLightNode );
/**
* This version of a node library represents the standard version
* used in {@link WebGPURenderer}. It maps lights, tone mapping
* techniques and materials to node-based implementations.
*
* @augments NodeLibrary
*/
class StandardNodeLibrary extends NodeLibrary {
/**
* Constructs a new standard node library.
*/
constructor() {
super();
this.addMaterial( MeshPhongNodeMaterial, 'MeshPhongMaterial' );
this.addMaterial( MeshStandardNodeMaterial, 'MeshStandardMaterial' );
this.addMaterial( MeshPhysicalNodeMaterial, 'MeshPhysicalMaterial' );
this.addMaterial( MeshToonNodeMaterial, 'MeshToonMaterial' );
this.addMaterial( MeshBasicNodeMaterial, 'MeshBasicMaterial' );
this.addMaterial( MeshLambertNodeMaterial, 'MeshLambertMaterial' );
this.addMaterial( MeshNormalNodeMaterial, 'MeshNormalMaterial' );
this.addMaterial( MeshMatcapNodeMaterial, 'MeshMatcapMaterial' );
this.addMaterial( LineBasicNodeMaterial, 'LineBasicMaterial' );
this.addMaterial( LineDashedNodeMaterial, 'LineDashedMaterial' );
this.addMaterial( PointsNodeMaterial, 'PointsMaterial' );
this.addMaterial( SpriteNodeMaterial, 'SpriteMaterial' );
this.addMaterial( ShadowNodeMaterial, 'ShadowMaterial' );
this.addToneMapping( linearToneMapping, LinearToneMapping );
this.addToneMapping( reinhardToneMapping, ReinhardToneMapping );
this.addToneMapping( cineonToneMapping, CineonToneMapping );
this.addToneMapping( acesFilmicToneMapping, ACESFilmicToneMapping );
this.addToneMapping( agxToneMapping, AgXToneMapping );
this.addToneMapping( neutralToneMapping, NeutralToneMapping );
}
}
/*
const debugHandler = {
get: function ( target, name ) {
// Add |update
if ( /^(create|destroy)/.test( name ) ) log( 'WebGPUBackend.' + name );
return target[ name ];
}
};
*/
/**
* This renderer is the new alternative of `WebGLRenderer`. `WebGPURenderer` has the ability
* to target different backends. By default, the renderer tries to use a WebGPU backend if the
* browser supports WebGPU. If not, `WebGPURenderer` falls backs to a WebGL 2 backend.
*
* @augments Renderer
*/
class WebGPURenderer extends Renderer {
/**
* WebGPURenderer options.
*
* @typedef {Object} WebGPURenderer~Options
* @property {boolean} [logarithmicDepthBuffer=false] - Whether logarithmic depth buffer is enabled or not. Ignored when a reversed depth buffer is in use.
* @property {boolean} [reversedDepthBuffer=false] - Whether reversed depth buffer is enabled or not.
* @property {boolean} [alpha=true] - Whether the default framebuffer (which represents the final contents of the canvas) should be transparent or opaque.
* @property {boolean} [depth=true] - Whether the default framebuffer should have a depth buffer or not.
* @property {boolean} [stencil=false] - Whether the default framebuffer should have a stencil buffer or not.
* @property {boolean} [antialias=false] - Whether MSAA as the default anti-aliasing should be enabled or not.
* @property {number} [samples=0] - When `antialias` is `true`, `4` samples are used by default. Set this parameter to any other integer value than 0 to overwrite the default.
* @property {boolean} [forceWebGL=false] - If set to `true`, the renderer uses a WebGL 2 backend no matter if WebGPU is supported or not.
* @property {boolean} [multiview=false] - If set to `true`, the renderer will use multiview during WebXR rendering if supported.
* @property {number} [outputType=undefined] - Texture type for output to canvas. By default, device's preferred format is used; other formats may incur overhead.
* @property {number} [outputBufferType=HalfFloatType] - Defines the type of output buffers. The default `HalfFloatType` is recommend for best
* quality. To save memory and bandwidth, `UnsignedByteType` might be used. This will reduce rendering quality though.
*/
/**
* Constructs a new WebGPU renderer.
*
* @param {WebGPURenderer~Options} [parameters] - The configuration parameter.
*/
constructor( parameters = {} ) {
let BackendClass;
if ( parameters.forceWebGL ) {
BackendClass = WebGLBackend;
} else {
BackendClass = WebGPUBackend;
parameters.getFallback = () => {
warn( 'WebGPURenderer: WebGPU is not available, running under WebGL2 backend.' );
return new WebGLBackend( parameters );
};
}
const backend = new BackendClass( parameters );
//super( new Proxy( backend, debugHandler ) );
super( backend, parameters );
/**
* The generic default value is overwritten with the
* standard node library for type mapping.
*
* @type {StandardNodeLibrary}
*/
this.library = new StandardNodeLibrary();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isWebGPURenderer = true;
if ( typeof __THREE_DEVTOOLS__ !== 'undefined' ) {
__THREE_DEVTOOLS__.dispatchEvent( new CustomEvent( 'observe', { detail: this } ) );
}
}
}
/**
* A specialized group which enables applications access to the
* Render Bundle API of WebGPU. The group with all its descendant nodes
* are considered as one render bundle and processed as such by
* the renderer.
*
* Only renderable 3D objects are allowed in a bundle group. Other
* types like lights are not supported and must be added to the scene
* outside of the group.
*
* This module is only fully supported by `WebGPURenderer` with a WebGPU backend.
* With a WebGL backend, the group can technically be rendered but without
* any performance improvements.
*
* @augments Group
*/
class BundleGroup extends Group {
/**
* Constructs a new bundle group.
*/
constructor() {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isBundleGroup = true;
/**
* This property is only relevant for detecting types
* during serialization/deserialization. It should always
* match the class name.
*
* @type {string}
* @readonly
* @default 'BundleGroup'
*/
this.type = 'BundleGroup';
/**
* Whether the bundle is static or not. When set to `true`, the structure
* is assumed to be static and does not change. E.g. no new objects are
* added to the group.
*
* If a change is required, an update can still be forced by setting the
* `needsUpdate` flag to `true`.
*
* @type {boolean}
* @default true
*/
this.static = true;
/**
* The bundle group's version.
*
* @type {number}
* @readonly
* @default 0
*/
this.version = 0;
}
/**
* Set this property to `true` when the bundle group has changed.
*
* @type {boolean}
* @default false
* @param {boolean} value
*/
set needsUpdate( value ) {
if ( value === true ) this.version ++;
}
}
/**
* This module is responsible to manage the rendering pipeline setups in apps.
* You usually create a single instance of this class and use it to define
* the output of your render pipeline and post processing effect chain.
* ```js
* const renderPipeline = new RenderPipeline( renderer );
*
* const scenePass = pass( scene, camera );
*
* renderPipeline.outputNode = scenePass;
* ```
*
* Note: This module can only be used with `WebGPURenderer`.
*/
class RenderPipeline {
/**
* Constructs a new render pipeline management module.
*
* @param {Renderer} renderer - A reference to the renderer.
* @param {Node<vec4>} outputNode - An optional output node.
*/
constructor( renderer, outputNode = vec4( 0, 0, 1, 1 ) ) {
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isRenderPipeline = true;
/**
* A reference to the renderer.
*
* @type {Renderer}
*/
this.renderer = renderer;
/**
* A node which defines the final output of the rendering
* pipeline. This is usually the last node in a chain
* of effect nodes.
*
* @type {Node<vec4>}
*/
this.outputNode = outputNode;
/**
* Whether the default output tone mapping and color
* space transformation should be enabled or not.
*
* This is enabled by default but it must be disabled for
* effects that expect to be executed after tone mapping and color
* space conversion. A typical example is FXAA which
* requires sRGB input.
*
* When set to `false`, the app must control the output
* transformation with `RenderOutputNode`.
*
* ```js
* const outputPass = renderOutput( scenePass );
* ```
*
* @type {boolean}
*/
this.outputColorTransform = true;
/**
* Must be set to `true` when the output node changes.
*
* @type {Node<vec4>}
*/
this.needsUpdate = true;
const material = new NodeMaterial();
material.name = 'RenderPipeline';
/**
* The full screen quad that is used to render
* the effects.
*
* @private
* @type {QuadMesh}
*/
this._quadMesh = new QuadMesh( material );
this._quadMesh.name = 'Render Pipeline';
/**
* The context data for the render pipeline.
*
* @private
* @type {?Object}
* @default null
*/
this._contextData = null;
/**
* The current tone mapping.
*
* @private
* @type {ToneMapping}
*/
this._toneMapping = renderer.toneMapping;
/**
* The current output color space.
*
* @private
* @type {ColorSpace}
*/
this._outputColorSpace = renderer.outputColorSpace;
}
/**
* When `RenderPipeline` is used to apply rendering pipeline and post processing effects,
* the application must use this version of `render()` inside
* its animation loop (not the one from the renderer).
*/
render() {
const renderer = this.renderer;
this._update();
for ( const callback of this._contextData.onBeforePipelineCallbacks ) callback();
const toneMapping = renderer.toneMapping;
const outputColorSpace = renderer.outputColorSpace;
renderer.toneMapping = NoToneMapping;
renderer.outputColorSpace = ColorManagement.workingColorSpace;
//
const currentXR = renderer.xr.enabled;
renderer.xr.enabled = false;
this._quadMesh.render( renderer );
renderer.xr.enabled = currentXR;
//
renderer.toneMapping = toneMapping;
renderer.outputColorSpace = outputColorSpace;
for ( const callback of this._contextData.onAfterPipelineCallbacks ) callback();
}
/**
* Frees internal resources.
*/
dispose() {
this._quadMesh.material.dispose();
}
/**
* Updates the context data.
*
* @private
*/
_updateContext() {
const toneMapping = this._toneMapping;
const outputColorSpace = this._outputColorSpace;
const contextData = {
renderPipeline: this,
renderPipelineState: {
viewOffsetOwner: null
},
onBeforePipelineCallbacks: [],
onAfterPipelineCallbacks: []
};
let outputNode = this.outputNode;
if ( this.outputColorTransform === true ) {
outputNode = renderOutput( outputNode, toneMapping, outputColorSpace );
} else {
contextData.toneMapping = toneMapping;
contextData.outputColorSpace = outputColorSpace;
}
this._contextData = contextData;
this._quadMesh.material.contextNode = context( contextData );
this._quadMesh.material.fragmentNode = outputNode;
this._quadMesh.material.needsUpdate = true;
}
/**
* Updates the state of the module.
*
* @private
*/
_update() {
if ( this._toneMapping !== this.renderer.toneMapping ) {
this._toneMapping = this.renderer.toneMapping;
this.needsUpdate = true;
}
if ( this._outputColorSpace !== this.renderer.outputColorSpace ) {
this._outputColorSpace = this.renderer.outputColorSpace;
this.needsUpdate = true;
}
if ( this.needsUpdate === true ) {
this._updateContext();
this.needsUpdate = false;
}
}
/**
* When `RenderPipeline` is used to apply rendering pipeline and post processing effects,
* the application must use this version of `renderAsync()` inside
* its animation loop (not the one from the renderer).
*
* @async
* @deprecated
* @return {Promise} A Promise that resolves when the render has been finished.
*/
async renderAsync() {
warnOnce( 'RenderPipeline: "renderAsync()" has been deprecated. Use "render()" and "await renderer.init();" when creating the renderer.' ); // @deprecated r181
await this.renderer.init();
this.render();
}
}
/**
* An alternative render pipeline that applies output processing directly in
* material shaders. This avoids the intermediate framebuffer and output pass
* used by {@link Renderer}, but changes blending and is not compatible with
* materials that sample the framebuffer, such as transmissive materials.
* With the WebGL backend, `antialias` has no effect.
*
* ```js
* const renderPipeline = new DirectRenderPipeline( renderer );
* renderPipeline.render( scene, camera );
* ```
*
* Note: This module can only be used with `WebGPURenderer`.
*
* @augments RenderPipeline
*/
class DirectRenderPipeline extends RenderPipeline {
/**
* Constructs a direct render pipeline.
*
* @param {Renderer} renderer - A reference to the renderer.
* @param {Node<vec4>} outputNode - An optional output node.
*/
constructor( renderer, outputNode = output ) {
super( renderer, outputNode );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isDirectRenderPipeline = true;
/**
* The context node used to apply output processing in material shaders.
*
* @private
* @type {?ContextNode}
* @default null
*/
this._contextNode = null;
/**
* The renderer context node wrapped by this pipeline.
*
* @private
* @type {?ContextNode}
* @default null
*/
this._rendererContextNode = null;
/**
* Cached node representations of solid scene backgrounds.
*
* @private
* @type {WeakMap<Color, UniformNode>}
*/
this._backgroundNodes = new WeakMap();
}
/**
* Renders the scene with output processing applied directly in material shaders.
*
* @param {Object3D} scene - The scene or object to render.
* @param {Camera} camera - The camera.
*/
render( scene, camera ) {
const renderer = this.renderer;
this._update();
const backgroundNode = this._getBackgroundNode( scene );
const currentBackgroundNode = backgroundNode !== null ? scene.backgroundNode : null;
const currentContextNode = renderer.contextNode;
const outputRenderTarget = renderer.getOutputRenderTarget();
const useDirectXRRenderTarget = outputRenderTarget !== null && renderer.xr.isPresenting === true &&
renderer.backend.isWebGPUBackend === true && renderer.isOutputTarget === true;
let samples;
let depthBuffer;
let toneMapping;
let outputColorSpace;
if ( backgroundNode !== null ) scene.backgroundNode = backgroundNode;
renderer.contextNode = this._contextNode;
if ( useDirectXRRenderTarget ) {
samples = outputRenderTarget.samples;
depthBuffer = outputRenderTarget.depthBuffer;
outputRenderTarget.samples = renderer.samples;
outputRenderTarget.depthBuffer = true;
}
try {
for ( const callback of this._contextData.onBeforePipelineCallbacks ) callback();
toneMapping = renderer.toneMapping;
outputColorSpace = renderer.outputColorSpace;
renderer.toneMapping = NoToneMapping;
renderer.outputColorSpace = ColorManagement.workingColorSpace;
if ( renderer.xr.enabled ) {
renderer.xr.renderLayers();
}
renderer.render( scene, camera );
} finally {
if ( toneMapping !== undefined ) renderer.toneMapping = toneMapping;
if ( outputColorSpace !== undefined ) renderer.outputColorSpace = outputColorSpace;
if ( backgroundNode !== null && scene.backgroundNode === backgroundNode ) scene.backgroundNode = currentBackgroundNode;
if ( renderer.contextNode === this._contextNode ) renderer.contextNode = currentContextNode;
if ( useDirectXRRenderTarget ) {
outputRenderTarget.samples = samples;
outputRenderTarget.depthBuffer = depthBuffer;
}
for ( const callback of this._contextData.onAfterPipelineCallbacks ) callback();
}
}
/**
* Returns a node representation of a solid scene background so it receives
* the same inline output processing as material fragments.
*
* @private
* @param {Object3D} scene - The scene or object to render.
* @return {?UniformNode} The background node.
*/
_getBackgroundNode( scene ) {
if ( scene.isScene !== true || scene.backgroundNode != null || scene.background?.isColor !== true ) return null;
const environmentBlendMode = this.renderer.xr.getEnvironmentBlendMode();
if ( environmentBlendMode === 'additive' || environmentBlendMode === 'alpha-blend' ) return null;
let backgroundNode = this._backgroundNodes.get( scene.background );
if ( backgroundNode === undefined ) {
backgroundNode = uniform( scene.background );
this._backgroundNodes.set( scene.background, backgroundNode );
}
return backgroundNode;
}
/**
* Updates the state of the module.
*
* @private
*/
_update() {
if ( this._rendererContextNode !== this.renderer.contextNode ) {
this._rendererContextNode = this.renderer.contextNode;
this.needsUpdate = true;
}
super._update();
}
/**
* Updates the context used to process material output directly.
*
* @private
*/
_updateContext() {
super._updateContext();
const pipelineOutputNode = this.outputNode;
const toneMapping = this._toneMapping;
const outputColorSpace = this._outputColorSpace;
const outputColorTransform = this.outputColorTransform;
this._contextNode = this._rendererContextNode.context( {
...this._contextData,
getOutput: ( materialOutputNode, builder ) => {
const renderer = builder.renderer;
const renderTarget = renderer.getRenderTarget();
if ( renderer.isOutputTarget === false && renderTarget?._hasExternalTextures !== true ) return materialOutputNode;
output.assign( materialOutputNode );
if ( outputColorTransform === false ) return pipelineOutputNode;
const renderOutputNode = renderOutput( pipelineOutputNode, toneMapping, outputColorSpace );
renderOutputNode.premultipliedAlpha = builder.material.premultipliedAlpha;
return renderOutputNode;
}
} );
}
}
/**
* @deprecated since r183. Use {@link RenderPipeline} instead. PostProcessing has been renamed to RenderPipeline.
*
* This class is a wrapper for backward compatibility and will be removed in a future version.
*/
class PostProcessing extends RenderPipeline {
/**
* Constructs a new post processing management module.
*
* @param {Renderer} renderer - A reference to the renderer.
* @param {Node<vec4>} outputNode - An optional output node.
* @deprecated since r183. Use {@link RenderPipeline} instead.
*/
constructor( renderer, outputNode ) {
warnOnce( 'PostProcessing: "PostProcessing" has been renamed to "RenderPipeline". Please update your code to use "THREE.RenderPipeline" instead.' ); // @deprecated, r183
super( renderer, outputNode );
}
}
/**
* A readback buffer is used to transfer data from the GPU to the CPU.
* It is primarily used to read back compute shader results.
*
* @augments EventDispatcher
*/
class ReadbackBuffer extends EventDispatcher {
/**
* Constructs a new readback buffer.
*
* @param {number} maxByteLength - The maximum size of the buffer to be read back.
*/
constructor( maxByteLength ) {
super();
/**
* Name used for debugging purposes.
*
* @type {string}
*/
this.name = '';
/**
* The mapped, read back array buffer.
*
* @type {ArrayBuffer|null}
*/
this.buffer = null;
/**
* The maximum size of the buffer to be read back.
*
* @type {number}
*/
this.maxByteLength = maxByteLength;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isReadbackBuffer = true;
this._mapped = false;
}
/**
* Releases the mapped buffer data so the GPU buffer can be
* used by the GPU again.
*
* Note: Any `ArrayBuffer` data associated with this readback buffer
* are removed and no longer accessible after calling this method.
*/
release() {
this.dispatchEvent( { type: 'release' } );
}
/**
* Frees internal resources.
*/
dispose() {
this.dispatchEvent( { type: 'dispose' } );
}
}
/**
* This special type of texture is intended for compute shaders.
* It can be used to compute the data of a texture with a compute shader.
*
* Note: This type of texture can only be used with `WebGPURenderer`
* and a WebGPU backend.
*
* @augments Texture
*/
class StorageTexture extends Texture {
/**
* Constructs a new storage texture.
*
* @param {number} [width=1] - The storage texture's width.
* @param {number} [height=1] - The storage texture's height.
*/
constructor( width = 1, height = 1 ) {
super();
/**
* The image object which just represents the texture's dimension.
*
* @type {{width: number, height: number}}
*/
this.image = { width, height };
/**
* The default `magFilter` for storage textures is `THREE.LinearFilter`.
*
* @type {number}
*/
this.magFilter = LinearFilter;
/**
* The default `minFilter` for storage textures is `THREE.LinearFilter`.
*
* @type {number}
*/
this.minFilter = LinearFilter;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStorageTexture = true;
}
/**
* Sets the size of the storage texture.
*
* @param {number} width - The new width of the storage texture.
* @param {number} height - The new height of the storage texture.
*/
setSize( width, height ) {
if ( this.image.width !== width || this.image.height !== height ) {
this.image.width = width;
this.image.height = height;
this.dispose();
}
}
}
/**
* This special type of texture is intended for compute shaders.
* It can be used to compute the data of a texture with a compute shader.
*
* Note: This type of texture can only be used with `WebGPURenderer`
* and a WebGPU backend.
*
* @augments Texture
*/
class Storage3DTexture extends Texture {
/**
* Constructs a new storage texture.
*
* @param {number} [width=1] - The storage texture's width.
* @param {number} [height=1] - The storage texture's height.
* @param {number} [depth=1] - The storage texture's depth.
*/
constructor( width = 1, height = 1, depth = 1 ) {
super();
//inherited from texture. Must be false for 3DTexture
this.isArrayTexture = false;
/**
* The image object which just represents the texture's dimension.
*
* @type {{width: number, height: number, depth: number}}
*/
this.image = { width, height, depth };
/**
* The default `magFilter` for storage textures is `THREE.LinearFilter`.
*
* @type {number}
*/
this.magFilter = LinearFilter;
/**
* The default `minFilter` for storage textures is `THREE.LinearFilter`.
*
* @type {number}
*/
this.minFilter = LinearFilter;
/**
* This defines how the texture is wrapped in the depth direction and corresponds to
* *W* in UVW mapping.
*
* @type {number}
*/
this.wrapR = ClampToEdgeWrapping;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStorageTexture = true;
/**
* Whether the renderer regenerates the mipmaps automatically.
*
* Overwritten and set to `false` by default since the WebGPU backend
* does not support mipmap generation for 3D textures.
*
* @type {boolean}
* @default false
*/
this.mipmapsAutoUpdate = false;
/**
* Indicates whether this texture is a 3D texture.
*
* @type {boolean}
*
*/
this.is3DTexture = true;
}
/**
* Copies the values of the given texture to this instance.
*
* @param {Storage3DTexture} source - The texture to copy.
* @return {Storage3DTexture} A reference to this instance.
*/
copy( source ) {
super.copy( source );
this.wrapR = source.wrapR;
return this;
}
/**
* Sets the size of the storage 3d texture.
*
* @param {number} width - The new width of the storage texture.
* @param {number} height - The new height of the storage texture.
* @param {number} depth - The new depth of the storage texture.
*/
setSize( width, height, depth ) {
if ( this.image.width !== width || this.image.height !== height || this.image.depth !== depth ) {
this.image.width = width;
this.image.height = height;
this.image.depth = depth;
this.dispose();
}
}
}
/**
* This special type of texture is intended for compute shaders.
* It can be used to compute the data of a texture with a compute shader.
*
* Note: This type of texture can only be used with `WebGPURenderer`
* and a WebGPU backend.
*
* @augments Texture
*/
class StorageArrayTexture extends Texture {
/**
* Constructs a new storage texture.
*
* @param {number} [width=1] - The storage texture's width.
* @param {number} [height=1] - The storage texture's height.
* @param {number} [depth=1] - The storage texture's depth.
*/
constructor( width = 1, height = 1, depth = 1 ) {
super();
//inherited from texture
this.isArrayTexture = true;
/**
* The image object which just represents the texture's dimension.
*
* @type {{width: number, height: number, depth: number}}
*/
this.image = { width, height, depth };
/**
* The default `magFilter` for storage textures is `THREE.LinearFilter`.
*
* @type {number}
*/
this.magFilter = LinearFilter;
/**
* The default `minFilter` for storage textures is `THREE.LinearFilter`.
*
* @type {number}
*/
this.minFilter = LinearFilter;
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isStorageTexture = true;
/**
* Whether the renderer regenerates the mipmaps automatically.
*
* Overwritten and set to `false` by default.
*
* @type {boolean}
* @default false
*/
this.mipmapsAutoUpdate = false;
}
/**
* Sets the size of the storage array texture.
*
* @param {number} width - The new width of the storage texture.
* @param {number} height - The new height of the storage texture.
* @param {number} depth - The new depth of the storage texture.
*/
setSize( width, height, depth ) {
if ( this.image.width !== width || this.image.height !== height || this.image.depth !== depth ) {
this.image.width = width;
this.image.height = height;
this.image.depth = depth;
this.dispose();
}
}
}
/**
* This special type of buffer attribute is intended for compute shaders.
* It can be used to encode draw parameters for indirect draw calls.
*
* Note: This type of buffer attribute can only be used with `WebGPURenderer`
* and a WebGPU backend.
*
* @augments StorageBufferAttribute
*/
class IndirectStorageBufferAttribute extends StorageBufferAttribute {
/**
* Constructs a new storage buffer attribute.
*
* @param {number|Uint32Array} count - The item count. It is also valid to pass a `Uint32Array` as an argument.
* The subsequent parameter is then obsolete.
* @param {number} itemSize - The item size.
*/
constructor( count, itemSize ) {
super( count, itemSize, Uint32Array );
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isIndirectStorageBufferAttribute = true;
}
}
/**
* A loader for loading node objects in the three.js JSON Object/Scene format.
*
* @augments Loader
*/
class NodeLoader extends Loader {
/**
* Constructs a new node loader.
*
* @param {LoadingManager} [manager] - A reference to a loading manager.
*/
constructor( manager ) {
super( manager );
/**
* Represents a dictionary of textures.
*
* @type {Object<string,Texture>}
*/
this.textures = {};
/**
* Represents a dictionary of node types.
*
* @type {Object<string,Node.constructor>}
*/
this.nodes = {};
}
/**
* Loads the node definitions from the given URL.
*
* @param {string} url - The path/URL of the file to be loaded.
* @param {Function} onLoad - Will be called when load completes.
* @param {Function} onProgress - Will be called while load progresses.
* @param {Function} onError - Will be called when errors are thrown during the loading process.
*/
load( url, onLoad, onProgress, onError ) {
const loader = new FileLoader( this.manager );
loader.setPriority( this.priority );
loader.setCache( this.cache );
loader.setPath( this.path );
loader.setRequestHeader( this.requestHeader );
loader.setWithCredentials( this.withCredentials );
loader.load( url, ( text ) => {
try {
onLoad( this.parse( JSON.parse( text ) ) );
} catch ( e ) {
if ( onError ) {
onError( e );
} else {
error( e );
}
this.manager.itemError( url );
}
}, onProgress, onError );
}
/**
* Parse the node dependencies for the loaded node.
*
* @param {Array<Object>} [json] - The JSON definition
* @return {Object<string,Node>} A dictionary with node dependencies.
*/
parseNodes( json ) {
const nodes = {};
if ( json !== undefined ) {
for ( const nodeJSON of json ) {
const { uuid, type } = nodeJSON;
nodes[ uuid ] = this.createNodeFromType( type );
nodes[ uuid ].uuid = uuid;
}
const meta = { nodes, textures: this.textures };
for ( const nodeJSON of json ) {
nodeJSON.meta = meta;
const node = nodes[ nodeJSON.uuid ];
node.deserialize( nodeJSON );
delete nodeJSON.meta;
}
}
return nodes;
}
/**
* Parses the node from the given JSON.
*
* @param {Object} json - The JSON definition
* @param {string} json.type - The node type.
* @param {string} json.uuid - The node UUID.
* @param {Array<Object>} [json.nodes] - The node dependencies.
* @param {Object} [json.meta] - The meta data.
* @return {Node} The parsed node.
*/
parse( json ) {
const node = this.createNodeFromType( json.type );
node.uuid = json.uuid;
const nodes = this.parseNodes( json.nodes );
const meta = { nodes, textures: this.textures };
json.meta = meta;
node.deserialize( json );
delete json.meta;
return node;
}
/**
* Defines the dictionary of textures.
*
* @param {Object<string,Texture>} value - The texture library defines as `<uuid,texture>`.
* @return {NodeLoader} A reference to this loader.
*/
setTextures( value ) {
this.textures = value;
return this;
}
/**
* Defines the dictionary of node types.
*
* @param {Object<string,Node.constructor>} value - The node library defined as `<classname,class>`.
* @return {NodeLoader} A reference to this loader.
*/
setNodes( value ) {
this.nodes = value;
return this;
}
/**
* Creates a node object from the given type.
*
* @param {string} type - The node type.
* @return {Node} The created node instance.
*/
createNodeFromType( type ) {
if ( this.nodes[ type ] === undefined ) {
error( 'NodeLoader: Node type not found:', type );
return float();
}
return new this.nodes[ type ]();
}
}
/**
* A special type of material loader for loading node materials.
*
* @augments MaterialLoader
*/
class NodeMaterialLoader extends MaterialLoader {
/**
* Constructs a new node material loader.
*
* @param {LoadingManager} [manager] - A reference to a loading manager.
*/
constructor( manager ) {
super( manager );
/**
* Represents a dictionary of node types.
*
* @type {Object<string,Node.constructor>}
*/
this.nodes = {};
/**
* Represents a dictionary of node material types.
*
* @type {Object<string,NodeMaterial.constructor>}
*/
this.nodeMaterials = {};
}
/**
* Parses the node material from the given JSON.
*
* @param {Object} json - The JSON definition
* @return {NodeMaterial}. The parsed material.
*/
parse( json ) {
const material = super.parse( json );
const nodes = this.nodes;
const inputNodes = json.inputNodes;
for ( const property in inputNodes ) {
const uuid = inputNodes[ property ];
material[ property ] = nodes[ uuid ];
}
return material;
}
/**
* Defines the dictionary of node types.
*
* @param {Object<string,Node.constructor>} value - The node library defined as `<classname,class>`.
* @return {NodeLoader} A reference to this loader.
*/
setNodes( value ) {
this.nodes = value;
return this;
}
/**
* Defines the dictionary of node material types.
*
* @param {Object<string,NodeMaterial.constructor>} value - The node material library defined as `<classname,class>`.
* @return {NodeLoader} A reference to this loader.
*/
setNodeMaterials( value ) {
this.nodeMaterials = value;
return this;
}
/**
* Creates a node material from the given type.
*
* @param {string} type - The node material type.
* @return {Node} The created node material instance.
*/
createMaterialFromType( type ) {
const materialClass = this.nodeMaterials[ type ];
if ( materialClass !== undefined ) {
return new materialClass();
}
return super.createMaterialFromType( type );
}
}
/**
* A special type of object loader for loading 3D objects using
* node materials.
*
* @augments ObjectLoader
*/
class NodeObjectLoader extends ObjectLoader {
/**
* Constructs a new node object loader.
*
* @param {LoadingManager} [manager] - A reference to a loading manager.
*/
constructor( manager ) {
super( manager );
/**
* Represents a dictionary of node types.
*
* @type {Object<string,Node.constructor>}
*/
this.nodes = {};
/**
* Represents a dictionary of node material types.
*
* @type {Object<string,NodeMaterial.constructor>}
*/
this.nodeMaterials = {};
/**
* A reference to hold the `nodes` JSON property.
*
* @private
* @type {?Object[]}
*/
this._nodesJSON = null;
}
/**
* Defines the dictionary of node types.
*
* @param {Object<string,Node.constructor>} value - The node library defined as `<classname,class>`.
* @return {NodeObjectLoader} A reference to this loader.
*/
setNodes( value ) {
this.nodes = value;
return this;
}
/**
* Defines the dictionary of node material types.
*
* @param {Object<string,NodeMaterial.constructor>} value - The node material library defined as `<classname,class>`.
* @return {NodeObjectLoader} A reference to this loader.
*/
setNodeMaterials( value ) {
this.nodeMaterials = value;
return this;
}
/**
* Parses the node objects from the given JSON.
*
* @param {Object} json - The JSON definition
* @param {Function} onLoad - The onLoad callback function.
* @return {Object3D}. The parsed 3D object.
*/
parse( json, onLoad ) {
this._nodesJSON = json.nodes;
const data = super.parse( json, onLoad );
this._nodesJSON = null; // dispose
return data;
}
/**
* Async version of {@link NodeObjectLoader#parse}.
*
* @param {Object} json - The JSON definition
* @return {Promise<Object3D>} A Promise that resolves with the parsed 3D object.
*/
async parseAsync( json ) {
this._nodesJSON = json.nodes;
const data = await super.parseAsync( json );
this._nodesJSON = null; // dispose
return data;
}
/**
* Parses the node objects from the given JSON and textures.
*
* @param {Object[]} json - The JSON definition
* @param {Object<string,Texture>} textures - The texture library.
* @return {Object<string,Node>}. The parsed nodes.
*/
parseNodes( json, textures ) {
if ( json !== undefined ) {
const loader = new NodeLoader();
loader.setNodes( this.nodes );
loader.setTextures( textures );
return loader.parseNodes( json );
}
return {};
}
/**
* Parses the node objects from the given JSON and textures.
*
* @param {Object} json - The JSON definition
* @param {Object<string,Texture>} textures - The texture library.
* @return {Object<string,NodeMaterial>}. The parsed materials.
*/
parseMaterials( json, textures ) {
const materials = {};
if ( json !== undefined ) {
const nodes = this.parseNodes( this._nodesJSON, textures );
const loader = new NodeMaterialLoader();
loader.setTextures( textures );
loader.setNodes( nodes );
loader.setNodeMaterials( this.nodeMaterials );
for ( let i = 0, l = json.length; i < l; i ++ ) {
const data = json[ i ];
materials[ data.uuid ] = loader.parse( data );
}
}
return materials;
}
}
PointLight.registerNode( PointLightNode );
DirectionalLight.registerNode( DirectionalLightNode );
RectAreaLight.registerNode( RectAreaLightNode );
SpotLight.registerNode( SpotLightNode );
AmbientLight.registerNode( AmbientLightNode );
HemisphereLight.registerNode( HemisphereLightNode );
LightProbe.registerNode( LightProbeNode );
IESSpotLight.registerNode( IESSpotLightNode );
ProjectorLight.registerNode( ProjectorLightNode );
/**
* This version of a node library represents a basic version
* just focusing on lights and tone mapping techniques.
*
* @private
* @augments NodeLibrary
*/
class BasicNodeLibrary extends NodeLibrary {
/**
* Constructs a new basic node library.
*/
constructor() {
super();
this.addToneMapping( linearToneMapping, LinearToneMapping );
this.addToneMapping( reinhardToneMapping, ReinhardToneMapping );
this.addToneMapping( cineonToneMapping, CineonToneMapping );
this.addToneMapping( acesFilmicToneMapping, ACESFilmicToneMapping );
this.addToneMapping( agxToneMapping, AgXToneMapping );
this.addToneMapping( neutralToneMapping, NeutralToneMapping );
}
}
/**
* In earlier three.js versions, clipping was defined globally
* on the renderer or on material level. This special version of
* `THREE.Group` allows to encode the clipping state into the scene
* graph. Meaning if you create an instance of this group, all
* descendant 3D objects will be affected by the respective clipping
* planes.
*
* Note: `ClippingGroup` can only be used with `WebGPURenderer`.
*
* @augments Group
*/
class ClippingGroup extends Group {
/**
* Constructs a new clipping group.
*/
constructor() {
super();
/**
* This flag can be used for type testing.
*
* @type {boolean}
* @readonly
* @default true
*/
this.isClippingGroup = true;
/**
* An array with clipping planes.
*
* @type {Array<Plane>}
*/
this.clippingPlanes = [];
/**
* Whether clipping should be enabled or not.
*
* @type {boolean}
* @default true
*/
this.enabled = true;
/**
* Whether the intersection of the clipping planes is used to clip objects, rather than their union.
*
* @type {boolean}
* @default false
*/
this.clipIntersection = false;
/**
* Whether shadows should be clipped or not.
*
* @type {boolean}
* @default false
*/
this.clipShadows = false;
}
}
export { ACESFilmicToneMapping, AddEquation, AddOperation, AdditiveBlending, AgXToneMapping, AlphaFormat, AlwaysCompare, AlwaysDepth, AlwaysStencilFunc, AmbientLight, AmbientLightNode, AnalyticLightNode, ArrayCamera, BackSide, Backend, BasicEnvironmentNode, BasicLightMapNode, BasicNodeLibrary, BundleGroup, ByteType, CanvasTarget, CineonToneMapping, ClampToEdgeWrapping, ClippingGroup, CodeNode, Color, ColorManagement, Compatibility, ConstantAlphaFactor, ConstantColorFactor, CubeMapNode, CubeReflectionMapping, CubeRefractionMapping, CubeRenderTarget, CubeTexture, CullFaceBack, CullFaceFront, CullFaceNone, CustomBlending, CylinderGeometry, DataTexture, DecrementStencilOp, DecrementWrapStencilOp, DepthFormat, DepthStencilFormat, DepthTexture, DirectRenderPipeline, DirectionalLight, DirectionalLightNode, DoubleSide, DstAlphaFactor, DstColorFactor, DynamicDrawUsage, EnvironmentNode, EqualCompare, EqualDepth, EqualStencilFunc, EquirectangularReflectionMapping, EquirectangularRefractionMapping, EventDispatcher, ExpressionNode, FileLoader, Float16BufferAttribute, FloatType, FramebufferTexture, FrontSide, Frustum, FrustumArray, FunctionNode, GLSLNodeBuilder, GLSLNodeParser, GreaterCompare, GreaterDepth, GreaterEqualCompare, GreaterEqualDepth, GreaterEqualStencilFunc, GreaterStencilFunc, Group, HalfFloatType, HemisphereLight, HemisphereLightNode, IESSpotLight, IESSpotLightNode, IncrementStencilOp, IncrementWrapStencilOp, IndirectStorageBufferAttribute, InspectorBase, IntType, InvertStencilOp, KeepStencilOp, LessCompare, LessDepth, LessEqualCompare, LessEqualDepth, LessEqualStencilFunc, LessStencilFunc, LightProbe, LightProbeNode, Lighting, LightingModel, LightingNode, LightsNode, Line2NodeMaterial, LineBasicMaterial, LineBasicNodeMaterial, LineDashedMaterial, LineDashedNodeMaterial, LinearFilter, LinearMipmapLinearFilter, LinearMipmapNearestFilter, LinearToneMapping, LinearTransfer, Loader, Material, MaterialBlending, MaterialLoader, Matrix2, Matrix3, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshBasicNodeMaterial, MeshLambertMaterial, MeshLambertNodeMaterial, MeshMatcapMaterial, MeshMatcapNodeMaterial, MeshNormalMaterial, MeshNormalNodeMaterial, MeshPhongMaterial, MeshPhongNodeMaterial, MeshPhysicalMaterial, MeshPhysicalNodeMaterial, MeshSSSNodeMaterial, MeshStandardMaterial, MeshStandardNodeMaterial, MeshToonMaterial, MeshToonNodeMaterial, MinEquation, MirroredRepeatWrapping, MixOperation, MultiplyBlending, MultiplyOperation, NearestFilter, NearestMipmapLinearFilter, NearestMipmapNearestFilter, NeutralToneMapping, NeverCompare, NeverDepth, NeverStencilFunc, NoBlending, NoColorSpace, NoToneMapping, Node, NodeAccess, NodeAttribute, NodeBuilder, NodeCache, NodeCode, NodeFrame, NodeFunctionInput, NodeLoader, NodeMaterial, NodeMaterialInstance, NodeMaterialLoader, NodeObjectLoader, NodeUniform, NodeUpdateType, NodeVar, NodeVarying, NormalBlending, NotEqualCompare, NotEqualDepth, NotEqualStencilFunc, ObjectLoader, OneFactor, OneMinusConstantAlphaFactor, OneMinusConstantColorFactor, OneMinusDstAlphaFactor, OneMinusDstColorFactor, OneMinusSrcAlphaFactor, OneMinusSrcColorFactor, PCFShadowMap, PCFSoftShadowMap, ParameterNode, PerspectiveCamera, PhongLightingModel, PhysicalLightingModel, Plane, PlaneGeometry, PointLight, PointLightNode, PointsMaterial, PointsNodeMaterial, PostProcessing, ProjectorLight, ProjectorLightNode, QuadMesh, Quaternion, R11_EAC_Format, RED_GREEN_RGTC2_Format, RED_RGTC1_Format, REVISION, RG11_EAC_Format, RGBAFormat, RGBAIntegerFormat, RGBA_ASTC_10x10_Format, RGBA_ASTC_10x5_Format, RGBA_ASTC_10x6_Format, RGBA_ASTC_10x8_Format, RGBA_ASTC_12x10_Format, RGBA_ASTC_12x12_Format, RGBA_ASTC_4x4_Format, RGBA_ASTC_5x4_Format, RGBA_ASTC_5x5_Format, RGBA_ASTC_6x5_Format, RGBA_ASTC_6x6_Format, RGBA_ASTC_8x5_Format, RGBA_ASTC_8x6_Format, RGBA_ASTC_8x8_Format, RGBA_BPTC_Format, RGBA_ETC2_EAC_Format, RGBA_PVRTC_2BPPV1_Format, RGBA_PVRTC_4BPPV1_Format, RGBA_S3TC_DXT1_Format, RGBA_S3TC_DXT3_Format, RGBA_S3TC_DXT5_Format, RGBFormat, RGBIntegerFormat, RGB_BPTC_SIGNED_Format, RGB_BPTC_UNSIGNED_Format, RGB_ETC1_Format, RGB_ETC2_Format, RGB_PVRTC_2BPPV1_Format, RGB_PVRTC_4BPPV1_Format, RGB_S3TC_DXT1_Format, RGFormat, RGIntegerFormat, ReadbackBuffer, RectAreaLight, RectAreaLightNode, RedFormat, RedIntegerFormat, ReinhardToneMapping, RenderObjectRefreshType, RenderPipeline, RenderTarget, Renderer, RepeatWrapping, ReplaceStencilOp, ReverseSubtractEquation, SIGNED_R11_EAC_Format, SIGNED_RED_GREEN_RGTC2_Format, SIGNED_RED_RGTC1_Format, SIGNED_RG11_EAC_Format, SRGBColorSpace, SRGBTransfer, Scene, ShadowMaterial, ShadowNodeMaterial, ShortType, SphereGeometry, SpotLight, SpotLightNode, SpriteMaterial, SpriteNodeMaterial, SrcAlphaFactor, SrcAlphaSaturateFactor, SrcColorFactor, StackTrace, StandardDrawingPipeline, StandardNodeLibrary, Storage3DTexture, StorageArrayTexture, StorageBufferAttribute, StorageTexture, SubtractEquation, SubtractiveBlending, TempNode, Texture, TextureNode, TimestampQuery, Uint16BufferAttribute, Uint32BufferAttribute, UnsignedByteType, UnsignedInt101111Type, UnsignedInt248Type, UnsignedInt5999Type, UnsignedIntType, UnsignedShort4444Type, UnsignedShort5551Type, UnsignedShortType, VSMShadowMap, VarNode, Vector2, Vector3, Vector4, VolumeNodeMaterial, WGSLNodeBuilder, WebGLBackend, WebGLCapabilities, WebGLCoordinateSystem, WebGPUBackend, WebGPUCoordinateSystem, WebGPURenderer, WebXRController, ZeroFactor, ZeroStencilOp, createCanvasElement, defaultBuildStages, error, shaderStages, vectorComponents, warn, warnOnce };