@aura3d/rendering 1.4.0 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +38 -4
- package/AGENTS.md +0 -46
- package/src/ArchitecturalLightingFixtures.ts +0 -241
- package/src/ArchitecturalMaterialCatalog.ts +0 -130
- package/src/ArchitecturalMeasurementFixtures.ts +0 -155
- package/src/BRDFLut.ts +0 -47
- package/src/CameraFraming.ts +0 -200
- package/src/CanonicalSceneFixtures.ts +0 -95
- package/src/CascadedShadowMaps.ts +0 -449
- package/src/ColorManagement.ts +0 -127
- package/src/CullingFixtures.ts +0 -280
- package/src/DecalGeometry.ts +0 -26
- package/src/DepthPass.ts +0 -110
- package/src/EnvironmentBackgroundPass.ts +0 -194
- package/src/EnvironmentBackgroundResources.ts +0 -309
- package/src/EnvironmentLighting.ts +0 -96
- package/src/EnvironmentMapResources.ts +0 -1026
- package/src/EnvironmentPipeline.ts +0 -69
- package/src/EnvironmentPlatform.ts +0 -1306
- package/src/EnvironmentPreset.ts +0 -155
- package/src/Exposure.ts +0 -66
- package/src/ExternalParityRenderPreset.ts +0 -725
- package/src/ForwardPass.ts +0 -1668
- package/src/FrameVisualMetrics.ts +0 -301
- package/src/Geometry.ts +0 -461
- package/src/GeometryPrimitives.ts +0 -2
- package/src/HDRRenderPipeline.ts +0 -94
- package/src/IBL.ts +0 -86
- package/src/IndexBuffer.ts +0 -57
- package/src/InstancedPBRMaterial.ts +0 -186
- package/src/InstancedUnlitMaterial.ts +0 -60
- package/src/Instancing.ts +0 -19
- package/src/LightCollector.ts +0 -82
- package/src/LightUniforms.ts +0 -47
- package/src/LightingDebug.ts +0 -80
- package/src/LightingDefaults.ts +0 -79
- package/src/LightingRig.ts +0 -301
- package/src/LineGeometry.ts +0 -2
- package/src/Material.ts +0 -209
- package/src/MaterialBinding.ts +0 -131
- package/src/MaterialInstance.ts +0 -59
- package/src/MaterialPresets.ts +0 -272
- package/src/MorphTarget.ts +0 -124
- package/src/MorphTargetPlan.ts +0 -178
- package/src/MorphUnlitMaterial.ts +0 -60
- package/src/NormalMappedPBRMaterial.ts +0 -188
- package/src/OceanFixtures.ts +0 -318
- package/src/PBRLightingDefaults.ts +0 -12
- package/src/PBRMaterial.ts +0 -442
- package/src/PMREM.ts +0 -53
- package/src/PbrReference.ts +0 -681
- package/src/PostProcessPass.ts +0 -2507
- package/src/ProceduralTextureFixtures.ts +0 -393
- package/src/ProductTurntableFixtures.ts +0 -815
- package/src/Raycaster.ts +0 -21
- package/src/ReflectionProbe.ts +0 -13
- package/src/ReflectionSurfaces.ts +0 -219
- package/src/RenderBackend.ts +0 -43
- package/src/RenderDebugViews.ts +0 -138
- package/src/RenderDevice.ts +0 -836
- package/src/RenderGraph.ts +0 -194
- package/src/RenderPass.ts +0 -27
- package/src/RenderPipeline.ts +0 -101
- package/src/RenderQueue.ts +0 -12
- package/src/RenderState.ts +0 -12
- package/src/Renderer.ts +0 -2631
- package/src/RendererDebugOverlay.ts +0 -80
- package/src/RendererFeatureGates.ts +0 -150
- package/src/RendererPostprocessPlan.ts +0 -279
- package/src/RendererTiming.ts +0 -251
- package/src/RendererVisualPipelineReport.ts +0 -836
- package/src/ResourceLifecycle.ts +0 -21
- package/src/Sampler.ts +0 -37
- package/src/SceneOptimization.ts +0 -518
- package/src/ShaderChunks.ts +0 -393
- package/src/ShaderLibrary.ts +0 -2766
- package/src/ShaderModule.ts +0 -36
- package/src/ShaderPreprocessor.ts +0 -260
- package/src/ShaderReflection.ts +0 -201
- package/src/ShadowMap.ts +0 -237
- package/src/ShadowPass.ts +0 -198
- package/src/ShadowProjection.ts +0 -115
- package/src/SkinnedLitMaterial.ts +0 -397
- package/src/SkinnedUnlitMaterial.ts +0 -63
- package/src/SkinningBounds.ts +0 -131
- package/src/SpaceEnvironmentFixtures.ts +0 -200
- package/src/SpriteGeometry.ts +0 -21
- package/src/StereoCameraRig.ts +0 -127
- package/src/StereoEffects.ts +0 -274
- package/src/TerrainFixtures.ts +0 -180
- package/src/Texture.ts +0 -307
- package/src/TextureBinding.ts +0 -91
- package/src/TexturedPBRMaterial.ts +0 -1112
- package/src/TexturedUnlitMaterial.ts +0 -67
- package/src/ToneMapping.ts +0 -105
- package/src/UniformBinder.ts +0 -16
- package/src/UniformLayout.ts +0 -129
- package/src/UnlitMaterial.ts +0 -75
- package/src/VegetationFixtures.ts +0 -314
- package/src/VertexBuffer.ts +0 -120
- package/src/VertexFormat.ts +0 -171
- package/src/VoxelWorldFixtures.ts +0 -280
- package/src/WeatherFixtures.ts +0 -223
- package/src/WebGL2Device.ts +0 -2280
- package/src/WebGL2StateCache.ts +0 -330
- package/src/WebGPUDevice.ts +0 -2877
- package/src/WebGPURenderToTextureProof.ts +0 -139
- package/src/advanced-runtime/AdvancedRenderer.ts +0 -73
- package/src/advanced-runtime/index.ts +0 -9
- package/src/animation/AnimationMaterialStyle.ts +0 -32
- package/src/animation/AnimationRenderPreset.ts +0 -45
- package/src/animation/AnimationToonMaterial.ts +0 -387
- package/src/animation/AnimationVisualQuality.ts +0 -71
- package/src/animation/applyAnimationRenderPreset.ts +0 -180
- package/src/animation/index.ts +0 -5
- package/src/cinematic/BloomPass.ts +0 -26
- package/src/cinematic/CinematicDepthComposition.ts +0 -30
- package/src/cinematic/CinematicEvidence.ts +0 -123
- package/src/cinematic/CinematicLightingRig.ts +0 -144
- package/src/cinematic/CinematicMaterialPresets.ts +0 -130
- package/src/cinematic/CinematicPostProcess.ts +0 -83
- package/src/cinematic/DepthHazePass.ts +0 -30
- package/src/cinematic/EmissivePracticalLightSystem.ts +0 -45
- package/src/cinematic/FilmGrainPass.ts +0 -24
- package/src/cinematic/FogVolumeSystem.ts +0 -30
- package/src/cinematic/GlowCardSystem.ts +0 -44
- package/src/cinematic/RainParticleSystem.ts +0 -151
- package/src/cinematic/VignettePass.ts +0 -25
- package/src/cinematic/WetReflectionApproximation.ts +0 -27
- package/src/cinematic/index.ts +0 -14
- package/src/effects/CollisionModule.ts +0 -64
- package/src/effects/ColorModule.ts +0 -60
- package/src/effects/ForceModule.ts +0 -25
- package/src/effects/GPUParticleBackend.ts +0 -569
- package/src/effects/Particle.ts +0 -108
- package/src/effects/ParticleDiagnostics.ts +0 -224
- package/src/effects/ParticleEffectPresets.ts +0 -147
- package/src/effects/ParticleEmitter.ts +0 -326
- package/src/effects/ParticleModule.ts +0 -26
- package/src/effects/ParticleRenderPass.ts +0 -117
- package/src/effects/ParticleRenderer.ts +0 -132
- package/src/effects/ParticleSystem.ts +0 -333
- package/src/effects/SizeModule.ts +0 -56
- package/src/effects/TrailModule.ts +0 -72
- package/src/effects/VelocityModule.ts +0 -75
- package/src/effects/index.ts +0 -15
- package/src/index.ts +0 -925
- package/src/materials/AlphaSorting.ts +0 -30
- package/src/materials/MaterialExtensions.ts +0 -42
- package/src/materials/PhysicalMaterial.ts +0 -133
- package/src/materials/TransmissionPass.ts +0 -51
- package/src/performance/BVH.ts +0 -18
- package/src/performance/Batcher.ts +0 -2
- package/src/performance/FrustumCuller.ts +0 -9
- package/src/performance/LOD.ts +0 -20
- package/src/performance/Octree.ts +0 -10
- package/src/performance/RenderItemSorting.ts +0 -170
- package/src/performance/RendererStats.ts +0 -43
- package/src/performance/ResourceBudget.ts +0 -44
- package/src/postprocess/BloomPass.ts +0 -28
- package/src/postprocess/CinematicDiagnostics.ts +0 -642
- package/src/postprocess/ColorGradingPass.ts +0 -24
- package/src/postprocess/DepthOfFieldPass.ts +0 -12
- package/src/postprocess/EffectComposer.ts +0 -349
- package/src/postprocess/SSAOPass.ts +0 -20
- package/src/postprocess/index.ts +0 -34
- package/src/production-runtime/AnimationControlsPipeline.ts +0 -77
- package/src/production-runtime/PBRHDRPipeline.ts +0 -381
- package/src/production-runtime/ProductionEffectsPipeline.ts +0 -85
- package/src/production-runtime/ProductionRendererTypes.ts +0 -136
- package/src/production-runtime/ProductionRuntimeRenderer.ts +0 -172
- package/src/production-runtime/ProductionWebGL2Renderer.ts +0 -313
- package/src/production-runtime/ProductionWebGPURenderer.ts +0 -396
- package/src/production-runtime/TransmissionBackdropCapture.ts +0 -130
- package/src/production-runtime/animation/MorphTargetRenderer.ts +0 -2
- package/src/production-runtime/animation/SkinningRenderer.ts +0 -2
- package/src/production-runtime/backends/RendererBackend.ts +0 -17
- package/src/production-runtime/backends/WebGL2RendererBackend.ts +0 -30
- package/src/production-runtime/backends/WebGPURendererBackend.ts +0 -24
- package/src/production-runtime/backends/webgl2/WebGL2Buffer.ts +0 -2
- package/src/production-runtime/backends/webgl2/WebGL2Capabilities.ts +0 -2
- package/src/production-runtime/backends/webgl2/WebGL2RenderTarget.ts +0 -2
- package/src/production-runtime/backends/webgl2/WebGL2Shader.ts +0 -2
- package/src/production-runtime/backends/webgl2/WebGL2StateCache.ts +0 -1
- package/src/production-runtime/backends/webgl2/WebGL2Texture.ts +0 -2
- package/src/production-runtime/backends/webgpu/WebGPUBuffer.ts +0 -2
- package/src/production-runtime/backends/webgpu/WebGPUCapabilities.ts +0 -2
- package/src/production-runtime/backends/webgpu/WebGPURenderTarget.ts +0 -2
- package/src/production-runtime/backends/webgpu/WebGPUShader.ts +0 -2
- package/src/production-runtime/backends/webgpu/WebGPUTexture.ts +0 -2
- package/src/production-runtime/color/ColorManagement.ts +0 -3
- package/src/production-runtime/color/ToneMapping.ts +0 -7
- package/src/production-runtime/diagnostics/FrameCapture.ts +0 -2
- package/src/production-runtime/diagnostics/GPUCapabilities.ts +0 -2
- package/src/production-runtime/diagnostics/RendererStats.ts +0 -2
- package/src/production-runtime/environment/EnvironmentMap.ts +0 -1
- package/src/production-runtime/environment/HDRLoader.ts +0 -57
- package/src/production-runtime/environment/PMREMGenerator.ts +0 -713
- package/src/production-runtime/framegraph/FrameGraph.ts +0 -18
- package/src/production-runtime/framegraph/RenderPass.ts +0 -15
- package/src/production-runtime/geometry/ProjectedDecalGeometry.ts +0 -439
- package/src/production-runtime/index.ts +0 -127
- package/src/production-runtime/lights/LightManager.ts +0 -7
- package/src/production-runtime/lights/ShadowMapRenderer.ts +0 -5
- package/src/production-runtime/materials/GLTFMaterialAdapter.ts +0 -6
- package/src/production-runtime/materials/GLTFPBRMaterialAdapter.ts +0 -2
- package/src/production-runtime/materials/MaterialCompiler.ts +0 -7
- package/src/production-runtime/materials/MaterialTextureBindings.ts +0 -5
- package/src/production-runtime/materials/PBRMaterial.ts +0 -2
- package/src/production-runtime/materials/PBRShaderFeatures.ts +0 -2
- package/src/production-runtime/passes/ContactShadowPass.ts +0 -184
- package/src/production-runtime/passes/DepthPrepass.ts +0 -7
- package/src/production-runtime/passes/OpaquePass.ts +0 -7
- package/src/production-runtime/passes/ShadowPass.ts +0 -7
- package/src/production-runtime/passes/SkyboxPass.ts +0 -7
- package/src/production-runtime/passes/ToneMappingPass.ts +0 -7
- package/src/production-runtime/passes/TransparentPass.ts +0 -7
- package/src/production-runtime/postprocess/BloomPass.ts +0 -51
- package/src/production-runtime/postprocess/ColorGradingPass.ts +0 -49
- package/src/production-runtime/postprocess/DOFPass.ts +0 -47
- package/src/production-runtime/postprocess/FXAAPass.ts +0 -53
- package/src/production-runtime/postprocess/ProductionEffectComposer.ts +0 -78
- package/src/production-runtime/postprocess/ProductionPostProcessTypes.ts +0 -174
- package/src/production-runtime/postprocess/SSAOPass.ts +0 -45
- package/src/production-runtime/resources/GPUBuffer.ts +0 -2
- package/src/production-runtime/resources/GPUTexture.ts +0 -2
- package/src/production-runtime/resources/RenderTarget.ts +0 -3
- package/src/production-runtime/resources/ResourceCache.ts +0 -7
- package/src/production-runtime/scene/Camera.ts +0 -1
- package/src/production-runtime/scene/Lights.ts +0 -2
- package/src/production-runtime/scene/RenderableMesh.ts +0 -2
- package/src/production-runtime/scene/RenderablePrimitive.ts +0 -1
- package/src/production-runtime/scene/RenderableScene.ts +0 -4
- package/src/production-runtime/shaders/ShaderProgramLibrary.ts +0 -7
- package/src/production-runtime/shaders/chunks/brdf.glsl +0 -2
- package/src/production-runtime/shaders/chunks/ibl.glsl +0 -3
- package/src/production-runtime/shaders/chunks/pbr.frag.glsl +0 -47
- package/src/production-runtime/shaders/chunks/pbr.vert.glsl +0 -8
- package/src/production-runtime/shaders/chunks/shadows.glsl +0 -2
- package/src/production-runtime/shaders/wgsl/pbr.wgsl +0 -3
- package/src/production-runtime/shaders/wgsl/postprocess.wgsl +0 -2
- package/src/production-runtime/shaders/wgsl/skybox.wgsl +0 -2
- package/src/shaders/depth.frag.glsl +0 -5
- package/src/shaders/depth.vert.glsl +0 -8
- package/src/shaders/pbr-direct.frag.glsl +0 -687
- package/src/shaders/pbr-direct.vert.glsl +0 -18
- package/src/shaders/unlit.frag.glsl +0 -18
- package/src/shaders/unlit.vert.glsl +0 -13
- package/src/shadows/CascadedShadowPipeline.ts +0 -53
- package/src/shadows/ContactShadows.ts +0 -106
- package/src/shadows/ShadowDebugViews.ts +0 -46
- package/src/threejs-compatibility/InstancingSystem.ts +0 -17
- package/src/threejs-compatibility/LightingSystem.ts +0 -22
- package/src/threejs-compatibility/MaterialSystem.ts +0 -9
- package/src/threejs-compatibility/RenderTargetSystem.ts +0 -35
- package/src/threejs-compatibility/RendererDiagnostics.ts +0 -61
- package/src/threejs-compatibility/SceneRenderer.ts +0 -34
- package/src/threejs-compatibility/ShadowSystem.ts +0 -19
- package/src/threejs-compatibility/TextureSystem.ts +0 -19
- package/src/threejs-compatibility/ThreeCompatRenderer.ts +0 -90
- package/src/threejs-compatibility/TransparencySystem.ts +0 -19
- package/src/threejs-compatibility/index.ts +0 -29
- package/src/threejs-compatibility/performance/BVH.ts +0 -14
- package/src/threejs-compatibility/performance/FrustumCulling.ts +0 -10
- package/src/threejs-compatibility/performance/Instancing.ts +0 -4
- package/src/threejs-compatibility/performance/LODSystem.ts +0 -16
- package/src/threejs-compatibility/performance/OcclusionCulling.ts +0 -6
- package/src/threejs-compatibility/performance/RaycastAcceleration.ts +0 -12
- package/src/threejs-compatibility/performance/RendererProfiler.ts +0 -19
- package/src/threejs-compatibility/performance/TextureStreaming.ts +0 -4
- package/src/threejs-compatibility/performance/index.ts +0 -12
- package/src/threejs-compatibility/postprocess/BloomPass.ts +0 -16
- package/src/threejs-compatibility/postprocess/ColorGradingPass.ts +0 -16
- package/src/threejs-compatibility/postprocess/DepthOfFieldPass.ts +0 -21
- package/src/threejs-compatibility/postprocess/EffectComposer.ts +0 -14
- package/src/threejs-compatibility/postprocess/FXAAPass.ts +0 -15
- package/src/threejs-compatibility/postprocess/MotionBlurPass.ts +0 -20
- package/src/threejs-compatibility/postprocess/OutlinePass.ts +0 -21
- package/src/threejs-compatibility/postprocess/PostProcessTypes.ts +0 -169
- package/src/threejs-compatibility/postprocess/RenderPass.ts +0 -9
- package/src/threejs-compatibility/postprocess/SMAAPass.ts +0 -15
- package/src/threejs-compatibility/postprocess/SSAOPass.ts +0 -21
- package/src/threejs-compatibility/postprocess/ShaderPass.ts +0 -7
- package/src/threejs-compatibility/postprocess/TAAPass.ts +0 -15
- package/src/threejs-compatibility/postprocess/VignettePass.ts +0 -16
- package/src/threejs-compatibility/postprocess/index.ts +0 -15
- package/src/threejs-compatibility/shaders/NodeMaterial.ts +0 -21
- package/src/threejs-compatibility/shaders/RawShaderMaterial.ts +0 -6
- package/src/threejs-compatibility/shaders/ShaderDiagnostics.ts +0 -17
- package/src/threejs-compatibility/shaders/ShaderMaterial.ts +0 -18
- package/src/threejs-compatibility/shaders/ThreeCompatShaderChunks.ts +0 -6
- package/src/threejs-compatibility/shaders/Uniforms.ts +0 -18
- package/src/threejs-compatibility/shaders/index.ts +0 -9
- package/src/threejs-compatibility/vfx/GPUPointCloud.ts +0 -7
- package/src/threejs-compatibility/vfx/LineRenderer.ts +0 -13
- package/src/threejs-compatibility/vfx/ParticleSystem.ts +0 -23
- package/src/threejs-compatibility/vfx/SpriteSystem.ts +0 -14
- package/src/threejs-compatibility/vfx/TrailRenderer.ts +0 -8
- package/src/threejs-compatibility/vfx/VFXDiagnostics.ts +0 -23
- package/src/threejs-compatibility/vfx/index.ts +0 -9
- package/src/threejs-example-parity/index.ts +0 -205
- package/src/webgpu/WebGPUBuffer.ts +0 -1
- package/src/webgpu/WebGPUCompute.ts +0 -2
- package/src/webgpu/WebGPUPipelineCache.ts +0 -15
- package/src/webgpu/WebGPUPostProcess.ts +0 -2
- package/src/webgpu/WebGPUTexture.ts +0 -2
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -20
package/src/WebGL2Device.ts
DELETED
|
@@ -1,2280 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type BufferUsage,
|
|
3
|
-
type DrawCommand,
|
|
4
|
-
type InstanceVertexAttribute,
|
|
5
|
-
type LdrPostprocessPresentationOptions,
|
|
6
|
-
type RenderBuffer,
|
|
7
|
-
type RenderDevice,
|
|
8
|
-
RenderDeviceError,
|
|
9
|
-
type RenderDeviceDiagnostics,
|
|
10
|
-
type RenderDeviceInfo,
|
|
11
|
-
type RenderShaderProgram,
|
|
12
|
-
type RenderTarget,
|
|
13
|
-
type RenderTargetDescriptor,
|
|
14
|
-
type ShaderReflection,
|
|
15
|
-
type ShaderSources,
|
|
16
|
-
} from "./RenderDevice";
|
|
17
|
-
import { Texture, isCompressedTextureFormat, type TextureCompressedFormat, type TextureCubeFace, type TexturePixelData } from "./Texture";
|
|
18
|
-
import { TextureBinding } from "./TextureBinding";
|
|
19
|
-
import type { Sampler, TextureMagFilter, TextureMinFilter } from "./Sampler";
|
|
20
|
-
import { type VertexAttribute, type VertexFormat } from "./VertexFormat";
|
|
21
|
-
import { WebGL2StateCache } from "./WebGL2StateCache";
|
|
22
|
-
|
|
23
|
-
interface TextureFilterAnisotropicExtension {
|
|
24
|
-
readonly TEXTURE_MAX_ANISOTROPY_EXT: GLenum;
|
|
25
|
-
readonly MAX_TEXTURE_MAX_ANISOTROPY_EXT: GLenum;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const WEBGL_CUBE_FACES: readonly TextureCubeFace[] = ["px", "nx", "py", "ny", "pz", "nz"];
|
|
29
|
-
|
|
30
|
-
export interface WebGL2DeviceOptions {
|
|
31
|
-
readonly canvas: HTMLCanvasElement | OffscreenCanvas;
|
|
32
|
-
readonly antialias?: boolean;
|
|
33
|
-
readonly alpha?: boolean;
|
|
34
|
-
readonly preserveDrawingBuffer?: boolean;
|
|
35
|
-
readonly errorCheckMode?: WebGL2ErrorCheckMode;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export type WebGL2ErrorCheckMode = "strict" | "frame";
|
|
39
|
-
|
|
40
|
-
class WebGL2Buffer implements RenderBuffer {
|
|
41
|
-
public disposed = false;
|
|
42
|
-
|
|
43
|
-
constructor(
|
|
44
|
-
public readonly id: number,
|
|
45
|
-
public readonly usage: BufferUsage,
|
|
46
|
-
public readonly byteLength: number,
|
|
47
|
-
public readonly target: GLenum,
|
|
48
|
-
public readonly handle: WebGLBuffer,
|
|
49
|
-
private readonly gl: WebGL2RenderingContext
|
|
50
|
-
) {}
|
|
51
|
-
|
|
52
|
-
dispose(): void {
|
|
53
|
-
if (!this.disposed) {
|
|
54
|
-
this.gl.deleteBuffer(this.handle);
|
|
55
|
-
this.disposed = true;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
class WebGL2ShaderProgram implements RenderShaderProgram {
|
|
61
|
-
public disposed = false;
|
|
62
|
-
|
|
63
|
-
constructor(
|
|
64
|
-
public readonly id: number,
|
|
65
|
-
public readonly label: string,
|
|
66
|
-
public readonly marker: string,
|
|
67
|
-
public readonly reflection: ShaderReflection,
|
|
68
|
-
public readonly handle: WebGLProgram,
|
|
69
|
-
private readonly gl: WebGL2RenderingContext
|
|
70
|
-
) {}
|
|
71
|
-
|
|
72
|
-
dispose(): void {
|
|
73
|
-
if (!this.disposed) {
|
|
74
|
-
this.gl.deleteProgram(this.handle);
|
|
75
|
-
this.disposed = true;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
class WebGL2RenderTarget implements RenderTarget {
|
|
81
|
-
public disposed = false;
|
|
82
|
-
|
|
83
|
-
constructor(
|
|
84
|
-
public readonly id: number,
|
|
85
|
-
public readonly width: number,
|
|
86
|
-
public readonly height: number,
|
|
87
|
-
public readonly label: string,
|
|
88
|
-
public readonly colorTexture: Texture,
|
|
89
|
-
public readonly depthTexture: Texture | undefined,
|
|
90
|
-
public readonly framebuffer: WebGLFramebuffer,
|
|
91
|
-
public readonly colorHandle: WebGLTexture,
|
|
92
|
-
public readonly depthHandle: WebGLRenderbuffer | null,
|
|
93
|
-
public readonly depthTextureHandle: WebGLTexture | null,
|
|
94
|
-
private readonly gl: WebGL2RenderingContext
|
|
95
|
-
) {}
|
|
96
|
-
|
|
97
|
-
dispose(): void {
|
|
98
|
-
if (!this.disposed) {
|
|
99
|
-
this.gl.deleteFramebuffer(this.framebuffer);
|
|
100
|
-
this.gl.deleteTexture(this.colorHandle);
|
|
101
|
-
if (this.depthHandle) this.gl.deleteRenderbuffer(this.depthHandle);
|
|
102
|
-
if (this.depthTextureHandle) this.gl.deleteTexture(this.depthTextureHandle);
|
|
103
|
-
this.colorTexture.dispose();
|
|
104
|
-
this.depthTexture?.dispose();
|
|
105
|
-
this.disposed = true;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
interface WebGL2VertexArrayCacheEntry {
|
|
111
|
-
readonly key: string;
|
|
112
|
-
readonly handle: WebGLVertexArrayObject;
|
|
113
|
-
readonly boundLocations: ReadonlySet<number>;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
interface WebGL2TextureUnit0Snapshot {
|
|
117
|
-
readonly activeTexture: GLenum;
|
|
118
|
-
readonly texture2d: WebGLTexture | null;
|
|
119
|
-
readonly sampler: WebGLSampler | null;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
interface WebGL2FullscreenPresentationStateSnapshot {
|
|
123
|
-
readonly framebuffer: WebGLFramebuffer | null;
|
|
124
|
-
readonly program: WebGLProgram | null;
|
|
125
|
-
readonly textureUnit0: WebGL2TextureUnit0Snapshot;
|
|
126
|
-
readonly vertexArray: WebGLVertexArrayObject | null;
|
|
127
|
-
readonly viewport: Int32Array | readonly number[];
|
|
128
|
-
readonly colorMask: readonly boolean[];
|
|
129
|
-
readonly depthTestEnabled: boolean;
|
|
130
|
-
readonly cullFaceEnabled: boolean;
|
|
131
|
-
readonly blendEnabled: boolean;
|
|
132
|
-
readonly scissorTestEnabled: boolean;
|
|
133
|
-
readonly stencilTestEnabled: boolean;
|
|
134
|
-
readonly polygonOffsetFillEnabled: boolean;
|
|
135
|
-
readonly depthMask: boolean;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export class WebGL2Device implements RenderDevice {
|
|
139
|
-
public readonly kind = "webgl2";
|
|
140
|
-
public readonly info: RenderDeviceInfo;
|
|
141
|
-
public disposed = false;
|
|
142
|
-
public contextLost = false;
|
|
143
|
-
private readonly gl: WebGL2RenderingContext;
|
|
144
|
-
private nextId = 1;
|
|
145
|
-
private drawCalls = 0;
|
|
146
|
-
private buffers = new Set<WebGL2Buffer>();
|
|
147
|
-
private shaders = new Set<WebGL2ShaderProgram>();
|
|
148
|
-
private renderTargets = new Set<WebGL2RenderTarget>();
|
|
149
|
-
private activeRenderTarget: WebGL2RenderTarget | null = null;
|
|
150
|
-
private textures = new Map<Texture, WebGLTexture>();
|
|
151
|
-
private textureUploadModes = new Map<Texture, "compressed" | "cube" | "depth-render-target" | "fallback" | "rgba8" | "rgba16f" | "rgba32f">();
|
|
152
|
-
private fallbackTexture: WebGLTexture | null = null;
|
|
153
|
-
private fallbackCubeTexture: WebGLTexture | null = null;
|
|
154
|
-
private readonly samplerObjectCache = new Map<string, WebGLSampler>();
|
|
155
|
-
private presentationProgram: WebGLProgram | null = null;
|
|
156
|
-
private ldrPostprocessProgram: WebGLProgram | null = null;
|
|
157
|
-
private depthReadbackProgram: WebGLProgram | null = null;
|
|
158
|
-
private readonly uniformLocationCache = new WeakMap<WebGL2ShaderProgram, Map<string, WebGLUniformLocation | null>>();
|
|
159
|
-
private readonly textureSamplerParameterCache = new WeakMap<WebGLTexture, Map<GLenum, GLenum | number>>();
|
|
160
|
-
private readonly stateCache = new WebGL2StateCache({ label: "webgl2-device-state-cache" });
|
|
161
|
-
private readonly textureUnitBindings = new Map<string, WebGLTexture>();
|
|
162
|
-
private readonly vertexArrayCache = new Map<string, WebGL2VertexArrayCacheEntry>();
|
|
163
|
-
private readonly vertexFormatIds = new WeakMap<VertexFormat, number>();
|
|
164
|
-
private presentationVertexArray: WebGLVertexArrayObject | null = null;
|
|
165
|
-
private activeTextureUnitIndex = -1;
|
|
166
|
-
private shaderProgramCreateCount = 0;
|
|
167
|
-
private uniformLocationLookupCount = 0;
|
|
168
|
-
private bufferUpdateCount = 0;
|
|
169
|
-
private textureBindCount = 0;
|
|
170
|
-
private samplerParameterUploadCount = 0;
|
|
171
|
-
private samplerAnisotropyUploadCount = 0;
|
|
172
|
-
private vertexArrayCreateCount = 0;
|
|
173
|
-
private nativeInstancedSubmissions = 0;
|
|
174
|
-
private nextVertexFormatId = 1;
|
|
175
|
-
private releasedTextureHandles = 0;
|
|
176
|
-
private nativeEnvironmentBindings = 0;
|
|
177
|
-
private nativeShadowMapBindings = 0;
|
|
178
|
-
private lastError: string | null = null;
|
|
179
|
-
private frameActive = false;
|
|
180
|
-
private viewportWidth = 0;
|
|
181
|
-
private viewportHeight = 0;
|
|
182
|
-
private readonly maxVertexAttributes: number;
|
|
183
|
-
private readonly anisotropicFilteringExtension: TextureFilterAnisotropicExtension | null;
|
|
184
|
-
private readonly maxTextureAnisotropy: number;
|
|
185
|
-
private readonly contextLostListener?: EventListener;
|
|
186
|
-
private readonly contextRestoredListener?: EventListener;
|
|
187
|
-
private readonly canvas?: HTMLCanvasElement | OffscreenCanvas;
|
|
188
|
-
|
|
189
|
-
static create(options: WebGL2DeviceOptions): WebGL2Device {
|
|
190
|
-
const gl = options.canvas.getContext("webgl2", {
|
|
191
|
-
antialias: options.antialias ?? true,
|
|
192
|
-
alpha: options.alpha ?? false,
|
|
193
|
-
preserveDrawingBuffer: options.preserveDrawingBuffer ?? false
|
|
194
|
-
});
|
|
195
|
-
if (!gl) {
|
|
196
|
-
throw new RenderDeviceError("WebGL2 is not available for the provided canvas", "WEBGL2_UNAVAILABLE");
|
|
197
|
-
}
|
|
198
|
-
return new WebGL2Device(gl as WebGL2RenderingContext, options.canvas, options.errorCheckMode ?? "strict");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private constructor(
|
|
202
|
-
gl: WebGL2RenderingContext,
|
|
203
|
-
canvas: HTMLCanvasElement | OffscreenCanvas,
|
|
204
|
-
private readonly errorCheckMode: WebGL2ErrorCheckMode
|
|
205
|
-
) {
|
|
206
|
-
this.gl = gl;
|
|
207
|
-
this.maxVertexAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS) as number;
|
|
208
|
-
this.anisotropicFilteringExtension = gl.getExtension("EXT_texture_filter_anisotropic") as TextureFilterAnisotropicExtension | null;
|
|
209
|
-
this.maxTextureAnisotropy = this.anisotropicFilteringExtension
|
|
210
|
-
? Math.max(1, gl.getParameter(this.anisotropicFilteringExtension.MAX_TEXTURE_MAX_ANISOTROPY_EXT) as number)
|
|
211
|
-
: 1;
|
|
212
|
-
this.info = {
|
|
213
|
-
backend: "webgl2",
|
|
214
|
-
vendor: gl.getParameter(gl.VENDOR) as string,
|
|
215
|
-
renderer: gl.getParameter(gl.RENDERER) as string,
|
|
216
|
-
capabilities: [
|
|
217
|
-
"buffers",
|
|
218
|
-
"buffer-readback",
|
|
219
|
-
"shader-validation",
|
|
220
|
-
"render-targets",
|
|
221
|
-
"pixel-readback",
|
|
222
|
-
"postprocess-presentation",
|
|
223
|
-
"draw-validation",
|
|
224
|
-
"rasterization",
|
|
225
|
-
"depth-render-targets",
|
|
226
|
-
"depth-textures",
|
|
227
|
-
"spot-shadow-maps",
|
|
228
|
-
"point-shadow-maps",
|
|
229
|
-
"hdr-image-based-lighting",
|
|
230
|
-
...(this.anisotropicFilteringExtension ? ["anisotropic-texture-filtering" as const] : []),
|
|
231
|
-
...(gl.getExtension("EXT_color_buffer_float") ? ["hdr-render-targets" as const, "float-readback" as const] : [])
|
|
232
|
-
]
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
this.canvas = canvas;
|
|
236
|
-
if ("addEventListener" in canvas) {
|
|
237
|
-
this.contextLostListener = ((event: Event) => {
|
|
238
|
-
event.preventDefault();
|
|
239
|
-
this.contextLost = true;
|
|
240
|
-
this.lastError = "CONTEXT_LOST";
|
|
241
|
-
this.frameActive = false;
|
|
242
|
-
}) as EventListener;
|
|
243
|
-
this.contextRestoredListener = (() => {
|
|
244
|
-
this.contextLost = false;
|
|
245
|
-
this.lastError = null;
|
|
246
|
-
}) as EventListener;
|
|
247
|
-
canvas.addEventListener("webglcontextlost", this.contextLostListener);
|
|
248
|
-
canvas.addEventListener("webglcontextrestored", this.contextRestoredListener);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
createBuffer(usage: BufferUsage, byteLength: number, initialData?: ArrayBufferView): RenderBuffer {
|
|
253
|
-
this.assertAlive();
|
|
254
|
-
if (byteLength <= 0 || !Number.isInteger(byteLength)) {
|
|
255
|
-
throw new RenderDeviceError("Buffer byteLength must be a positive integer", "INVALID_BUFFER_SIZE", { byteLength });
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const handle = this.gl.createBuffer();
|
|
259
|
-
if (!handle) {
|
|
260
|
-
throw new RenderDeviceError("Failed to allocate WebGL buffer", "WEBGL_ALLOCATION_FAILED");
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const target = usage === "index" ? this.gl.ELEMENT_ARRAY_BUFFER : this.gl.ARRAY_BUFFER;
|
|
264
|
-
this.bindNoVertexArray();
|
|
265
|
-
this.stateCache.bindBuffer(target, handle, () => this.gl.bindBuffer(target, handle));
|
|
266
|
-
this.gl.bufferData(target, byteLength, this.gl.DYNAMIC_DRAW);
|
|
267
|
-
if (initialData) {
|
|
268
|
-
if (initialData.byteLength > byteLength) {
|
|
269
|
-
throw new RenderDeviceError("Initial data exceeds buffer size", "BUFFER_OVERFLOW", {
|
|
270
|
-
byteLength,
|
|
271
|
-
dataByteLength: initialData.byteLength
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
this.gl.bufferSubData(target, 0, initialData);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
const buffer = new WebGL2Buffer(this.nextId++, usage, byteLength, target, handle, this.gl);
|
|
278
|
-
this.buffers.add(buffer);
|
|
279
|
-
return buffer;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
updateBuffer(buffer: RenderBuffer, byteOffset: number, data: ArrayBufferView): void {
|
|
283
|
-
this.assertAlive();
|
|
284
|
-
const webglBuffer = this.requireBuffer(buffer);
|
|
285
|
-
if (byteOffset < 0 || byteOffset + data.byteLength > webglBuffer.byteLength) {
|
|
286
|
-
throw new RenderDeviceError("Buffer update range is out of bounds", "BUFFER_RANGE_OUT_OF_BOUNDS", {
|
|
287
|
-
byteOffset,
|
|
288
|
-
dataByteLength: data.byteLength,
|
|
289
|
-
byteLength: webglBuffer.byteLength
|
|
290
|
-
});
|
|
291
|
-
}
|
|
292
|
-
this.bindNoVertexArray();
|
|
293
|
-
this.stateCache.bindBuffer(webglBuffer.target, webglBuffer.handle, () => this.gl.bindBuffer(webglBuffer.target, webglBuffer.handle));
|
|
294
|
-
this.gl.bufferSubData(webglBuffer.target, byteOffset, data);
|
|
295
|
-
this.bufferUpdateCount += 1;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
readBuffer(buffer: RenderBuffer, byteOffset = 0, byteLength = buffer.byteLength - byteOffset): Uint8Array {
|
|
299
|
-
this.assertAlive();
|
|
300
|
-
const webglBuffer = this.requireBuffer(buffer);
|
|
301
|
-
if (byteOffset < 0 || byteLength < 0 || byteOffset + byteLength > webglBuffer.byteLength) {
|
|
302
|
-
throw new RenderDeviceError("Buffer read range is out of bounds", "BUFFER_RANGE_OUT_OF_BOUNDS", {
|
|
303
|
-
byteOffset,
|
|
304
|
-
byteLength,
|
|
305
|
-
bufferByteLength: webglBuffer.byteLength
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
const output = new Uint8Array(byteLength);
|
|
309
|
-
this.bindNoVertexArray();
|
|
310
|
-
this.stateCache.bindBuffer(webglBuffer.target, webglBuffer.handle, () => this.gl.bindBuffer(webglBuffer.target, webglBuffer.handle));
|
|
311
|
-
this.gl.getBufferSubData(webglBuffer.target, byteOffset, output);
|
|
312
|
-
return output;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
createShaderProgram(sources: ShaderSources): RenderShaderProgram {
|
|
316
|
-
this.assertAlive();
|
|
317
|
-
if (!sources.vertex.includes(sources.marker) || !sources.fragment.includes(sources.marker)) {
|
|
318
|
-
throw new RenderDeviceError("Shader source marker is missing from compiled sources", "SHADER_MARKER_MISSING", {
|
|
319
|
-
label: sources.label,
|
|
320
|
-
marker: sources.marker
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
const vertexShader = this.compileShader(this.gl.VERTEX_SHADER, sources.vertex, sources.label);
|
|
324
|
-
const fragmentShader = this.compileShader(this.gl.FRAGMENT_SHADER, sources.fragment, sources.label);
|
|
325
|
-
const program = this.gl.createProgram();
|
|
326
|
-
if (!program) {
|
|
327
|
-
throw new RenderDeviceError("Failed to allocate WebGL shader program", "WEBGL_ALLOCATION_FAILED");
|
|
328
|
-
}
|
|
329
|
-
this.shaderProgramCreateCount += 1;
|
|
330
|
-
this.gl.attachShader(program, vertexShader);
|
|
331
|
-
this.gl.attachShader(program, fragmentShader);
|
|
332
|
-
this.gl.linkProgram(program);
|
|
333
|
-
this.gl.deleteShader(vertexShader);
|
|
334
|
-
this.gl.deleteShader(fragmentShader);
|
|
335
|
-
|
|
336
|
-
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
337
|
-
const log = this.gl.getProgramInfoLog(program) ?? "Unknown shader link error";
|
|
338
|
-
this.gl.deleteProgram(program);
|
|
339
|
-
throw new RenderDeviceError("WebGL shader link failed", "SHADER_LINK_FAILED", { label: sources.label, log });
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
const shader = new WebGL2ShaderProgram(
|
|
343
|
-
this.nextId++,
|
|
344
|
-
sources.label,
|
|
345
|
-
sources.marker,
|
|
346
|
-
this.reflectProgram(program),
|
|
347
|
-
program,
|
|
348
|
-
this.gl
|
|
349
|
-
);
|
|
350
|
-
this.shaders.add(shader);
|
|
351
|
-
return shader;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
createRenderTarget(descriptor: RenderTargetDescriptor): RenderTarget {
|
|
355
|
-
this.assertAlive();
|
|
356
|
-
if (
|
|
357
|
-
!Number.isInteger(descriptor.width) ||
|
|
358
|
-
descriptor.width <= 0 ||
|
|
359
|
-
!Number.isInteger(descriptor.height) ||
|
|
360
|
-
descriptor.height <= 0
|
|
361
|
-
) {
|
|
362
|
-
throw new RenderDeviceError("Render target dimensions must be positive integers", "INVALID_RENDER_TARGET_SIZE", {
|
|
363
|
-
width: descriptor.width,
|
|
364
|
-
height: descriptor.height,
|
|
365
|
-
label: descriptor.label
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
|
-
const colorHandle = this.gl.createTexture();
|
|
369
|
-
const framebuffer = this.gl.createFramebuffer();
|
|
370
|
-
const depthMode = descriptor.depth === "texture" ? "texture" : descriptor.depth === false ? "none" : "renderbuffer";
|
|
371
|
-
const depthHandle = depthMode === "renderbuffer" ? this.gl.createRenderbuffer() : null;
|
|
372
|
-
const depthTextureHandle = depthMode === "texture" ? this.gl.createTexture() : null;
|
|
373
|
-
if (!colorHandle || !framebuffer || (depthMode === "renderbuffer" && !depthHandle) || (depthMode === "texture" && !depthTextureHandle)) {
|
|
374
|
-
if (colorHandle) this.gl.deleteTexture(colorHandle);
|
|
375
|
-
if (framebuffer) this.gl.deleteFramebuffer(framebuffer);
|
|
376
|
-
if (depthHandle) this.gl.deleteRenderbuffer(depthHandle);
|
|
377
|
-
if (depthTextureHandle) this.gl.deleteTexture(depthTextureHandle);
|
|
378
|
-
throw new RenderDeviceError("Failed to allocate WebGL render target", "WEBGL_ALLOCATION_FAILED", {
|
|
379
|
-
width: descriptor.width,
|
|
380
|
-
height: descriptor.height,
|
|
381
|
-
label: descriptor.label
|
|
382
|
-
});
|
|
383
|
-
}
|
|
384
|
-
const previousActiveTexture = this.gl.getParameter(this.gl.ACTIVE_TEXTURE) as GLenum;
|
|
385
|
-
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
386
|
-
const previousTexture = this.gl.getParameter(this.gl.TEXTURE_BINDING_2D) as WebGLTexture | null;
|
|
387
|
-
const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
|
|
388
|
-
const previousRenderbuffer = this.gl.getParameter(this.gl.RENDERBUFFER_BINDING) as WebGLRenderbuffer | null;
|
|
389
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, colorHandle);
|
|
390
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
|
|
391
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
392
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
393
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
394
|
-
const format = descriptor.format ?? "rgba8";
|
|
395
|
-
const textureFormat = this.resolveRenderTargetFormat(format);
|
|
396
|
-
this.gl.texImage2D(
|
|
397
|
-
this.gl.TEXTURE_2D,
|
|
398
|
-
0,
|
|
399
|
-
textureFormat.internalFormat,
|
|
400
|
-
descriptor.width,
|
|
401
|
-
descriptor.height,
|
|
402
|
-
0,
|
|
403
|
-
this.gl.RGBA,
|
|
404
|
-
textureFormat.type,
|
|
405
|
-
null
|
|
406
|
-
);
|
|
407
|
-
|
|
408
|
-
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
|
|
409
|
-
this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, colorHandle, 0);
|
|
410
|
-
if (depthHandle) {
|
|
411
|
-
this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, depthHandle);
|
|
412
|
-
this.gl.renderbufferStorage(this.gl.RENDERBUFFER, this.gl.DEPTH_COMPONENT16, descriptor.width, descriptor.height);
|
|
413
|
-
this.gl.framebufferRenderbuffer(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.RENDERBUFFER, depthHandle);
|
|
414
|
-
}
|
|
415
|
-
if (depthTextureHandle) {
|
|
416
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, depthTextureHandle);
|
|
417
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
|
|
418
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
|
|
419
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
420
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
421
|
-
this.gl.texImage2D(
|
|
422
|
-
this.gl.TEXTURE_2D,
|
|
423
|
-
0,
|
|
424
|
-
this.gl.DEPTH_COMPONENT24,
|
|
425
|
-
descriptor.width,
|
|
426
|
-
descriptor.height,
|
|
427
|
-
0,
|
|
428
|
-
this.gl.DEPTH_COMPONENT,
|
|
429
|
-
this.gl.UNSIGNED_INT,
|
|
430
|
-
null
|
|
431
|
-
);
|
|
432
|
-
this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.TEXTURE_2D, depthTextureHandle, 0);
|
|
433
|
-
}
|
|
434
|
-
const status = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
|
|
435
|
-
this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, previousRenderbuffer);
|
|
436
|
-
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
|
|
437
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, previousTexture);
|
|
438
|
-
this.gl.activeTexture(previousActiveTexture);
|
|
439
|
-
this.stateCache.invalidate();
|
|
440
|
-
if (status !== this.gl.FRAMEBUFFER_COMPLETE) {
|
|
441
|
-
this.gl.deleteTexture(colorHandle);
|
|
442
|
-
this.gl.deleteFramebuffer(framebuffer);
|
|
443
|
-
this.gl.deleteRenderbuffer(depthHandle);
|
|
444
|
-
if (depthTextureHandle) this.gl.deleteTexture(depthTextureHandle);
|
|
445
|
-
throw new RenderDeviceError("WebGL render target framebuffer status is invalid", "FRAMEBUFFER_INVALID", { status });
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const depthTexture = depthTextureHandle
|
|
449
|
-
? new Texture({ width: descriptor.width, height: descriptor.height, format: "depth24", label: `${descriptor.label ?? "render-target"}-depth` })
|
|
450
|
-
: undefined;
|
|
451
|
-
const target = new WebGL2RenderTarget(
|
|
452
|
-
this.nextId++,
|
|
453
|
-
descriptor.width,
|
|
454
|
-
descriptor.height,
|
|
455
|
-
descriptor.label ?? "render-target",
|
|
456
|
-
new Texture({ width: descriptor.width, height: descriptor.height, format, label: descriptor.label ?? "render-target-color" }),
|
|
457
|
-
depthTexture,
|
|
458
|
-
framebuffer,
|
|
459
|
-
colorHandle,
|
|
460
|
-
depthHandle,
|
|
461
|
-
depthTextureHandle,
|
|
462
|
-
this.gl
|
|
463
|
-
);
|
|
464
|
-
this.renderTargets.add(target);
|
|
465
|
-
this.textures.set(target.colorTexture, colorHandle);
|
|
466
|
-
this.textureUploadModes.set(target.colorTexture, "rgba8");
|
|
467
|
-
if (depthTexture && depthTextureHandle) {
|
|
468
|
-
this.textures.set(depthTexture, depthTextureHandle);
|
|
469
|
-
this.textureUploadModes.set(depthTexture, "depth-render-target");
|
|
470
|
-
}
|
|
471
|
-
return target;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
setRenderTarget(target: RenderTarget | null): void {
|
|
475
|
-
this.assertAlive();
|
|
476
|
-
if (target === null) {
|
|
477
|
-
this.activeRenderTarget = null;
|
|
478
|
-
this.stateCache.bindFramebuffer(this.gl.FRAMEBUFFER, null, () => this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null));
|
|
479
|
-
this.stateCache.viewport(
|
|
480
|
-
0,
|
|
481
|
-
0,
|
|
482
|
-
this.viewportWidth || this.gl.drawingBufferWidth,
|
|
483
|
-
this.viewportHeight || this.gl.drawingBufferHeight,
|
|
484
|
-
() => this.gl.viewport(0, 0, this.viewportWidth || this.gl.drawingBufferWidth, this.viewportHeight || this.gl.drawingBufferHeight)
|
|
485
|
-
);
|
|
486
|
-
return;
|
|
487
|
-
}
|
|
488
|
-
if (!(target instanceof WebGL2RenderTarget) || !this.renderTargets.has(target) || target.disposed) {
|
|
489
|
-
throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
490
|
-
targetId: target.id
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
this.activeRenderTarget = target;
|
|
494
|
-
this.stateCache.bindFramebuffer(this.gl.FRAMEBUFFER, target.framebuffer, () => this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, target.framebuffer));
|
|
495
|
-
this.stateCache.viewport(0, 0, target.width, target.height, () => this.gl.viewport(0, 0, target.width, target.height));
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
writeRenderTargetPixels(target: RenderTarget, pixels: Uint8Array): void {
|
|
499
|
-
this.assertAlive();
|
|
500
|
-
if (!(target instanceof WebGL2RenderTarget) || !this.renderTargets.has(target) || target.disposed) {
|
|
501
|
-
throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
502
|
-
targetId: target.id
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
if (target.colorTexture.format !== "rgba8") {
|
|
506
|
-
throw new RenderDeviceError("Byte pixel uploads are only supported for rgba8 WebGL2 render targets", "INVALID_PIXEL_UPLOAD_FORMAT", {
|
|
507
|
-
targetId: target.id,
|
|
508
|
-
format: target.colorTexture.format
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
if (pixels.length !== target.width * target.height * 4) {
|
|
512
|
-
throw new RenderDeviceError("Render-target pixel upload must contain width * height * 4 bytes", "INVALID_PIXEL_UPLOAD_SIZE", {
|
|
513
|
-
targetId: target.id,
|
|
514
|
-
byteLength: pixels.length,
|
|
515
|
-
expectedByteLength: target.width * target.height * 4
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
const textureUnit0 = this.captureTextureUnit0();
|
|
519
|
-
try {
|
|
520
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, target.colorHandle);
|
|
521
|
-
this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, target.width, target.height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
|
|
522
|
-
} finally {
|
|
523
|
-
this.restoreTextureUnit0(textureUnit0);
|
|
524
|
-
}
|
|
525
|
-
this.lastError = this.readError();
|
|
526
|
-
if (this.lastError) {
|
|
527
|
-
throw new RenderDeviceError("WebGL2 render-target pixel upload failed", "WEBGL_PIXEL_UPLOAD_FAILED", {
|
|
528
|
-
targetId: target.id,
|
|
529
|
-
error: this.lastError
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
presentRenderTarget(source: RenderTarget): void {
|
|
535
|
-
this.assertAlive();
|
|
536
|
-
if (!(source instanceof WebGL2RenderTarget) || !this.renderTargets.has(source) || source.disposed) {
|
|
537
|
-
throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
538
|
-
targetId: source.id
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
const outputWidth = this.viewportWidth || this.gl.drawingBufferWidth;
|
|
542
|
-
const outputHeight = this.viewportHeight || this.gl.drawingBufferHeight;
|
|
543
|
-
this.drawRenderTargetToBackbuffer(source, outputWidth, outputHeight);
|
|
544
|
-
this.activeRenderTarget = null;
|
|
545
|
-
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
|
|
546
|
-
this.lastError = this.readError();
|
|
547
|
-
if (this.lastError) {
|
|
548
|
-
throw new RenderDeviceError(`WebGL2 render-target presentation failed: ${this.lastError}`, "WEBGL_PRESENT_FAILED", {
|
|
549
|
-
targetId: source.id,
|
|
550
|
-
error: this.lastError
|
|
551
|
-
});
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
presentLdrPostprocess(source: RenderTarget, options: LdrPostprocessPresentationOptions): void {
|
|
556
|
-
this.assertAlive();
|
|
557
|
-
if (!(source instanceof WebGL2RenderTarget) || !this.renderTargets.has(source) || source.disposed) {
|
|
558
|
-
throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
559
|
-
targetId: source.id
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
const tonePass = options.passes.find((pass) => pass.name === "tone-mapping");
|
|
563
|
-
if (source.colorTexture.format !== "rgba8" && !tonePass) {
|
|
564
|
-
throw new RenderDeviceError("WebGL2 HDR postprocess presentation requires a tone-mapping pass before LDR output.", "WEBGL_LDR_POSTPROCESS_FORMAT_UNSUPPORTED", {
|
|
565
|
-
targetId: source.id,
|
|
566
|
-
format: source.colorTexture.format
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
const outputTarget = options.outputTarget;
|
|
570
|
-
if (outputTarget && (!(outputTarget instanceof WebGL2RenderTarget) || !this.renderTargets.has(outputTarget) || outputTarget.disposed)) {
|
|
571
|
-
throw new RenderDeviceError("Output render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
572
|
-
targetId: outputTarget.id
|
|
573
|
-
});
|
|
574
|
-
}
|
|
575
|
-
if (outputTarget && outputTarget.colorTexture.format !== "rgba8") {
|
|
576
|
-
throw new RenderDeviceError("WebGL2 LDR postprocess output target must be rgba8.", "WEBGL_LDR_POSTPROCESS_OUTPUT_FORMAT_UNSUPPORTED", {
|
|
577
|
-
targetId: outputTarget.id,
|
|
578
|
-
format: outputTarget.colorTexture.format
|
|
579
|
-
});
|
|
580
|
-
}
|
|
581
|
-
if (outputTarget && (outputTarget.width !== source.width || outputTarget.height !== source.height)) {
|
|
582
|
-
throw new RenderDeviceError("WebGL2 LDR postprocess source and output dimensions must match.", "WEBGL_LDR_POSTPROCESS_SIZE_MISMATCH", {
|
|
583
|
-
sourceWidth: source.width,
|
|
584
|
-
sourceHeight: source.height,
|
|
585
|
-
targetWidth: outputTarget.width,
|
|
586
|
-
targetHeight: outputTarget.height
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
const webglOutputTarget = outputTarget as WebGL2RenderTarget | undefined;
|
|
590
|
-
|
|
591
|
-
const colorPass = options.passes.find((pass) => pass.name === "color-grade");
|
|
592
|
-
const fxaaPass = options.passes.find((pass) => pass.name === "fxaa");
|
|
593
|
-
const toneOptions = { ...(options.toneMappingDefaults ?? {}), ...(tonePass?.options ?? {}) };
|
|
594
|
-
const colorOptions = colorPass?.options ?? {};
|
|
595
|
-
const fxaaOptions = fxaaPass?.options ?? {};
|
|
596
|
-
const program = this.ensureLdrPostprocessProgram();
|
|
597
|
-
const vertexArray = this.ensurePresentationVertexArray();
|
|
598
|
-
const outputWidth = webglOutputTarget?.width ?? (this.viewportWidth || this.gl.drawingBufferWidth);
|
|
599
|
-
const outputHeight = webglOutputTarget?.height ?? (this.viewportHeight || this.gl.drawingBufferHeight);
|
|
600
|
-
const previousState = this.prepareFullscreenPresentation(webglOutputTarget?.framebuffer ?? null, outputWidth, outputHeight);
|
|
601
|
-
try {
|
|
602
|
-
this.gl.useProgram(program);
|
|
603
|
-
this.gl.bindVertexArray(vertexArray);
|
|
604
|
-
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
605
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, source.colorHandle);
|
|
606
|
-
this.gl.bindSampler(0, null);
|
|
607
|
-
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
|
|
608
|
-
this.gl.uniform2f(this.gl.getUniformLocation(program, "u_texelSize"), 1 / source.width, 1 / source.height);
|
|
609
|
-
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_hasToneMapping"), tonePass ? 1 : 0);
|
|
610
|
-
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_toneOperator"), toneMappingOperatorId(stringOption(toneOptions, "operator", "reinhard")));
|
|
611
|
-
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_inputColorSpace"), colorSpaceId(stringOption(toneOptions, "inputColorSpace", "linear")));
|
|
612
|
-
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_outputColorSpace"), colorSpaceId(stringOption(toneOptions, "outputColorSpace", "linear")));
|
|
613
|
-
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_exposure"), numberOption(toneOptions, "exposure", 1));
|
|
614
|
-
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_whitePoint"), Math.max(0.0001, numberOption(toneOptions, "whitePoint", 1)));
|
|
615
|
-
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_gamma"), Math.max(0.0001, numberOption(toneOptions, "gamma", 2.2)));
|
|
616
|
-
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_hasColorGrade"), colorPass ? 1 : 0);
|
|
617
|
-
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_contrast"), numberOption(colorOptions, "contrast", 1));
|
|
618
|
-
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_temperature"), numberOption(colorOptions, "temperature", 0));
|
|
619
|
-
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_tint"), numberOption(colorOptions, "tint", 0));
|
|
620
|
-
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_saturation"), numberOption(colorOptions, "saturation", 1));
|
|
621
|
-
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_vibrance"), numberOption(colorOptions, "vibrance", 0));
|
|
622
|
-
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_vignette"), numberOption(colorOptions, "vignette", 0));
|
|
623
|
-
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_sharpening"), numberOption(colorOptions, "sharpening", 0));
|
|
624
|
-
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_hasFxaa"), fxaaPass ? 1 : 0);
|
|
625
|
-
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_edgeThreshold"), numberOption(fxaaOptions, "edgeThreshold", 0.125));
|
|
626
|
-
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_subpixelBlend"), numberOption(fxaaOptions, "subpixelBlend", 0.75));
|
|
627
|
-
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
628
|
-
this.gl.flush();
|
|
629
|
-
} finally {
|
|
630
|
-
this.restoreFullscreenPresentationState(previousState, outputWidth, outputHeight);
|
|
631
|
-
}
|
|
632
|
-
this.lastError = this.readError();
|
|
633
|
-
if (this.lastError) {
|
|
634
|
-
throw new RenderDeviceError(`WebGL2 LDR postprocess presentation failed: ${this.lastError}`, "WEBGL_LDR_POSTPROCESS_FAILED", {
|
|
635
|
-
targetId: source.id,
|
|
636
|
-
error: this.lastError
|
|
637
|
-
});
|
|
638
|
-
}
|
|
639
|
-
this.setRenderTarget(webglOutputTarget ?? null);
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
readPixels(x: number, y: number, width: number, height: number): Uint8Array {
|
|
643
|
-
this.assertAlive();
|
|
644
|
-
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
|
|
645
|
-
throw new RenderDeviceError("Readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
|
|
646
|
-
}
|
|
647
|
-
const boundsWidth = this.activeRenderTarget?.width ?? this.gl.drawingBufferWidth;
|
|
648
|
-
const boundsHeight = this.activeRenderTarget?.height ?? this.gl.drawingBufferHeight;
|
|
649
|
-
if (x + width > boundsWidth || y + height > boundsHeight) {
|
|
650
|
-
throw new RenderDeviceError("Readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
|
|
651
|
-
x,
|
|
652
|
-
y,
|
|
653
|
-
width,
|
|
654
|
-
height,
|
|
655
|
-
boundsWidth,
|
|
656
|
-
boundsHeight
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
const pixels = new Uint8Array(width * height * 4);
|
|
660
|
-
this.gl.readPixels(x, y, width, height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
|
|
661
|
-
return pixels;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
readFloatPixels(x: number, y: number, width: number, height: number): Float32Array {
|
|
665
|
-
this.assertAlive();
|
|
666
|
-
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
|
|
667
|
-
throw new RenderDeviceError("Float readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
|
|
668
|
-
}
|
|
669
|
-
const boundsWidth = this.activeRenderTarget?.width ?? this.gl.drawingBufferWidth;
|
|
670
|
-
const boundsHeight = this.activeRenderTarget?.height ?? this.gl.drawingBufferHeight;
|
|
671
|
-
if (x + width > boundsWidth || y + height > boundsHeight) {
|
|
672
|
-
throw new RenderDeviceError("Float readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
|
|
673
|
-
x,
|
|
674
|
-
y,
|
|
675
|
-
width,
|
|
676
|
-
height,
|
|
677
|
-
boundsWidth,
|
|
678
|
-
boundsHeight
|
|
679
|
-
});
|
|
680
|
-
}
|
|
681
|
-
const pixels = new Float32Array(width * height * 4);
|
|
682
|
-
this.gl.readPixels(x, y, width, height, this.gl.RGBA, this.gl.FLOAT, pixels);
|
|
683
|
-
return pixels;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
readDepthPixels(x: number, y: number, width: number, height: number): Float32Array {
|
|
687
|
-
this.assertAlive();
|
|
688
|
-
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
|
|
689
|
-
throw new RenderDeviceError("Depth readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
|
|
690
|
-
}
|
|
691
|
-
const target = this.activeRenderTarget;
|
|
692
|
-
if (!target?.depthTextureHandle) {
|
|
693
|
-
throw new RenderDeviceError("Depth readback requires an active WebGL2 render target with a sampleable depth texture.", "DEPTH_READBACK_UNAVAILABLE", {
|
|
694
|
-
renderTarget: target?.label ?? null
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
if (x + width > target.width || y + height > target.height) {
|
|
698
|
-
throw new RenderDeviceError("Depth readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
|
|
699
|
-
x,
|
|
700
|
-
y,
|
|
701
|
-
width,
|
|
702
|
-
height,
|
|
703
|
-
boundsWidth: target.width,
|
|
704
|
-
boundsHeight: target.height
|
|
705
|
-
});
|
|
706
|
-
}
|
|
707
|
-
const encoded = this.copyDepthTextureToBytes(target, x, y, width, height);
|
|
708
|
-
const pixels = new Float32Array(width * height);
|
|
709
|
-
for (let index = 0; index < pixels.length; index += 1) {
|
|
710
|
-
const byteIndex = index * 4;
|
|
711
|
-
const r = encoded[byteIndex] ?? 255;
|
|
712
|
-
const g = encoded[byteIndex + 1] ?? 255;
|
|
713
|
-
const b = encoded[byteIndex + 2] ?? 255;
|
|
714
|
-
pixels[index] = Math.max(0, Math.min(1, r / 255 + g / 65025 + b / 16581375));
|
|
715
|
-
}
|
|
716
|
-
return pixels;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
beginFrame(width: number, height: number): void {
|
|
720
|
-
this.assertAlive();
|
|
721
|
-
if (this.frameActive) {
|
|
722
|
-
throw new RenderDeviceError("Frame is already active", "FRAME_ALREADY_ACTIVE");
|
|
723
|
-
}
|
|
724
|
-
if (width <= 0 || height <= 0) {
|
|
725
|
-
throw new RenderDeviceError("Frame dimensions must be positive", "INVALID_FRAME_SIZE", { width, height });
|
|
726
|
-
}
|
|
727
|
-
this.frameActive = true;
|
|
728
|
-
this.drawCalls = 0;
|
|
729
|
-
this.nativeEnvironmentBindings = 0;
|
|
730
|
-
this.nativeShadowMapBindings = 0;
|
|
731
|
-
this.activeTextureUnitIndex = -1;
|
|
732
|
-
this.textureUnitBindings.clear();
|
|
733
|
-
this.stateCache.invalidate();
|
|
734
|
-
this.viewportWidth = width;
|
|
735
|
-
this.viewportHeight = height;
|
|
736
|
-
this.stateCache.bindFramebuffer(this.gl.FRAMEBUFFER, this.activeRenderTarget?.framebuffer ?? null, () => this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.activeRenderTarget?.framebuffer ?? null));
|
|
737
|
-
this.stateCache.viewport(0, 0, width, height, () => this.gl.viewport(0, 0, width, height));
|
|
738
|
-
this.stateCache.setEnabled(this.gl.DEPTH_TEST, true, () => this.gl.enable(this.gl.DEPTH_TEST));
|
|
739
|
-
this.stateCache.depthFunc(this.gl.LEQUAL, () => this.gl.depthFunc(this.gl.LEQUAL));
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
clear(color: readonly [number, number, number, number]): void {
|
|
743
|
-
this.assertFrame();
|
|
744
|
-
this.gl.clearColor(color[0], color[1], color[2], color[3]);
|
|
745
|
-
this.stateCache.depthMask(true, () => this.gl.depthMask(true));
|
|
746
|
-
this.gl.clearDepth(1);
|
|
747
|
-
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
clearRenderTarget(color: readonly [number, number, number, number]): void {
|
|
751
|
-
this.assertFrame();
|
|
752
|
-
this.gl.clearColor(color[0], color[1], color[2], color[3]);
|
|
753
|
-
this.stateCache.depthMask(true, () => this.gl.depthMask(true));
|
|
754
|
-
this.gl.clearDepth(1);
|
|
755
|
-
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
draw(command: DrawCommand): void {
|
|
759
|
-
this.assertFrame();
|
|
760
|
-
const vertexBuffer = this.requireBuffer(command.vertexBuffer);
|
|
761
|
-
this.applyRenderState(command.renderState);
|
|
762
|
-
let shader: WebGL2ShaderProgram | undefined;
|
|
763
|
-
if (command.shader) {
|
|
764
|
-
const activeShader = this.requireShader(command.shader);
|
|
765
|
-
shader = activeShader;
|
|
766
|
-
this.stateCache.useProgram(activeShader.handle, () => this.gl.useProgram(activeShader.handle));
|
|
767
|
-
if (command.uniforms) {
|
|
768
|
-
this.uploadUniforms(activeShader, command.uniforms);
|
|
769
|
-
if (this.errorCheckMode === "strict") {
|
|
770
|
-
const uniformError = this.readError();
|
|
771
|
-
if (uniformError) {
|
|
772
|
-
throw new RenderDeviceError(`WebGL2 uniform upload failed for draw ${command.label ?? "unnamed"}: ${uniformError}`, "WEBGL_DRAW_FAILED", {
|
|
773
|
-
label: command.label,
|
|
774
|
-
stage: "uniforms",
|
|
775
|
-
error: uniformError
|
|
776
|
-
});
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
if (shader && command.vertexFormat) {
|
|
783
|
-
this.bindVertexArrayForCommand(command, shader, vertexBuffer);
|
|
784
|
-
if (this.errorCheckMode === "strict") {
|
|
785
|
-
const vertexFormatError = this.readError();
|
|
786
|
-
if (vertexFormatError) {
|
|
787
|
-
throw new RenderDeviceError(`WebGL2 vertex format binding failed for draw ${command.label ?? "unnamed"}: ${vertexFormatError}`, "WEBGL_DRAW_FAILED", {
|
|
788
|
-
label: command.label,
|
|
789
|
-
stage: "vertex-format",
|
|
790
|
-
error: vertexFormatError
|
|
791
|
-
});
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
} else {
|
|
795
|
-
this.bindNoVertexArray();
|
|
796
|
-
this.stateCache.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle, () => this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle));
|
|
797
|
-
}
|
|
798
|
-
if (command.indexBuffer) {
|
|
799
|
-
const indexBuffer = this.requireBuffer(command.indexBuffer);
|
|
800
|
-
if (!shader || !command.vertexFormat) {
|
|
801
|
-
this.stateCache.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle, () => this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle));
|
|
802
|
-
}
|
|
803
|
-
const type = command.indexType === "uint32" ? this.gl.UNSIGNED_INT : this.gl.UNSIGNED_SHORT;
|
|
804
|
-
const indexByteSize = command.indexType === "uint32" ? 4 : 2;
|
|
805
|
-
const indexOffset = (command.firstIndex ?? 0) * indexByteSize;
|
|
806
|
-
if ((command.instanceCount ?? 1) > 1) {
|
|
807
|
-
this.gl.drawElementsInstanced(this.primitive(command.topology), command.indexCount ?? 0, type, indexOffset, command.instanceCount ?? 1);
|
|
808
|
-
this.nativeInstancedSubmissions += 1;
|
|
809
|
-
} else {
|
|
810
|
-
this.gl.drawElements(this.primitive(command.topology), command.indexCount ?? 0, type, indexOffset);
|
|
811
|
-
}
|
|
812
|
-
} else {
|
|
813
|
-
if ((command.instanceCount ?? 1) > 1) {
|
|
814
|
-
this.gl.drawArraysInstanced(this.primitive(command.topology), command.firstVertex ?? 0, command.vertexCount, command.instanceCount ?? 1);
|
|
815
|
-
this.nativeInstancedSubmissions += 1;
|
|
816
|
-
} else {
|
|
817
|
-
this.gl.drawArrays(this.primitive(command.topology), command.firstVertex ?? 0, command.vertexCount);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
this.drawCalls += 1;
|
|
821
|
-
if (this.errorCheckMode === "strict") {
|
|
822
|
-
const drawError = this.readError();
|
|
823
|
-
if (drawError) {
|
|
824
|
-
throw new RenderDeviceError(`WebGL2 draw failed for ${command.label ?? "unnamed"}: ${drawError}`, "WEBGL_DRAW_FAILED", {
|
|
825
|
-
label: command.label,
|
|
826
|
-
stage: "draw",
|
|
827
|
-
error: drawError
|
|
828
|
-
});
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
endFrame(): void {
|
|
834
|
-
this.assertFrame();
|
|
835
|
-
this.frameActive = false;
|
|
836
|
-
this.lastError = this.readError();
|
|
837
|
-
}
|
|
838
|
-
|
|
839
|
-
captureState(): ReadonlyMap<string, string | number | boolean | null> {
|
|
840
|
-
const viewport = this.gl.getParameter(this.gl.VIEWPORT) as Int32Array | readonly number[];
|
|
841
|
-
return new Map<string, string | number | boolean | null>([
|
|
842
|
-
["backend", this.kind],
|
|
843
|
-
["disposed", this.disposed],
|
|
844
|
-
["contextLost", this.contextLost],
|
|
845
|
-
["frameActive", this.frameActive],
|
|
846
|
-
["depthTest", this.gl.isEnabled(this.gl.DEPTH_TEST)],
|
|
847
|
-
["blend", this.gl.isEnabled(this.gl.BLEND)],
|
|
848
|
-
["cullFace", this.gl.isEnabled(this.gl.CULL_FACE)],
|
|
849
|
-
["program", this.gl.getParameter(this.gl.CURRENT_PROGRAM) ? "bound" : null],
|
|
850
|
-
["arrayBuffer", this.gl.getParameter(this.gl.ARRAY_BUFFER_BINDING) ? "bound" : null],
|
|
851
|
-
["elementArrayBuffer", this.gl.getParameter(this.gl.ELEMENT_ARRAY_BUFFER_BINDING) ? "bound" : null],
|
|
852
|
-
["viewportWidth", this.viewportWidth],
|
|
853
|
-
["viewportHeight", this.viewportHeight],
|
|
854
|
-
["actualViewportWidth", viewport[2] ?? 0],
|
|
855
|
-
["actualViewportHeight", viewport[3] ?? 0],
|
|
856
|
-
["renderTarget", this.activeRenderTarget?.label ?? null],
|
|
857
|
-
["drawCalls", this.drawCalls],
|
|
858
|
-
["shaderProgramCreates", this.shaderProgramCreateCount],
|
|
859
|
-
["uniformLocationLookups", this.uniformLocationLookupCount],
|
|
860
|
-
["bufferUpdates", this.bufferUpdateCount],
|
|
861
|
-
["textureBinds", this.textureBindCount],
|
|
862
|
-
["samplerParameterUploads", this.samplerParameterUploadCount],
|
|
863
|
-
["vertexArrayCreates", this.vertexArrayCreateCount],
|
|
864
|
-
["samplerObjects", this.samplerObjectCache.size],
|
|
865
|
-
["samplerAnisotropyUploads", this.samplerAnisotropyUploadCount],
|
|
866
|
-
["maxTextureAnisotropy", this.maxTextureAnisotropy],
|
|
867
|
-
["nativeEnvironmentBindings", this.nativeEnvironmentBindings],
|
|
868
|
-
["nativeShadowMapBindings", this.nativeShadowMapBindings],
|
|
869
|
-
["nativeInstancedSubmissions", this.nativeInstancedSubmissions],
|
|
870
|
-
["stateCacheIssued", this.stateCache.stats().issued],
|
|
871
|
-
["stateCacheSkipped", this.stateCache.stats().skipped]
|
|
872
|
-
]);
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
getDiagnostics(): RenderDeviceDiagnostics {
|
|
876
|
-
this.releaseDisposedTextureHandles();
|
|
877
|
-
const liveRenderTargets = [...this.renderTargets].filter((target) => !target.disposed);
|
|
878
|
-
const liveTextures = new Set<Texture>([
|
|
879
|
-
...liveRenderTargets.map((target) => target.colorTexture),
|
|
880
|
-
...[...this.textures.keys()].filter((texture) => !texture.disposed)
|
|
881
|
-
]);
|
|
882
|
-
const liveTextureList = [...liveTextures];
|
|
883
|
-
const compressedTextures = liveTextureList.filter((texture) => isCompressedTextureFormat(texture.format));
|
|
884
|
-
const fallbackTextures = liveTextureList.filter((texture) => this.textureUploadModes.get(texture) === "fallback");
|
|
885
|
-
const stateCacheStats = this.stateCache.stats();
|
|
886
|
-
const bufferBytes = [...this.buffers].filter((buffer) => !buffer.disposed).reduce((total, buffer) => total + buffer.byteLength, 0);
|
|
887
|
-
const textureBytes = liveTextureList.reduce((total, texture) => total + texture.byteLength, 0);
|
|
888
|
-
return {
|
|
889
|
-
drawCalls: this.drawCalls,
|
|
890
|
-
buffers: [...this.buffers].filter((buffer) => !buffer.disposed).length,
|
|
891
|
-
shaders: [...this.shaders].filter((shader) => !shader.disposed).length,
|
|
892
|
-
renderTargets: liveRenderTargets.length,
|
|
893
|
-
textures: liveTextures.size,
|
|
894
|
-
bufferBytes,
|
|
895
|
-
textureBytes,
|
|
896
|
-
approximateGpuMemoryBytes: bufferBytes + textureBytes,
|
|
897
|
-
compressedTextures: compressedTextures.length,
|
|
898
|
-
compressedTextureBytes: compressedTextures.reduce((total, texture) => total + texture.byteLength, 0),
|
|
899
|
-
textureFallbacks: fallbackTextures.length,
|
|
900
|
-
textureFallbackBytes: fallbackTextures.reduce((total, texture) => total + texture.fallbackByteLength, 0),
|
|
901
|
-
nativeEnvironmentBindings: this.nativeEnvironmentBindings,
|
|
902
|
-
nativeShadowMapBindings: this.nativeShadowMapBindings,
|
|
903
|
-
nativeInstancedSubmissions: this.nativeInstancedSubmissions,
|
|
904
|
-
samplerAnisotropyUploads: this.samplerAnisotropyUploadCount,
|
|
905
|
-
maxTextureAnisotropy: this.maxTextureAnisotropy,
|
|
906
|
-
stateCacheIssued: stateCacheStats.issued,
|
|
907
|
-
stateCacheSkipped: stateCacheStats.skipped,
|
|
908
|
-
stateCacheProgramSwitches: stateCacheStats.byOperation.useProgram?.issued ?? 0,
|
|
909
|
-
stateCacheTextureBinds: stateCacheStats.byOperation.bindTexture?.issued ?? 0,
|
|
910
|
-
stateCacheBufferBinds: stateCacheStats.byOperation.bindBuffer?.issued ?? 0,
|
|
911
|
-
stateCacheVertexArrayBinds: stateCacheStats.byOperation.bindVertexArray?.issued ?? 0,
|
|
912
|
-
stateCacheSamplerBinds: stateCacheStats.byOperation.bindSampler?.issued ?? 0,
|
|
913
|
-
disposedBuffers: [...this.buffers].filter((buffer) => buffer.disposed).length,
|
|
914
|
-
disposedShaders: [...this.shaders].filter((shader) => shader.disposed).length,
|
|
915
|
-
disposedRenderTargets: [...this.renderTargets].filter((target) => target.disposed).length,
|
|
916
|
-
disposedTextures: [...this.renderTargets].filter((target) => target.colorTexture.disposed).length + this.releasedTextureHandles,
|
|
917
|
-
lastError: this.lastError,
|
|
918
|
-
contextLost: this.contextLost
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
|
|
922
|
-
dispose(): void {
|
|
923
|
-
if (this.canvas && "removeEventListener" in this.canvas) {
|
|
924
|
-
if (this.contextLostListener) this.canvas.removeEventListener("webglcontextlost", this.contextLostListener);
|
|
925
|
-
if (this.contextRestoredListener) this.canvas.removeEventListener("webglcontextrestored", this.contextRestoredListener);
|
|
926
|
-
}
|
|
927
|
-
for (const buffer of this.buffers) {
|
|
928
|
-
buffer.dispose();
|
|
929
|
-
}
|
|
930
|
-
for (const shader of this.shaders) {
|
|
931
|
-
shader.dispose();
|
|
932
|
-
}
|
|
933
|
-
for (const target of this.renderTargets) {
|
|
934
|
-
target.dispose();
|
|
935
|
-
}
|
|
936
|
-
for (const entry of this.vertexArrayCache.values()) {
|
|
937
|
-
this.gl.deleteVertexArray(entry.handle);
|
|
938
|
-
}
|
|
939
|
-
this.vertexArrayCache.clear();
|
|
940
|
-
for (const sampler of this.samplerObjectCache.values()) {
|
|
941
|
-
this.gl.deleteSampler(sampler);
|
|
942
|
-
}
|
|
943
|
-
this.samplerObjectCache.clear();
|
|
944
|
-
for (const texture of this.textures.values()) {
|
|
945
|
-
this.gl.deleteTexture(texture);
|
|
946
|
-
}
|
|
947
|
-
this.textureUploadModes.clear();
|
|
948
|
-
if (this.fallbackTexture) {
|
|
949
|
-
this.gl.deleteTexture(this.fallbackTexture);
|
|
950
|
-
this.fallbackTexture = null;
|
|
951
|
-
}
|
|
952
|
-
if (this.fallbackCubeTexture) {
|
|
953
|
-
this.gl.deleteTexture(this.fallbackCubeTexture);
|
|
954
|
-
this.fallbackCubeTexture = null;
|
|
955
|
-
}
|
|
956
|
-
if (this.presentationProgram) {
|
|
957
|
-
this.gl.deleteProgram(this.presentationProgram);
|
|
958
|
-
this.presentationProgram = null;
|
|
959
|
-
}
|
|
960
|
-
if (this.ldrPostprocessProgram) {
|
|
961
|
-
this.gl.deleteProgram(this.ldrPostprocessProgram);
|
|
962
|
-
this.ldrPostprocessProgram = null;
|
|
963
|
-
}
|
|
964
|
-
if (this.presentationVertexArray) {
|
|
965
|
-
this.gl.deleteVertexArray(this.presentationVertexArray);
|
|
966
|
-
this.presentationVertexArray = null;
|
|
967
|
-
}
|
|
968
|
-
if (this.depthReadbackProgram) {
|
|
969
|
-
this.gl.deleteProgram(this.depthReadbackProgram);
|
|
970
|
-
this.depthReadbackProgram = null;
|
|
971
|
-
}
|
|
972
|
-
this.textures.clear();
|
|
973
|
-
this.disposed = true;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
private copyDepthTextureToBytes(source: WebGL2RenderTarget, x: number, y: number, width: number, height: number): Uint8Array {
|
|
977
|
-
const colorHandle = this.gl.createTexture();
|
|
978
|
-
const framebuffer = this.gl.createFramebuffer();
|
|
979
|
-
if (!colorHandle || !framebuffer) {
|
|
980
|
-
if (colorHandle) this.gl.deleteTexture(colorHandle);
|
|
981
|
-
if (framebuffer) this.gl.deleteFramebuffer(framebuffer);
|
|
982
|
-
throw new RenderDeviceError("Failed to allocate WebGL2 depth readback target", "WEBGL_ALLOCATION_FAILED", {
|
|
983
|
-
renderTarget: source.label
|
|
984
|
-
});
|
|
985
|
-
}
|
|
986
|
-
const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING) as WebGLFramebuffer | null;
|
|
987
|
-
const previousProgram = this.gl.getParameter(this.gl.CURRENT_PROGRAM) as WebGLProgram | null;
|
|
988
|
-
const textureUnit0 = this.captureTextureUnit0();
|
|
989
|
-
const viewport = this.gl.getParameter(this.gl.VIEWPORT) as Int32Array | readonly number[];
|
|
990
|
-
const depthTestEnabled = this.gl.isEnabled(this.gl.DEPTH_TEST);
|
|
991
|
-
const depthMask = this.gl.getParameter(this.gl.DEPTH_WRITEMASK) as boolean;
|
|
992
|
-
const cullFaceEnabled = this.gl.isEnabled(this.gl.CULL_FACE);
|
|
993
|
-
const blendEnabled = this.gl.isEnabled(this.gl.BLEND);
|
|
994
|
-
const pixels = new Uint8Array(width * height * 4);
|
|
995
|
-
try {
|
|
996
|
-
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
997
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, colorHandle);
|
|
998
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
|
|
999
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
|
|
1000
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
1001
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
1002
|
-
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA8, width, height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, null);
|
|
1003
|
-
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
|
|
1004
|
-
this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, colorHandle, 0);
|
|
1005
|
-
const status = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
|
|
1006
|
-
if (status !== this.gl.FRAMEBUFFER_COMPLETE) {
|
|
1007
|
-
throw new RenderDeviceError("WebGL2 depth readback framebuffer status is invalid", "FRAMEBUFFER_INVALID", {
|
|
1008
|
-
renderTarget: source.label,
|
|
1009
|
-
status
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
this.gl.viewport(0, 0, width, height);
|
|
1013
|
-
this.gl.disable(this.gl.DEPTH_TEST);
|
|
1014
|
-
this.gl.disable(this.gl.CULL_FACE);
|
|
1015
|
-
this.gl.disable(this.gl.BLEND);
|
|
1016
|
-
this.gl.depthMask(false);
|
|
1017
|
-
this.gl.useProgram(this.ensureDepthReadbackProgram());
|
|
1018
|
-
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
1019
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, source.depthTextureHandle);
|
|
1020
|
-
this.gl.bindSampler(0, null);
|
|
1021
|
-
this.gl.uniform1i(this.gl.getUniformLocation(this.ensureDepthReadbackProgram(), "u_depth"), 0);
|
|
1022
|
-
this.gl.uniform4f(this.gl.getUniformLocation(this.ensureDepthReadbackProgram(), "u_sourceRect"), x / source.width, y / source.height, width / source.width, height / source.height);
|
|
1023
|
-
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1024
|
-
this.gl.readPixels(0, 0, width, height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
|
|
1025
|
-
const error = this.readError();
|
|
1026
|
-
if (error) {
|
|
1027
|
-
throw new RenderDeviceError("WebGL2 depth texture readback failed", "WEBGL_DEPTH_READBACK_FAILED", {
|
|
1028
|
-
renderTarget: source.label,
|
|
1029
|
-
error
|
|
1030
|
-
});
|
|
1031
|
-
}
|
|
1032
|
-
return pixels;
|
|
1033
|
-
} finally {
|
|
1034
|
-
this.restoreTextureUnit0(textureUnit0);
|
|
1035
|
-
this.gl.useProgram(previousProgram);
|
|
1036
|
-
if (depthTestEnabled) this.gl.enable(this.gl.DEPTH_TEST);
|
|
1037
|
-
else this.gl.disable(this.gl.DEPTH_TEST);
|
|
1038
|
-
if (cullFaceEnabled) this.gl.enable(this.gl.CULL_FACE);
|
|
1039
|
-
else this.gl.disable(this.gl.CULL_FACE);
|
|
1040
|
-
if (blendEnabled) this.gl.enable(this.gl.BLEND);
|
|
1041
|
-
else this.gl.disable(this.gl.BLEND);
|
|
1042
|
-
this.gl.depthMask(depthMask);
|
|
1043
|
-
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
|
|
1044
|
-
this.gl.viewport(viewport[0] ?? 0, viewport[1] ?? 0, viewport[2] ?? this.viewportWidth, viewport[3] ?? this.viewportHeight);
|
|
1045
|
-
this.gl.deleteTexture(colorHandle);
|
|
1046
|
-
this.gl.deleteFramebuffer(framebuffer);
|
|
1047
|
-
this.stateCache.invalidate();
|
|
1048
|
-
}
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
private drawRenderTargetToBackbuffer(source: WebGL2RenderTarget, outputWidth: number, outputHeight: number): void {
|
|
1052
|
-
const program = this.ensurePresentationProgram();
|
|
1053
|
-
const vertexArray = this.ensurePresentationVertexArray();
|
|
1054
|
-
const previousState = this.prepareFullscreenPresentation(null, outputWidth, outputHeight);
|
|
1055
|
-
this.gl.useProgram(program);
|
|
1056
|
-
this.gl.bindVertexArray(vertexArray);
|
|
1057
|
-
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
1058
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, source.colorHandle);
|
|
1059
|
-
this.gl.bindSampler(0, null);
|
|
1060
|
-
try {
|
|
1061
|
-
const uniformLocation = this.gl.getUniformLocation(program, "u_source");
|
|
1062
|
-
this.gl.uniform1i(uniformLocation, 0);
|
|
1063
|
-
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1064
|
-
this.gl.flush();
|
|
1065
|
-
} finally {
|
|
1066
|
-
this.restoreFullscreenPresentationState(previousState, outputWidth, outputHeight);
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
private captureTextureUnit0(): WebGL2TextureUnit0Snapshot {
|
|
1071
|
-
const activeTexture = this.gl.getParameter(this.gl.ACTIVE_TEXTURE) as GLenum;
|
|
1072
|
-
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
1073
|
-
return {
|
|
1074
|
-
activeTexture,
|
|
1075
|
-
texture2d: this.gl.getParameter(this.gl.TEXTURE_BINDING_2D) as WebGLTexture | null,
|
|
1076
|
-
sampler: this.gl.getParameter(this.gl.SAMPLER_BINDING) as WebGLSampler | null
|
|
1077
|
-
};
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
private restoreTextureUnit0(snapshot: WebGL2TextureUnit0Snapshot): void {
|
|
1081
|
-
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
1082
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, snapshot.texture2d);
|
|
1083
|
-
this.gl.bindSampler(0, snapshot.sampler);
|
|
1084
|
-
this.gl.activeTexture(snapshot.activeTexture);
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
private prepareFullscreenPresentation(framebuffer: WebGLFramebuffer | null, outputWidth: number, outputHeight: number): WebGL2FullscreenPresentationStateSnapshot {
|
|
1088
|
-
const snapshot: WebGL2FullscreenPresentationStateSnapshot = {
|
|
1089
|
-
framebuffer: this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING) as WebGLFramebuffer | null,
|
|
1090
|
-
program: this.gl.getParameter(this.gl.CURRENT_PROGRAM) as WebGLProgram | null,
|
|
1091
|
-
textureUnit0: this.captureTextureUnit0(),
|
|
1092
|
-
vertexArray: this.gl.getParameter(this.gl.VERTEX_ARRAY_BINDING) as WebGLVertexArrayObject | null,
|
|
1093
|
-
viewport: this.gl.getParameter(this.gl.VIEWPORT) as Int32Array | readonly number[],
|
|
1094
|
-
colorMask: this.gl.getParameter(this.gl.COLOR_WRITEMASK) as readonly boolean[],
|
|
1095
|
-
depthTestEnabled: this.gl.isEnabled(this.gl.DEPTH_TEST),
|
|
1096
|
-
cullFaceEnabled: this.gl.isEnabled(this.gl.CULL_FACE),
|
|
1097
|
-
blendEnabled: this.gl.isEnabled(this.gl.BLEND),
|
|
1098
|
-
scissorTestEnabled: this.gl.isEnabled(this.gl.SCISSOR_TEST),
|
|
1099
|
-
stencilTestEnabled: this.gl.isEnabled(this.gl.STENCIL_TEST),
|
|
1100
|
-
polygonOffsetFillEnabled: this.gl.isEnabled(this.gl.POLYGON_OFFSET_FILL),
|
|
1101
|
-
depthMask: this.gl.getParameter(this.gl.DEPTH_WRITEMASK) as boolean
|
|
1102
|
-
};
|
|
1103
|
-
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
|
|
1104
|
-
this.gl.viewport(0, 0, outputWidth, outputHeight);
|
|
1105
|
-
this.gl.colorMask(true, true, true, true);
|
|
1106
|
-
this.gl.disable(this.gl.SCISSOR_TEST);
|
|
1107
|
-
this.gl.disable(this.gl.STENCIL_TEST);
|
|
1108
|
-
this.gl.disable(this.gl.POLYGON_OFFSET_FILL);
|
|
1109
|
-
this.gl.disable(this.gl.DEPTH_TEST);
|
|
1110
|
-
this.gl.disable(this.gl.CULL_FACE);
|
|
1111
|
-
this.gl.disable(this.gl.BLEND);
|
|
1112
|
-
this.gl.depthMask(false);
|
|
1113
|
-
return snapshot;
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
private restoreFullscreenPresentationState(snapshot: WebGL2FullscreenPresentationStateSnapshot, fallbackWidth: number, fallbackHeight: number): void {
|
|
1117
|
-
this.gl.bindVertexArray(snapshot.vertexArray);
|
|
1118
|
-
this.restoreTextureUnit0(snapshot.textureUnit0);
|
|
1119
|
-
this.gl.useProgram(snapshot.program);
|
|
1120
|
-
if (snapshot.depthTestEnabled) this.gl.enable(this.gl.DEPTH_TEST);
|
|
1121
|
-
else this.gl.disable(this.gl.DEPTH_TEST);
|
|
1122
|
-
if (snapshot.cullFaceEnabled) this.gl.enable(this.gl.CULL_FACE);
|
|
1123
|
-
else this.gl.disable(this.gl.CULL_FACE);
|
|
1124
|
-
if (snapshot.blendEnabled) this.gl.enable(this.gl.BLEND);
|
|
1125
|
-
else this.gl.disable(this.gl.BLEND);
|
|
1126
|
-
if (snapshot.scissorTestEnabled) this.gl.enable(this.gl.SCISSOR_TEST);
|
|
1127
|
-
else this.gl.disable(this.gl.SCISSOR_TEST);
|
|
1128
|
-
if (snapshot.stencilTestEnabled) this.gl.enable(this.gl.STENCIL_TEST);
|
|
1129
|
-
else this.gl.disable(this.gl.STENCIL_TEST);
|
|
1130
|
-
if (snapshot.polygonOffsetFillEnabled) this.gl.enable(this.gl.POLYGON_OFFSET_FILL);
|
|
1131
|
-
else this.gl.disable(this.gl.POLYGON_OFFSET_FILL);
|
|
1132
|
-
this.gl.colorMask(snapshot.colorMask[0] ?? true, snapshot.colorMask[1] ?? true, snapshot.colorMask[2] ?? true, snapshot.colorMask[3] ?? true);
|
|
1133
|
-
this.gl.depthMask(snapshot.depthMask);
|
|
1134
|
-
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, snapshot.framebuffer);
|
|
1135
|
-
this.gl.viewport(snapshot.viewport[0] ?? 0, snapshot.viewport[1] ?? 0, snapshot.viewport[2] ?? fallbackWidth, snapshot.viewport[3] ?? fallbackHeight);
|
|
1136
|
-
this.stateCache.invalidate();
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
private ensurePresentationVertexArray(): WebGLVertexArrayObject {
|
|
1140
|
-
if (this.presentationVertexArray) return this.presentationVertexArray;
|
|
1141
|
-
const vertexArray = this.gl.createVertexArray();
|
|
1142
|
-
if (!vertexArray) {
|
|
1143
|
-
throw new RenderDeviceError("Failed to allocate WebGL presentation vertex array", "WEBGL_ALLOCATION_FAILED");
|
|
1144
|
-
}
|
|
1145
|
-
this.presentationVertexArray = vertexArray;
|
|
1146
|
-
return vertexArray;
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
private ensureDepthReadbackProgram(): WebGLProgram {
|
|
1150
|
-
if (this.depthReadbackProgram) {
|
|
1151
|
-
return this.depthReadbackProgram;
|
|
1152
|
-
}
|
|
1153
|
-
const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
|
|
1154
|
-
precision highp float;
|
|
1155
|
-
out vec2 v_uv;
|
|
1156
|
-
void main() {
|
|
1157
|
-
vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
1158
|
-
v_uv = position;
|
|
1159
|
-
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
|
|
1160
|
-
}
|
|
1161
|
-
`, "webgl2-depth-readback");
|
|
1162
|
-
const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, `#version 300 es
|
|
1163
|
-
precision highp float;
|
|
1164
|
-
uniform sampler2D u_depth;
|
|
1165
|
-
uniform vec4 u_sourceRect;
|
|
1166
|
-
in vec2 v_uv;
|
|
1167
|
-
out vec4 outColor;
|
|
1168
|
-
vec3 packDepth24(float value) {
|
|
1169
|
-
value = clamp(value, 0.0, 1.0);
|
|
1170
|
-
vec3 encoded = fract(value * vec3(1.0, 255.0, 65025.0));
|
|
1171
|
-
encoded -= encoded.yzz * vec3(1.0 / 255.0, 1.0 / 255.0, 0.0);
|
|
1172
|
-
return encoded;
|
|
1173
|
-
}
|
|
1174
|
-
void main() {
|
|
1175
|
-
vec2 uv = u_sourceRect.xy + v_uv * u_sourceRect.zw;
|
|
1176
|
-
outColor = vec4(packDepth24(texture(u_depth, uv).r), 1.0);
|
|
1177
|
-
}
|
|
1178
|
-
`, "webgl2-depth-readback");
|
|
1179
|
-
const program = this.gl.createProgram();
|
|
1180
|
-
if (!program) {
|
|
1181
|
-
this.gl.deleteShader(vertex);
|
|
1182
|
-
this.gl.deleteShader(fragment);
|
|
1183
|
-
throw new RenderDeviceError("Failed to allocate WebGL2 depth readback shader", "WEBGL_ALLOCATION_FAILED");
|
|
1184
|
-
}
|
|
1185
|
-
this.gl.attachShader(program, vertex);
|
|
1186
|
-
this.gl.attachShader(program, fragment);
|
|
1187
|
-
this.gl.linkProgram(program);
|
|
1188
|
-
this.gl.deleteShader(vertex);
|
|
1189
|
-
this.gl.deleteShader(fragment);
|
|
1190
|
-
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
1191
|
-
const log = this.gl.getProgramInfoLog(program) ?? "Unknown depth readback shader link error";
|
|
1192
|
-
this.gl.deleteProgram(program);
|
|
1193
|
-
throw new RenderDeviceError("WebGL2 depth readback shader link failed", "SHADER_LINK_FAILED", { log });
|
|
1194
|
-
}
|
|
1195
|
-
this.depthReadbackProgram = program;
|
|
1196
|
-
return program;
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
private ensurePresentationProgram(): WebGLProgram {
|
|
1200
|
-
if (this.presentationProgram) {
|
|
1201
|
-
return this.presentationProgram;
|
|
1202
|
-
}
|
|
1203
|
-
const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
|
|
1204
|
-
precision highp float;
|
|
1205
|
-
out vec2 v_uv;
|
|
1206
|
-
void main() {
|
|
1207
|
-
vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
1208
|
-
v_uv = position;
|
|
1209
|
-
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
|
|
1210
|
-
}
|
|
1211
|
-
`, "webgl2-present-render-target");
|
|
1212
|
-
const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, `#version 300 es
|
|
1213
|
-
precision highp float;
|
|
1214
|
-
uniform sampler2D u_source;
|
|
1215
|
-
in vec2 v_uv;
|
|
1216
|
-
out vec4 outColor;
|
|
1217
|
-
void main() {
|
|
1218
|
-
outColor = texture(u_source, v_uv);
|
|
1219
|
-
}
|
|
1220
|
-
`, "webgl2-present-render-target");
|
|
1221
|
-
const program = this.gl.createProgram();
|
|
1222
|
-
if (!program) {
|
|
1223
|
-
this.gl.deleteShader(vertex);
|
|
1224
|
-
this.gl.deleteShader(fragment);
|
|
1225
|
-
throw new RenderDeviceError("Failed to allocate WebGL presentation shader", "WEBGL_ALLOCATION_FAILED");
|
|
1226
|
-
}
|
|
1227
|
-
this.gl.attachShader(program, vertex);
|
|
1228
|
-
this.gl.attachShader(program, fragment);
|
|
1229
|
-
this.gl.linkProgram(program);
|
|
1230
|
-
this.gl.deleteShader(vertex);
|
|
1231
|
-
this.gl.deleteShader(fragment);
|
|
1232
|
-
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
1233
|
-
const log = this.gl.getProgramInfoLog(program) ?? "Unknown presentation shader link error";
|
|
1234
|
-
this.gl.deleteProgram(program);
|
|
1235
|
-
throw new RenderDeviceError("WebGL presentation shader link failed", "SHADER_LINK_FAILED", { log });
|
|
1236
|
-
}
|
|
1237
|
-
this.presentationProgram = program;
|
|
1238
|
-
return program;
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
private ensureLdrPostprocessProgram(): WebGLProgram {
|
|
1242
|
-
if (this.ldrPostprocessProgram) {
|
|
1243
|
-
return this.ldrPostprocessProgram;
|
|
1244
|
-
}
|
|
1245
|
-
const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
|
|
1246
|
-
precision highp float;
|
|
1247
|
-
out vec2 v_uv;
|
|
1248
|
-
void main() {
|
|
1249
|
-
vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
1250
|
-
v_uv = position;
|
|
1251
|
-
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
|
|
1252
|
-
}
|
|
1253
|
-
`, "webgl2-ldr-postprocess");
|
|
1254
|
-
const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, `#version 300 es
|
|
1255
|
-
precision highp float;
|
|
1256
|
-
uniform sampler2D u_source;
|
|
1257
|
-
uniform vec2 u_texelSize;
|
|
1258
|
-
uniform int u_hasToneMapping;
|
|
1259
|
-
uniform int u_toneOperator;
|
|
1260
|
-
uniform int u_inputColorSpace;
|
|
1261
|
-
uniform int u_outputColorSpace;
|
|
1262
|
-
uniform float u_exposure;
|
|
1263
|
-
uniform float u_whitePoint;
|
|
1264
|
-
uniform float u_gamma;
|
|
1265
|
-
uniform int u_hasColorGrade;
|
|
1266
|
-
uniform float u_contrast;
|
|
1267
|
-
uniform float u_temperature;
|
|
1268
|
-
uniform float u_tint;
|
|
1269
|
-
uniform float u_saturation;
|
|
1270
|
-
uniform float u_vibrance;
|
|
1271
|
-
uniform float u_vignette;
|
|
1272
|
-
uniform float u_sharpening;
|
|
1273
|
-
uniform int u_hasFxaa;
|
|
1274
|
-
uniform float u_edgeThreshold;
|
|
1275
|
-
uniform float u_subpixelBlend;
|
|
1276
|
-
in vec2 v_uv;
|
|
1277
|
-
out vec4 outColor;
|
|
1278
|
-
|
|
1279
|
-
float srgbToLinear(float value) {
|
|
1280
|
-
return value <= 0.04045 ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4);
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
float linearToSrgb(float value) {
|
|
1284
|
-
return value <= 0.0031308 ? value * 12.92 : 1.055 * pow(value, 1.0 / 2.4) - 0.055;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
vec3 decodeColor(vec3 color) {
|
|
1288
|
-
return u_inputColorSpace == 1 ? vec3(srgbToLinear(color.r), srgbToLinear(color.g), srgbToLinear(color.b)) : color;
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
vec3 encodeColor(vec3 color) {
|
|
1292
|
-
vec3 linear = clamp(color, 0.0, 1.0);
|
|
1293
|
-
return u_outputColorSpace == 1 ? vec3(linearToSrgb(linear.r), linearToSrgb(linear.g), linearToSrgb(linear.b)) : linear;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
float aces(float value) {
|
|
1297
|
-
return (value * (2.51 * value + 0.03)) / (value * (2.43 * value + 0.59) + 0.14);
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
float filmic(float value) {
|
|
1301
|
-
float x = max(0.0, value);
|
|
1302
|
-
float toe = max(0.0, x - 0.004);
|
|
1303
|
-
float curve = (toe * (6.2 * toe + 0.5)) / (toe * (6.2 * toe + 1.7) + 0.06);
|
|
1304
|
-
return min(curve, x * 1.08);
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
float uncharted2(float value) {
|
|
1308
|
-
float a = 0.15;
|
|
1309
|
-
float b = 0.5;
|
|
1310
|
-
float c = 0.1;
|
|
1311
|
-
float d = 0.2;
|
|
1312
|
-
float e = 0.02;
|
|
1313
|
-
float f = 0.3;
|
|
1314
|
-
float w = 11.2;
|
|
1315
|
-
float x = value * 2.0;
|
|
1316
|
-
float curve = ((x * (a * x + c * b) + d * e) / (x * (a * x + b) + d * f)) - e / f;
|
|
1317
|
-
float wx = w;
|
|
1318
|
-
float white = ((wx * (a * wx + c * b) + d * e) / (wx * (a * wx + b) + d * f)) - e / f;
|
|
1319
|
-
return clamp(curve / white, 0.0, 1.0);
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
float agx(float value) {
|
|
1323
|
-
float x = max(0.0, value);
|
|
1324
|
-
float encoded = log2(1.0 + x) / log2(17.0);
|
|
1325
|
-
return clamp(encoded * encoded * (3.0 - 2.0 * encoded), 0.0, 1.0);
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
float neutral(float value) {
|
|
1329
|
-
float x = max(0.0, value);
|
|
1330
|
-
return min(1.0, (x * (1.0 + x / 7.5)) / (1.0 + x));
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
float toneMapChannel(float value) {
|
|
1334
|
-
float exposed = max(0.0, value * u_exposure) / max(0.0001, u_whitePoint);
|
|
1335
|
-
if (u_toneOperator == 0) return min(1.0, exposed);
|
|
1336
|
-
if (u_toneOperator == 1) return exposed / (1.0 + exposed);
|
|
1337
|
-
if (u_toneOperator == 2) return aces(exposed);
|
|
1338
|
-
if (u_toneOperator == 3) return filmic(exposed);
|
|
1339
|
-
if (u_toneOperator == 4) return uncharted2(exposed);
|
|
1340
|
-
if (u_toneOperator == 5) return agx(exposed);
|
|
1341
|
-
return neutral(exposed);
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
vec3 applyToneMapping(vec3 color) {
|
|
1345
|
-
if (u_hasToneMapping == 0) return color;
|
|
1346
|
-
vec3 decoded = decodeColor(color);
|
|
1347
|
-
vec3 mapped = vec3(toneMapChannel(decoded.r), toneMapChannel(decoded.g), toneMapChannel(decoded.b));
|
|
1348
|
-
return encodeColor(mapped);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
vec3 applyColorGrade(vec3 color, vec2 uv) {
|
|
1352
|
-
if (u_hasColorGrade == 0) return color;
|
|
1353
|
-
float contrastOffset = 0.5 - 0.5 * u_contrast;
|
|
1354
|
-
float redShift = u_temperature * 0.08 - u_tint * 0.02;
|
|
1355
|
-
float greenShift = u_tint * 0.06;
|
|
1356
|
-
float blueShift = -u_temperature * 0.08 - u_tint * 0.02;
|
|
1357
|
-
float luma = dot(color, vec3(0.2126, 0.7152, 0.0722));
|
|
1358
|
-
vec3 graded = color * u_contrast + vec3(contrastOffset + redShift, contrastOffset + greenShift, contrastOffset + blueShift);
|
|
1359
|
-
float distanceFromLuma = min(1.0, abs(graded.r - luma) + abs(graded.g - luma) + abs(graded.b - luma));
|
|
1360
|
-
float vibranceBoost = u_vibrance == 0.0 ? 0.0 : u_vibrance * (1.0 - distanceFromLuma);
|
|
1361
|
-
float saturation = u_saturation + vibranceBoost;
|
|
1362
|
-
graded = vec3(luma) + (graded - vec3(luma)) * saturation;
|
|
1363
|
-
vec2 centered = uv * 2.0 - 1.0;
|
|
1364
|
-
float vignette = 1.0 - u_vignette * clamp((length(centered) - 0.28) / 1.12, 0.0, 1.0);
|
|
1365
|
-
return clamp(graded * vignette, 0.0, 1.0);
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
vec3 baseColorAt(vec2 uv) {
|
|
1369
|
-
vec2 clampedUv = clamp(uv, vec2(0.0), vec2(1.0));
|
|
1370
|
-
vec3 color = texture(u_source, clampedUv).rgb;
|
|
1371
|
-
return applyColorGrade(applyToneMapping(color), clampedUv);
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
vec3 finalColorAt(vec2 uv) {
|
|
1375
|
-
vec3 center = baseColorAt(uv);
|
|
1376
|
-
if (u_hasColorGrade == 0 || u_sharpening <= 0.0) return center;
|
|
1377
|
-
vec3 blur = (
|
|
1378
|
-
baseColorAt(uv + vec2(-u_texelSize.x, 0.0)) +
|
|
1379
|
-
baseColorAt(uv + vec2(u_texelSize.x, 0.0)) +
|
|
1380
|
-
baseColorAt(uv + vec2(0.0, -u_texelSize.y)) +
|
|
1381
|
-
baseColorAt(uv + vec2(0.0, u_texelSize.y))
|
|
1382
|
-
) * 0.25;
|
|
1383
|
-
return clamp(center + (center - blur) * u_sharpening, 0.0, 1.0);
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
float luma(vec3 color) {
|
|
1387
|
-
return dot(color, vec3(0.2126, 0.7152, 0.0722));
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
void main() {
|
|
1391
|
-
vec3 center = finalColorAt(v_uv);
|
|
1392
|
-
float alpha = texture(u_source, clamp(v_uv, vec2(0.0), vec2(1.0))).a;
|
|
1393
|
-
if (u_hasFxaa == 0) {
|
|
1394
|
-
outColor = vec4(center, alpha);
|
|
1395
|
-
return;
|
|
1396
|
-
}
|
|
1397
|
-
vec3 north = finalColorAt(v_uv + vec2(0.0, -u_texelSize.y));
|
|
1398
|
-
vec3 south = finalColorAt(v_uv + vec2(0.0, u_texelSize.y));
|
|
1399
|
-
vec3 west = finalColorAt(v_uv + vec2(-u_texelSize.x, 0.0));
|
|
1400
|
-
vec3 east = finalColorAt(v_uv + vec2(u_texelSize.x, 0.0));
|
|
1401
|
-
float centerLuma = luma(center);
|
|
1402
|
-
float minLuma = min(centerLuma, min(min(luma(north), luma(south)), min(luma(west), luma(east))));
|
|
1403
|
-
float maxLuma = max(centerLuma, max(max(luma(north), luma(south)), max(luma(west), luma(east))));
|
|
1404
|
-
if (maxLuma - minLuma < u_edgeThreshold) {
|
|
1405
|
-
outColor = vec4(center, alpha);
|
|
1406
|
-
return;
|
|
1407
|
-
}
|
|
1408
|
-
vec3 average = (north + south + west + east) * 0.25;
|
|
1409
|
-
outColor = vec4(mix(center, average, clamp(u_subpixelBlend, 0.0, 1.0)), alpha);
|
|
1410
|
-
}
|
|
1411
|
-
`, "webgl2-ldr-postprocess");
|
|
1412
|
-
const program = this.gl.createProgram();
|
|
1413
|
-
if (!program) {
|
|
1414
|
-
this.gl.deleteShader(vertex);
|
|
1415
|
-
this.gl.deleteShader(fragment);
|
|
1416
|
-
throw new RenderDeviceError("Failed to allocate WebGL2 LDR postprocess shader", "WEBGL_ALLOCATION_FAILED");
|
|
1417
|
-
}
|
|
1418
|
-
this.gl.attachShader(program, vertex);
|
|
1419
|
-
this.gl.attachShader(program, fragment);
|
|
1420
|
-
this.gl.linkProgram(program);
|
|
1421
|
-
this.gl.deleteShader(vertex);
|
|
1422
|
-
this.gl.deleteShader(fragment);
|
|
1423
|
-
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
1424
|
-
const log = this.gl.getProgramInfoLog(program) ?? "Unknown LDR postprocess shader link error";
|
|
1425
|
-
this.gl.deleteProgram(program);
|
|
1426
|
-
throw new RenderDeviceError("WebGL2 LDR postprocess shader link failed", "SHADER_LINK_FAILED", { log });
|
|
1427
|
-
}
|
|
1428
|
-
this.ldrPostprocessProgram = program;
|
|
1429
|
-
return program;
|
|
1430
|
-
}
|
|
1431
|
-
|
|
1432
|
-
private compileShader(type: GLenum, source: string, label: string): WebGLShader {
|
|
1433
|
-
const shader = this.gl.createShader(type);
|
|
1434
|
-
if (!shader) {
|
|
1435
|
-
throw new RenderDeviceError("Failed to allocate WebGL shader", "WEBGL_ALLOCATION_FAILED", { label });
|
|
1436
|
-
}
|
|
1437
|
-
this.gl.shaderSource(shader, source);
|
|
1438
|
-
this.gl.compileShader(shader);
|
|
1439
|
-
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
1440
|
-
const log = this.gl.getShaderInfoLog(shader) ?? "Unknown shader compile error";
|
|
1441
|
-
this.gl.deleteShader(shader);
|
|
1442
|
-
throw new RenderDeviceError("WebGL shader compile failed", "SHADER_COMPILE_FAILED", { label, log });
|
|
1443
|
-
}
|
|
1444
|
-
return shader;
|
|
1445
|
-
}
|
|
1446
|
-
|
|
1447
|
-
private reflectProgram(program: WebGLProgram): ShaderReflection {
|
|
1448
|
-
const attributes = new Map<string, number>();
|
|
1449
|
-
const uniforms = new Set<string>();
|
|
1450
|
-
const attributeDetails = new Map<string, ShaderReflection["attributeDetails"] extends ReadonlyMap<string, infer T> ? T : never>();
|
|
1451
|
-
const uniformDetails = new Map<string, ShaderReflection["uniformDetails"] extends ReadonlyMap<string, infer T> ? T : never>();
|
|
1452
|
-
const activeAttributes = this.gl.getProgramParameter(program, this.gl.ACTIVE_ATTRIBUTES) as number;
|
|
1453
|
-
const activeUniforms = this.gl.getProgramParameter(program, this.gl.ACTIVE_UNIFORMS) as number;
|
|
1454
|
-
|
|
1455
|
-
for (let i = 0; i < activeAttributes; i += 1) {
|
|
1456
|
-
const info = this.gl.getActiveAttrib(program, i);
|
|
1457
|
-
if (info) {
|
|
1458
|
-
const location = this.gl.getAttribLocation(program, info.name);
|
|
1459
|
-
attributes.set(info.name, location);
|
|
1460
|
-
attributeDetails.set(info.name, { name: info.name, type: String(info.type), location, source: "vertex", line: 0 });
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
for (let i = 0; i < activeUniforms; i += 1) {
|
|
1464
|
-
const info = this.gl.getActiveUniform(program, i);
|
|
1465
|
-
if (info) {
|
|
1466
|
-
const name = info.name.replace(/\[0\]$/, "");
|
|
1467
|
-
uniforms.add(name);
|
|
1468
|
-
uniformDetails.set(name, { name, type: String(info.type), arraySize: info.size > 1 ? info.size : null, source: "fragment", line: 0 });
|
|
1469
|
-
}
|
|
1470
|
-
}
|
|
1471
|
-
return { attributes, uniforms, attributeDetails, uniformDetails };
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
private uploadUniforms(shader: WebGL2ShaderProgram, uniforms: ReadonlyMap<string, unknown>): void {
|
|
1475
|
-
let textureUnit = 0;
|
|
1476
|
-
for (const [name, value] of uniforms) {
|
|
1477
|
-
if (!shader.reflection.uniforms.has(name)) {
|
|
1478
|
-
continue;
|
|
1479
|
-
}
|
|
1480
|
-
const location = this.getUniformLocation(shader, name);
|
|
1481
|
-
if (location === null) {
|
|
1482
|
-
throw new RenderDeviceError("Material tried to bind a missing shader uniform", "MISSING_UNIFORM", { name });
|
|
1483
|
-
}
|
|
1484
|
-
if (value instanceof TextureBinding) {
|
|
1485
|
-
this.uploadTextureUniform(location, value, textureUnit);
|
|
1486
|
-
textureUnit += 1;
|
|
1487
|
-
} else if (typeof value === "number") {
|
|
1488
|
-
this.gl.uniform1f(location, value);
|
|
1489
|
-
} else if (Array.isArray(value) || ArrayBuffer.isView(value)) {
|
|
1490
|
-
const length = (value as ArrayLike<number>).length;
|
|
1491
|
-
const floatData = value as Float32List;
|
|
1492
|
-
if (length === 16 || (length > 16 && length % 16 === 0 && /(?:Matrix|Matrices)$/.test(name))) {
|
|
1493
|
-
this.gl.uniformMatrix4fv(location, false, floatData);
|
|
1494
|
-
} else if (length > 16 && length % 4 === 0) {
|
|
1495
|
-
this.gl.uniform4fv(location, floatData);
|
|
1496
|
-
} else if (length === 4) {
|
|
1497
|
-
this.gl.uniform4fv(location, floatData);
|
|
1498
|
-
} else if (length === 3) {
|
|
1499
|
-
this.gl.uniform3fv(location, floatData);
|
|
1500
|
-
} else if (length === 2) {
|
|
1501
|
-
this.gl.uniform2fv(location, floatData);
|
|
1502
|
-
} else {
|
|
1503
|
-
throw new RenderDeviceError("Unsupported uniform array length", "UNSUPPORTED_UNIFORM", { name, length });
|
|
1504
|
-
}
|
|
1505
|
-
} else {
|
|
1506
|
-
throw new RenderDeviceError("Unsupported uniform value", "UNSUPPORTED_UNIFORM", { name, valueType: typeof value });
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
private getUniformLocation(shader: WebGL2ShaderProgram, name: string): WebGLUniformLocation | null {
|
|
1512
|
-
let cache = this.uniformLocationCache.get(shader);
|
|
1513
|
-
if (!cache) {
|
|
1514
|
-
cache = new Map();
|
|
1515
|
-
this.uniformLocationCache.set(shader, cache);
|
|
1516
|
-
}
|
|
1517
|
-
if (!cache.has(name)) {
|
|
1518
|
-
this.uniformLocationLookupCount += 1;
|
|
1519
|
-
cache.set(name, this.gl.getUniformLocation(shader.handle, name) ?? this.gl.getUniformLocation(shader.handle, `${name}[0]`));
|
|
1520
|
-
}
|
|
1521
|
-
return cache.get(name) ?? null;
|
|
1522
|
-
}
|
|
1523
|
-
|
|
1524
|
-
private uploadTextureUniform(location: WebGLUniformLocation, binding: TextureBinding, textureUnit: number): void {
|
|
1525
|
-
const validation = binding.validate();
|
|
1526
|
-
if (!validation.ok) {
|
|
1527
|
-
throw new RenderDeviceError("Texture binding validation failed", "INVALID_TEXTURE_BINDING", {
|
|
1528
|
-
diagnostics: validation.diagnostics,
|
|
1529
|
-
name: binding.name
|
|
1530
|
-
});
|
|
1531
|
-
}
|
|
1532
|
-
this.activateTextureUnit(textureUnit);
|
|
1533
|
-
const dimension = binding.texture?.dimension ?? (binding.name.toLowerCase().includes("cubemap") || binding.name.toLowerCase().includes("cube") ? "cube" : "2d");
|
|
1534
|
-
const target = dimension === "cube" ? this.gl.TEXTURE_CUBE_MAP : this.gl.TEXTURE_2D;
|
|
1535
|
-
const handle = binding.texture ? this.getTextureHandle(binding.texture) : dimension === "cube" ? this.getFallbackCubeTextureHandle() : this.getFallbackTextureHandle();
|
|
1536
|
-
if (binding.texture) {
|
|
1537
|
-
const lowerName = binding.name.toLowerCase();
|
|
1538
|
-
if (lowerName.includes("environment")) this.nativeEnvironmentBindings += 1;
|
|
1539
|
-
if (lowerName.includes("shadow")) this.nativeShadowMapBindings += 1;
|
|
1540
|
-
}
|
|
1541
|
-
this.bindTextureForUnit(textureUnit, target, handle);
|
|
1542
|
-
const samplerHandle = this.getSamplerHandle(binding.sampler, target);
|
|
1543
|
-
this.stateCache.bindSampler(textureUnit, samplerHandle, () => this.gl.bindSampler(textureUnit, samplerHandle));
|
|
1544
|
-
this.gl.uniform1i(location, textureUnit);
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
private activateTextureUnit(textureUnit: number): void {
|
|
1548
|
-
if (this.activeTextureUnitIndex === textureUnit) return;
|
|
1549
|
-
this.stateCache.activeTexture(textureUnit, () => this.gl.activeTexture(this.gl.TEXTURE0 + textureUnit));
|
|
1550
|
-
this.activeTextureUnitIndex = textureUnit;
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
private bindTextureForUnit(textureUnit: number, target: GLenum, handle: WebGLTexture): void {
|
|
1554
|
-
const key = `${textureUnit}:${target}`;
|
|
1555
|
-
if (this.textureUnitBindings.get(key) === handle) return;
|
|
1556
|
-
this.stateCache.bindTexture(target, handle, () => this.gl.bindTexture(target, handle));
|
|
1557
|
-
this.textureUnitBindings.set(key, handle);
|
|
1558
|
-
this.textureBindCount += 1;
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
private setTextureParameterIfNeeded(handle: WebGLTexture, target: GLenum, parameter: GLenum, value: GLenum | number): void {
|
|
1562
|
-
let parameters = this.textureSamplerParameterCache.get(handle);
|
|
1563
|
-
if (!parameters) {
|
|
1564
|
-
parameters = new Map();
|
|
1565
|
-
this.textureSamplerParameterCache.set(handle, parameters);
|
|
1566
|
-
}
|
|
1567
|
-
if (parameters.get(parameter) === value) return;
|
|
1568
|
-
this.gl.texParameteri(target, parameter, value);
|
|
1569
|
-
parameters.set(parameter, value);
|
|
1570
|
-
this.samplerParameterUploadCount += 1;
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
private applySamplerAnisotropy(maxAnisotropy: number, target: GLenum = this.gl.TEXTURE_2D): void {
|
|
1574
|
-
if (maxAnisotropy <= 1) return;
|
|
1575
|
-
if (!this.anisotropicFilteringExtension) return;
|
|
1576
|
-
this.gl.texParameterf(
|
|
1577
|
-
target,
|
|
1578
|
-
this.anisotropicFilteringExtension.TEXTURE_MAX_ANISOTROPY_EXT,
|
|
1579
|
-
Math.min(Math.max(1, maxAnisotropy), this.maxTextureAnisotropy)
|
|
1580
|
-
);
|
|
1581
|
-
this.samplerAnisotropyUploadCount += 1;
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
private getSamplerHandle(sampler: Sampler, target: GLenum): WebGLSampler {
|
|
1585
|
-
const key = this.samplerKey(sampler, target);
|
|
1586
|
-
const cached = this.samplerObjectCache.get(key);
|
|
1587
|
-
if (cached) return cached;
|
|
1588
|
-
const handle = this.gl.createSampler();
|
|
1589
|
-
if (!handle) {
|
|
1590
|
-
throw new RenderDeviceError("Failed to allocate WebGL sampler", "WEBGL_ALLOCATION_FAILED");
|
|
1591
|
-
}
|
|
1592
|
-
this.gl.samplerParameteri(handle, this.gl.TEXTURE_MIN_FILTER, this.minFilter(sampler.minFilter));
|
|
1593
|
-
this.gl.samplerParameteri(handle, this.gl.TEXTURE_MAG_FILTER, this.magFilter(sampler.magFilter));
|
|
1594
|
-
this.gl.samplerParameteri(handle, this.gl.TEXTURE_WRAP_S, this.addressMode(sampler.addressU));
|
|
1595
|
-
this.gl.samplerParameteri(handle, this.gl.TEXTURE_WRAP_T, this.addressMode(sampler.addressV));
|
|
1596
|
-
this.samplerParameterUploadCount += 4;
|
|
1597
|
-
if (target === this.gl.TEXTURE_CUBE_MAP) {
|
|
1598
|
-
this.gl.samplerParameteri(handle, this.gl.TEXTURE_WRAP_R, this.addressMode(sampler.addressV));
|
|
1599
|
-
this.samplerParameterUploadCount += 1;
|
|
1600
|
-
}
|
|
1601
|
-
if (sampler.maxAnisotropy > 1 && this.anisotropicFilteringExtension) {
|
|
1602
|
-
this.gl.samplerParameterf(
|
|
1603
|
-
handle,
|
|
1604
|
-
this.anisotropicFilteringExtension.TEXTURE_MAX_ANISOTROPY_EXT,
|
|
1605
|
-
Math.min(Math.max(1, sampler.maxAnisotropy), this.maxTextureAnisotropy)
|
|
1606
|
-
);
|
|
1607
|
-
this.samplerAnisotropyUploadCount += 1;
|
|
1608
|
-
}
|
|
1609
|
-
this.samplerObjectCache.set(key, handle);
|
|
1610
|
-
return handle;
|
|
1611
|
-
}
|
|
1612
|
-
|
|
1613
|
-
private samplerKey(sampler: Sampler, target: GLenum): string {
|
|
1614
|
-
return [
|
|
1615
|
-
sampler.minFilter,
|
|
1616
|
-
sampler.magFilter,
|
|
1617
|
-
sampler.addressU,
|
|
1618
|
-
sampler.addressV,
|
|
1619
|
-
sampler.maxAnisotropy,
|
|
1620
|
-
target === this.gl.TEXTURE_CUBE_MAP ? "cube" : "2d"
|
|
1621
|
-
].join("|");
|
|
1622
|
-
}
|
|
1623
|
-
|
|
1624
|
-
private getFallbackTextureHandle(): WebGLTexture {
|
|
1625
|
-
if (this.fallbackTexture) {
|
|
1626
|
-
return this.fallbackTexture;
|
|
1627
|
-
}
|
|
1628
|
-
const handle = this.gl.createTexture();
|
|
1629
|
-
if (!handle) {
|
|
1630
|
-
throw new RenderDeviceError("Failed to allocate WebGL fallback texture", "WEBGL_ALLOCATION_FAILED");
|
|
1631
|
-
}
|
|
1632
|
-
this.stateCache.invalidate();
|
|
1633
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, handle);
|
|
1634
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
|
|
1635
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
|
|
1636
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
1637
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
1638
|
-
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, 1, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255, 255]));
|
|
1639
|
-
this.fallbackTexture = handle;
|
|
1640
|
-
return handle;
|
|
1641
|
-
}
|
|
1642
|
-
|
|
1643
|
-
private getFallbackCubeTextureHandle(): WebGLTexture {
|
|
1644
|
-
if (this.fallbackCubeTexture) {
|
|
1645
|
-
return this.fallbackCubeTexture;
|
|
1646
|
-
}
|
|
1647
|
-
const handle = this.gl.createTexture();
|
|
1648
|
-
if (!handle) {
|
|
1649
|
-
throw new RenderDeviceError("Failed to allocate WebGL fallback cube texture", "WEBGL_ALLOCATION_FAILED");
|
|
1650
|
-
}
|
|
1651
|
-
this.stateCache.invalidate();
|
|
1652
|
-
this.gl.bindTexture(this.gl.TEXTURE_CUBE_MAP, handle);
|
|
1653
|
-
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
|
|
1654
|
-
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
|
|
1655
|
-
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
1656
|
-
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
1657
|
-
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_R, this.gl.CLAMP_TO_EDGE);
|
|
1658
|
-
const pixel = new Uint8Array([255, 255, 255, 255]);
|
|
1659
|
-
for (const face of WEBGL_CUBE_FACES) {
|
|
1660
|
-
this.gl.texImage2D(this.cubeFaceTarget(face), 0, this.gl.RGBA, 1, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixel);
|
|
1661
|
-
}
|
|
1662
|
-
this.fallbackCubeTexture = handle;
|
|
1663
|
-
return handle;
|
|
1664
|
-
}
|
|
1665
|
-
|
|
1666
|
-
private getTextureHandle(texture: Texture): WebGLTexture {
|
|
1667
|
-
this.releaseDisposedTextureHandles();
|
|
1668
|
-
if (texture.disposed) {
|
|
1669
|
-
throw new RenderDeviceError("Texture is disposed", "DISPOSED_RESOURCE", { label: texture.label });
|
|
1670
|
-
}
|
|
1671
|
-
const cached = this.textures.get(texture);
|
|
1672
|
-
if (cached) {
|
|
1673
|
-
return cached;
|
|
1674
|
-
}
|
|
1675
|
-
const handle = this.gl.createTexture();
|
|
1676
|
-
if (!handle) {
|
|
1677
|
-
throw new RenderDeviceError("Failed to allocate WebGL texture", "WEBGL_ALLOCATION_FAILED", { label: texture.label });
|
|
1678
|
-
}
|
|
1679
|
-
this.stateCache.invalidate();
|
|
1680
|
-
this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
1681
|
-
this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
1682
|
-
this.gl.pixelStorei(this.gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, this.gl.NONE);
|
|
1683
|
-
if (texture.dimension === "cube") {
|
|
1684
|
-
this.uploadCubeTexture(texture, handle);
|
|
1685
|
-
this.textureUploadModes.set(texture, "cube");
|
|
1686
|
-
this.textures.set(texture, handle);
|
|
1687
|
-
return handle;
|
|
1688
|
-
}
|
|
1689
|
-
this.gl.bindTexture(this.gl.TEXTURE_2D, handle);
|
|
1690
|
-
if (isCompressedTextureFormat(texture.format)) {
|
|
1691
|
-
const compressed = this.resolveCompressedTextureFormat(texture.format);
|
|
1692
|
-
if (compressed) {
|
|
1693
|
-
const uploadLevels = completeUploadLevels(texture.textureLevels);
|
|
1694
|
-
for (const [levelIndex, level] of uploadLevels.entries()) {
|
|
1695
|
-
this.gl.compressedTexImage2D(this.gl.TEXTURE_2D, levelIndex, compressed.internalFormat, level.width, level.height, 0, level.data);
|
|
1696
|
-
}
|
|
1697
|
-
const compressedUploadError = this.readError();
|
|
1698
|
-
if (!compressedUploadError) {
|
|
1699
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
|
|
1700
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, uploadLevels.length - 1);
|
|
1701
|
-
this.textureUploadModes.set(texture, "compressed");
|
|
1702
|
-
this.textures.set(texture, handle);
|
|
1703
|
-
return handle;
|
|
1704
|
-
}
|
|
1705
|
-
}
|
|
1706
|
-
const fallbackLevels = completeUploadLevels(texture.fallbackTextureLevels);
|
|
1707
|
-
if (fallbackLevels.length === 0) {
|
|
1708
|
-
this.gl.deleteTexture(handle);
|
|
1709
|
-
throw new RenderDeviceError("Compressed texture format is not supported and no RGBA8 fallback data was provided", "COMPRESSED_TEXTURE_UNSUPPORTED", {
|
|
1710
|
-
label: texture.label,
|
|
1711
|
-
format: texture.format
|
|
1712
|
-
});
|
|
1713
|
-
}
|
|
1714
|
-
this.uploadRgba8FallbackTexture(texture, fallbackLevels);
|
|
1715
|
-
this.textureUploadModes.set(texture, "fallback");
|
|
1716
|
-
this.textures.set(texture, handle);
|
|
1717
|
-
return handle;
|
|
1718
|
-
}
|
|
1719
|
-
if (texture.format === "depth24") {
|
|
1720
|
-
throw new RenderDeviceError("Depth textures cannot be uploaded to WebGL2 color samplers", "UNSUPPORTED_TEXTURE_FORMAT", {
|
|
1721
|
-
label: texture.label,
|
|
1722
|
-
format: texture.format
|
|
1723
|
-
});
|
|
1724
|
-
}
|
|
1725
|
-
if (texture.source) {
|
|
1726
|
-
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.rgba8TextureInternalFormat(texture), this.gl.RGBA, this.gl.UNSIGNED_BYTE, texture.source);
|
|
1727
|
-
this.gl.generateMipmap(this.gl.TEXTURE_2D);
|
|
1728
|
-
} else if (texture.mipLevels.length > 0) {
|
|
1729
|
-
const uploadFormat = this.textureUploadFormat(texture);
|
|
1730
|
-
const uploadLevels = completeUploadLevels(texture.textureLevels);
|
|
1731
|
-
for (const [levelIndex, level] of uploadLevels.entries()) {
|
|
1732
|
-
this.gl.texImage2D(
|
|
1733
|
-
this.gl.TEXTURE_2D,
|
|
1734
|
-
levelIndex,
|
|
1735
|
-
uploadFormat.internalFormat,
|
|
1736
|
-
level.width,
|
|
1737
|
-
level.height,
|
|
1738
|
-
0,
|
|
1739
|
-
uploadFormat.format,
|
|
1740
|
-
uploadFormat.type,
|
|
1741
|
-
texturePixelUploadData(level.data, texture.format)
|
|
1742
|
-
);
|
|
1743
|
-
}
|
|
1744
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
|
|
1745
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, uploadLevels.length - 1);
|
|
1746
|
-
} else {
|
|
1747
|
-
const uploadFormat = this.textureUploadFormat(texture);
|
|
1748
|
-
this.gl.texImage2D(
|
|
1749
|
-
this.gl.TEXTURE_2D,
|
|
1750
|
-
0,
|
|
1751
|
-
uploadFormat.internalFormat,
|
|
1752
|
-
texture.width,
|
|
1753
|
-
texture.height,
|
|
1754
|
-
0,
|
|
1755
|
-
uploadFormat.format,
|
|
1756
|
-
uploadFormat.type,
|
|
1757
|
-
texture.data ? texturePixelUploadData(texture.data, texture.format) : null
|
|
1758
|
-
);
|
|
1759
|
-
this.gl.generateMipmap(this.gl.TEXTURE_2D);
|
|
1760
|
-
}
|
|
1761
|
-
this.textureUploadModes.set(texture, texture.format);
|
|
1762
|
-
this.textures.set(texture, handle);
|
|
1763
|
-
return handle;
|
|
1764
|
-
}
|
|
1765
|
-
|
|
1766
|
-
private uploadCubeTexture(texture: Texture, handle: WebGLTexture): void {
|
|
1767
|
-
this.gl.bindTexture(this.gl.TEXTURE_CUBE_MAP, handle);
|
|
1768
|
-
const uploadFormat = this.textureUploadFormat(texture);
|
|
1769
|
-
for (const face of texture.cubeFaces) {
|
|
1770
|
-
const uploadLevels = completeUploadLevels(face.mipLevels);
|
|
1771
|
-
for (const [levelIndex, level] of uploadLevels.entries()) {
|
|
1772
|
-
this.gl.texImage2D(
|
|
1773
|
-
this.cubeFaceTarget(face.face),
|
|
1774
|
-
levelIndex,
|
|
1775
|
-
uploadFormat.internalFormat,
|
|
1776
|
-
level.width,
|
|
1777
|
-
level.height,
|
|
1778
|
-
0,
|
|
1779
|
-
uploadFormat.format,
|
|
1780
|
-
uploadFormat.type,
|
|
1781
|
-
texturePixelUploadData(level.data, texture.format)
|
|
1782
|
-
);
|
|
1783
|
-
}
|
|
1784
|
-
}
|
|
1785
|
-
const firstFaceLevels = completeUploadLevels(texture.cubeFaces[0]?.mipLevels ?? []);
|
|
1786
|
-
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_BASE_LEVEL, 0);
|
|
1787
|
-
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_MAX_LEVEL, Math.max(0, firstFaceLevels.length - 1));
|
|
1788
|
-
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
1789
|
-
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
1790
|
-
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_R, this.gl.CLAMP_TO_EDGE);
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
private uploadRgba8FallbackTexture(texture: Texture, fallbackLevels: readonly { readonly width: number; readonly height: number; readonly data: TexturePixelData }[]): void {
|
|
1794
|
-
const internalFormat = this.rgba8TextureInternalFormat(texture);
|
|
1795
|
-
for (const [levelIndex, level] of fallbackLevels.entries()) {
|
|
1796
|
-
this.gl.texImage2D(
|
|
1797
|
-
this.gl.TEXTURE_2D,
|
|
1798
|
-
levelIndex,
|
|
1799
|
-
internalFormat,
|
|
1800
|
-
level.width,
|
|
1801
|
-
level.height,
|
|
1802
|
-
0,
|
|
1803
|
-
this.gl.RGBA,
|
|
1804
|
-
this.gl.UNSIGNED_BYTE,
|
|
1805
|
-
texturePixelUploadData(level.data, "rgba8")
|
|
1806
|
-
);
|
|
1807
|
-
}
|
|
1808
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
|
|
1809
|
-
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, fallbackLevels.length - 1);
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
private cubeFaceTarget(face: TextureCubeFace): GLenum {
|
|
1813
|
-
switch (face) {
|
|
1814
|
-
case "px": return this.gl.TEXTURE_CUBE_MAP_POSITIVE_X;
|
|
1815
|
-
case "nx": return this.gl.TEXTURE_CUBE_MAP_NEGATIVE_X;
|
|
1816
|
-
case "py": return this.gl.TEXTURE_CUBE_MAP_POSITIVE_Y;
|
|
1817
|
-
case "ny": return this.gl.TEXTURE_CUBE_MAP_NEGATIVE_Y;
|
|
1818
|
-
case "pz": return this.gl.TEXTURE_CUBE_MAP_POSITIVE_Z;
|
|
1819
|
-
case "nz": return this.gl.TEXTURE_CUBE_MAP_NEGATIVE_Z;
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
private rgba8TextureInternalFormat(texture: Texture): GLenum {
|
|
1824
|
-
return texture.colorSpace === "srgb" ? this.gl.SRGB8_ALPHA8 : this.gl.RGBA;
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
private textureUploadFormat(texture: Texture): { readonly internalFormat: GLenum; readonly format: GLenum; readonly type: GLenum } {
|
|
1828
|
-
if (texture.format === "rgba8") {
|
|
1829
|
-
return { internalFormat: this.rgba8TextureInternalFormat(texture), format: this.gl.RGBA, type: this.gl.UNSIGNED_BYTE };
|
|
1830
|
-
}
|
|
1831
|
-
if (texture.colorSpace === "srgb") {
|
|
1832
|
-
throw new RenderDeviceError("Floating-point texture uploads must use linear colorSpace", "UNSUPPORTED_TEXTURE_FORMAT", {
|
|
1833
|
-
label: texture.label,
|
|
1834
|
-
format: texture.format,
|
|
1835
|
-
colorSpace: texture.colorSpace
|
|
1836
|
-
});
|
|
1837
|
-
}
|
|
1838
|
-
if (texture.format === "rgba16f") {
|
|
1839
|
-
return { internalFormat: this.gl.RGBA16F, format: this.gl.RGBA, type: this.gl.HALF_FLOAT };
|
|
1840
|
-
}
|
|
1841
|
-
if (texture.format === "rgba32f") {
|
|
1842
|
-
return { internalFormat: this.gl.RGBA32F, format: this.gl.RGBA, type: this.gl.FLOAT };
|
|
1843
|
-
}
|
|
1844
|
-
throw new RenderDeviceError("Unsupported texture upload format", "UNSUPPORTED_TEXTURE_FORMAT", {
|
|
1845
|
-
label: texture.label,
|
|
1846
|
-
format: texture.format
|
|
1847
|
-
});
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
private resolveRenderTargetFormat(format: "rgba8" | "rgba16f" | "rgba32f"): { readonly internalFormat: GLenum; readonly type: GLenum } {
|
|
1851
|
-
if (format === "rgba8") {
|
|
1852
|
-
return { internalFormat: this.gl.RGBA, type: this.gl.UNSIGNED_BYTE };
|
|
1853
|
-
}
|
|
1854
|
-
if (!this.gl.getExtension("EXT_color_buffer_float")) {
|
|
1855
|
-
throw new RenderDeviceError("Floating-point color render targets require EXT_color_buffer_float", "HDR_RENDER_TARGET_UNSUPPORTED", { format });
|
|
1856
|
-
}
|
|
1857
|
-
if (format === "rgba16f") {
|
|
1858
|
-
return { internalFormat: this.gl.RGBA16F, type: this.gl.HALF_FLOAT };
|
|
1859
|
-
}
|
|
1860
|
-
return { internalFormat: this.gl.RGBA32F, type: this.gl.FLOAT };
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
private releaseDisposedTextureHandles(): void {
|
|
1864
|
-
for (const [texture, handle] of [...this.textures]) {
|
|
1865
|
-
if (texture.disposed) {
|
|
1866
|
-
this.gl.deleteTexture(handle);
|
|
1867
|
-
this.textures.delete(texture);
|
|
1868
|
-
this.textureUploadModes.delete(texture);
|
|
1869
|
-
this.releasedTextureHandles += 1;
|
|
1870
|
-
}
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
private resolveCompressedTextureFormat(format: TextureCompressedFormat): { readonly internalFormat: GLenum } | null {
|
|
1875
|
-
switch (format) {
|
|
1876
|
-
case "bc1-rgba-unorm": {
|
|
1877
|
-
const extension = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
|
|
1878
|
-
return extension ? { internalFormat: extension.COMPRESSED_RGBA_S3TC_DXT1_EXT } : null;
|
|
1879
|
-
}
|
|
1880
|
-
case "bc3-rgba-unorm": {
|
|
1881
|
-
const extension = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
|
|
1882
|
-
return extension ? { internalFormat: extension.COMPRESSED_RGBA_S3TC_DXT5_EXT } : null;
|
|
1883
|
-
}
|
|
1884
|
-
case "etc2-rgba8unorm":
|
|
1885
|
-
return { internalFormat: 0x9278 };
|
|
1886
|
-
case "astc-4x4-rgba-unorm": {
|
|
1887
|
-
const extension = this.gl.getExtension("WEBGL_compressed_texture_astc");
|
|
1888
|
-
return extension ? { internalFormat: extension.COMPRESSED_RGBA_ASTC_4x4_KHR } : null;
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
private magFilter(filter: TextureMagFilter): GLenum {
|
|
1894
|
-
return filter === "nearest" ? this.gl.NEAREST : this.gl.LINEAR;
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
private minFilter(filter: TextureMinFilter): GLenum {
|
|
1898
|
-
switch (filter) {
|
|
1899
|
-
case "nearest":
|
|
1900
|
-
return this.gl.NEAREST;
|
|
1901
|
-
case "linear":
|
|
1902
|
-
return this.gl.LINEAR;
|
|
1903
|
-
case "nearest-mipmap-nearest":
|
|
1904
|
-
return this.gl.NEAREST_MIPMAP_NEAREST;
|
|
1905
|
-
case "linear-mipmap-nearest":
|
|
1906
|
-
return this.gl.LINEAR_MIPMAP_NEAREST;
|
|
1907
|
-
case "nearest-mipmap-linear":
|
|
1908
|
-
return this.gl.NEAREST_MIPMAP_LINEAR;
|
|
1909
|
-
case "linear-mipmap-linear":
|
|
1910
|
-
return this.gl.LINEAR_MIPMAP_LINEAR;
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
private addressMode(mode: "clamp-to-edge" | "repeat" | "mirror-repeat"): GLenum {
|
|
1915
|
-
if (mode === "repeat") return this.gl.REPEAT;
|
|
1916
|
-
if (mode === "mirror-repeat") return this.gl.MIRRORED_REPEAT;
|
|
1917
|
-
return this.gl.CLAMP_TO_EDGE;
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
private bindVertexFormat(shader: RenderShaderProgram, format: VertexFormat): Set<number> {
|
|
1921
|
-
const boundLocations = new Set<number>();
|
|
1922
|
-
for (const attribute of format.attributes) {
|
|
1923
|
-
const location = this.resolveAttributeLocation(shader, attribute);
|
|
1924
|
-
if (location < 0) {
|
|
1925
|
-
continue;
|
|
1926
|
-
}
|
|
1927
|
-
if (attribute.type !== "float32") {
|
|
1928
|
-
throw new RenderDeviceError("Unsupported vertex attribute type", "UNSUPPORTED_VERTEX_ATTRIBUTE", {
|
|
1929
|
-
attribute: attribute.shaderName,
|
|
1930
|
-
type: attribute.type
|
|
1931
|
-
});
|
|
1932
|
-
}
|
|
1933
|
-
boundLocations.add(location);
|
|
1934
|
-
this.gl.enableVertexAttribArray(location);
|
|
1935
|
-
this.gl.vertexAttribDivisor(location, 0);
|
|
1936
|
-
this.gl.vertexAttribPointer(
|
|
1937
|
-
location,
|
|
1938
|
-
attribute.components,
|
|
1939
|
-
this.gl.FLOAT,
|
|
1940
|
-
attribute.normalized,
|
|
1941
|
-
format.stride,
|
|
1942
|
-
attribute.offset
|
|
1943
|
-
);
|
|
1944
|
-
}
|
|
1945
|
-
return boundLocations;
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
private bindVertexArrayForCommand(command: DrawCommand, shader: WebGL2ShaderProgram, vertexBuffer: WebGL2Buffer): WebGL2VertexArrayCacheEntry {
|
|
1949
|
-
const indexBuffer = command.indexBuffer ? this.requireBuffer(command.indexBuffer) : undefined;
|
|
1950
|
-
const key = this.vertexArrayCacheKey(command, shader, vertexBuffer, indexBuffer);
|
|
1951
|
-
const cached = this.vertexArrayCache.get(key);
|
|
1952
|
-
if (cached) {
|
|
1953
|
-
this.stateCache.bindVertexArray(cached.handle, () => this.gl.bindVertexArray(cached.handle));
|
|
1954
|
-
return cached;
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
const handle = this.gl.createVertexArray();
|
|
1958
|
-
if (!handle) {
|
|
1959
|
-
throw new RenderDeviceError("Failed to allocate WebGL vertex array", "WEBGL_ALLOCATION_FAILED");
|
|
1960
|
-
}
|
|
1961
|
-
this.vertexArrayCreateCount += 1;
|
|
1962
|
-
this.stateCache.bindVertexArray(handle, () => this.gl.bindVertexArray(handle));
|
|
1963
|
-
this.stateCache.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle, () => this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle));
|
|
1964
|
-
const boundLocations = this.bindVertexFormat(shader, command.vertexFormat!);
|
|
1965
|
-
if (command.instanceAttributes && command.instanceAttributes.length > 0) {
|
|
1966
|
-
this.bindInstanceAttributes(shader, command.instanceAttributes, boundLocations);
|
|
1967
|
-
}
|
|
1968
|
-
if (indexBuffer) {
|
|
1969
|
-
this.stateCache.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle, () => this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle));
|
|
1970
|
-
}
|
|
1971
|
-
this.disableUnboundVertexAttributes(boundLocations);
|
|
1972
|
-
this.applyDefaultAttributes(shader, boundLocations);
|
|
1973
|
-
const entry: WebGL2VertexArrayCacheEntry = { key, handle, boundLocations };
|
|
1974
|
-
this.vertexArrayCache.set(key, entry);
|
|
1975
|
-
return entry;
|
|
1976
|
-
}
|
|
1977
|
-
|
|
1978
|
-
private bindNoVertexArray(): void {
|
|
1979
|
-
this.stateCache.bindVertexArray(null, () => this.gl.bindVertexArray(null));
|
|
1980
|
-
}
|
|
1981
|
-
|
|
1982
|
-
private vertexArrayCacheKey(command: DrawCommand, shader: WebGL2ShaderProgram, vertexBuffer: WebGL2Buffer, indexBuffer: WebGL2Buffer | undefined): string {
|
|
1983
|
-
const instanceKey = (command.instanceAttributes ?? []).map((attribute) => {
|
|
1984
|
-
const buffer = this.requireBuffer(attribute.buffer);
|
|
1985
|
-
return `${attribute.shaderName}:${buffer.id}:${attribute.components}:${attribute.offset}:${attribute.stride}:${attribute.normalized === true ? 1 : 0}:${attribute.divisor ?? 1}`;
|
|
1986
|
-
}).join(",");
|
|
1987
|
-
return [
|
|
1988
|
-
shader.id,
|
|
1989
|
-
vertexBuffer.id,
|
|
1990
|
-
this.vertexFormatId(command.vertexFormat!),
|
|
1991
|
-
indexBuffer?.id ?? 0,
|
|
1992
|
-
instanceKey
|
|
1993
|
-
].join("|");
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
private vertexFormatId(format: VertexFormat): number {
|
|
1997
|
-
const existing = this.vertexFormatIds.get(format);
|
|
1998
|
-
if (existing !== undefined) return existing;
|
|
1999
|
-
const next = this.nextVertexFormatId;
|
|
2000
|
-
this.nextVertexFormatId += 1;
|
|
2001
|
-
this.vertexFormatIds.set(format, next);
|
|
2002
|
-
return next;
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
private bindInstanceAttributes(shader: RenderShaderProgram, attributes: readonly InstanceVertexAttribute[], boundLocations: Set<number>): void {
|
|
2006
|
-
for (const attribute of attributes) {
|
|
2007
|
-
const location = shader.reflection.attributes.get(attribute.shaderName);
|
|
2008
|
-
if (location === undefined || location < 0) {
|
|
2009
|
-
continue;
|
|
2010
|
-
}
|
|
2011
|
-
if (![1, 2, 3, 4].includes(attribute.components)) {
|
|
2012
|
-
throw new RenderDeviceError("Instance vertex attribute components must be 1, 2, 3, or 4", "INVALID_DRAW_COMMAND", {
|
|
2013
|
-
attribute: attribute.shaderName,
|
|
2014
|
-
components: attribute.components
|
|
2015
|
-
});
|
|
2016
|
-
}
|
|
2017
|
-
if (!Number.isInteger(attribute.offset) || attribute.offset < 0 || attribute.offset % 4 !== 0) {
|
|
2018
|
-
throw new RenderDeviceError("Instance vertex attribute offset must be non-negative and 4-byte aligned", "INVALID_DRAW_COMMAND", {
|
|
2019
|
-
attribute: attribute.shaderName,
|
|
2020
|
-
offset: attribute.offset
|
|
2021
|
-
});
|
|
2022
|
-
}
|
|
2023
|
-
if (!Number.isInteger(attribute.stride) || attribute.stride <= 0 || attribute.stride % 4 !== 0) {
|
|
2024
|
-
throw new RenderDeviceError("Instance vertex attribute stride must be positive and 4-byte aligned", "INVALID_DRAW_COMMAND", {
|
|
2025
|
-
attribute: attribute.shaderName,
|
|
2026
|
-
stride: attribute.stride
|
|
2027
|
-
});
|
|
2028
|
-
}
|
|
2029
|
-
if (attribute.offset + attribute.components * 4 > attribute.stride) {
|
|
2030
|
-
throw new RenderDeviceError("Instance vertex attribute range must fit inside its stride", "INVALID_DRAW_COMMAND", {
|
|
2031
|
-
attribute: attribute.shaderName,
|
|
2032
|
-
offset: attribute.offset,
|
|
2033
|
-
components: attribute.components,
|
|
2034
|
-
stride: attribute.stride
|
|
2035
|
-
});
|
|
2036
|
-
}
|
|
2037
|
-
const buffer = this.requireBuffer(attribute.buffer);
|
|
2038
|
-
boundLocations.add(location);
|
|
2039
|
-
this.stateCache.bindBuffer(this.gl.ARRAY_BUFFER, buffer.handle, () => this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer.handle));
|
|
2040
|
-
this.gl.enableVertexAttribArray(location);
|
|
2041
|
-
this.gl.vertexAttribPointer(
|
|
2042
|
-
location,
|
|
2043
|
-
attribute.components,
|
|
2044
|
-
this.gl.FLOAT,
|
|
2045
|
-
attribute.normalized ?? false,
|
|
2046
|
-
attribute.stride,
|
|
2047
|
-
attribute.offset
|
|
2048
|
-
);
|
|
2049
|
-
this.gl.vertexAttribDivisor(location, attribute.divisor ?? 1);
|
|
2050
|
-
}
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
private resolveAttributeLocation(shader: RenderShaderProgram, attribute: VertexAttribute): number {
|
|
2054
|
-
const reflected = shader.reflection.attributes.get(attribute.shaderName);
|
|
2055
|
-
if (reflected !== undefined) {
|
|
2056
|
-
return reflected;
|
|
2057
|
-
}
|
|
2058
|
-
return shader.reflection.attributes.get(attribute.semantic) ?? -1;
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
private applyDefaultAttributes(shader: RenderShaderProgram, boundLocations: ReadonlySet<number>): void {
|
|
2062
|
-
const colorLocation = shader.reflection.attributes.get("a_color") ?? shader.reflection.attributes.get("color");
|
|
2063
|
-
if (colorLocation !== undefined && colorLocation >= 0 && !boundLocations.has(colorLocation)) {
|
|
2064
|
-
this.gl.disableVertexAttribArray(colorLocation);
|
|
2065
|
-
this.gl.vertexAttribDivisor(colorLocation, 0);
|
|
2066
|
-
this.gl.vertexAttrib4f(colorLocation, 1, 1, 1, 1);
|
|
2067
|
-
}
|
|
2068
|
-
const tangentLocation = shader.reflection.attributes.get("a_tangent") ?? shader.reflection.attributes.get("tangent");
|
|
2069
|
-
if (tangentLocation !== undefined && tangentLocation >= 0 && !boundLocations.has(tangentLocation)) {
|
|
2070
|
-
this.gl.disableVertexAttribArray(tangentLocation);
|
|
2071
|
-
this.gl.vertexAttribDivisor(tangentLocation, 0);
|
|
2072
|
-
this.gl.vertexAttrib4f(tangentLocation, 1, 0, 0, 1);
|
|
2073
|
-
}
|
|
2074
|
-
const uv1Location = shader.reflection.attributes.get("a_uv1") ?? shader.reflection.attributes.get("uv1");
|
|
2075
|
-
if (uv1Location !== undefined && uv1Location >= 0 && !boundLocations.has(uv1Location)) {
|
|
2076
|
-
this.gl.disableVertexAttribArray(uv1Location);
|
|
2077
|
-
this.gl.vertexAttribDivisor(uv1Location, 0);
|
|
2078
|
-
this.gl.vertexAttrib2f(uv1Location, 0, 0);
|
|
2079
|
-
}
|
|
2080
|
-
const instanceColorLocation = shader.reflection.attributes.get("a_instanceColor") ?? shader.reflection.attributes.get("instanceColor");
|
|
2081
|
-
if (instanceColorLocation !== undefined && instanceColorLocation >= 0 && !boundLocations.has(instanceColorLocation)) {
|
|
2082
|
-
this.gl.disableVertexAttribArray(instanceColorLocation);
|
|
2083
|
-
this.gl.vertexAttribDivisor(instanceColorLocation, 0);
|
|
2084
|
-
this.gl.vertexAttrib4f(instanceColorLocation, 1, 1, 1, 1);
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
private disableUnboundVertexAttributes(boundLocations: ReadonlySet<number>): void {
|
|
2089
|
-
for (let location = 0; location < this.maxVertexAttributes; location += 1) {
|
|
2090
|
-
if (!boundLocations.has(location)) {
|
|
2091
|
-
this.gl.disableVertexAttribArray(location);
|
|
2092
|
-
this.gl.vertexAttribDivisor(location, 0);
|
|
2093
|
-
}
|
|
2094
|
-
}
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
private primitive(topology: DrawCommand["topology"]): GLenum {
|
|
2098
|
-
if (topology === "lines") return this.gl.LINES;
|
|
2099
|
-
if (topology === "points") return this.gl.POINTS;
|
|
2100
|
-
return this.gl.TRIANGLES;
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
private applyRenderState(state: DrawCommand["renderState"]): void {
|
|
2104
|
-
const renderState = state ?? {
|
|
2105
|
-
depthTest: true,
|
|
2106
|
-
depthWrite: true,
|
|
2107
|
-
cullMode: "back" as const,
|
|
2108
|
-
blend: false,
|
|
2109
|
-
depthCompare: "less-equal" as const,
|
|
2110
|
-
colorWrite: [true, true, true, true] as const,
|
|
2111
|
-
scissor: null,
|
|
2112
|
-
polygonOffset: null,
|
|
2113
|
-
stencil: null
|
|
2114
|
-
};
|
|
2115
|
-
this.stateCache.setEnabled(this.gl.DEPTH_TEST, renderState.depthTest, () => {
|
|
2116
|
-
if (renderState.depthTest) this.gl.enable(this.gl.DEPTH_TEST);
|
|
2117
|
-
else this.gl.disable(this.gl.DEPTH_TEST);
|
|
2118
|
-
});
|
|
2119
|
-
this.stateCache.depthMask(renderState.depthWrite, () => this.gl.depthMask(renderState.depthWrite));
|
|
2120
|
-
this.stateCache.depthFunc(renderState.depthCompare === "always" ? this.gl.ALWAYS : this.gl.LEQUAL, () => this.gl.depthFunc(renderState.depthCompare === "always" ? this.gl.ALWAYS : this.gl.LEQUAL));
|
|
2121
|
-
const colorWrite = renderState.colorWrite ?? [true, true, true, true] as const;
|
|
2122
|
-
this.stateCache.colorMask(colorWrite[0], colorWrite[1], colorWrite[2], colorWrite[3], () => this.gl.colorMask(colorWrite[0], colorWrite[1], colorWrite[2], colorWrite[3]));
|
|
2123
|
-
if (renderState.scissor) {
|
|
2124
|
-
this.stateCache.setEnabled(this.gl.SCISSOR_TEST, true, () => this.gl.enable(this.gl.SCISSOR_TEST));
|
|
2125
|
-
this.stateCache.scissor(renderState.scissor.x, renderState.scissor.y, renderState.scissor.width, renderState.scissor.height, () => {
|
|
2126
|
-
this.gl.scissor(renderState.scissor!.x, renderState.scissor!.y, renderState.scissor!.width, renderState.scissor!.height);
|
|
2127
|
-
});
|
|
2128
|
-
} else {
|
|
2129
|
-
this.stateCache.setEnabled(this.gl.SCISSOR_TEST, false, () => this.gl.disable(this.gl.SCISSOR_TEST));
|
|
2130
|
-
}
|
|
2131
|
-
if (renderState.polygonOffset) {
|
|
2132
|
-
this.stateCache.setEnabled(this.gl.POLYGON_OFFSET_FILL, true, () => this.gl.enable(this.gl.POLYGON_OFFSET_FILL));
|
|
2133
|
-
this.stateCache.polygonOffset(renderState.polygonOffset.factor, renderState.polygonOffset.units, () => this.gl.polygonOffset(renderState.polygonOffset!.factor, renderState.polygonOffset!.units));
|
|
2134
|
-
} else {
|
|
2135
|
-
this.stateCache.setEnabled(this.gl.POLYGON_OFFSET_FILL, false, () => this.gl.disable(this.gl.POLYGON_OFFSET_FILL));
|
|
2136
|
-
}
|
|
2137
|
-
if (renderState.stencil) {
|
|
2138
|
-
const stencil = renderState.stencil;
|
|
2139
|
-
const compare = this.stencilCompare(stencil.compare ?? "always");
|
|
2140
|
-
const reference = stencil.reference ?? 0;
|
|
2141
|
-
const readMask = stencil.readMask ?? 0xff;
|
|
2142
|
-
const writeMask = stencil.writeMask ?? 0xff;
|
|
2143
|
-
const fail = this.stencilOperation(stencil.fail ?? "keep");
|
|
2144
|
-
const depthFail = this.stencilOperation(stencil.depthFail ?? "keep");
|
|
2145
|
-
const depthPass = this.stencilOperation(stencil.depthPass ?? "keep");
|
|
2146
|
-
this.stateCache.setEnabled(this.gl.STENCIL_TEST, true, () => this.gl.enable(this.gl.STENCIL_TEST));
|
|
2147
|
-
this.stateCache.stencilFunc(compare, reference, readMask, () => this.gl.stencilFunc(compare, reference, readMask));
|
|
2148
|
-
this.stateCache.stencilMask(writeMask, () => this.gl.stencilMask(writeMask));
|
|
2149
|
-
this.stateCache.stencilOp(fail, depthFail, depthPass, () => this.gl.stencilOp(fail, depthFail, depthPass));
|
|
2150
|
-
} else {
|
|
2151
|
-
this.stateCache.setEnabled(this.gl.STENCIL_TEST, false, () => this.gl.disable(this.gl.STENCIL_TEST));
|
|
2152
|
-
}
|
|
2153
|
-
if (renderState.cullMode === "none") {
|
|
2154
|
-
this.stateCache.setEnabled(this.gl.CULL_FACE, false, () => this.gl.disable(this.gl.CULL_FACE));
|
|
2155
|
-
} else {
|
|
2156
|
-
this.stateCache.setEnabled(this.gl.CULL_FACE, true, () => this.gl.enable(this.gl.CULL_FACE));
|
|
2157
|
-
this.stateCache.cullFace(renderState.cullMode === "front" ? this.gl.FRONT : this.gl.BACK, () => this.gl.cullFace(renderState.cullMode === "front" ? this.gl.FRONT : this.gl.BACK));
|
|
2158
|
-
}
|
|
2159
|
-
if (renderState.blend) {
|
|
2160
|
-
this.stateCache.setEnabled(this.gl.BLEND, true, () => this.gl.enable(this.gl.BLEND));
|
|
2161
|
-
this.stateCache.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA, () => this.gl.blendFunc(this.gl.SRC_ALPHA, this.gl.ONE_MINUS_SRC_ALPHA));
|
|
2162
|
-
} else {
|
|
2163
|
-
this.stateCache.setEnabled(this.gl.BLEND, false, () => this.gl.disable(this.gl.BLEND));
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
private requireBuffer(buffer: RenderBuffer): WebGL2Buffer {
|
|
2168
|
-
if (!(buffer instanceof WebGL2Buffer) || !this.buffers.has(buffer) || buffer.disposed) {
|
|
2169
|
-
throw new RenderDeviceError("Buffer is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
2170
|
-
bufferId: buffer.id
|
|
2171
|
-
});
|
|
2172
|
-
}
|
|
2173
|
-
return buffer;
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
private stencilCompare(compare: NonNullable<DrawCommand["renderState"]>["stencil"] extends infer Stencil ? Stencil extends { readonly compare?: infer Compare } ? NonNullable<Compare> : never : never): GLenum {
|
|
2177
|
-
switch (compare) {
|
|
2178
|
-
case "never": return this.gl.NEVER;
|
|
2179
|
-
case "less": return this.gl.LESS;
|
|
2180
|
-
case "less-equal": return this.gl.LEQUAL;
|
|
2181
|
-
case "greater": return this.gl.GREATER;
|
|
2182
|
-
case "greater-equal": return this.gl.GEQUAL;
|
|
2183
|
-
case "equal": return this.gl.EQUAL;
|
|
2184
|
-
case "not-equal": return this.gl.NOTEQUAL;
|
|
2185
|
-
case "always": return this.gl.ALWAYS;
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
|
-
private stencilOperation(operation: NonNullable<DrawCommand["renderState"]>["stencil"] extends infer Stencil ? Stencil extends { readonly fail?: infer Operation } ? NonNullable<Operation> : never : never): GLenum {
|
|
2190
|
-
switch (operation) {
|
|
2191
|
-
case "keep": return this.gl.KEEP;
|
|
2192
|
-
case "zero": return this.gl.ZERO;
|
|
2193
|
-
case "replace": return this.gl.REPLACE;
|
|
2194
|
-
case "increment": return this.gl.INCR;
|
|
2195
|
-
case "decrement": return this.gl.DECR;
|
|
2196
|
-
case "invert": return this.gl.INVERT;
|
|
2197
|
-
case "increment-wrap": return this.gl.INCR_WRAP;
|
|
2198
|
-
case "decrement-wrap": return this.gl.DECR_WRAP;
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
private requireShader(shader: RenderShaderProgram): WebGL2ShaderProgram {
|
|
2203
|
-
if (!(shader instanceof WebGL2ShaderProgram) || !this.shaders.has(shader) || shader.disposed) {
|
|
2204
|
-
throw new RenderDeviceError("Shader is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
2205
|
-
shaderId: shader.id
|
|
2206
|
-
});
|
|
2207
|
-
}
|
|
2208
|
-
return shader;
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
private assertAlive(): void {
|
|
2212
|
-
if (this.disposed) {
|
|
2213
|
-
throw new RenderDeviceError("Render device is disposed", "DISPOSED_DEVICE");
|
|
2214
|
-
}
|
|
2215
|
-
if (this.contextLost) {
|
|
2216
|
-
throw new RenderDeviceError("Render context is lost", "CONTEXT_LOST");
|
|
2217
|
-
}
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
private assertFrame(): void {
|
|
2221
|
-
this.assertAlive();
|
|
2222
|
-
if (!this.frameActive) {
|
|
2223
|
-
throw new RenderDeviceError("No active frame", "NO_ACTIVE_FRAME");
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
|
|
2227
|
-
private readError(): string | null {
|
|
2228
|
-
const error = this.gl.getError();
|
|
2229
|
-
return error === this.gl.NO_ERROR ? null : `0x${error.toString(16)}`;
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
|
|
2233
|
-
function numberOption(options: Readonly<Record<string, unknown>>, key: string, fallback: number): number {
|
|
2234
|
-
const value = options[key];
|
|
2235
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
2236
|
-
}
|
|
2237
|
-
|
|
2238
|
-
function stringOption(options: Readonly<Record<string, unknown>>, key: string, fallback: string): string {
|
|
2239
|
-
const value = options[key];
|
|
2240
|
-
return typeof value === "string" ? value : fallback;
|
|
2241
|
-
}
|
|
2242
|
-
|
|
2243
|
-
function colorSpaceId(value: string): number {
|
|
2244
|
-
return value === "srgb" ? 1 : 0;
|
|
2245
|
-
}
|
|
2246
|
-
|
|
2247
|
-
function toneMappingOperatorId(value: string): number {
|
|
2248
|
-
if (value === "linear") return 0;
|
|
2249
|
-
if (value === "reinhard") return 1;
|
|
2250
|
-
if (value === "aces") return 2;
|
|
2251
|
-
if (value === "filmic") return 3;
|
|
2252
|
-
if (value === "uncharted2") return 4;
|
|
2253
|
-
if (value === "agx") return 5;
|
|
2254
|
-
return 6;
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
function completeUploadLevels<T extends { readonly width: number; readonly height: number }>(levels: readonly T[]): readonly T[] {
|
|
2258
|
-
const uploadLevels: T[] = [];
|
|
2259
|
-
for (const level of levels) {
|
|
2260
|
-
if (uploadLevels.length === 0) {
|
|
2261
|
-
uploadLevels.push(level);
|
|
2262
|
-
continue;
|
|
2263
|
-
}
|
|
2264
|
-
const previous = uploadLevels[uploadLevels.length - 1]!;
|
|
2265
|
-
const expectedWidth = Math.max(1, Math.floor(previous.width / 2));
|
|
2266
|
-
const expectedHeight = Math.max(1, Math.floor(previous.height / 2));
|
|
2267
|
-
if (level.width !== expectedWidth || level.height !== expectedHeight) {
|
|
2268
|
-
break;
|
|
2269
|
-
}
|
|
2270
|
-
uploadLevels.push(level);
|
|
2271
|
-
}
|
|
2272
|
-
return uploadLevels;
|
|
2273
|
-
}
|
|
2274
|
-
|
|
2275
|
-
function texturePixelUploadData(data: TexturePixelData, format: Texture["format"]): ArrayBufferView {
|
|
2276
|
-
if (format === "rgba16f" && data instanceof Uint16Array) return data;
|
|
2277
|
-
if (format === "rgba32f" && data instanceof Float32Array) return data;
|
|
2278
|
-
if (format === "rgba8" && (data instanceof Uint8Array || data instanceof Uint8ClampedArray)) return data;
|
|
2279
|
-
throw new RenderDeviceError("Texture pixel data type does not match texture format", "UNSUPPORTED_TEXTURE_FORMAT", { format });
|
|
2280
|
-
}
|