@aura3d/rendering 2.0.4 → 3.0.1
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/dist/AtmosphereWetness.d.ts +61 -0
- package/dist/AtmosphereWetness.d.ts.map +1 -0
- package/dist/AtmosphereWetness.js +99 -0
- package/dist/AtmosphereWetness.js.map +1 -0
- package/dist/CascadedShadowMaps.d.ts.map +1 -1
- package/dist/CascadedShadowMaps.js +3 -1
- package/dist/CascadedShadowMaps.js.map +1 -1
- package/dist/ClusteredForwardLighting.d.ts +25 -1
- package/dist/ClusteredForwardLighting.d.ts.map +1 -1
- package/dist/ClusteredForwardLighting.js +80 -3
- package/dist/ClusteredForwardLighting.js.map +1 -1
- package/dist/DayNightSky.d.ts +63 -0
- package/dist/DayNightSky.d.ts.map +1 -0
- package/dist/DayNightSky.js +156 -0
- package/dist/DayNightSky.js.map +1 -0
- package/dist/EnvironmentPresetPack.d.ts +30 -0
- package/dist/EnvironmentPresetPack.d.ts.map +1 -0
- package/dist/EnvironmentPresetPack.js +49 -0
- package/dist/EnvironmentPresetPack.js.map +1 -0
- package/dist/ExtensionScalarAtlas.d.ts +18 -0
- package/dist/ExtensionScalarAtlas.d.ts.map +1 -0
- package/dist/ExtensionScalarAtlas.js +107 -0
- package/dist/ExtensionScalarAtlas.js.map +1 -0
- package/dist/ForwardPass.d.ts +65 -0
- package/dist/ForwardPass.d.ts.map +1 -1
- package/dist/ForwardPass.js +196 -2
- package/dist/ForwardPass.js.map +1 -1
- package/dist/InstancedPBRMaterial.d.ts +1 -0
- package/dist/InstancedPBRMaterial.d.ts.map +1 -1
- package/dist/InstancedPBRMaterial.js +3 -0
- package/dist/InstancedPBRMaterial.js.map +1 -1
- package/dist/InstancingDiagnostics.d.ts +69 -0
- package/dist/InstancingDiagnostics.d.ts.map +1 -0
- package/dist/InstancingDiagnostics.js +98 -0
- package/dist/InstancingDiagnostics.js.map +1 -0
- package/dist/LeanWebGL2Device.d.ts +265 -0
- package/dist/LeanWebGL2Device.d.ts.map +1 -0
- package/dist/LeanWebGL2Device.js +4001 -0
- package/dist/LeanWebGL2Device.js.map +1 -0
- package/dist/LightingRig.d.ts +19 -1
- package/dist/LightingRig.d.ts.map +1 -1
- package/dist/LightingRig.js +61 -1
- package/dist/LightingRig.js.map +1 -1
- package/dist/MaterialBinding.js +1 -1
- package/dist/MaterialBinding.js.map +1 -1
- package/dist/MeshConsolidation.d.ts +12 -0
- package/dist/MeshConsolidation.d.ts.map +1 -1
- package/dist/MeshConsolidation.js +28 -0
- package/dist/MeshConsolidation.js.map +1 -1
- package/dist/MorphTargetPlan.d.ts +29 -2
- package/dist/MorphTargetPlan.d.ts.map +1 -1
- package/dist/MorphTargetPlan.js +23 -4
- package/dist/MorphTargetPlan.js.map +1 -1
- package/dist/NormalMappedPBRMaterial.d.ts.map +1 -1
- package/dist/NormalMappedPBRMaterial.js +22 -2
- package/dist/NormalMappedPBRMaterial.js.map +1 -1
- package/dist/OceanSurface.d.ts +75 -0
- package/dist/OceanSurface.d.ts.map +1 -1
- package/dist/OceanSurface.js +211 -0
- package/dist/OceanSurface.js.map +1 -1
- package/dist/PBRMaterial.d.ts +1 -0
- package/dist/PBRMaterial.d.ts.map +1 -1
- package/dist/PBRMaterial.js +23 -2
- package/dist/PBRMaterial.js.map +1 -1
- package/dist/PbrReference.d.ts +48 -0
- package/dist/PbrReference.d.ts.map +1 -1
- package/dist/PbrReference.js +126 -0
- package/dist/PbrReference.js.map +1 -1
- package/dist/PlanarReflection.d.ts +221 -0
- package/dist/PlanarReflection.d.ts.map +1 -0
- package/dist/PlanarReflection.js +541 -0
- package/dist/PlanarReflection.js.map +1 -0
- package/dist/PostProcessPass.d.ts +32 -0
- package/dist/PostProcessPass.d.ts.map +1 -1
- package/dist/PostProcessPass.js +79 -36
- package/dist/PostProcessPass.js.map +1 -1
- package/dist/ProjectionMath.d.ts +3 -0
- package/dist/ProjectionMath.d.ts.map +1 -0
- package/dist/ProjectionMath.js +26 -0
- package/dist/ProjectionMath.js.map +1 -0
- package/dist/ReflectionSurfaces.d.ts +17 -0
- package/dist/ReflectionSurfaces.d.ts.map +1 -1
- package/dist/ReflectionSurfaces.js +103 -1
- package/dist/ReflectionSurfaces.js.map +1 -1
- package/dist/RenderDevice.d.ts +92 -0
- package/dist/RenderDevice.d.ts.map +1 -1
- package/dist/RenderDevice.js +85 -0
- package/dist/RenderDevice.js.map +1 -1
- package/dist/Renderer.d.ts +9 -0
- package/dist/Renderer.d.ts.map +1 -1
- package/dist/Renderer.js +97 -9
- package/dist/Renderer.js.map +1 -1
- package/dist/RendererPostprocessPlan.d.ts +64 -3
- package/dist/RendererPostprocessPlan.d.ts.map +1 -1
- package/dist/RendererPostprocessPlan.js +116 -12
- package/dist/RendererPostprocessPlan.js.map +1 -1
- package/dist/RendererVisualPipelineReport.d.ts.map +1 -1
- package/dist/RendererVisualPipelineReport.js +3 -1
- package/dist/RendererVisualPipelineReport.js.map +1 -1
- package/dist/Sampler.d.ts +29 -0
- package/dist/Sampler.d.ts.map +1 -1
- package/dist/Sampler.js +47 -0
- package/dist/Sampler.js.map +1 -1
- package/dist/ScreenSpaceReflectionPass.d.ts +43 -0
- package/dist/ScreenSpaceReflectionPass.d.ts.map +1 -0
- package/dist/ScreenSpaceReflectionPass.js +58 -0
- package/dist/ScreenSpaceReflectionPass.js.map +1 -0
- package/dist/SdfText.d.ts +250 -0
- package/dist/SdfText.d.ts.map +1 -0
- package/dist/SdfText.js +551 -0
- package/dist/SdfText.js.map +1 -0
- package/dist/ShaderChunks.d.ts.map +1 -1
- package/dist/ShaderChunks.js +30 -12
- package/dist/ShaderChunks.js.map +1 -1
- package/dist/ShaderLibrary.d.ts.map +1 -1
- package/dist/ShaderLibrary.js +601 -186
- package/dist/ShaderLibrary.js.map +1 -1
- package/dist/ShaderLibraryCore.d.ts +3 -0
- package/dist/ShaderLibraryCore.d.ts.map +1 -1
- package/dist/ShaderLibraryCore.js +73 -12
- package/dist/ShaderLibraryCore.js.map +1 -1
- package/dist/ShadowMap.d.ts +24 -0
- package/dist/ShadowMap.d.ts.map +1 -1
- package/dist/ShadowMap.js +43 -0
- package/dist/ShadowMap.js.map +1 -1
- package/dist/SkinnedLitMaterial.d.ts.map +1 -1
- package/dist/SkinnedLitMaterial.js +2 -0
- package/dist/SkinnedLitMaterial.js.map +1 -1
- package/dist/SkinningBounds.d.ts +10 -0
- package/dist/SkinningBounds.d.ts.map +1 -1
- package/dist/SkinningBounds.js +27 -0
- package/dist/SkinningBounds.js.map +1 -1
- package/dist/SpriteFlipbook.d.ts +60 -0
- package/dist/SpriteFlipbook.d.ts.map +1 -0
- package/dist/SpriteFlipbook.js +128 -0
- package/dist/SpriteFlipbook.js.map +1 -0
- package/dist/TemporalHistory.d.ts +37 -0
- package/dist/TemporalHistory.d.ts.map +1 -0
- package/dist/TemporalHistory.js +135 -0
- package/dist/TemporalHistory.js.map +1 -0
- package/dist/TemporalMath.d.ts +3 -0
- package/dist/TemporalMath.d.ts.map +1 -0
- package/dist/TemporalMath.js +9 -0
- package/dist/TemporalMath.js.map +1 -0
- package/dist/TerrainTiles.d.ts +120 -0
- package/dist/TerrainTiles.d.ts.map +1 -0
- package/dist/TerrainTiles.js +221 -0
- package/dist/TerrainTiles.js.map +1 -0
- package/dist/TexturedPBRMaterial.d.ts +4 -0
- package/dist/TexturedPBRMaterial.d.ts.map +1 -1
- package/dist/TexturedPBRMaterial.js +88 -5
- package/dist/TexturedPBRMaterial.js.map +1 -1
- package/dist/TexturedUnlitMaterial.d.ts.map +1 -1
- package/dist/TexturedUnlitMaterial.js +20 -2
- package/dist/TexturedUnlitMaterial.js.map +1 -1
- package/dist/UnlitMaterial.d.ts.map +1 -1
- package/dist/UnlitMaterial.js +20 -2
- package/dist/UnlitMaterial.js.map +1 -1
- package/dist/VertexFormat.d.ts +6 -0
- package/dist/VertexFormat.d.ts.map +1 -1
- package/dist/VertexFormat.js +12 -0
- package/dist/VertexFormat.js.map +1 -1
- package/dist/VolumetricFog.d.ts +55 -0
- package/dist/VolumetricFog.d.ts.map +1 -0
- package/dist/VolumetricFog.js +91 -0
- package/dist/VolumetricFog.js.map +1 -0
- package/dist/WaterSurface.d.ts +77 -0
- package/dist/WaterSurface.d.ts.map +1 -0
- package/dist/WaterSurface.js +133 -0
- package/dist/WaterSurface.js.map +1 -0
- package/dist/WebGL2Device.d.ts +73 -0
- package/dist/WebGL2Device.d.ts.map +1 -1
- package/dist/WebGL2Device.js +836 -79
- package/dist/WebGL2Device.js.map +1 -1
- package/dist/WebGPUDevice.d.ts +62 -1
- package/dist/WebGPUDevice.d.ts.map +1 -1
- package/dist/WebGPUDevice.js +844 -51
- package/dist/WebGPUDevice.js.map +1 -1
- package/dist/WebGPUExtensionAtlas.d.ts +6 -0
- package/dist/WebGPUExtensionAtlas.d.ts.map +1 -0
- package/dist/WebGPUExtensionAtlas.js +225 -0
- package/dist/WebGPUExtensionAtlas.js.map +1 -0
- package/dist/WebGPUSkinningLimits.d.ts +40 -0
- package/dist/WebGPUSkinningLimits.d.ts.map +1 -1
- package/dist/WebGPUSkinningLimits.js +32 -0
- package/dist/WebGPUSkinningLimits.js.map +1 -1
- package/dist/effects/CollisionModule.d.ts +9 -0
- package/dist/effects/CollisionModule.d.ts.map +1 -1
- package/dist/effects/CollisionModule.js +10 -0
- package/dist/effects/CollisionModule.js.map +1 -1
- package/dist/effects/ColorModule.d.ts +7 -0
- package/dist/effects/ColorModule.d.ts.map +1 -1
- package/dist/effects/ColorModule.js +23 -0
- package/dist/effects/ColorModule.js.map +1 -1
- package/dist/effects/ForceModule.d.ts +35 -0
- package/dist/effects/ForceModule.d.ts.map +1 -1
- package/dist/effects/ForceModule.js +49 -0
- package/dist/effects/ForceModule.js.map +1 -1
- package/dist/effects/GPUParticleBackend.d.ts +188 -0
- package/dist/effects/GPUParticleBackend.d.ts.map +1 -1
- package/dist/effects/GPUParticleBackend.js +710 -1
- package/dist/effects/GPUParticleBackend.js.map +1 -1
- package/dist/effects/HeightfieldModule.d.ts +58 -0
- package/dist/effects/HeightfieldModule.d.ts.map +1 -0
- package/dist/effects/HeightfieldModule.js +129 -0
- package/dist/effects/HeightfieldModule.js.map +1 -0
- package/dist/effects/LightingModule.d.ts +34 -0
- package/dist/effects/LightingModule.d.ts.map +1 -0
- package/dist/effects/LightingModule.js +54 -0
- package/dist/effects/LightingModule.js.map +1 -0
- package/dist/effects/ParticleDiagnostics.d.ts +8 -0
- package/dist/effects/ParticleDiagnostics.d.ts.map +1 -1
- package/dist/effects/ParticleDiagnostics.js +3 -0
- package/dist/effects/ParticleDiagnostics.js.map +1 -1
- package/dist/effects/ParticleModule.d.ts +8 -0
- package/dist/effects/ParticleModule.d.ts.map +1 -1
- package/dist/effects/ParticleModule.js.map +1 -1
- package/dist/effects/ParticleRenderer.d.ts +20 -0
- package/dist/effects/ParticleRenderer.d.ts.map +1 -1
- package/dist/effects/ParticleRenderer.js +57 -7
- package/dist/effects/ParticleRenderer.js.map +1 -1
- package/dist/effects/ParticleSystem.d.ts +45 -1
- package/dist/effects/ParticleSystem.d.ts.map +1 -1
- package/dist/effects/ParticleSystem.js +373 -4
- package/dist/effects/ParticleSystem.js.map +1 -1
- package/dist/effects/ResidentGPUParticleRenderer.d.ts +158 -0
- package/dist/effects/ResidentGPUParticleRenderer.d.ts.map +1 -0
- package/dist/effects/ResidentGPUParticleRenderer.js +576 -0
- package/dist/effects/ResidentGPUParticleRenderer.js.map +1 -0
- package/dist/effects/SizeModule.d.ts +4 -0
- package/dist/effects/SizeModule.d.ts.map +1 -1
- package/dist/effects/SizeModule.js +16 -0
- package/dist/effects/SizeModule.js.map +1 -1
- package/dist/effects/SubEmitterModule.d.ts +61 -0
- package/dist/effects/SubEmitterModule.d.ts.map +1 -0
- package/dist/effects/SubEmitterModule.js +119 -0
- package/dist/effects/SubEmitterModule.js.map +1 -0
- package/dist/effects/TrailModule.d.ts +38 -0
- package/dist/effects/TrailModule.d.ts.map +1 -1
- package/dist/effects/TrailModule.js +95 -2
- package/dist/effects/TrailModule.js.map +1 -1
- package/dist/effects/TurbulenceModule.d.ts +43 -0
- package/dist/effects/TurbulenceModule.d.ts.map +1 -0
- package/dist/effects/TurbulenceModule.js +130 -0
- package/dist/effects/TurbulenceModule.js.map +1 -0
- package/dist/effects/index.d.ts +4 -0
- package/dist/effects/index.d.ts.map +1 -1
- package/dist/effects/index.js +4 -0
- package/dist/effects/index.js.map +1 -1
- package/dist/extension-scalar-atlas.d.ts +3 -0
- package/dist/extension-scalar-atlas.d.ts.map +1 -0
- package/dist/extension-scalar-atlas.js +2 -0
- package/dist/extension-scalar-atlas.js.map +1 -0
- package/dist/index.d.ts +58 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -10
- package/dist/index.js.map +1 -1
- package/dist/lean/LeanProductionRenderer.d.ts.map +1 -1
- package/dist/lean/LeanProductionRenderer.js +2 -2
- package/dist/lean/LeanProductionRenderer.js.map +1 -1
- package/dist/lean-core-runtime.d.ts +13 -0
- package/dist/lean-core-runtime.d.ts.map +1 -0
- package/dist/lean-core-runtime.js +10 -0
- package/dist/lean-core-runtime.js.map +1 -0
- package/dist/materials/MaterialExtensions.js +3 -3
- package/dist/materials/MaterialExtensions.js.map +1 -1
- package/dist/postprocess/NativeBloomPyramid.d.ts +44 -0
- package/dist/postprocess/NativeBloomPyramid.d.ts.map +1 -0
- package/dist/postprocess/NativeBloomPyramid.js +84 -0
- package/dist/postprocess/NativeBloomPyramid.js.map +1 -0
- package/dist/postprocess/NativeLdrEffectLuts.d.ts +10 -1
- package/dist/postprocess/NativeLdrEffectLuts.d.ts.map +1 -1
- package/dist/postprocess/NativeLdrEffectLuts.js +19 -2
- package/dist/postprocess/NativeLdrEffectLuts.js.map +1 -1
- package/dist/postprocess/index.d.ts +2 -0
- package/dist/postprocess/index.d.ts.map +1 -1
- package/dist/postprocess/index.js +1 -0
- package/dist/postprocess/index.js.map +1 -1
- package/dist/production-runtime/PBRHDRPipeline.d.ts +22 -0
- package/dist/production-runtime/PBRHDRPipeline.d.ts.map +1 -1
- package/dist/production-runtime/PBRHDRPipeline.js +125 -0
- package/dist/production-runtime/PBRHDRPipeline.js.map +1 -1
- package/dist/production-runtime/ProductionRendererTypes.d.ts +1 -0
- package/dist/production-runtime/ProductionRendererTypes.d.ts.map +1 -1
- package/dist/production-runtime/ProductionRendererTypes.js.map +1 -1
- package/dist/production-runtime/ProductionRuntimeRenderer.d.ts +3 -0
- package/dist/production-runtime/ProductionRuntimeRenderer.d.ts.map +1 -1
- package/dist/production-runtime/ProductionRuntimeRenderer.js +8 -1
- package/dist/production-runtime/ProductionRuntimeRenderer.js.map +1 -1
- package/dist/production-runtime/ProductionWebGL2Renderer.d.ts +4 -0
- package/dist/production-runtime/ProductionWebGL2Renderer.d.ts.map +1 -1
- package/dist/production-runtime/ProductionWebGL2Renderer.js +15 -0
- package/dist/production-runtime/ProductionWebGL2Renderer.js.map +1 -1
- package/dist/production-runtime/ProductionWebGPURenderer.d.ts +58 -0
- package/dist/production-runtime/ProductionWebGPURenderer.d.ts.map +1 -1
- package/dist/production-runtime/ProductionWebGPURenderer.js +127 -0
- package/dist/production-runtime/ProductionWebGPURenderer.js.map +1 -1
- package/dist/production-runtime/framegraph/FrameGraph.d.ts +4 -2
- package/dist/production-runtime/framegraph/FrameGraph.d.ts.map +1 -1
- package/dist/production-runtime/framegraph/FrameGraph.js +62 -4
- package/dist/production-runtime/framegraph/FrameGraph.js.map +1 -1
- package/dist/production-runtime/framegraph/NativeFrameGraphBindings.d.ts +32 -0
- package/dist/production-runtime/framegraph/NativeFrameGraphBindings.d.ts.map +1 -0
- package/dist/production-runtime/framegraph/NativeFrameGraphBindings.js +111 -0
- package/dist/production-runtime/framegraph/NativeFrameGraphBindings.js.map +1 -0
- package/dist/production-runtime/framegraph/RenderPass.d.ts +23 -0
- package/dist/production-runtime/framegraph/RenderPass.d.ts.map +1 -1
- package/dist/production-runtime/framegraph/RenderPass.js +87 -1
- package/dist/production-runtime/framegraph/RenderPass.js.map +1 -1
- package/dist/production-runtime/index.d.ts +7 -4
- package/dist/production-runtime/index.d.ts.map +1 -1
- package/dist/production-runtime/index.js +4 -2
- package/dist/production-runtime/index.js.map +1 -1
- package/dist/production-runtime/passes/DepthPrepass.d.ts +18 -1
- package/dist/production-runtime/passes/DepthPrepass.d.ts.map +1 -1
- package/dist/production-runtime/passes/DepthPrepass.js +41 -2
- package/dist/production-runtime/passes/DepthPrepass.js.map +1 -1
- package/dist/production-runtime/passes/FramegraphTopology.d.ts +13 -0
- package/dist/production-runtime/passes/FramegraphTopology.d.ts.map +1 -0
- package/dist/production-runtime/passes/FramegraphTopology.js +59 -0
- package/dist/production-runtime/passes/FramegraphTopology.js.map +1 -0
- package/dist/production-runtime/passes/OpaquePass.d.ts +19 -1
- package/dist/production-runtime/passes/OpaquePass.d.ts.map +1 -1
- package/dist/production-runtime/passes/OpaquePass.js +43 -2
- package/dist/production-runtime/passes/OpaquePass.js.map +1 -1
- package/dist/production-runtime/passes/ShadowPass.d.ts +19 -1
- package/dist/production-runtime/passes/ShadowPass.d.ts.map +1 -1
- package/dist/production-runtime/passes/ShadowPass.js +45 -2
- package/dist/production-runtime/passes/ShadowPass.js.map +1 -1
- package/dist/production-runtime/passes/SkyboxPass.d.ts +17 -1
- package/dist/production-runtime/passes/SkyboxPass.d.ts.map +1 -1
- package/dist/production-runtime/passes/SkyboxPass.js +41 -2
- package/dist/production-runtime/passes/SkyboxPass.js.map +1 -1
- package/dist/production-runtime/passes/ToneMappingPass.d.ts +26 -1
- package/dist/production-runtime/passes/ToneMappingPass.d.ts.map +1 -1
- package/dist/production-runtime/passes/ToneMappingPass.js +62 -2
- package/dist/production-runtime/passes/ToneMappingPass.js.map +1 -1
- package/dist/production-runtime/passes/TransparentPass.d.ts +19 -1
- package/dist/production-runtime/passes/TransparentPass.d.ts.map +1 -1
- package/dist/production-runtime/passes/TransparentPass.js +47 -2
- package/dist/production-runtime/passes/TransparentPass.js.map +1 -1
- package/dist/reflection-surfaces.d.ts +5 -0
- package/dist/reflection-surfaces.d.ts.map +1 -0
- package/dist/reflection-surfaces.js +3 -0
- package/dist/reflection-surfaces.js.map +1 -0
- package/dist/shadows/CascadeHysteresis.d.ts +67 -0
- package/dist/shadows/CascadeHysteresis.d.ts.map +1 -0
- package/dist/shadows/CascadeHysteresis.js +104 -0
- package/dist/shadows/CascadeHysteresis.js.map +1 -0
- package/dist/shadows/ContactShadows.d.ts +52 -0
- package/dist/shadows/ContactShadows.d.ts.map +1 -1
- package/dist/shadows/ContactShadows.js +122 -0
- package/dist/shadows/ContactShadows.js.map +1 -1
- package/dist/shadows/SpotShadowMaps.d.ts +77 -0
- package/dist/shadows/SpotShadowMaps.d.ts.map +1 -0
- package/dist/shadows/SpotShadowMaps.js +106 -0
- package/dist/shadows/SpotShadowMaps.js.map +1 -0
- package/dist/webgpu/WebGPUPostProcess.d.ts +2 -0
- package/dist/webgpu/WebGPUPostProcess.d.ts.map +1 -1
- package/dist/webgpu/WebGPUPostProcess.js +1 -0
- package/dist/webgpu/WebGPUPostProcess.js.map +1 -1
- package/dist/webgpu/WebGPUPostShaders.d.ts +77 -0
- package/dist/webgpu/WebGPUPostShaders.d.ts.map +1 -0
- package/dist/webgpu/WebGPUPostShaders.js +261 -0
- package/dist/webgpu/WebGPUPostShaders.js.map +1 -0
- package/dist/webgpu/WebGPUTemporal.d.ts +17 -0
- package/dist/webgpu/WebGPUTemporal.d.ts.map +1 -0
- package/dist/webgpu/WebGPUTemporal.js +64 -0
- package/dist/webgpu/WebGPUTemporal.js.map +1 -0
- package/dist/webgpu.d.ts +6 -0
- package/dist/webgpu.d.ts.map +1 -1
- package/dist/webgpu.js +4 -0
- package/dist/webgpu.js.map +1 -1
- package/package.json +15 -3
|
@@ -0,0 +1,4001 @@
|
|
|
1
|
+
import { RenderDeviceError, resolveGpuTargetOwner, spreadGpuTargetInventory, } from "./RenderDevice.js";
|
|
2
|
+
import { Texture, isCompressedTextureFormat, isFloatColorTextureFormat } from "./Texture.js";
|
|
3
|
+
import { isTextureBinding, TextureBinding } from "./TextureBinding.js";
|
|
4
|
+
import {} from "./VertexFormat.js";
|
|
5
|
+
import { WebGL2StateCache } from "./WebGL2StateCache.js";
|
|
6
|
+
import { BLOOM_BRIGHT_LUT_HEIGHT, BLOOM_BRIGHT_LUT_WIDTH, BLOOM_COMPOSITE_LUT_SIZE, OUTLINE_BLEND_LUT_WIDTH, OUTLINE_LIMB_RADIX, createOutlineBlendLut, createOutlineGradientBound, createBloomBrightThresholdLut, createBloomCompositeLut } from "./postprocess/NativeLdrEffectLuts.js";
|
|
7
|
+
import { normalizeBloomQualityPreset, resolveBloomPyramidBlurRadii, resolveBloomPyramidPlan, resolveBloomPyramidResponseGain, } from "./postprocess/NativeBloomPyramid.js";
|
|
8
|
+
const WEBGL_CUBE_FACES = ["px", "nx", "py", "ny", "pz", "nz"];
|
|
9
|
+
class WebGL2Buffer {
|
|
10
|
+
id;
|
|
11
|
+
usage;
|
|
12
|
+
byteLength;
|
|
13
|
+
target;
|
|
14
|
+
handle;
|
|
15
|
+
gl;
|
|
16
|
+
disposed = false;
|
|
17
|
+
constructor(id, usage, byteLength, target, handle, gl) {
|
|
18
|
+
this.id = id;
|
|
19
|
+
this.usage = usage;
|
|
20
|
+
this.byteLength = byteLength;
|
|
21
|
+
this.target = target;
|
|
22
|
+
this.handle = handle;
|
|
23
|
+
this.gl = gl;
|
|
24
|
+
}
|
|
25
|
+
dispose() {
|
|
26
|
+
if (!this.disposed) {
|
|
27
|
+
this.gl.deleteBuffer(this.handle);
|
|
28
|
+
this.disposed = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
class WebGL2ShaderProgram {
|
|
33
|
+
id;
|
|
34
|
+
label;
|
|
35
|
+
marker;
|
|
36
|
+
reflection;
|
|
37
|
+
handle;
|
|
38
|
+
gl;
|
|
39
|
+
disposed = false;
|
|
40
|
+
constructor(id, label, marker, reflection, handle, gl) {
|
|
41
|
+
this.id = id;
|
|
42
|
+
this.label = label;
|
|
43
|
+
this.marker = marker;
|
|
44
|
+
this.reflection = reflection;
|
|
45
|
+
this.handle = handle;
|
|
46
|
+
this.gl = gl;
|
|
47
|
+
}
|
|
48
|
+
dispose() {
|
|
49
|
+
if (!this.disposed) {
|
|
50
|
+
this.gl.deleteProgram(this.handle);
|
|
51
|
+
this.disposed = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
class WebGL2RenderTarget {
|
|
56
|
+
id;
|
|
57
|
+
width;
|
|
58
|
+
height;
|
|
59
|
+
label;
|
|
60
|
+
colorTexture;
|
|
61
|
+
depthTexture;
|
|
62
|
+
framebuffer;
|
|
63
|
+
colorHandle;
|
|
64
|
+
depthHandle;
|
|
65
|
+
depthTextureHandle;
|
|
66
|
+
sampleCount;
|
|
67
|
+
drawFramebuffer;
|
|
68
|
+
multisampleColorHandle;
|
|
69
|
+
gl;
|
|
70
|
+
disposed = false;
|
|
71
|
+
needsResolve = false;
|
|
72
|
+
constructor(id, width, height, label, colorTexture, depthTexture, framebuffer, colorHandle, depthHandle, depthTextureHandle, sampleCount, drawFramebuffer, multisampleColorHandle, gl) {
|
|
73
|
+
this.id = id;
|
|
74
|
+
this.width = width;
|
|
75
|
+
this.height = height;
|
|
76
|
+
this.label = label;
|
|
77
|
+
this.colorTexture = colorTexture;
|
|
78
|
+
this.depthTexture = depthTexture;
|
|
79
|
+
this.framebuffer = framebuffer;
|
|
80
|
+
this.colorHandle = colorHandle;
|
|
81
|
+
this.depthHandle = depthHandle;
|
|
82
|
+
this.depthTextureHandle = depthTextureHandle;
|
|
83
|
+
this.sampleCount = sampleCount;
|
|
84
|
+
this.drawFramebuffer = drawFramebuffer;
|
|
85
|
+
this.multisampleColorHandle = multisampleColorHandle;
|
|
86
|
+
this.gl = gl;
|
|
87
|
+
}
|
|
88
|
+
dispose() {
|
|
89
|
+
if (!this.disposed) {
|
|
90
|
+
this.gl.deleteFramebuffer(this.framebuffer);
|
|
91
|
+
if (this.drawFramebuffer !== this.framebuffer)
|
|
92
|
+
this.gl.deleteFramebuffer(this.drawFramebuffer);
|
|
93
|
+
this.gl.deleteTexture(this.colorHandle);
|
|
94
|
+
if (this.depthHandle)
|
|
95
|
+
this.gl.deleteRenderbuffer(this.depthHandle);
|
|
96
|
+
if (this.multisampleColorHandle)
|
|
97
|
+
this.gl.deleteRenderbuffer(this.multisampleColorHandle);
|
|
98
|
+
if (this.depthTextureHandle)
|
|
99
|
+
this.gl.deleteTexture(this.depthTextureHandle);
|
|
100
|
+
this.colorTexture.dispose();
|
|
101
|
+
this.depthTexture?.dispose();
|
|
102
|
+
this.disposed = true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export class LeanWebGL2Device {
|
|
107
|
+
errorCheckMode;
|
|
108
|
+
kind = "webgl2";
|
|
109
|
+
info;
|
|
110
|
+
disposed = false;
|
|
111
|
+
contextLost = false;
|
|
112
|
+
gl;
|
|
113
|
+
nextId = 1;
|
|
114
|
+
drawCalls = 0;
|
|
115
|
+
buffers = new Set();
|
|
116
|
+
shaders = new Set();
|
|
117
|
+
renderTargets = new Set();
|
|
118
|
+
activeRenderTarget = null;
|
|
119
|
+
textures = new Map();
|
|
120
|
+
textureUploadModes = new Map();
|
|
121
|
+
fallbackTexture = null;
|
|
122
|
+
fallbackCubeTexture = null;
|
|
123
|
+
samplerObjectCache = new Map();
|
|
124
|
+
presentationProgram = null;
|
|
125
|
+
ldrPostprocessProgram = null;
|
|
126
|
+
bloomBrightExtractProgram = null;
|
|
127
|
+
bloomBlurProgram = null;
|
|
128
|
+
bloomCompositeProgram = null;
|
|
129
|
+
bloomDownsampleProgram = null;
|
|
130
|
+
bloomAccumulateProgram = null;
|
|
131
|
+
bloomPyramidResources = null;
|
|
132
|
+
outlineProgram = null;
|
|
133
|
+
ssaoProgram = null;
|
|
134
|
+
ssrProgram = null;
|
|
135
|
+
depthOfFieldProgram = null;
|
|
136
|
+
motionBlurProgram = null;
|
|
137
|
+
motionBlurVelocityTexture = null;
|
|
138
|
+
motionBlurVelocitySize = null;
|
|
139
|
+
taaProgram = null;
|
|
140
|
+
taaHistoryTexture = null;
|
|
141
|
+
taaHistorySize = null;
|
|
142
|
+
bloomPingPongResources = null;
|
|
143
|
+
bloomBrightLutTexture = null;
|
|
144
|
+
bloomBrightLutThreshold = null;
|
|
145
|
+
bloomCompositeLutTexture = null;
|
|
146
|
+
bloomCompositeLutIntensity = null;
|
|
147
|
+
bloomCompositeLutShoulder = null;
|
|
148
|
+
lastBloomDiagnostics = null;
|
|
149
|
+
outlineBlendLutTexture = null;
|
|
150
|
+
outlineBlendLutKey = null;
|
|
151
|
+
depthReadbackProgram = null;
|
|
152
|
+
uniformLocationCache = new WeakMap();
|
|
153
|
+
textureSamplerParameterCache = new WeakMap();
|
|
154
|
+
stateCache = new WebGL2StateCache({ label: "webgl2-device-state-cache" });
|
|
155
|
+
textureUnitBindings = new Map();
|
|
156
|
+
vertexArrayCache = new Map();
|
|
157
|
+
vertexFormatIds = new WeakMap();
|
|
158
|
+
presentationVertexArray = null;
|
|
159
|
+
activeTextureUnitIndex = -1;
|
|
160
|
+
shaderProgramCreateCount = 0;
|
|
161
|
+
uniformLocationLookupCount = 0;
|
|
162
|
+
bufferUpdateCount = 0;
|
|
163
|
+
textureBindCount = 0;
|
|
164
|
+
samplerParameterUploadCount = 0;
|
|
165
|
+
samplerAnisotropyUploadCount = 0;
|
|
166
|
+
vertexArrayCreateCount = 0;
|
|
167
|
+
nativeInstancedSubmissions = 0;
|
|
168
|
+
nextVertexFormatId = 1;
|
|
169
|
+
releasedTextureHandles = 0;
|
|
170
|
+
nativeEnvironmentBindings = 0;
|
|
171
|
+
nativeShadowMapBindings = 0;
|
|
172
|
+
shadowRenderTargetsAllocated = 0;
|
|
173
|
+
lastError = null;
|
|
174
|
+
frameActive = false;
|
|
175
|
+
viewportWidth = 0;
|
|
176
|
+
viewportHeight = 0;
|
|
177
|
+
maxVertexAttributes;
|
|
178
|
+
anisotropicFilteringExtension;
|
|
179
|
+
maxTextureAnisotropy;
|
|
180
|
+
/**
|
|
181
|
+
* WS-2.6 — subscribers for context loss and restoration.
|
|
182
|
+
*
|
|
183
|
+
* The device has tracked `contextLost` since before 1.6 and acted on it internally, but nothing
|
|
184
|
+
* could *observe* it: there was no callback, so a consumer could only poll `diagnostics()`. That is
|
|
185
|
+
* why the parity table listed context-loss recovery as a gap while the listeners existed — the
|
|
186
|
+
* device layer was never the gap, the absence of a public signal was.
|
|
187
|
+
*/
|
|
188
|
+
deviceLostListeners = new Set();
|
|
189
|
+
deviceRestoredListeners = new Set();
|
|
190
|
+
contextLostListener;
|
|
191
|
+
contextRestoredListener;
|
|
192
|
+
canvas;
|
|
193
|
+
static create(options) {
|
|
194
|
+
const gl = options.canvas.getContext("webgl2", {
|
|
195
|
+
antialias: options.antialias ?? true,
|
|
196
|
+
alpha: options.alpha ?? false,
|
|
197
|
+
preserveDrawingBuffer: options.preserveDrawingBuffer ?? false
|
|
198
|
+
});
|
|
199
|
+
if (!gl) {
|
|
200
|
+
throw new RenderDeviceError("WebGL2 is not available for the provided canvas", "WEBGL2_UNAVAILABLE");
|
|
201
|
+
}
|
|
202
|
+
// Default to frame-level error checking.
|
|
203
|
+
//
|
|
204
|
+
// `strict` calls `gl.getError()` after every uniform upload, vertex-format
|
|
205
|
+
// bind, and draw. `gl.getError()` forces a synchronous CPU/GPU sync, so on a
|
|
206
|
+
// scene with a few dozen draws and many uniforms per draw it dominates frame
|
|
207
|
+
// time: profiling Aura Clash attributed ~93% of frame time to `getError`,
|
|
208
|
+
// holding the route at ~11 FPS on an Apple M4 Max. Frame-level checking still
|
|
209
|
+
// reports real WebGL errors (read once in `endFrame` and surfaced through
|
|
210
|
+
// `lastError`); callers that need per-operation attribution can still opt into
|
|
211
|
+
// `strict` explicitly.
|
|
212
|
+
return new LeanWebGL2Device(gl, options.canvas, options.errorCheckMode ?? "frame");
|
|
213
|
+
}
|
|
214
|
+
constructor(gl, canvas, errorCheckMode) {
|
|
215
|
+
this.errorCheckMode = errorCheckMode;
|
|
216
|
+
this.gl = gl;
|
|
217
|
+
// A browser returns the same WebGL2 context object after `webglcontextrestored`.
|
|
218
|
+
// The controller that owned it before the loss is disposed before a replacement
|
|
219
|
+
// device mounts, and Chromium can leave an INVALID_OPERATION from that stale
|
|
220
|
+
// resource teardown in the context's error queue. A fresh device must establish
|
|
221
|
+
// its own error boundary; otherwise its first valid uniform upload or presentation
|
|
222
|
+
// is blamed for an error produced by the previous owner.
|
|
223
|
+
for (let index = 0; index < 16 && gl.getError() !== gl.NO_ERROR; index += 1) {
|
|
224
|
+
// Drain only errors that predate this device. Normal frame/strict checks still
|
|
225
|
+
// report every error generated after construction.
|
|
226
|
+
}
|
|
227
|
+
this.maxVertexAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
|
|
228
|
+
this.anisotropicFilteringExtension = gl.getExtension("EXT_texture_filter_anisotropic");
|
|
229
|
+
this.maxTextureAnisotropy = this.anisotropicFilteringExtension
|
|
230
|
+
? Math.max(1, gl.getParameter(this.anisotropicFilteringExtension.MAX_TEXTURE_MAX_ANISOTROPY_EXT))
|
|
231
|
+
: 1;
|
|
232
|
+
this.info = {
|
|
233
|
+
backend: "webgl2",
|
|
234
|
+
vendor: gl.getParameter(gl.VENDOR),
|
|
235
|
+
renderer: gl.getParameter(gl.RENDERER),
|
|
236
|
+
capabilities: [
|
|
237
|
+
"buffers",
|
|
238
|
+
"buffer-readback",
|
|
239
|
+
"shader-validation",
|
|
240
|
+
"render-targets",
|
|
241
|
+
"pixel-readback",
|
|
242
|
+
"postprocess-presentation",
|
|
243
|
+
"draw-validation",
|
|
244
|
+
"rasterization",
|
|
245
|
+
"depth-render-targets",
|
|
246
|
+
"depth-textures",
|
|
247
|
+
"spot-shadow-maps",
|
|
248
|
+
"point-shadow-maps",
|
|
249
|
+
"hdr-image-based-lighting",
|
|
250
|
+
...(this.anisotropicFilteringExtension ? ["anisotropic-texture-filtering"] : []),
|
|
251
|
+
...(gl.getExtension("EXT_color_buffer_float") ? ["hdr-render-targets", "float-readback"] : [])
|
|
252
|
+
]
|
|
253
|
+
};
|
|
254
|
+
this.canvas = canvas;
|
|
255
|
+
if ("addEventListener" in canvas) {
|
|
256
|
+
this.contextLostListener = ((event) => {
|
|
257
|
+
event.preventDefault();
|
|
258
|
+
this.contextLost = true;
|
|
259
|
+
this.lastError = "CONTEXT_LOST";
|
|
260
|
+
this.frameActive = false;
|
|
261
|
+
// WS-2.6: notify subscribers. Errors in a listener must not prevent the others from running,
|
|
262
|
+
// nor leave the device in a half-notified state during an already-degraded condition.
|
|
263
|
+
for (const listener of [...this.deviceLostListeners]) {
|
|
264
|
+
try {
|
|
265
|
+
listener();
|
|
266
|
+
}
|
|
267
|
+
catch {
|
|
268
|
+
// A listener that throws is the listener's problem; the device stays consistent.
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
this.contextRestoredListener = (() => {
|
|
273
|
+
this.contextLost = false;
|
|
274
|
+
this.lastError = null;
|
|
275
|
+
for (const listener of [...this.deviceRestoredListeners]) {
|
|
276
|
+
try {
|
|
277
|
+
listener();
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
// As above.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
canvas.addEventListener("webglcontextlost", this.contextLostListener);
|
|
285
|
+
canvas.addEventListener("webglcontextrestored", this.contextRestoredListener);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Subscribe to WebGL context loss. Returns an unsubscribe function.
|
|
290
|
+
*
|
|
291
|
+
* A lost context is not an error a caller can prevent — the browser reclaims GPU resources under
|
|
292
|
+
* memory pressure, on driver reset, or when a tab is backgrounded too long. What a caller can do is
|
|
293
|
+
* be told, so it can recreate resources rather than render nothing.
|
|
294
|
+
*/
|
|
295
|
+
onDeviceLost(listener) {
|
|
296
|
+
this.deviceLostListeners.add(listener);
|
|
297
|
+
return () => this.deviceLostListeners.delete(listener);
|
|
298
|
+
}
|
|
299
|
+
/** Subscribe to context restoration. Returns an unsubscribe function. */
|
|
300
|
+
onDeviceRestored(listener) {
|
|
301
|
+
this.deviceRestoredListeners.add(listener);
|
|
302
|
+
return () => this.deviceRestoredListeners.delete(listener);
|
|
303
|
+
}
|
|
304
|
+
/** True while the WebGL context is lost. Polling alternative to {@link onDeviceLost}. */
|
|
305
|
+
isDeviceLost() {
|
|
306
|
+
return this.contextLost;
|
|
307
|
+
}
|
|
308
|
+
createBuffer(usage, byteLength, initialData) {
|
|
309
|
+
this.assertAlive();
|
|
310
|
+
if (byteLength <= 0 || !Number.isInteger(byteLength)) {
|
|
311
|
+
throw new RenderDeviceError("Buffer byteLength must be a positive integer", "INVALID_BUFFER_SIZE", { byteLength });
|
|
312
|
+
}
|
|
313
|
+
const handle = this.gl.createBuffer();
|
|
314
|
+
if (!handle) {
|
|
315
|
+
throw new RenderDeviceError("Failed to allocate WebGL buffer", "WEBGL_ALLOCATION_FAILED");
|
|
316
|
+
}
|
|
317
|
+
const target = usage === "index" ? this.gl.ELEMENT_ARRAY_BUFFER : this.gl.ARRAY_BUFFER;
|
|
318
|
+
this.bindNoVertexArray();
|
|
319
|
+
this.stateCache.bindBuffer(target, handle, () => this.gl.bindBuffer(target, handle));
|
|
320
|
+
this.gl.bufferData(target, byteLength, this.gl.DYNAMIC_DRAW);
|
|
321
|
+
if (initialData) {
|
|
322
|
+
if (initialData.byteLength > byteLength) {
|
|
323
|
+
throw new RenderDeviceError("Initial data exceeds buffer size", "BUFFER_OVERFLOW", {
|
|
324
|
+
byteLength,
|
|
325
|
+
dataByteLength: initialData.byteLength
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
this.gl.bufferSubData(target, 0, initialData);
|
|
329
|
+
}
|
|
330
|
+
const buffer = new WebGL2Buffer(this.nextId++, usage, byteLength, target, handle, this.gl);
|
|
331
|
+
this.buffers.add(buffer);
|
|
332
|
+
return buffer;
|
|
333
|
+
}
|
|
334
|
+
updateBuffer(buffer, byteOffset, data) {
|
|
335
|
+
this.assertAlive();
|
|
336
|
+
const webglBuffer = this.requireBuffer(buffer);
|
|
337
|
+
if (byteOffset < 0 || byteOffset + data.byteLength > webglBuffer.byteLength) {
|
|
338
|
+
throw new RenderDeviceError("Buffer update range is out of bounds", "BUFFER_RANGE_OUT_OF_BOUNDS", {
|
|
339
|
+
byteOffset,
|
|
340
|
+
dataByteLength: data.byteLength,
|
|
341
|
+
byteLength: webglBuffer.byteLength
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
this.bindNoVertexArray();
|
|
345
|
+
this.stateCache.bindBuffer(webglBuffer.target, webglBuffer.handle, () => this.gl.bindBuffer(webglBuffer.target, webglBuffer.handle));
|
|
346
|
+
this.gl.bufferSubData(webglBuffer.target, byteOffset, data);
|
|
347
|
+
this.bufferUpdateCount += 1;
|
|
348
|
+
}
|
|
349
|
+
readBuffer(buffer, byteOffset = 0, byteLength = buffer.byteLength - byteOffset) {
|
|
350
|
+
this.assertAlive();
|
|
351
|
+
const webglBuffer = this.requireBuffer(buffer);
|
|
352
|
+
if (byteOffset < 0 || byteLength < 0 || byteOffset + byteLength > webglBuffer.byteLength) {
|
|
353
|
+
throw new RenderDeviceError("Buffer read range is out of bounds", "BUFFER_RANGE_OUT_OF_BOUNDS", {
|
|
354
|
+
byteOffset,
|
|
355
|
+
byteLength,
|
|
356
|
+
bufferByteLength: webglBuffer.byteLength
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const output = new Uint8Array(byteLength);
|
|
360
|
+
this.bindNoVertexArray();
|
|
361
|
+
this.stateCache.bindBuffer(webglBuffer.target, webglBuffer.handle, () => this.gl.bindBuffer(webglBuffer.target, webglBuffer.handle));
|
|
362
|
+
this.gl.getBufferSubData(webglBuffer.target, byteOffset, output);
|
|
363
|
+
return output;
|
|
364
|
+
}
|
|
365
|
+
createShaderProgram(sources) {
|
|
366
|
+
this.assertAlive();
|
|
367
|
+
if (!sources.vertex.includes(sources.marker) || !sources.fragment.includes(sources.marker)) {
|
|
368
|
+
throw new RenderDeviceError("Shader source marker is missing from compiled sources", "SHADER_MARKER_MISSING", {
|
|
369
|
+
label: sources.label,
|
|
370
|
+
marker: sources.marker
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const vertexShader = this.compileShader(this.gl.VERTEX_SHADER, sources.vertex, sources.label);
|
|
374
|
+
const fragmentShader = this.compileShader(this.gl.FRAGMENT_SHADER, sources.fragment, sources.label);
|
|
375
|
+
const program = this.gl.createProgram();
|
|
376
|
+
if (!program) {
|
|
377
|
+
throw new RenderDeviceError("Failed to allocate WebGL shader program", "WEBGL_ALLOCATION_FAILED");
|
|
378
|
+
}
|
|
379
|
+
this.shaderProgramCreateCount += 1;
|
|
380
|
+
this.gl.attachShader(program, vertexShader);
|
|
381
|
+
this.gl.attachShader(program, fragmentShader);
|
|
382
|
+
this.gl.linkProgram(program);
|
|
383
|
+
this.gl.deleteShader(vertexShader);
|
|
384
|
+
this.gl.deleteShader(fragmentShader);
|
|
385
|
+
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
386
|
+
const log = this.gl.getProgramInfoLog(program) ?? "Unknown shader link error";
|
|
387
|
+
this.gl.deleteProgram(program);
|
|
388
|
+
throw new RenderDeviceError("WebGL shader link failed", "SHADER_LINK_FAILED", { label: sources.label, log });
|
|
389
|
+
}
|
|
390
|
+
const shader = new WebGL2ShaderProgram(this.nextId++, sources.label, sources.marker, this.reflectProgram(program), program, this.gl);
|
|
391
|
+
this.shaders.add(shader);
|
|
392
|
+
return shader;
|
|
393
|
+
}
|
|
394
|
+
createRenderTarget(descriptor) {
|
|
395
|
+
this.assertAlive();
|
|
396
|
+
if (!Number.isInteger(descriptor.width) ||
|
|
397
|
+
descriptor.width <= 0 ||
|
|
398
|
+
!Number.isInteger(descriptor.height) ||
|
|
399
|
+
descriptor.height <= 0) {
|
|
400
|
+
throw new RenderDeviceError("Render target dimensions must be positive integers", "INVALID_RENDER_TARGET_SIZE", {
|
|
401
|
+
width: descriptor.width,
|
|
402
|
+
height: descriptor.height,
|
|
403
|
+
label: descriptor.label
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const requestedSampleCount = descriptor.sampleCount ?? 1;
|
|
407
|
+
const maxSamples = Number(this.gl.getParameter(this.gl.MAX_SAMPLES) ?? 1);
|
|
408
|
+
if (!Number.isInteger(requestedSampleCount) || requestedSampleCount < 1 || requestedSampleCount > maxSamples) {
|
|
409
|
+
throw new RenderDeviceError("WebGL2 render-target sampleCount must be an integer supported by MAX_SAMPLES.", "INVALID_RENDER_TARGET_SAMPLE_COUNT", {
|
|
410
|
+
sampleCount: requestedSampleCount,
|
|
411
|
+
maxSamples,
|
|
412
|
+
label: descriptor.label
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
const sampleCount = requestedSampleCount;
|
|
416
|
+
const colorHandle = this.gl.createTexture();
|
|
417
|
+
const framebuffer = this.gl.createFramebuffer();
|
|
418
|
+
const drawFramebuffer = sampleCount > 1 ? this.gl.createFramebuffer() : framebuffer;
|
|
419
|
+
const multisampleColorHandle = sampleCount > 1 ? this.gl.createRenderbuffer() : null;
|
|
420
|
+
const depthMode = descriptor.depth === "texture" ? "texture" : descriptor.depth === false ? "none" : "renderbuffer";
|
|
421
|
+
const depthHandle = depthMode === "renderbuffer" ? this.gl.createRenderbuffer() : null;
|
|
422
|
+
const multisampleDepthHandle = sampleCount > 1 && depthMode !== "none"
|
|
423
|
+
? (depthHandle ?? this.gl.createRenderbuffer())
|
|
424
|
+
: depthHandle;
|
|
425
|
+
const depthTextureHandle = depthMode === "texture" ? this.gl.createTexture() : null;
|
|
426
|
+
if (!colorHandle
|
|
427
|
+
|| !framebuffer
|
|
428
|
+
|| !drawFramebuffer
|
|
429
|
+
|| (sampleCount > 1 && !multisampleColorHandle)
|
|
430
|
+
|| ((depthMode === "renderbuffer" || (sampleCount > 1 && depthMode !== "none")) && !multisampleDepthHandle)
|
|
431
|
+
|| (depthMode === "texture" && !depthTextureHandle)) {
|
|
432
|
+
if (colorHandle)
|
|
433
|
+
this.gl.deleteTexture(colorHandle);
|
|
434
|
+
if (framebuffer)
|
|
435
|
+
this.gl.deleteFramebuffer(framebuffer);
|
|
436
|
+
if (drawFramebuffer && drawFramebuffer !== framebuffer)
|
|
437
|
+
this.gl.deleteFramebuffer(drawFramebuffer);
|
|
438
|
+
if (multisampleColorHandle)
|
|
439
|
+
this.gl.deleteRenderbuffer(multisampleColorHandle);
|
|
440
|
+
if (multisampleDepthHandle)
|
|
441
|
+
this.gl.deleteRenderbuffer(multisampleDepthHandle);
|
|
442
|
+
if (depthTextureHandle)
|
|
443
|
+
this.gl.deleteTexture(depthTextureHandle);
|
|
444
|
+
throw new RenderDeviceError("Failed to allocate WebGL render target", "WEBGL_ALLOCATION_FAILED", {
|
|
445
|
+
width: descriptor.width,
|
|
446
|
+
height: descriptor.height,
|
|
447
|
+
label: descriptor.label
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
const previousActiveTexture = this.gl.getParameter(this.gl.ACTIVE_TEXTURE);
|
|
451
|
+
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
452
|
+
const previousTexture = this.gl.getParameter(this.gl.TEXTURE_BINDING_2D);
|
|
453
|
+
const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING);
|
|
454
|
+
const previousRenderbuffer = this.gl.getParameter(this.gl.RENDERBUFFER_BINDING);
|
|
455
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, colorHandle);
|
|
456
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
|
|
457
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
458
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
459
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
460
|
+
const format = descriptor.format ?? "rgba8";
|
|
461
|
+
const textureFormat = this.resolveRenderTargetFormat(format);
|
|
462
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, textureFormat.internalFormat, descriptor.width, descriptor.height, 0, this.gl.RGBA, textureFormat.type, null);
|
|
463
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
|
|
464
|
+
this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, colorHandle, 0);
|
|
465
|
+
if (depthHandle && sampleCount === 1) {
|
|
466
|
+
this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, depthHandle);
|
|
467
|
+
this.gl.renderbufferStorage(this.gl.RENDERBUFFER, this.gl.DEPTH_COMPONENT16, descriptor.width, descriptor.height);
|
|
468
|
+
this.gl.framebufferRenderbuffer(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.RENDERBUFFER, depthHandle);
|
|
469
|
+
}
|
|
470
|
+
if (depthTextureHandle) {
|
|
471
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, depthTextureHandle);
|
|
472
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
|
|
473
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
|
|
474
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
475
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
476
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.DEPTH_COMPONENT24, descriptor.width, descriptor.height, 0, this.gl.DEPTH_COMPONENT, this.gl.UNSIGNED_INT, null);
|
|
477
|
+
this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.TEXTURE_2D, depthTextureHandle, 0);
|
|
478
|
+
}
|
|
479
|
+
const resolveStatus = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
|
|
480
|
+
let drawStatus = resolveStatus;
|
|
481
|
+
if (sampleCount > 1 && multisampleColorHandle) {
|
|
482
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, drawFramebuffer);
|
|
483
|
+
this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, multisampleColorHandle);
|
|
484
|
+
const multisampleColorFormat = format === "rgba8" ? this.gl.RGBA8 : textureFormat.internalFormat;
|
|
485
|
+
this.gl.renderbufferStorageMultisample(this.gl.RENDERBUFFER, sampleCount, multisampleColorFormat, descriptor.width, descriptor.height);
|
|
486
|
+
this.gl.framebufferRenderbuffer(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.RENDERBUFFER, multisampleColorHandle);
|
|
487
|
+
if (multisampleDepthHandle) {
|
|
488
|
+
this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, multisampleDepthHandle);
|
|
489
|
+
this.gl.renderbufferStorageMultisample(this.gl.RENDERBUFFER, sampleCount, this.gl.DEPTH_COMPONENT24, descriptor.width, descriptor.height);
|
|
490
|
+
this.gl.framebufferRenderbuffer(this.gl.FRAMEBUFFER, this.gl.DEPTH_ATTACHMENT, this.gl.RENDERBUFFER, multisampleDepthHandle);
|
|
491
|
+
}
|
|
492
|
+
drawStatus = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
|
|
493
|
+
}
|
|
494
|
+
this.gl.bindRenderbuffer(this.gl.RENDERBUFFER, previousRenderbuffer);
|
|
495
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
|
|
496
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, previousTexture);
|
|
497
|
+
this.gl.activeTexture(previousActiveTexture);
|
|
498
|
+
this.stateCache.invalidate();
|
|
499
|
+
if (resolveStatus !== this.gl.FRAMEBUFFER_COMPLETE || drawStatus !== this.gl.FRAMEBUFFER_COMPLETE) {
|
|
500
|
+
this.gl.deleteTexture(colorHandle);
|
|
501
|
+
this.gl.deleteFramebuffer(framebuffer);
|
|
502
|
+
if (drawFramebuffer !== framebuffer)
|
|
503
|
+
this.gl.deleteFramebuffer(drawFramebuffer);
|
|
504
|
+
if (multisampleColorHandle)
|
|
505
|
+
this.gl.deleteRenderbuffer(multisampleColorHandle);
|
|
506
|
+
if (multisampleDepthHandle)
|
|
507
|
+
this.gl.deleteRenderbuffer(multisampleDepthHandle);
|
|
508
|
+
if (depthTextureHandle)
|
|
509
|
+
this.gl.deleteTexture(depthTextureHandle);
|
|
510
|
+
throw new RenderDeviceError("WebGL render target framebuffer status is invalid", "FRAMEBUFFER_INVALID", { resolveStatus, drawStatus });
|
|
511
|
+
}
|
|
512
|
+
const depthTexture = depthTextureHandle
|
|
513
|
+
? new Texture({ width: descriptor.width, height: descriptor.height, format: "depth24", label: `${descriptor.label ?? "render-target"}-depth` })
|
|
514
|
+
: undefined;
|
|
515
|
+
const target = new WebGL2RenderTarget(this.nextId++, descriptor.width, descriptor.height, descriptor.label ?? "render-target", new Texture({ width: descriptor.width, height: descriptor.height, format, label: descriptor.label ?? "render-target-color" }), depthTexture, framebuffer, colorHandle, multisampleDepthHandle, depthTextureHandle, sampleCount, drawFramebuffer, multisampleColorHandle, this.gl);
|
|
516
|
+
this.renderTargets.add(target);
|
|
517
|
+
// Shadow passes label their depth target after the shadow map texture, so the
|
|
518
|
+
// label is the device-side signal that a shadow depth target was allocated.
|
|
519
|
+
if ((descriptor.label ?? "").toLowerCase().includes("shadow"))
|
|
520
|
+
this.shadowRenderTargetsAllocated += 1;
|
|
521
|
+
this.textures.set(target.colorTexture, colorHandle);
|
|
522
|
+
this.textureUploadModes.set(target.colorTexture, "rgba8");
|
|
523
|
+
if (depthTexture && depthTextureHandle) {
|
|
524
|
+
this.textures.set(depthTexture, depthTextureHandle);
|
|
525
|
+
this.textureUploadModes.set(depthTexture, "depth-render-target");
|
|
526
|
+
}
|
|
527
|
+
return target;
|
|
528
|
+
}
|
|
529
|
+
setRenderTarget(target) {
|
|
530
|
+
this.assertAlive();
|
|
531
|
+
if (this.activeRenderTarget && this.activeRenderTarget !== target) {
|
|
532
|
+
this.resolveMultisampleTarget(this.activeRenderTarget);
|
|
533
|
+
}
|
|
534
|
+
if (target === null) {
|
|
535
|
+
this.activeRenderTarget = null;
|
|
536
|
+
this.stateCache.bindFramebuffer(this.gl.FRAMEBUFFER, null, () => this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null));
|
|
537
|
+
this.stateCache.viewport(0, 0, this.viewportWidth || this.gl.drawingBufferWidth, this.viewportHeight || this.gl.drawingBufferHeight, () => this.gl.viewport(0, 0, this.viewportWidth || this.gl.drawingBufferWidth, this.viewportHeight || this.gl.drawingBufferHeight));
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (!(target instanceof WebGL2RenderTarget) || !this.renderTargets.has(target) || target.disposed) {
|
|
541
|
+
throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
542
|
+
targetId: target.id
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
this.activeRenderTarget = target;
|
|
546
|
+
this.stateCache.bindFramebuffer(this.gl.FRAMEBUFFER, target.drawFramebuffer, () => this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, target.drawFramebuffer));
|
|
547
|
+
this.stateCache.viewport(0, 0, target.width, target.height, () => this.gl.viewport(0, 0, target.width, target.height));
|
|
548
|
+
}
|
|
549
|
+
resolveMultisampleTarget(target) {
|
|
550
|
+
if (target.sampleCount <= 1 || !target.needsResolve)
|
|
551
|
+
return;
|
|
552
|
+
const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING);
|
|
553
|
+
this.gl.bindFramebuffer(this.gl.READ_FRAMEBUFFER, target.drawFramebuffer);
|
|
554
|
+
this.gl.bindFramebuffer(this.gl.DRAW_FRAMEBUFFER, target.framebuffer);
|
|
555
|
+
let mask = this.gl.COLOR_BUFFER_BIT;
|
|
556
|
+
if (target.depthTextureHandle)
|
|
557
|
+
mask |= this.gl.DEPTH_BUFFER_BIT;
|
|
558
|
+
this.gl.blitFramebuffer(0, 0, target.width, target.height, 0, 0, target.width, target.height, mask, this.gl.NEAREST);
|
|
559
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
|
|
560
|
+
target.needsResolve = false;
|
|
561
|
+
this.stateCache.invalidate();
|
|
562
|
+
}
|
|
563
|
+
writeRenderTargetPixels(target, pixels) {
|
|
564
|
+
this.assertAlive();
|
|
565
|
+
if (!(target instanceof WebGL2RenderTarget) || !this.renderTargets.has(target) || target.disposed) {
|
|
566
|
+
throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
567
|
+
targetId: target.id
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
if (target.colorTexture.format !== "rgba8") {
|
|
571
|
+
throw new RenderDeviceError("Byte pixel uploads are only supported for rgba8 WebGL2 render targets", "INVALID_PIXEL_UPLOAD_FORMAT", {
|
|
572
|
+
targetId: target.id,
|
|
573
|
+
format: target.colorTexture.format
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
if (pixels.length !== target.width * target.height * 4) {
|
|
577
|
+
throw new RenderDeviceError("Render-target pixel upload must contain width * height * 4 bytes", "INVALID_PIXEL_UPLOAD_SIZE", {
|
|
578
|
+
targetId: target.id,
|
|
579
|
+
byteLength: pixels.length,
|
|
580
|
+
expectedByteLength: target.width * target.height * 4
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
const textureUnit0 = this.captureTextureUnit0();
|
|
584
|
+
try {
|
|
585
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, target.colorHandle);
|
|
586
|
+
this.gl.texSubImage2D(this.gl.TEXTURE_2D, 0, 0, 0, target.width, target.height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
|
|
587
|
+
target.needsResolve = false;
|
|
588
|
+
}
|
|
589
|
+
finally {
|
|
590
|
+
this.restoreTextureUnit0(textureUnit0);
|
|
591
|
+
}
|
|
592
|
+
this.lastError = this.readError();
|
|
593
|
+
if (this.lastError) {
|
|
594
|
+
throw new RenderDeviceError("WebGL2 render-target pixel upload failed", "WEBGL_PIXEL_UPLOAD_FAILED", {
|
|
595
|
+
targetId: target.id,
|
|
596
|
+
error: this.lastError
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
presentRenderTarget(source) {
|
|
601
|
+
this.assertAlive();
|
|
602
|
+
if (!(source instanceof WebGL2RenderTarget) || !this.renderTargets.has(source) || source.disposed) {
|
|
603
|
+
throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
604
|
+
targetId: source.id
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
this.resolveMultisampleTarget(source);
|
|
608
|
+
const outputWidth = this.viewportWidth || this.gl.drawingBufferWidth;
|
|
609
|
+
const outputHeight = this.viewportHeight || this.gl.drawingBufferHeight;
|
|
610
|
+
this.drawRenderTargetToBackbuffer(source, outputWidth, outputHeight);
|
|
611
|
+
this.activeRenderTarget = null;
|
|
612
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, null);
|
|
613
|
+
this.lastError = this.readError();
|
|
614
|
+
if (this.lastError) {
|
|
615
|
+
throw new RenderDeviceError(`WebGL2 render-target presentation failed: ${this.lastError}`, "WEBGL_PRESENT_FAILED", {
|
|
616
|
+
targetId: source.id,
|
|
617
|
+
error: this.lastError
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
presentLdrPostprocess(source, options) {
|
|
622
|
+
this.assertAlive();
|
|
623
|
+
if (!(source instanceof WebGL2RenderTarget) || !this.renderTargets.has(source) || source.disposed) {
|
|
624
|
+
throw new RenderDeviceError("Render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
625
|
+
targetId: source.id
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
this.resolveMultisampleTarget(source);
|
|
629
|
+
const tonePass = options.passes.find((pass) => pass.name === "tone-mapping");
|
|
630
|
+
const bloomPass = options.passes.find((pass) => pass.name === "bloom");
|
|
631
|
+
const depthOfFieldPass = options.passes.find((pass) => pass.name === "depth-of-field");
|
|
632
|
+
const motionBlurPass = options.passes.find((pass) => pass.name === "motion-blur");
|
|
633
|
+
const ssaoPass = options.passes.find((pass) => pass.name === "ssao");
|
|
634
|
+
const ssrPass = options.passes.find((pass) => pass.name === "ssr");
|
|
635
|
+
const taaPass = options.passes.find((pass) => pass.name === "taa");
|
|
636
|
+
const outlinePass = options.passes.find((pass) => pass.name === "outline");
|
|
637
|
+
if (source.colorTexture.format !== "rgba8" && !tonePass) {
|
|
638
|
+
throw new RenderDeviceError("WebGL2 HDR postprocess presentation requires a tone-mapping pass before LDR output.", "WEBGL_LDR_POSTPROCESS_FORMAT_UNSUPPORTED", {
|
|
639
|
+
targetId: source.id,
|
|
640
|
+
format: source.colorTexture.format
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
if (source.colorTexture.format !== "rgba8" && (depthOfFieldPass || motionBlurPass || ssaoPass || ssrPass || taaPass || outlinePass) && !tonePass) {
|
|
644
|
+
throw new RenderDeviceError("WebGL2 byte-kernel postprocess requires rgba8 input or a preceding tone-mapping pass.", "WEBGL_LDR_POSTPROCESS_FORMAT_UNSUPPORTED", {
|
|
645
|
+
targetId: source.id,
|
|
646
|
+
format: source.colorTexture.format,
|
|
647
|
+
pass: depthOfFieldPass ? "depth-of-field" : motionBlurPass ? "motion-blur" : ssaoPass ? "ssao" : ssrPass ? "ssr" : taaPass ? "taa" : "outline"
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
if ((depthOfFieldPass || ssaoPass || ssrPass) && !source.depthTextureHandle) {
|
|
651
|
+
throw new RenderDeviceError("WebGL2 native depth postprocess requires a renderer-owned sampleable depth texture.", "WEBGL_LDR_POSTPROCESS_DEPTH_REQUIRED", {
|
|
652
|
+
targetId: source.id,
|
|
653
|
+
pass: depthOfFieldPass ? "depth-of-field" : ssaoPass ? "ssao" : "ssr"
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
// Raw GL depth is nonlinear (0.1/1000 defaults park the play area past
|
|
657
|
+
// 0.97), which blinded every depth-gated native pass. SSR and DOF compare
|
|
658
|
+
// linearized depth; SSAO keeps its legacy response (documented partial).
|
|
659
|
+
const depthRange = normalizeLdrDepthRange(options.depthRange);
|
|
660
|
+
const outputTarget = options.outputTarget;
|
|
661
|
+
if (outputTarget && (!(outputTarget instanceof WebGL2RenderTarget) || !this.renderTargets.has(outputTarget) || outputTarget.disposed)) {
|
|
662
|
+
throw new RenderDeviceError("Output render target is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
663
|
+
targetId: outputTarget.id
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
if (outputTarget && outputTarget.colorTexture.format !== "rgba8") {
|
|
667
|
+
throw new RenderDeviceError("WebGL2 LDR postprocess output target must be rgba8.", "WEBGL_LDR_POSTPROCESS_OUTPUT_FORMAT_UNSUPPORTED", {
|
|
668
|
+
targetId: outputTarget.id,
|
|
669
|
+
format: outputTarget.colorTexture.format
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
if (outputTarget && (outputTarget.width !== source.width || outputTarget.height !== source.height)) {
|
|
673
|
+
throw new RenderDeviceError("WebGL2 LDR postprocess source and output dimensions must match.", "WEBGL_LDR_POSTPROCESS_SIZE_MISMATCH", {
|
|
674
|
+
sourceWidth: source.width,
|
|
675
|
+
sourceHeight: source.height,
|
|
676
|
+
targetWidth: outputTarget.width,
|
|
677
|
+
targetHeight: outputTarget.height
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
const webglOutputTarget = outputTarget;
|
|
681
|
+
const colorPass = options.passes.find((pass) => pass.name === "color-grade");
|
|
682
|
+
const fxaaPass = options.passes.find((pass) => pass.name === "fxaa");
|
|
683
|
+
const bloomOptions = bloomPass ? normalizeNativeBloomOptions(bloomPass.options) : undefined;
|
|
684
|
+
const depthOfFieldOptions = depthOfFieldPass ? normalizeNativeDepthOfFieldOptions(depthOfFieldPass.options) : undefined;
|
|
685
|
+
const motionBlurOptions = motionBlurPass
|
|
686
|
+
? normalizeNativeMotionBlurOptions(motionBlurPass.options, source.width, source.height)
|
|
687
|
+
: undefined;
|
|
688
|
+
const ssaoOptions = ssaoPass ? normalizeNativeSsaoOptions(ssaoPass.options) : undefined;
|
|
689
|
+
const ssrOptions = ssrPass ? normalizeNativeSsrOptions(ssrPass.options) : undefined;
|
|
690
|
+
const taaOptions = taaPass ? normalizeNativeTaaOptions(taaPass.options, source.width, source.height) : undefined;
|
|
691
|
+
const outlineOptions = outlinePass ? normalizeNativeOutlineOptions(outlinePass.options) : undefined;
|
|
692
|
+
const program = this.ensureLdrPostprocessProgram();
|
|
693
|
+
const vertexArray = this.ensurePresentationVertexArray();
|
|
694
|
+
const sourceIsHdr = source.colorTexture.format !== "rgba8";
|
|
695
|
+
const bloomResources = bloomOptions || depthOfFieldOptions || motionBlurOptions || ssaoOptions || ssrOptions || taaOptions || outlineOptions
|
|
696
|
+
? this.ensureBloomPingPongResources(source.width, source.height, sourceIsHdr && Boolean(bloomOptions))
|
|
697
|
+
: undefined;
|
|
698
|
+
if (bloomOptions && !sourceIsHdr) {
|
|
699
|
+
this.ensureBloomLutTextures(bloomOptions);
|
|
700
|
+
}
|
|
701
|
+
if (outlineOptions) {
|
|
702
|
+
this.ensureOutlineBlendLutTexture(outlineOptions);
|
|
703
|
+
}
|
|
704
|
+
if (motionBlurOptions) {
|
|
705
|
+
this.ensureMotionBlurVelocityTexture(source.width, source.height, motionBlurOptions.velocity);
|
|
706
|
+
}
|
|
707
|
+
if (taaOptions) {
|
|
708
|
+
this.ensureTaaHistoryTexture(source.width, source.height, taaOptions.history);
|
|
709
|
+
}
|
|
710
|
+
const outputWidth = webglOutputTarget?.width ?? (this.viewportWidth || this.gl.drawingBufferWidth);
|
|
711
|
+
const outputHeight = webglOutputTarget?.height ?? (this.viewportHeight || this.gl.drawingBufferHeight);
|
|
712
|
+
const previousState = this.prepareFullscreenPresentation(webglOutputTarget?.framebuffer ?? null, outputWidth, outputHeight);
|
|
713
|
+
try {
|
|
714
|
+
let ldrSourceHandle = source.colorHandle;
|
|
715
|
+
let temporarySourceIndex = -1;
|
|
716
|
+
if (bloomOptions && bloomResources) {
|
|
717
|
+
if (bloomOptions.quality === "performance") {
|
|
718
|
+
ldrSourceHandle = this.executeNativeBloomPasses(ldrSourceHandle, bloomResources, bloomOptions, vertexArray, sourceIsHdr);
|
|
719
|
+
temporarySourceIndex = 1;
|
|
720
|
+
this.recordBloomDiagnostics(bloomOptions, source.width, source.height, sourceIsHdr, 1);
|
|
721
|
+
}
|
|
722
|
+
else {
|
|
723
|
+
const plan = resolveBloomPyramidPlan(source.width, source.height, bloomOptions.quality, sourceIsHdr);
|
|
724
|
+
const pyramid = this.ensureBloomPyramidResources(plan, sourceIsHdr);
|
|
725
|
+
const pyramidResult = this.executeNativeBloomPyramidPasses(ldrSourceHandle, source.width, source.height, pyramid, {
|
|
726
|
+
textureA: bloomResources.textures[0],
|
|
727
|
+
framebufferA: bloomResources.framebuffers[0],
|
|
728
|
+
textureB: bloomResources.textures[1],
|
|
729
|
+
framebufferB: bloomResources.framebuffers[1]
|
|
730
|
+
}, bloomOptions, vertexArray, sourceIsHdr);
|
|
731
|
+
// The pyramid sum lands in ping-pong texture A; composite it with the
|
|
732
|
+
// source through the proven single-scale composite program.
|
|
733
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, bloomResources.framebuffers[1]);
|
|
734
|
+
this.gl.viewport(0, 0, source.width, source.height);
|
|
735
|
+
const pyramidComposite = this.ensureBloomCompositeProgram();
|
|
736
|
+
this.gl.useProgram(pyramidComposite);
|
|
737
|
+
this.gl.bindVertexArray(vertexArray);
|
|
738
|
+
this.bindFullscreenTexture(0, ldrSourceHandle);
|
|
739
|
+
this.bindFullscreenTexture(1, pyramidResult);
|
|
740
|
+
if (this.bloomCompositeLutTexture)
|
|
741
|
+
this.bindFullscreenTexture(2, this.bloomCompositeLutTexture);
|
|
742
|
+
this.gl.uniform1i(this.gl.getUniformLocation(pyramidComposite, "u_source"), 0);
|
|
743
|
+
this.gl.uniform1i(this.gl.getUniformLocation(pyramidComposite, "u_blurred"), 1);
|
|
744
|
+
this.gl.uniform1i(this.gl.getUniformLocation(pyramidComposite, "u_compositeLut"), 2);
|
|
745
|
+
this.gl.uniform1i(this.gl.getUniformLocation(pyramidComposite, "u_hdr"), sourceIsHdr ? 1 : 0);
|
|
746
|
+
this.gl.uniform1f(this.gl.getUniformLocation(pyramidComposite, "u_intensity"), bloomOptions.intensity * resolveBloomPyramidResponseGain(plan));
|
|
747
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
748
|
+
ldrSourceHandle = bloomResources.textures[1];
|
|
749
|
+
// The composited result lives in texture B, so subsequent passes
|
|
750
|
+
// must write texture A (index 0) to avoid sampling a bound target.
|
|
751
|
+
temporarySourceIndex = 0;
|
|
752
|
+
this.recordBloomDiagnostics(bloomOptions, source.width, source.height, sourceIsHdr, plan.mipCount);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if ((depthOfFieldOptions || motionBlurOptions || ssaoOptions || ssrOptions || taaOptions || outlineOptions) && bloomResources) {
|
|
756
|
+
if (tonePass || colorPass) {
|
|
757
|
+
const baseTargetIndex = 0;
|
|
758
|
+
this.drawNativeLdrStage(ldrSourceHandle, bloomResources.framebuffers[baseTargetIndex], source.width, source.height, program, vertexArray, tonePass, colorPass, undefined, options.toneMappingDefaults);
|
|
759
|
+
ldrSourceHandle = bloomResources.textures[baseTargetIndex];
|
|
760
|
+
temporarySourceIndex = baseTargetIndex;
|
|
761
|
+
}
|
|
762
|
+
if (depthOfFieldOptions && source.depthTextureHandle) {
|
|
763
|
+
const depthOfFieldTargetIndex = temporarySourceIndex === 0 ? 1 : 0;
|
|
764
|
+
ldrSourceHandle = this.executeNativeDepthOfFieldPass(ldrSourceHandle, source.depthTextureHandle, bloomResources, depthOfFieldOptions, vertexArray, depthOfFieldTargetIndex, depthRange);
|
|
765
|
+
temporarySourceIndex = depthOfFieldTargetIndex;
|
|
766
|
+
}
|
|
767
|
+
if (motionBlurOptions) {
|
|
768
|
+
const motionBlurTargetIndex = temporarySourceIndex === 0 ? 1 : 0;
|
|
769
|
+
ldrSourceHandle = this.executeNativeMotionBlurPass(ldrSourceHandle, bloomResources, motionBlurOptions, vertexArray, motionBlurTargetIndex);
|
|
770
|
+
temporarySourceIndex = motionBlurTargetIndex;
|
|
771
|
+
}
|
|
772
|
+
if (ssaoOptions && source.depthTextureHandle) {
|
|
773
|
+
const ssaoTargetIndex = temporarySourceIndex === 0 ? 1 : 0;
|
|
774
|
+
ldrSourceHandle = this.executeNativeSsaoPass(ldrSourceHandle, source.depthTextureHandle, bloomResources, ssaoOptions, vertexArray, ssaoTargetIndex);
|
|
775
|
+
temporarySourceIndex = ssaoTargetIndex;
|
|
776
|
+
}
|
|
777
|
+
if (ssrOptions && source.depthTextureHandle) {
|
|
778
|
+
const ssrTargetIndex = temporarySourceIndex === 0 ? 1 : 0;
|
|
779
|
+
ldrSourceHandle = this.executeNativeSsrPass(ldrSourceHandle, source.depthTextureHandle, bloomResources, ssrOptions, vertexArray, ssrTargetIndex, depthRange);
|
|
780
|
+
temporarySourceIndex = ssrTargetIndex;
|
|
781
|
+
}
|
|
782
|
+
if (taaOptions) {
|
|
783
|
+
const taaTargetIndex = temporarySourceIndex === 0 ? 1 : 0;
|
|
784
|
+
ldrSourceHandle = this.executeNativeTaaPass(ldrSourceHandle, bloomResources, taaOptions, vertexArray, taaTargetIndex);
|
|
785
|
+
temporarySourceIndex = taaTargetIndex;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (outlineOptions && bloomResources) {
|
|
789
|
+
const outlineTargetIndex = temporarySourceIndex === 0 ? 1 : 0;
|
|
790
|
+
ldrSourceHandle = this.executeNativeOutlinePasses(ldrSourceHandle, bloomResources, outlineOptions, vertexArray, outlineTargetIndex);
|
|
791
|
+
temporarySourceIndex = outlineTargetIndex;
|
|
792
|
+
}
|
|
793
|
+
this.drawNativeLdrStage(ldrSourceHandle, webglOutputTarget?.framebuffer ?? null, outputWidth, outputHeight, program, vertexArray, depthOfFieldOptions || motionBlurOptions || ssaoOptions || ssrOptions || taaOptions || outlineOptions ? undefined : tonePass, depthOfFieldOptions || motionBlurOptions || ssaoOptions || ssrOptions || taaOptions || outlineOptions ? undefined : colorPass, fxaaPass, options.toneMappingDefaults);
|
|
794
|
+
this.gl.flush();
|
|
795
|
+
}
|
|
796
|
+
finally {
|
|
797
|
+
this.restoreFullscreenPresentationState(previousState, outputWidth, outputHeight);
|
|
798
|
+
}
|
|
799
|
+
this.lastError = this.readError();
|
|
800
|
+
if (this.lastError) {
|
|
801
|
+
throw new RenderDeviceError(`WebGL2 LDR postprocess presentation failed: ${this.lastError}`, "WEBGL_LDR_POSTPROCESS_FAILED", {
|
|
802
|
+
targetId: source.id,
|
|
803
|
+
error: this.lastError
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
this.setRenderTarget(webglOutputTarget ?? null);
|
|
807
|
+
}
|
|
808
|
+
readPixels(x, y, width, height) {
|
|
809
|
+
this.assertAlive();
|
|
810
|
+
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
|
|
811
|
+
throw new RenderDeviceError("Readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
|
|
812
|
+
}
|
|
813
|
+
const boundsWidth = this.activeRenderTarget?.width ?? this.gl.drawingBufferWidth;
|
|
814
|
+
const boundsHeight = this.activeRenderTarget?.height ?? this.gl.drawingBufferHeight;
|
|
815
|
+
if (x + width > boundsWidth || y + height > boundsHeight) {
|
|
816
|
+
throw new RenderDeviceError("Readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
|
|
817
|
+
x,
|
|
818
|
+
y,
|
|
819
|
+
width,
|
|
820
|
+
height,
|
|
821
|
+
boundsWidth,
|
|
822
|
+
boundsHeight
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
const readTarget = this.activeRenderTarget;
|
|
826
|
+
if (readTarget) {
|
|
827
|
+
this.resolveMultisampleTarget(readTarget);
|
|
828
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, readTarget.framebuffer);
|
|
829
|
+
}
|
|
830
|
+
const pixels = new Uint8Array(width * height * 4);
|
|
831
|
+
this.gl.readPixels(x, y, width, height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
|
|
832
|
+
if (readTarget && readTarget.sampleCount > 1) {
|
|
833
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, readTarget.drawFramebuffer);
|
|
834
|
+
this.stateCache.invalidate();
|
|
835
|
+
}
|
|
836
|
+
return pixels;
|
|
837
|
+
}
|
|
838
|
+
readFloatPixels(x, y, width, height) {
|
|
839
|
+
this.assertAlive();
|
|
840
|
+
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
|
|
841
|
+
throw new RenderDeviceError("Float readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
|
|
842
|
+
}
|
|
843
|
+
const boundsWidth = this.activeRenderTarget?.width ?? this.gl.drawingBufferWidth;
|
|
844
|
+
const boundsHeight = this.activeRenderTarget?.height ?? this.gl.drawingBufferHeight;
|
|
845
|
+
if (x + width > boundsWidth || y + height > boundsHeight) {
|
|
846
|
+
throw new RenderDeviceError("Float readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
|
|
847
|
+
x,
|
|
848
|
+
y,
|
|
849
|
+
width,
|
|
850
|
+
height,
|
|
851
|
+
boundsWidth,
|
|
852
|
+
boundsHeight
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
const readTarget = this.activeRenderTarget;
|
|
856
|
+
if (readTarget) {
|
|
857
|
+
this.resolveMultisampleTarget(readTarget);
|
|
858
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, readTarget.framebuffer);
|
|
859
|
+
}
|
|
860
|
+
const pixels = new Float32Array(width * height * 4);
|
|
861
|
+
this.gl.readPixels(x, y, width, height, this.gl.RGBA, this.gl.FLOAT, pixels);
|
|
862
|
+
if (readTarget && readTarget.sampleCount > 1) {
|
|
863
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, readTarget.drawFramebuffer);
|
|
864
|
+
this.stateCache.invalidate();
|
|
865
|
+
}
|
|
866
|
+
return pixels;
|
|
867
|
+
}
|
|
868
|
+
readDepthPixels(x, y, width, height) {
|
|
869
|
+
this.assertAlive();
|
|
870
|
+
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
|
|
871
|
+
throw new RenderDeviceError("Depth readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
|
|
872
|
+
}
|
|
873
|
+
const target = this.activeRenderTarget;
|
|
874
|
+
if (!target?.depthTextureHandle) {
|
|
875
|
+
throw new RenderDeviceError("Depth readback requires an active WebGL2 render target with a sampleable depth texture.", "DEPTH_READBACK_UNAVAILABLE", {
|
|
876
|
+
renderTarget: target?.label ?? null
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
if (x + width > target.width || y + height > target.height) {
|
|
880
|
+
throw new RenderDeviceError("Depth readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
|
|
881
|
+
x,
|
|
882
|
+
y,
|
|
883
|
+
width,
|
|
884
|
+
height,
|
|
885
|
+
boundsWidth: target.width,
|
|
886
|
+
boundsHeight: target.height
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
this.resolveMultisampleTarget(target);
|
|
890
|
+
const encoded = this.copyDepthTextureToBytes(target, x, y, width, height);
|
|
891
|
+
const pixels = new Float32Array(width * height);
|
|
892
|
+
for (let index = 0; index < pixels.length; index += 1) {
|
|
893
|
+
const byteIndex = index * 4;
|
|
894
|
+
const r = encoded[byteIndex] ?? 255;
|
|
895
|
+
const g = encoded[byteIndex + 1] ?? 255;
|
|
896
|
+
const b = encoded[byteIndex + 2] ?? 255;
|
|
897
|
+
pixels[index] = Math.max(0, Math.min(1, r / 255 + g / 65025 + b / 16581375));
|
|
898
|
+
}
|
|
899
|
+
return pixels;
|
|
900
|
+
}
|
|
901
|
+
beginFrame(width, height) {
|
|
902
|
+
this.assertAlive();
|
|
903
|
+
if (this.frameActive) {
|
|
904
|
+
throw new RenderDeviceError("Frame is already active", "FRAME_ALREADY_ACTIVE");
|
|
905
|
+
}
|
|
906
|
+
if (width <= 0 || height <= 0) {
|
|
907
|
+
throw new RenderDeviceError("Frame dimensions must be positive", "INVALID_FRAME_SIZE", { width, height });
|
|
908
|
+
}
|
|
909
|
+
this.frameActive = true;
|
|
910
|
+
this.drawCalls = 0;
|
|
911
|
+
this.nativeEnvironmentBindings = 0;
|
|
912
|
+
this.nativeShadowMapBindings = 0;
|
|
913
|
+
this.activeTextureUnitIndex = -1;
|
|
914
|
+
this.textureUnitBindings.clear();
|
|
915
|
+
this.stateCache.invalidate();
|
|
916
|
+
this.viewportWidth = width;
|
|
917
|
+
this.viewportHeight = height;
|
|
918
|
+
this.stateCache.bindFramebuffer(this.gl.FRAMEBUFFER, this.activeRenderTarget?.framebuffer ?? null, () => this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, this.activeRenderTarget?.framebuffer ?? null));
|
|
919
|
+
this.stateCache.viewport(0, 0, width, height, () => this.gl.viewport(0, 0, width, height));
|
|
920
|
+
this.stateCache.setEnabled(this.gl.DEPTH_TEST, true, () => this.gl.enable(this.gl.DEPTH_TEST));
|
|
921
|
+
this.stateCache.depthFunc(this.gl.LEQUAL, () => this.gl.depthFunc(this.gl.LEQUAL));
|
|
922
|
+
}
|
|
923
|
+
clear(color) {
|
|
924
|
+
this.assertFrame();
|
|
925
|
+
this.gl.clearColor(color[0], color[1], color[2], color[3]);
|
|
926
|
+
this.stateCache.depthMask(true, () => this.gl.depthMask(true));
|
|
927
|
+
this.gl.clearDepth(1);
|
|
928
|
+
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
|
929
|
+
if (this.activeRenderTarget?.sampleCount && this.activeRenderTarget.sampleCount > 1)
|
|
930
|
+
this.activeRenderTarget.needsResolve = true;
|
|
931
|
+
}
|
|
932
|
+
clearRenderTarget(color) {
|
|
933
|
+
this.assertFrame();
|
|
934
|
+
this.gl.clearColor(color[0], color[1], color[2], color[3]);
|
|
935
|
+
this.stateCache.depthMask(true, () => this.gl.depthMask(true));
|
|
936
|
+
this.gl.clearDepth(1);
|
|
937
|
+
this.gl.clear(this.gl.COLOR_BUFFER_BIT | this.gl.DEPTH_BUFFER_BIT);
|
|
938
|
+
if (this.activeRenderTarget?.sampleCount && this.activeRenderTarget.sampleCount > 1)
|
|
939
|
+
this.activeRenderTarget.needsResolve = true;
|
|
940
|
+
}
|
|
941
|
+
draw(command) {
|
|
942
|
+
this.assertFrame();
|
|
943
|
+
const vertexBuffer = this.requireBuffer(command.vertexBuffer);
|
|
944
|
+
this.applyRenderState(command.renderState);
|
|
945
|
+
let shader;
|
|
946
|
+
if (command.shader) {
|
|
947
|
+
const activeShader = this.requireShader(command.shader);
|
|
948
|
+
shader = activeShader;
|
|
949
|
+
this.stateCache.useProgram(activeShader.handle, () => this.gl.useProgram(activeShader.handle));
|
|
950
|
+
if (command.uniforms) {
|
|
951
|
+
this.uploadUniforms(activeShader, command.uniforms);
|
|
952
|
+
if (this.errorCheckMode === "strict") {
|
|
953
|
+
const uniformError = this.readError();
|
|
954
|
+
if (uniformError) {
|
|
955
|
+
throw new RenderDeviceError(`WebGL2 uniform upload failed for draw ${command.label ?? "unnamed"}: ${uniformError}`, "WEBGL_DRAW_FAILED", {
|
|
956
|
+
label: command.label,
|
|
957
|
+
stage: "uniforms",
|
|
958
|
+
error: uniformError
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
if (shader && command.vertexFormat) {
|
|
965
|
+
this.bindVertexArrayForCommand(command, shader, vertexBuffer);
|
|
966
|
+
if (this.errorCheckMode === "strict") {
|
|
967
|
+
const vertexFormatError = this.readError();
|
|
968
|
+
if (vertexFormatError) {
|
|
969
|
+
throw new RenderDeviceError(`WebGL2 vertex format binding failed for draw ${command.label ?? "unnamed"}: ${vertexFormatError}`, "WEBGL_DRAW_FAILED", {
|
|
970
|
+
label: command.label,
|
|
971
|
+
stage: "vertex-format",
|
|
972
|
+
error: vertexFormatError
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
else {
|
|
978
|
+
this.bindNoVertexArray();
|
|
979
|
+
this.stateCache.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle, () => this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle));
|
|
980
|
+
}
|
|
981
|
+
if (command.indexBuffer) {
|
|
982
|
+
const indexBuffer = this.requireBuffer(command.indexBuffer);
|
|
983
|
+
if (!shader || !command.vertexFormat) {
|
|
984
|
+
this.stateCache.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle, () => this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle));
|
|
985
|
+
}
|
|
986
|
+
const type = command.indexType === "uint32" ? this.gl.UNSIGNED_INT : this.gl.UNSIGNED_SHORT;
|
|
987
|
+
const indexByteSize = command.indexType === "uint32" ? 4 : 2;
|
|
988
|
+
const indexOffset = (command.firstIndex ?? 0) * indexByteSize;
|
|
989
|
+
if ((command.instanceCount ?? 1) > 1) {
|
|
990
|
+
this.gl.drawElementsInstanced(this.primitive(command.topology), command.indexCount ?? 0, type, indexOffset, command.instanceCount ?? 1);
|
|
991
|
+
this.nativeInstancedSubmissions += 1;
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
this.gl.drawElements(this.primitive(command.topology), command.indexCount ?? 0, type, indexOffset);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
else {
|
|
998
|
+
if ((command.instanceCount ?? 1) > 1) {
|
|
999
|
+
this.gl.drawArraysInstanced(this.primitive(command.topology), command.firstVertex ?? 0, command.vertexCount, command.instanceCount ?? 1);
|
|
1000
|
+
this.nativeInstancedSubmissions += 1;
|
|
1001
|
+
}
|
|
1002
|
+
else {
|
|
1003
|
+
this.gl.drawArrays(this.primitive(command.topology), command.firstVertex ?? 0, command.vertexCount);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
this.drawCalls += 1;
|
|
1007
|
+
if (this.activeRenderTarget?.sampleCount && this.activeRenderTarget.sampleCount > 1)
|
|
1008
|
+
this.activeRenderTarget.needsResolve = true;
|
|
1009
|
+
if (this.errorCheckMode === "strict") {
|
|
1010
|
+
const drawError = this.readError();
|
|
1011
|
+
if (drawError) {
|
|
1012
|
+
throw new RenderDeviceError(`WebGL2 draw failed for ${command.label ?? "unnamed"}: ${drawError}`, "WEBGL_DRAW_FAILED", {
|
|
1013
|
+
label: command.label,
|
|
1014
|
+
stage: "draw",
|
|
1015
|
+
error: drawError
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
endFrame() {
|
|
1021
|
+
this.assertFrame();
|
|
1022
|
+
this.frameActive = false;
|
|
1023
|
+
this.lastError = this.readError();
|
|
1024
|
+
}
|
|
1025
|
+
captureState() {
|
|
1026
|
+
const viewport = this.gl.getParameter(this.gl.VIEWPORT);
|
|
1027
|
+
return new Map([
|
|
1028
|
+
["backend", this.kind],
|
|
1029
|
+
["disposed", this.disposed],
|
|
1030
|
+
["contextLost", this.contextLost],
|
|
1031
|
+
["frameActive", this.frameActive],
|
|
1032
|
+
["depthTest", this.gl.isEnabled(this.gl.DEPTH_TEST)],
|
|
1033
|
+
["blend", this.gl.isEnabled(this.gl.BLEND)],
|
|
1034
|
+
["cullFace", this.gl.isEnabled(this.gl.CULL_FACE)],
|
|
1035
|
+
["program", this.gl.getParameter(this.gl.CURRENT_PROGRAM) ? "bound" : null],
|
|
1036
|
+
["arrayBuffer", this.gl.getParameter(this.gl.ARRAY_BUFFER_BINDING) ? "bound" : null],
|
|
1037
|
+
["elementArrayBuffer", this.gl.getParameter(this.gl.ELEMENT_ARRAY_BUFFER_BINDING) ? "bound" : null],
|
|
1038
|
+
["viewportWidth", this.viewportWidth],
|
|
1039
|
+
["viewportHeight", this.viewportHeight],
|
|
1040
|
+
["actualViewportWidth", viewport[2] ?? 0],
|
|
1041
|
+
["actualViewportHeight", viewport[3] ?? 0],
|
|
1042
|
+
["renderTarget", this.activeRenderTarget?.label ?? null],
|
|
1043
|
+
["drawCalls", this.drawCalls],
|
|
1044
|
+
["shaderProgramCreates", this.shaderProgramCreateCount],
|
|
1045
|
+
["uniformLocationLookups", this.uniformLocationLookupCount],
|
|
1046
|
+
["bufferUpdates", this.bufferUpdateCount],
|
|
1047
|
+
["textureBinds", this.textureBindCount],
|
|
1048
|
+
["samplerParameterUploads", this.samplerParameterUploadCount],
|
|
1049
|
+
["vertexArrayCreates", this.vertexArrayCreateCount],
|
|
1050
|
+
["samplerObjects", this.samplerObjectCache.size],
|
|
1051
|
+
["samplerAnisotropyUploads", this.samplerAnisotropyUploadCount],
|
|
1052
|
+
["maxTextureAnisotropy", this.maxTextureAnisotropy],
|
|
1053
|
+
["nativeEnvironmentBindings", this.nativeEnvironmentBindings],
|
|
1054
|
+
["nativeShadowMapBindings", this.nativeShadowMapBindings],
|
|
1055
|
+
["nativeInstancedSubmissions", this.nativeInstancedSubmissions],
|
|
1056
|
+
["stateCacheIssued", this.stateCache.stats().issued],
|
|
1057
|
+
["stateCacheSkipped", this.stateCache.stats().skipped]
|
|
1058
|
+
]);
|
|
1059
|
+
}
|
|
1060
|
+
getDiagnostics() {
|
|
1061
|
+
this.releaseDisposedTextureHandles();
|
|
1062
|
+
const liveRenderTargets = [...this.renderTargets].filter((target) => !target.disposed);
|
|
1063
|
+
const liveTextures = new Set([
|
|
1064
|
+
...liveRenderTargets.map((target) => target.colorTexture),
|
|
1065
|
+
...[...this.textures.keys()].filter((texture) => !texture.disposed)
|
|
1066
|
+
]);
|
|
1067
|
+
const liveTextureList = [...liveTextures];
|
|
1068
|
+
const compressedTextures = liveTextureList.filter((texture) => isCompressedTextureFormat(texture.format));
|
|
1069
|
+
const fallbackTextures = liveTextureList.filter((texture) => this.textureUploadModes.get(texture) === "fallback");
|
|
1070
|
+
const stateCacheStats = this.stateCache.stats();
|
|
1071
|
+
const bufferBytes = [...this.buffers].filter((buffer) => !buffer.disposed).reduce((total, buffer) => total + buffer.byteLength, 0);
|
|
1072
|
+
const textureBytes = liveTextureList.reduce((total, texture) => total + texture.byteLength, 0);
|
|
1073
|
+
const gpuTargetEntries = this.describeGpuTargetInventory(liveRenderTargets);
|
|
1074
|
+
return {
|
|
1075
|
+
drawCalls: this.drawCalls,
|
|
1076
|
+
buffers: [...this.buffers].filter((buffer) => !buffer.disposed).length,
|
|
1077
|
+
shaders: [...this.shaders].filter((shader) => !shader.disposed).length,
|
|
1078
|
+
renderTargets: liveRenderTargets.length,
|
|
1079
|
+
maxRenderTargetSampleCount: Math.max(1, ...liveRenderTargets.map((target) => target.sampleCount)),
|
|
1080
|
+
textures: liveTextures.size,
|
|
1081
|
+
bufferBytes,
|
|
1082
|
+
textureBytes,
|
|
1083
|
+
approximateGpuMemoryBytes: bufferBytes + textureBytes,
|
|
1084
|
+
compressedTextures: compressedTextures.length,
|
|
1085
|
+
compressedTextureBytes: compressedTextures.reduce((total, texture) => total + texture.byteLength, 0),
|
|
1086
|
+
textureFallbacks: fallbackTextures.length,
|
|
1087
|
+
textureFallbackBytes: fallbackTextures.reduce((total, texture) => total + texture.fallbackByteLength, 0),
|
|
1088
|
+
nativeEnvironmentBindings: this.nativeEnvironmentBindings,
|
|
1089
|
+
nativeShadowMapBindings: this.nativeShadowMapBindings,
|
|
1090
|
+
shadowRenderTargetsAllocated: this.shadowRenderTargetsAllocated,
|
|
1091
|
+
nativeInstancedSubmissions: this.nativeInstancedSubmissions,
|
|
1092
|
+
bloom: this.lastBloomDiagnostics,
|
|
1093
|
+
samplerAnisotropyUploads: this.samplerAnisotropyUploadCount,
|
|
1094
|
+
maxTextureAnisotropy: this.maxTextureAnisotropy,
|
|
1095
|
+
stateCacheIssued: stateCacheStats.issued,
|
|
1096
|
+
stateCacheSkipped: stateCacheStats.skipped,
|
|
1097
|
+
stateCacheProgramSwitches: stateCacheStats.byOperation.useProgram?.issued ?? 0,
|
|
1098
|
+
stateCacheTextureBinds: stateCacheStats.byOperation.bindTexture?.issued ?? 0,
|
|
1099
|
+
stateCacheBufferBinds: stateCacheStats.byOperation.bindBuffer?.issued ?? 0,
|
|
1100
|
+
stateCacheVertexArrayBinds: stateCacheStats.byOperation.bindVertexArray?.issued ?? 0,
|
|
1101
|
+
stateCacheSamplerBinds: stateCacheStats.byOperation.bindSampler?.issued ?? 0,
|
|
1102
|
+
disposedBuffers: [...this.buffers].filter((buffer) => buffer.disposed).length,
|
|
1103
|
+
disposedShaders: [...this.shaders].filter((shader) => shader.disposed).length,
|
|
1104
|
+
disposedRenderTargets: [...this.renderTargets].filter((target) => target.disposed).length,
|
|
1105
|
+
disposedTextures: [...this.renderTargets].filter((target) => target.colorTexture.disposed).length + this.releasedTextureHandles,
|
|
1106
|
+
...spreadGpuTargetInventory(gpuTargetEntries),
|
|
1107
|
+
lastError: this.lastError,
|
|
1108
|
+
contextLost: this.contextLost
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
dispose() {
|
|
1112
|
+
if (this.canvas && "removeEventListener" in this.canvas) {
|
|
1113
|
+
this.deviceLostListeners.clear();
|
|
1114
|
+
this.deviceRestoredListeners.clear();
|
|
1115
|
+
if (this.contextLostListener)
|
|
1116
|
+
this.canvas.removeEventListener("webglcontextlost", this.contextLostListener);
|
|
1117
|
+
if (this.contextRestoredListener)
|
|
1118
|
+
this.canvas.removeEventListener("webglcontextrestored", this.contextRestoredListener);
|
|
1119
|
+
}
|
|
1120
|
+
for (const buffer of this.buffers) {
|
|
1121
|
+
buffer.dispose();
|
|
1122
|
+
}
|
|
1123
|
+
for (const shader of this.shaders) {
|
|
1124
|
+
shader.dispose();
|
|
1125
|
+
}
|
|
1126
|
+
for (const target of this.renderTargets) {
|
|
1127
|
+
target.dispose();
|
|
1128
|
+
}
|
|
1129
|
+
for (const entry of this.vertexArrayCache.values()) {
|
|
1130
|
+
this.gl.deleteVertexArray(entry.handle);
|
|
1131
|
+
}
|
|
1132
|
+
this.vertexArrayCache.clear();
|
|
1133
|
+
for (const sampler of this.samplerObjectCache.values()) {
|
|
1134
|
+
this.gl.deleteSampler(sampler);
|
|
1135
|
+
}
|
|
1136
|
+
this.samplerObjectCache.clear();
|
|
1137
|
+
for (const texture of this.textures.values()) {
|
|
1138
|
+
this.gl.deleteTexture(texture);
|
|
1139
|
+
}
|
|
1140
|
+
this.textureUploadModes.clear();
|
|
1141
|
+
if (this.fallbackTexture) {
|
|
1142
|
+
this.gl.deleteTexture(this.fallbackTexture);
|
|
1143
|
+
this.fallbackTexture = null;
|
|
1144
|
+
}
|
|
1145
|
+
if (this.fallbackCubeTexture) {
|
|
1146
|
+
this.gl.deleteTexture(this.fallbackCubeTexture);
|
|
1147
|
+
this.fallbackCubeTexture = null;
|
|
1148
|
+
}
|
|
1149
|
+
if (this.presentationProgram) {
|
|
1150
|
+
this.gl.deleteProgram(this.presentationProgram);
|
|
1151
|
+
this.presentationProgram = null;
|
|
1152
|
+
}
|
|
1153
|
+
if (this.ldrPostprocessProgram) {
|
|
1154
|
+
this.gl.deleteProgram(this.ldrPostprocessProgram);
|
|
1155
|
+
this.ldrPostprocessProgram = null;
|
|
1156
|
+
}
|
|
1157
|
+
if (this.bloomBrightExtractProgram) {
|
|
1158
|
+
this.gl.deleteProgram(this.bloomBrightExtractProgram);
|
|
1159
|
+
this.bloomBrightExtractProgram = null;
|
|
1160
|
+
}
|
|
1161
|
+
if (this.bloomBlurProgram) {
|
|
1162
|
+
this.gl.deleteProgram(this.bloomBlurProgram);
|
|
1163
|
+
this.bloomBlurProgram = null;
|
|
1164
|
+
}
|
|
1165
|
+
if (this.bloomCompositeProgram) {
|
|
1166
|
+
this.gl.deleteProgram(this.bloomCompositeProgram);
|
|
1167
|
+
this.bloomCompositeProgram = null;
|
|
1168
|
+
}
|
|
1169
|
+
if (this.bloomDownsampleProgram) {
|
|
1170
|
+
this.gl.deleteProgram(this.bloomDownsampleProgram);
|
|
1171
|
+
this.bloomDownsampleProgram = null;
|
|
1172
|
+
}
|
|
1173
|
+
if (this.bloomAccumulateProgram) {
|
|
1174
|
+
this.gl.deleteProgram(this.bloomAccumulateProgram);
|
|
1175
|
+
this.bloomAccumulateProgram = null;
|
|
1176
|
+
}
|
|
1177
|
+
if (this.outlineProgram) {
|
|
1178
|
+
this.gl.deleteProgram(this.outlineProgram);
|
|
1179
|
+
this.outlineProgram = null;
|
|
1180
|
+
}
|
|
1181
|
+
if (this.ssaoProgram) {
|
|
1182
|
+
this.gl.deleteProgram(this.ssaoProgram);
|
|
1183
|
+
this.ssaoProgram = null;
|
|
1184
|
+
}
|
|
1185
|
+
if (this.ssrProgram) {
|
|
1186
|
+
this.gl.deleteProgram(this.ssrProgram);
|
|
1187
|
+
this.ssrProgram = null;
|
|
1188
|
+
}
|
|
1189
|
+
if (this.depthOfFieldProgram) {
|
|
1190
|
+
this.gl.deleteProgram(this.depthOfFieldProgram);
|
|
1191
|
+
this.depthOfFieldProgram = null;
|
|
1192
|
+
}
|
|
1193
|
+
if (this.motionBlurProgram) {
|
|
1194
|
+
this.gl.deleteProgram(this.motionBlurProgram);
|
|
1195
|
+
this.motionBlurProgram = null;
|
|
1196
|
+
}
|
|
1197
|
+
if (this.motionBlurVelocityTexture) {
|
|
1198
|
+
this.gl.deleteTexture(this.motionBlurVelocityTexture);
|
|
1199
|
+
this.motionBlurVelocityTexture = null;
|
|
1200
|
+
this.motionBlurVelocitySize = null;
|
|
1201
|
+
}
|
|
1202
|
+
if (this.taaProgram) {
|
|
1203
|
+
this.gl.deleteProgram(this.taaProgram);
|
|
1204
|
+
this.taaProgram = null;
|
|
1205
|
+
}
|
|
1206
|
+
if (this.taaHistoryTexture) {
|
|
1207
|
+
this.gl.deleteTexture(this.taaHistoryTexture);
|
|
1208
|
+
this.taaHistoryTexture = null;
|
|
1209
|
+
this.taaHistorySize = null;
|
|
1210
|
+
}
|
|
1211
|
+
this.disposeBloomPingPongResources();
|
|
1212
|
+
this.disposeBloomPyramidResources();
|
|
1213
|
+
if (this.bloomBrightLutTexture) {
|
|
1214
|
+
this.gl.deleteTexture(this.bloomBrightLutTexture);
|
|
1215
|
+
this.bloomBrightLutTexture = null;
|
|
1216
|
+
this.bloomBrightLutThreshold = null;
|
|
1217
|
+
}
|
|
1218
|
+
if (this.bloomCompositeLutTexture) {
|
|
1219
|
+
this.gl.deleteTexture(this.bloomCompositeLutTexture);
|
|
1220
|
+
this.bloomCompositeLutTexture = null;
|
|
1221
|
+
this.bloomCompositeLutIntensity = null;
|
|
1222
|
+
this.bloomCompositeLutShoulder = null;
|
|
1223
|
+
}
|
|
1224
|
+
if (this.outlineBlendLutTexture) {
|
|
1225
|
+
this.gl.deleteTexture(this.outlineBlendLutTexture);
|
|
1226
|
+
this.outlineBlendLutTexture = null;
|
|
1227
|
+
this.outlineBlendLutKey = null;
|
|
1228
|
+
}
|
|
1229
|
+
if (this.presentationVertexArray) {
|
|
1230
|
+
this.gl.deleteVertexArray(this.presentationVertexArray);
|
|
1231
|
+
this.presentationVertexArray = null;
|
|
1232
|
+
}
|
|
1233
|
+
if (this.depthReadbackProgram) {
|
|
1234
|
+
this.gl.deleteProgram(this.depthReadbackProgram);
|
|
1235
|
+
this.depthReadbackProgram = null;
|
|
1236
|
+
}
|
|
1237
|
+
this.textures.clear();
|
|
1238
|
+
this.disposed = true;
|
|
1239
|
+
}
|
|
1240
|
+
copyDepthTextureToBytes(source, x, y, width, height) {
|
|
1241
|
+
const colorHandle = this.gl.createTexture();
|
|
1242
|
+
const framebuffer = this.gl.createFramebuffer();
|
|
1243
|
+
if (!colorHandle || !framebuffer) {
|
|
1244
|
+
if (colorHandle)
|
|
1245
|
+
this.gl.deleteTexture(colorHandle);
|
|
1246
|
+
if (framebuffer)
|
|
1247
|
+
this.gl.deleteFramebuffer(framebuffer);
|
|
1248
|
+
throw new RenderDeviceError("Failed to allocate WebGL2 depth readback target", "WEBGL_ALLOCATION_FAILED", {
|
|
1249
|
+
renderTarget: source.label
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING);
|
|
1253
|
+
const previousProgram = this.gl.getParameter(this.gl.CURRENT_PROGRAM);
|
|
1254
|
+
const textureUnit0 = this.captureTextureUnit0();
|
|
1255
|
+
const viewport = this.gl.getParameter(this.gl.VIEWPORT);
|
|
1256
|
+
const depthTestEnabled = this.gl.isEnabled(this.gl.DEPTH_TEST);
|
|
1257
|
+
const depthMask = this.gl.getParameter(this.gl.DEPTH_WRITEMASK);
|
|
1258
|
+
const cullFaceEnabled = this.gl.isEnabled(this.gl.CULL_FACE);
|
|
1259
|
+
const blendEnabled = this.gl.isEnabled(this.gl.BLEND);
|
|
1260
|
+
const pixels = new Uint8Array(width * height * 4);
|
|
1261
|
+
try {
|
|
1262
|
+
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
1263
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, colorHandle);
|
|
1264
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
|
|
1265
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
|
|
1266
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
1267
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
1268
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA8, width, height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, null);
|
|
1269
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
|
|
1270
|
+
this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, colorHandle, 0);
|
|
1271
|
+
const status = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
|
|
1272
|
+
if (status !== this.gl.FRAMEBUFFER_COMPLETE) {
|
|
1273
|
+
throw new RenderDeviceError("WebGL2 depth readback framebuffer status is invalid", "FRAMEBUFFER_INVALID", {
|
|
1274
|
+
renderTarget: source.label,
|
|
1275
|
+
status
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
this.gl.viewport(0, 0, width, height);
|
|
1279
|
+
this.gl.disable(this.gl.DEPTH_TEST);
|
|
1280
|
+
this.gl.disable(this.gl.CULL_FACE);
|
|
1281
|
+
this.gl.disable(this.gl.BLEND);
|
|
1282
|
+
this.gl.depthMask(false);
|
|
1283
|
+
this.gl.useProgram(this.ensureDepthReadbackProgram());
|
|
1284
|
+
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
1285
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, source.depthTextureHandle);
|
|
1286
|
+
this.gl.bindSampler(0, null);
|
|
1287
|
+
this.gl.uniform1i(this.gl.getUniformLocation(this.ensureDepthReadbackProgram(), "u_depth"), 0);
|
|
1288
|
+
this.gl.uniform4f(this.gl.getUniformLocation(this.ensureDepthReadbackProgram(), "u_sourceRect"), x / source.width, y / source.height, width / source.width, height / source.height);
|
|
1289
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1290
|
+
this.gl.readPixels(0, 0, width, height, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
|
|
1291
|
+
const error = this.readError();
|
|
1292
|
+
if (error) {
|
|
1293
|
+
throw new RenderDeviceError("WebGL2 depth texture readback failed", "WEBGL_DEPTH_READBACK_FAILED", {
|
|
1294
|
+
renderTarget: source.label,
|
|
1295
|
+
error
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
return pixels;
|
|
1299
|
+
}
|
|
1300
|
+
finally {
|
|
1301
|
+
this.restoreTextureUnit0(textureUnit0);
|
|
1302
|
+
this.gl.useProgram(previousProgram);
|
|
1303
|
+
if (depthTestEnabled)
|
|
1304
|
+
this.gl.enable(this.gl.DEPTH_TEST);
|
|
1305
|
+
else
|
|
1306
|
+
this.gl.disable(this.gl.DEPTH_TEST);
|
|
1307
|
+
if (cullFaceEnabled)
|
|
1308
|
+
this.gl.enable(this.gl.CULL_FACE);
|
|
1309
|
+
else
|
|
1310
|
+
this.gl.disable(this.gl.CULL_FACE);
|
|
1311
|
+
if (blendEnabled)
|
|
1312
|
+
this.gl.enable(this.gl.BLEND);
|
|
1313
|
+
else
|
|
1314
|
+
this.gl.disable(this.gl.BLEND);
|
|
1315
|
+
this.gl.depthMask(depthMask);
|
|
1316
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
|
|
1317
|
+
this.gl.viewport(viewport[0] ?? 0, viewport[1] ?? 0, viewport[2] ?? this.viewportWidth, viewport[3] ?? this.viewportHeight);
|
|
1318
|
+
this.gl.deleteTexture(colorHandle);
|
|
1319
|
+
this.gl.deleteFramebuffer(framebuffer);
|
|
1320
|
+
this.stateCache.invalidate();
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
drawRenderTargetToBackbuffer(source, outputWidth, outputHeight) {
|
|
1324
|
+
const program = this.ensurePresentationProgram();
|
|
1325
|
+
const vertexArray = this.ensurePresentationVertexArray();
|
|
1326
|
+
const previousState = this.prepareFullscreenPresentation(null, outputWidth, outputHeight);
|
|
1327
|
+
this.gl.useProgram(program);
|
|
1328
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1329
|
+
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
1330
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, source.colorHandle);
|
|
1331
|
+
this.gl.bindSampler(0, null);
|
|
1332
|
+
try {
|
|
1333
|
+
const uniformLocation = this.gl.getUniformLocation(program, "u_source");
|
|
1334
|
+
this.gl.uniform1i(uniformLocation, 0);
|
|
1335
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1336
|
+
this.gl.flush();
|
|
1337
|
+
}
|
|
1338
|
+
finally {
|
|
1339
|
+
this.restoreFullscreenPresentationState(previousState, outputWidth, outputHeight);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
ensureBloomPingPongResources(width, height, hdr = false) {
|
|
1343
|
+
const current = this.bloomPingPongResources;
|
|
1344
|
+
if (current && current.width === width && current.height === height && current.hdr === hdr) {
|
|
1345
|
+
return current;
|
|
1346
|
+
}
|
|
1347
|
+
this.disposeBloomPingPongResources();
|
|
1348
|
+
this.disposeBloomPyramidResources();
|
|
1349
|
+
const textureUnit0 = this.captureTextureUnit0();
|
|
1350
|
+
const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING);
|
|
1351
|
+
const textures = [];
|
|
1352
|
+
const framebuffers = [];
|
|
1353
|
+
try {
|
|
1354
|
+
for (let index = 0; index < 2; index += 1) {
|
|
1355
|
+
const texture = this.gl.createTexture();
|
|
1356
|
+
const framebuffer = this.gl.createFramebuffer();
|
|
1357
|
+
if (!texture || !framebuffer) {
|
|
1358
|
+
if (texture)
|
|
1359
|
+
this.gl.deleteTexture(texture);
|
|
1360
|
+
if (framebuffer)
|
|
1361
|
+
this.gl.deleteFramebuffer(framebuffer);
|
|
1362
|
+
throw new RenderDeviceError("Failed to allocate WebGL2 bloom ping-pong resources", "WEBGL_ALLOCATION_FAILED", {
|
|
1363
|
+
width,
|
|
1364
|
+
height,
|
|
1365
|
+
index
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
textures.push(texture);
|
|
1369
|
+
framebuffers.push(framebuffer);
|
|
1370
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
|
|
1371
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
|
|
1372
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
|
|
1373
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
1374
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
1375
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, hdr ? this.gl.RGBA16F : this.gl.RGBA8, width, height, 0, this.gl.RGBA, hdr ? this.gl.HALF_FLOAT : this.gl.UNSIGNED_BYTE, null);
|
|
1376
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
|
|
1377
|
+
this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, texture, 0);
|
|
1378
|
+
const status = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
|
|
1379
|
+
if (status !== this.gl.FRAMEBUFFER_COMPLETE) {
|
|
1380
|
+
throw new RenderDeviceError("WebGL2 bloom ping-pong framebuffer status is invalid", "FRAMEBUFFER_INVALID", {
|
|
1381
|
+
width,
|
|
1382
|
+
height,
|
|
1383
|
+
index,
|
|
1384
|
+
status
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
const resources = {
|
|
1389
|
+
width,
|
|
1390
|
+
height,
|
|
1391
|
+
hdr,
|
|
1392
|
+
textures: [textures[0], textures[1]],
|
|
1393
|
+
framebuffers: [framebuffers[0], framebuffers[1]]
|
|
1394
|
+
};
|
|
1395
|
+
this.bloomPingPongResources = resources;
|
|
1396
|
+
return resources;
|
|
1397
|
+
}
|
|
1398
|
+
catch (error) {
|
|
1399
|
+
for (const texture of textures)
|
|
1400
|
+
this.gl.deleteTexture(texture);
|
|
1401
|
+
for (const framebuffer of framebuffers)
|
|
1402
|
+
this.gl.deleteFramebuffer(framebuffer);
|
|
1403
|
+
throw error;
|
|
1404
|
+
}
|
|
1405
|
+
finally {
|
|
1406
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
|
|
1407
|
+
this.restoreTextureUnit0(textureUnit0);
|
|
1408
|
+
this.stateCache.invalidate();
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
ensureBloomLutTextures(options) {
|
|
1412
|
+
const updateBright = this.bloomBrightLutThreshold !== options.threshold;
|
|
1413
|
+
const updateComposite = this.bloomCompositeLutIntensity !== options.intensity ||
|
|
1414
|
+
this.bloomCompositeLutShoulder !== options.shoulder;
|
|
1415
|
+
if (this.bloomBrightLutTexture && this.bloomCompositeLutTexture && !updateBright && !updateComposite) {
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const textureUnit0 = this.captureTextureUnit0();
|
|
1419
|
+
try {
|
|
1420
|
+
if (!this.bloomBrightLutTexture) {
|
|
1421
|
+
this.bloomBrightLutTexture = this.createNativeBloomDataTexture();
|
|
1422
|
+
}
|
|
1423
|
+
if (updateBright) {
|
|
1424
|
+
const pixels = createBloomBrightThresholdLut(options.threshold);
|
|
1425
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.bloomBrightLutTexture);
|
|
1426
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA8, BLOOM_BRIGHT_LUT_WIDTH, BLOOM_BRIGHT_LUT_HEIGHT, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
|
|
1427
|
+
this.bloomBrightLutThreshold = options.threshold;
|
|
1428
|
+
}
|
|
1429
|
+
if (!this.bloomCompositeLutTexture) {
|
|
1430
|
+
this.bloomCompositeLutTexture = this.createNativeBloomDataTexture();
|
|
1431
|
+
}
|
|
1432
|
+
if (updateComposite) {
|
|
1433
|
+
const pixels = createBloomCompositeLut(options.intensity, { shoulder: options.shoulder });
|
|
1434
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.bloomCompositeLutTexture);
|
|
1435
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA8, BLOOM_COMPOSITE_LUT_SIZE, BLOOM_COMPOSITE_LUT_SIZE, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixels);
|
|
1436
|
+
this.bloomCompositeLutIntensity = options.intensity;
|
|
1437
|
+
this.bloomCompositeLutShoulder = options.shoulder;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
finally {
|
|
1441
|
+
this.restoreTextureUnit0(textureUnit0);
|
|
1442
|
+
this.stateCache.invalidate();
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
createNativeBloomDataTexture() {
|
|
1446
|
+
const texture = this.gl.createTexture();
|
|
1447
|
+
if (!texture) {
|
|
1448
|
+
throw new RenderDeviceError("Failed to allocate WebGL2 bloom lookup texture", "WEBGL_ALLOCATION_FAILED");
|
|
1449
|
+
}
|
|
1450
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
|
|
1451
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
|
|
1452
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
|
|
1453
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
1454
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
1455
|
+
return texture;
|
|
1456
|
+
}
|
|
1457
|
+
ensureOutlineBlendLutTexture(options) {
|
|
1458
|
+
const combinedAlpha = options.opacity * (options.color[3] / 255);
|
|
1459
|
+
const key = `${options.color.join(",")}|${combinedAlpha}`;
|
|
1460
|
+
if (this.outlineBlendLutTexture && this.outlineBlendLutKey === key)
|
|
1461
|
+
return;
|
|
1462
|
+
const textureUnit0 = this.captureTextureUnit0();
|
|
1463
|
+
try {
|
|
1464
|
+
if (!this.outlineBlendLutTexture) {
|
|
1465
|
+
this.outlineBlendLutTexture = this.createNativeBloomDataTexture();
|
|
1466
|
+
}
|
|
1467
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.outlineBlendLutTexture);
|
|
1468
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA8, OUTLINE_BLEND_LUT_WIDTH, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, createOutlineBlendLut(options.color, combinedAlpha));
|
|
1469
|
+
this.outlineBlendLutKey = key;
|
|
1470
|
+
}
|
|
1471
|
+
finally {
|
|
1472
|
+
this.restoreTextureUnit0(textureUnit0);
|
|
1473
|
+
this.stateCache.invalidate();
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
ensureMotionBlurVelocityTexture(width, height, velocity) {
|
|
1477
|
+
const textureUnit0 = this.captureTextureUnit0();
|
|
1478
|
+
try {
|
|
1479
|
+
if (!this.motionBlurVelocityTexture) {
|
|
1480
|
+
this.motionBlurVelocityTexture = this.createNativeBloomDataTexture();
|
|
1481
|
+
}
|
|
1482
|
+
this.motionBlurVelocitySize = { width, height };
|
|
1483
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.motionBlurVelocityTexture);
|
|
1484
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RG32F, width, height, 0, this.gl.RG, this.gl.FLOAT, velocity);
|
|
1485
|
+
}
|
|
1486
|
+
finally {
|
|
1487
|
+
this.restoreTextureUnit0(textureUnit0);
|
|
1488
|
+
this.stateCache.invalidate();
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
ensureTaaHistoryTexture(width, height, history) {
|
|
1492
|
+
const textureUnit0 = this.captureTextureUnit0();
|
|
1493
|
+
try {
|
|
1494
|
+
if (!this.taaHistoryTexture) {
|
|
1495
|
+
this.taaHistoryTexture = this.createNativeBloomDataTexture();
|
|
1496
|
+
}
|
|
1497
|
+
this.taaHistorySize = { width, height };
|
|
1498
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, this.taaHistoryTexture);
|
|
1499
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA8, width, height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, history);
|
|
1500
|
+
}
|
|
1501
|
+
finally {
|
|
1502
|
+
this.restoreTextureUnit0(textureUnit0);
|
|
1503
|
+
this.stateCache.invalidate();
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
drawNativeLdrStage(sourceTexture, framebuffer, width, height, program, vertexArray, tonePass, colorPass, fxaaPass, toneMappingDefaults) {
|
|
1507
|
+
const toneOptions = { ...(toneMappingDefaults ?? {}), ...(tonePass?.options ?? {}) };
|
|
1508
|
+
const colorOptions = colorPass?.options ?? {};
|
|
1509
|
+
const fxaaOptions = fxaaPass?.options ?? {};
|
|
1510
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
|
|
1511
|
+
this.gl.viewport(0, 0, width, height);
|
|
1512
|
+
this.gl.useProgram(program);
|
|
1513
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1514
|
+
this.bindFullscreenTexture(0, sourceTexture);
|
|
1515
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
|
|
1516
|
+
this.gl.uniform2f(this.gl.getUniformLocation(program, "u_texelSize"), 1 / width, 1 / height);
|
|
1517
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_hasToneMapping"), tonePass ? 1 : 0);
|
|
1518
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_toneOperator"), toneMappingOperatorId(stringOption(toneOptions, "operator", "reinhard")));
|
|
1519
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_inputColorSpace"), colorSpaceId(stringOption(toneOptions, "inputColorSpace", "linear")));
|
|
1520
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_outputColorSpace"), colorSpaceId(stringOption(toneOptions, "outputColorSpace", "linear")));
|
|
1521
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_exposure"), numberOption(toneOptions, "exposure", 1));
|
|
1522
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_whitePoint"), Math.max(0.0001, numberOption(toneOptions, "whitePoint", 1)));
|
|
1523
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_gamma"), Math.max(0.0001, numberOption(toneOptions, "gamma", 2.2)));
|
|
1524
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_hasColorGrade"), colorPass ? 1 : 0);
|
|
1525
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_contrast"), numberOption(colorOptions, "contrast", 1));
|
|
1526
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_temperature"), numberOption(colorOptions, "temperature", 0));
|
|
1527
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_tint"), numberOption(colorOptions, "tint", 0));
|
|
1528
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_saturation"), numberOption(colorOptions, "saturation", 1));
|
|
1529
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_vibrance"), numberOption(colorOptions, "vibrance", 0));
|
|
1530
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_vignette"), numberOption(colorOptions, "vignette", 0));
|
|
1531
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_sharpening"), numberOption(colorOptions, "sharpening", 0));
|
|
1532
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_hasFxaa"), fxaaPass ? 1 : 0);
|
|
1533
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_edgeThreshold"), numberOption(fxaaOptions, "edgeThreshold", 0.125));
|
|
1534
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_subpixelBlend"), numberOption(fxaaOptions, "subpixelBlend", 0.75));
|
|
1535
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1536
|
+
}
|
|
1537
|
+
executeNativeBloomPasses(sourceTexture, resources, options, vertexArray, hdr) {
|
|
1538
|
+
const brightLut = this.bloomBrightLutTexture;
|
|
1539
|
+
const compositeLut = this.bloomCompositeLutTexture;
|
|
1540
|
+
if (!hdr && (!brightLut || !compositeLut)) {
|
|
1541
|
+
throw new RenderDeviceError("WebGL2 bloom lookup textures were not initialized", "WEBGL_ALLOCATION_FAILED");
|
|
1542
|
+
}
|
|
1543
|
+
const [textureA, textureB] = resources.textures;
|
|
1544
|
+
const [framebufferA, framebufferB] = resources.framebuffers;
|
|
1545
|
+
const brightProgram = this.ensureBloomBrightExtractProgram();
|
|
1546
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebufferA);
|
|
1547
|
+
this.gl.viewport(0, 0, resources.width, resources.height);
|
|
1548
|
+
this.gl.useProgram(brightProgram);
|
|
1549
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1550
|
+
this.bindFullscreenTexture(0, sourceTexture);
|
|
1551
|
+
if (brightLut)
|
|
1552
|
+
this.bindFullscreenTexture(1, brightLut);
|
|
1553
|
+
this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_source"), 0);
|
|
1554
|
+
this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_brightLut"), 1);
|
|
1555
|
+
this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_hdr"), hdr ? 1 : 0);
|
|
1556
|
+
this.gl.uniform1f(this.gl.getUniformLocation(brightProgram, "u_threshold"), options.threshold);
|
|
1557
|
+
this.gl.uniform1f(this.gl.getUniformLocation(brightProgram, "u_softKnee"), options.softKnee);
|
|
1558
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1559
|
+
const blurProgram = this.ensureBloomBlurProgram();
|
|
1560
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebufferB);
|
|
1561
|
+
this.gl.useProgram(blurProgram);
|
|
1562
|
+
this.bindFullscreenTexture(0, textureA);
|
|
1563
|
+
this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_source"), 0);
|
|
1564
|
+
this.gl.uniform2i(this.gl.getUniformLocation(blurProgram, "u_size"), resources.width, resources.height);
|
|
1565
|
+
this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_radius"), options.radius);
|
|
1566
|
+
this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_horizontal"), 1);
|
|
1567
|
+
this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_hdr"), hdr ? 1 : 0);
|
|
1568
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1569
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebufferA);
|
|
1570
|
+
this.bindFullscreenTexture(0, textureB);
|
|
1571
|
+
this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_horizontal"), 0);
|
|
1572
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1573
|
+
const compositeProgram = this.ensureBloomCompositeProgram();
|
|
1574
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebufferB);
|
|
1575
|
+
this.gl.useProgram(compositeProgram);
|
|
1576
|
+
this.bindFullscreenTexture(0, sourceTexture);
|
|
1577
|
+
this.bindFullscreenTexture(1, textureA);
|
|
1578
|
+
if (compositeLut)
|
|
1579
|
+
this.bindFullscreenTexture(2, compositeLut);
|
|
1580
|
+
this.gl.uniform1i(this.gl.getUniformLocation(compositeProgram, "u_source"), 0);
|
|
1581
|
+
this.gl.uniform1i(this.gl.getUniformLocation(compositeProgram, "u_blurred"), 1);
|
|
1582
|
+
this.gl.uniform1i(this.gl.getUniformLocation(compositeProgram, "u_compositeLut"), 2);
|
|
1583
|
+
this.gl.uniform1i(this.gl.getUniformLocation(compositeProgram, "u_hdr"), hdr ? 1 : 0);
|
|
1584
|
+
this.gl.uniform1f(this.gl.getUniformLocation(compositeProgram, "u_intensity"), options.intensity);
|
|
1585
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1586
|
+
return textureB;
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* Balanced/cinematic bloom: bright-extract once, then a half-resolution mip
|
|
1590
|
+
* chain where every level is downsampled, separably blurred, and accumulated
|
|
1591
|
+
* with its energy-preserving weight. Returns a full-resolution bloom texture
|
|
1592
|
+
* holding the weighted sum, ready for the existing composite program.
|
|
1593
|
+
*/
|
|
1594
|
+
executeNativeBloomPyramidPasses(sourceTexture, sourceWidth, sourceHeight, resources, fullResStaging, options, vertexArray, hdr) {
|
|
1595
|
+
const plan = resources.plan;
|
|
1596
|
+
const mipCount = plan.mips.length;
|
|
1597
|
+
const blurRadii = resolveBloomPyramidBlurRadii(plan, options.radius);
|
|
1598
|
+
const downsampleProgram = this.ensureBloomDownsampleProgram();
|
|
1599
|
+
const brightProgram = this.ensureBloomBrightExtractProgram();
|
|
1600
|
+
const blurProgram = this.ensureBloomBlurProgram();
|
|
1601
|
+
const accumulateProgram = this.ensureBloomAccumulateProgram();
|
|
1602
|
+
const brightLut = this.bloomBrightLutTexture;
|
|
1603
|
+
const mipTexture = (level, slot) => resources.textures[level * 2 + slot];
|
|
1604
|
+
const mipFramebuffer = (level, slot) => resources.framebuffers[level * 2 + slot];
|
|
1605
|
+
const mipSize = (level) => plan.mips[level];
|
|
1606
|
+
// Every level settles in slot 0 after the vertical blur.
|
|
1607
|
+
const blurredSlot = (_level) => 0;
|
|
1608
|
+
const drawDownsample = (fromTexture, fromWidth, fromHeight, toFramebuffer, toWidth, toHeight) => {
|
|
1609
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, toFramebuffer);
|
|
1610
|
+
this.gl.viewport(0, 0, toWidth, toHeight);
|
|
1611
|
+
this.gl.useProgram(downsampleProgram);
|
|
1612
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1613
|
+
this.bindFullscreenTexture(0, fromTexture);
|
|
1614
|
+
this.gl.uniform1i(this.gl.getUniformLocation(downsampleProgram, "u_source"), 0);
|
|
1615
|
+
this.gl.uniform2f(this.gl.getUniformLocation(downsampleProgram, "u_texelSize"), 1 / fromWidth, 1 / fromHeight);
|
|
1616
|
+
this.gl.uniform2f(this.gl.getUniformLocation(downsampleProgram, "u_targetSize"), toWidth, toHeight);
|
|
1617
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1618
|
+
};
|
|
1619
|
+
const drawBrightExtract = (fromTexture, toFramebuffer, size) => {
|
|
1620
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, toFramebuffer);
|
|
1621
|
+
this.gl.viewport(0, 0, size.width, size.height);
|
|
1622
|
+
this.gl.useProgram(brightProgram);
|
|
1623
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1624
|
+
this.bindFullscreenTexture(0, fromTexture);
|
|
1625
|
+
if (brightLut)
|
|
1626
|
+
this.bindFullscreenTexture(1, brightLut);
|
|
1627
|
+
this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_source"), 0);
|
|
1628
|
+
this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_brightLut"), 1);
|
|
1629
|
+
this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_hdr"), hdr ? 1 : 0);
|
|
1630
|
+
this.gl.uniform1f(this.gl.getUniformLocation(brightProgram, "u_threshold"), options.threshold);
|
|
1631
|
+
this.gl.uniform1f(this.gl.getUniformLocation(brightProgram, "u_softKnee"), options.softKnee);
|
|
1632
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1633
|
+
};
|
|
1634
|
+
const drawSeparableBlur = (readTexture, tmpTexture, tmpFramebuffer, outFramebuffer, size, radius) => {
|
|
1635
|
+
// Horizontal into the temp target, vertical back into the output target.
|
|
1636
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, tmpFramebuffer);
|
|
1637
|
+
this.gl.viewport(0, 0, size.width, size.height);
|
|
1638
|
+
this.gl.useProgram(blurProgram);
|
|
1639
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1640
|
+
this.bindFullscreenTexture(0, readTexture);
|
|
1641
|
+
this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_source"), 0);
|
|
1642
|
+
this.gl.uniform2i(this.gl.getUniformLocation(blurProgram, "u_size"), size.width, size.height);
|
|
1643
|
+
this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_radius"), radius);
|
|
1644
|
+
this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_horizontal"), 1);
|
|
1645
|
+
this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_hdr"), hdr ? 1 : 0);
|
|
1646
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1647
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, outFramebuffer);
|
|
1648
|
+
this.bindFullscreenTexture(0, tmpTexture);
|
|
1649
|
+
this.gl.uniform1i(this.gl.getUniformLocation(blurProgram, "u_horizontal"), 0);
|
|
1650
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1651
|
+
};
|
|
1652
|
+
// Extract highlights at the source resolution before the first downsample.
|
|
1653
|
+
// Downsampling the complete scene first averaged narrow emissive sources
|
|
1654
|
+
// below the authored threshold, so balanced/cinematic bloom could execute
|
|
1655
|
+
// all native passes yet remain pixel-identical to disabled output.
|
|
1656
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, fullResStaging.framebufferA);
|
|
1657
|
+
this.gl.viewport(0, 0, sourceWidth, sourceHeight);
|
|
1658
|
+
this.gl.useProgram(brightProgram);
|
|
1659
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1660
|
+
this.bindFullscreenTexture(0, sourceTexture);
|
|
1661
|
+
if (brightLut)
|
|
1662
|
+
this.bindFullscreenTexture(1, brightLut);
|
|
1663
|
+
this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_source"), 0);
|
|
1664
|
+
this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_brightLut"), 1);
|
|
1665
|
+
this.gl.uniform1i(this.gl.getUniformLocation(brightProgram, "u_hdr"), hdr ? 1 : 0);
|
|
1666
|
+
this.gl.uniform1f(this.gl.getUniformLocation(brightProgram, "u_threshold"), options.threshold);
|
|
1667
|
+
this.gl.uniform1f(this.gl.getUniformLocation(brightProgram, "u_softKnee"), options.softKnee);
|
|
1668
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1669
|
+
// Level 0 downsamples the extracted highlight field, then blurs H into
|
|
1670
|
+
// slot 1 and V back into slot 0. The result therefore settles in slot 0.
|
|
1671
|
+
const level0 = mipSize(0);
|
|
1672
|
+
drawDownsample(fullResStaging.textureA, sourceWidth, sourceHeight, mipFramebuffer(0, 0), level0.width, level0.height);
|
|
1673
|
+
drawSeparableBlur(mipTexture(0, 0), mipTexture(0, 1), mipFramebuffer(0, 1), mipFramebuffer(0, 0), level0, blurRadii[0]);
|
|
1674
|
+
for (let level = 1; level < mipCount; level += 1) {
|
|
1675
|
+
const size = mipSize(level);
|
|
1676
|
+
const previous = mipSize(level - 1);
|
|
1677
|
+
drawDownsample(mipTexture(level - 1, blurredSlot(level - 1)), previous.width, previous.height, mipFramebuffer(level, 0), size.width, size.height);
|
|
1678
|
+
// H into slot 1, V back into slot 0: blurred result settles in slot 0.
|
|
1679
|
+
drawSeparableBlur(mipTexture(level, 0), mipTexture(level, 1), mipFramebuffer(level, 1), mipFramebuffer(level, 0), size, blurRadii[level]);
|
|
1680
|
+
}
|
|
1681
|
+
// Accumulate smallest-to-largest, ping-ponging between the two
|
|
1682
|
+
// accumulators (a pass must never sample its own bound target).
|
|
1683
|
+
const weights = plan.weights;
|
|
1684
|
+
let readAccumulator = { texture: resources.accumulatorTextureA, framebuffer: resources.accumulatorFramebufferA };
|
|
1685
|
+
let writeAccumulator = { texture: resources.accumulatorTextureB, framebuffer: resources.accumulatorFramebufferB };
|
|
1686
|
+
// Clear both accumulators; the seed reads the cleared sibling, never the
|
|
1687
|
+
// bound target (feedback loop).
|
|
1688
|
+
for (const acc of [readAccumulator, writeAccumulator]) {
|
|
1689
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, acc.framebuffer);
|
|
1690
|
+
this.gl.viewport(0, 0, level0.width, level0.height);
|
|
1691
|
+
this.gl.clearColor(0, 0, 0, 0);
|
|
1692
|
+
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
1693
|
+
}
|
|
1694
|
+
// Seed: smallest mip onto the cleared sibling accumulator.
|
|
1695
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, writeAccumulator.framebuffer);
|
|
1696
|
+
this.gl.viewport(0, 0, level0.width, level0.height);
|
|
1697
|
+
this.gl.useProgram(accumulateProgram);
|
|
1698
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1699
|
+
this.bindFullscreenTexture(0, readAccumulator.texture);
|
|
1700
|
+
this.bindFullscreenTexture(1, mipTexture(mipCount - 1, blurredSlot(mipCount - 1)));
|
|
1701
|
+
this.gl.uniform1i(this.gl.getUniformLocation(accumulateProgram, "u_base"), 0);
|
|
1702
|
+
this.gl.uniform1i(this.gl.getUniformLocation(accumulateProgram, "u_bloom"), 1);
|
|
1703
|
+
this.gl.uniform1f(this.gl.getUniformLocation(accumulateProgram, "u_weight"), weights[mipCount - 1]);
|
|
1704
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1705
|
+
for (let level = mipCount - 2; level >= 0; level -= 1) {
|
|
1706
|
+
const nextRead = writeAccumulator;
|
|
1707
|
+
const nextWrite = readAccumulator;
|
|
1708
|
+
readAccumulator = nextRead;
|
|
1709
|
+
writeAccumulator = nextWrite;
|
|
1710
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, writeAccumulator.framebuffer);
|
|
1711
|
+
this.gl.viewport(0, 0, level0.width, level0.height);
|
|
1712
|
+
this.gl.useProgram(accumulateProgram);
|
|
1713
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1714
|
+
this.bindFullscreenTexture(0, readAccumulator.texture);
|
|
1715
|
+
this.bindFullscreenTexture(1, mipTexture(level, blurredSlot(level)));
|
|
1716
|
+
this.gl.uniform1i(this.gl.getUniformLocation(accumulateProgram, "u_base"), 0);
|
|
1717
|
+
this.gl.uniform1i(this.gl.getUniformLocation(accumulateProgram, "u_bloom"), 1);
|
|
1718
|
+
this.gl.uniform1f(this.gl.getUniformLocation(accumulateProgram, "u_weight"), weights[level]);
|
|
1719
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1720
|
+
}
|
|
1721
|
+
// Upsample the weighted sum onto full-resolution staging target A, using
|
|
1722
|
+
// the cleared staging target B as the zero base (never sample A while
|
|
1723
|
+
// rendering into it).
|
|
1724
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, fullResStaging.framebufferB);
|
|
1725
|
+
this.gl.viewport(0, 0, sourceWidth, sourceHeight);
|
|
1726
|
+
this.gl.clearColor(0, 0, 0, 0);
|
|
1727
|
+
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
|
|
1728
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, fullResStaging.framebufferA);
|
|
1729
|
+
this.gl.viewport(0, 0, sourceWidth, sourceHeight);
|
|
1730
|
+
this.gl.useProgram(accumulateProgram);
|
|
1731
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1732
|
+
this.bindFullscreenTexture(0, fullResStaging.textureB);
|
|
1733
|
+
this.bindFullscreenTexture(1, writeAccumulator.texture);
|
|
1734
|
+
this.gl.uniform1i(this.gl.getUniformLocation(accumulateProgram, "u_base"), 0);
|
|
1735
|
+
this.gl.uniform1i(this.gl.getUniformLocation(accumulateProgram, "u_bloom"), 1);
|
|
1736
|
+
this.gl.uniform1f(this.gl.getUniformLocation(accumulateProgram, "u_weight"), 1);
|
|
1737
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1738
|
+
return fullResStaging.textureA;
|
|
1739
|
+
}
|
|
1740
|
+
executeNativeOutlinePasses(sourceTexture, resources, options, vertexArray, targetIndex) {
|
|
1741
|
+
const blendLut = this.outlineBlendLutTexture;
|
|
1742
|
+
if (!blendLut) {
|
|
1743
|
+
throw new RenderDeviceError("WebGL2 outline lookup texture was not initialized", "WEBGL_ALLOCATION_FAILED");
|
|
1744
|
+
}
|
|
1745
|
+
const program = this.ensureOutlineProgram();
|
|
1746
|
+
const targetTexture = resources.textures[targetIndex];
|
|
1747
|
+
const targetFramebuffer = resources.framebuffers[targetIndex];
|
|
1748
|
+
const bound = createOutlineGradientBound(options.threshold);
|
|
1749
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, targetFramebuffer);
|
|
1750
|
+
this.gl.viewport(0, 0, resources.width, resources.height);
|
|
1751
|
+
this.gl.useProgram(program);
|
|
1752
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1753
|
+
this.bindFullscreenTexture(0, sourceTexture);
|
|
1754
|
+
this.bindFullscreenTexture(1, blendLut);
|
|
1755
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
|
|
1756
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_blendLut"), 1);
|
|
1757
|
+
this.gl.uniform2i(this.gl.getUniformLocation(program, "u_size"), resources.width, resources.height);
|
|
1758
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_width"), options.width);
|
|
1759
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_boundHigh"), bound.highWord);
|
|
1760
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_boundLow"), bound.lowWord);
|
|
1761
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1762
|
+
return targetTexture;
|
|
1763
|
+
}
|
|
1764
|
+
executeNativeSsaoPass(sourceTexture, depthTexture, resources, options, vertexArray, targetIndex) {
|
|
1765
|
+
const program = this.ensureSsaoProgram();
|
|
1766
|
+
const targetTexture = resources.textures[targetIndex];
|
|
1767
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, resources.framebuffers[targetIndex]);
|
|
1768
|
+
this.gl.viewport(0, 0, resources.width, resources.height);
|
|
1769
|
+
this.gl.useProgram(program);
|
|
1770
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1771
|
+
this.bindFullscreenTexture(0, sourceTexture);
|
|
1772
|
+
this.bindFullscreenTexture(1, depthTexture);
|
|
1773
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
|
|
1774
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_depth"), 1);
|
|
1775
|
+
this.gl.uniform2i(this.gl.getUniformLocation(program, "u_size"), resources.width, resources.height);
|
|
1776
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_radius"), options.radius);
|
|
1777
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_intensity"), options.intensity);
|
|
1778
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_bias"), options.bias);
|
|
1779
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1780
|
+
return targetTexture;
|
|
1781
|
+
}
|
|
1782
|
+
executeNativeSsrPass(sourceTexture, depthTexture, resources, options, vertexArray, targetIndex, depthRange) {
|
|
1783
|
+
const program = this.ensureSsrProgram();
|
|
1784
|
+
const targetTexture = resources.textures[targetIndex];
|
|
1785
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, resources.framebuffers[targetIndex]);
|
|
1786
|
+
this.gl.viewport(0, 0, resources.width, resources.height);
|
|
1787
|
+
this.gl.useProgram(program);
|
|
1788
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1789
|
+
this.bindFullscreenTexture(0, sourceTexture);
|
|
1790
|
+
this.bindFullscreenTexture(1, depthTexture);
|
|
1791
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
|
|
1792
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_depth"), 1);
|
|
1793
|
+
this.gl.uniform2i(this.gl.getUniformLocation(program, "u_size"), resources.width, resources.height);
|
|
1794
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_intensity"), options.intensity);
|
|
1795
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_maxDistance"), options.maxDistance);
|
|
1796
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_depthNear"), depthRange.near);
|
|
1797
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_depthFar"), depthRange.far);
|
|
1798
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1799
|
+
return targetTexture;
|
|
1800
|
+
}
|
|
1801
|
+
executeNativeDepthOfFieldPass(sourceTexture, depthTexture, resources, options, vertexArray, targetIndex, depthRange) {
|
|
1802
|
+
const program = this.ensureDepthOfFieldProgram();
|
|
1803
|
+
const targetTexture = resources.textures[targetIndex];
|
|
1804
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, resources.framebuffers[targetIndex]);
|
|
1805
|
+
this.gl.viewport(0, 0, resources.width, resources.height);
|
|
1806
|
+
this.gl.useProgram(program);
|
|
1807
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1808
|
+
this.bindFullscreenTexture(0, sourceTexture);
|
|
1809
|
+
this.bindFullscreenTexture(1, depthTexture);
|
|
1810
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
|
|
1811
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_depth"), 1);
|
|
1812
|
+
this.gl.uniform2i(this.gl.getUniformLocation(program, "u_size"), resources.width, resources.height);
|
|
1813
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_focusDepth"), options.focusDepth);
|
|
1814
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_focusRange"), options.focusRange);
|
|
1815
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_maxRadius"), options.maxRadius);
|
|
1816
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_depthNear"), depthRange.near);
|
|
1817
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_depthFar"), depthRange.far);
|
|
1818
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1819
|
+
return targetTexture;
|
|
1820
|
+
}
|
|
1821
|
+
executeNativeMotionBlurPass(sourceTexture, resources, options, vertexArray, targetIndex) {
|
|
1822
|
+
const velocityTexture = this.motionBlurVelocityTexture;
|
|
1823
|
+
if (!velocityTexture) {
|
|
1824
|
+
throw new RenderDeviceError("WebGL2 motion-blur velocity texture was not initialized", "WEBGL_ALLOCATION_FAILED");
|
|
1825
|
+
}
|
|
1826
|
+
const program = this.ensureMotionBlurProgram();
|
|
1827
|
+
const targetTexture = resources.textures[targetIndex];
|
|
1828
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, resources.framebuffers[targetIndex]);
|
|
1829
|
+
this.gl.viewport(0, 0, resources.width, resources.height);
|
|
1830
|
+
this.gl.useProgram(program);
|
|
1831
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1832
|
+
this.bindFullscreenTexture(0, sourceTexture);
|
|
1833
|
+
this.bindFullscreenTexture(1, velocityTexture);
|
|
1834
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
|
|
1835
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_velocity"), 1);
|
|
1836
|
+
this.gl.uniform2i(this.gl.getUniformLocation(program, "u_size"), resources.width, resources.height);
|
|
1837
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_samples"), options.samples);
|
|
1838
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_scale"), options.scale);
|
|
1839
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1840
|
+
return targetTexture;
|
|
1841
|
+
}
|
|
1842
|
+
executeNativeTaaPass(sourceTexture, resources, options, vertexArray, targetIndex) {
|
|
1843
|
+
const historyTexture = this.taaHistoryTexture;
|
|
1844
|
+
if (!historyTexture) {
|
|
1845
|
+
throw new RenderDeviceError("WebGL2 TAA history texture was not initialized", "WEBGL_ALLOCATION_FAILED");
|
|
1846
|
+
}
|
|
1847
|
+
const program = this.ensureTaaProgram();
|
|
1848
|
+
const targetTexture = resources.textures[targetIndex];
|
|
1849
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, resources.framebuffers[targetIndex]);
|
|
1850
|
+
this.gl.viewport(0, 0, resources.width, resources.height);
|
|
1851
|
+
this.gl.useProgram(program);
|
|
1852
|
+
this.gl.bindVertexArray(vertexArray);
|
|
1853
|
+
this.bindFullscreenTexture(0, sourceTexture);
|
|
1854
|
+
this.bindFullscreenTexture(1, historyTexture);
|
|
1855
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_source"), 0);
|
|
1856
|
+
this.gl.uniform1i(this.gl.getUniformLocation(program, "u_history"), 1);
|
|
1857
|
+
this.gl.uniform1f(this.gl.getUniformLocation(program, "u_blend"), options.blend);
|
|
1858
|
+
this.gl.drawArrays(this.gl.TRIANGLES, 0, 3);
|
|
1859
|
+
return targetTexture;
|
|
1860
|
+
}
|
|
1861
|
+
bindFullscreenTexture(unit, texture) {
|
|
1862
|
+
this.gl.activeTexture(this.gl.TEXTURE0 + unit);
|
|
1863
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
|
|
1864
|
+
this.gl.bindSampler(unit, null);
|
|
1865
|
+
}
|
|
1866
|
+
disposeBloomPingPongResources() {
|
|
1867
|
+
if (!this.bloomPingPongResources)
|
|
1868
|
+
return;
|
|
1869
|
+
for (const texture of this.bloomPingPongResources.textures)
|
|
1870
|
+
this.gl.deleteTexture(texture);
|
|
1871
|
+
for (const framebuffer of this.bloomPingPongResources.framebuffers)
|
|
1872
|
+
this.gl.deleteFramebuffer(framebuffer);
|
|
1873
|
+
this.bloomPingPongResources = null;
|
|
1874
|
+
}
|
|
1875
|
+
/**
|
|
1876
|
+
* Diagnostics for the most recent native bloom execution (muse3jsparity-PRD
|
|
1877
|
+
* A1). Null when no native bloom pass has run yet on this device.
|
|
1878
|
+
*/
|
|
1879
|
+
getBloomDiagnostics() {
|
|
1880
|
+
return this.lastBloomDiagnostics;
|
|
1881
|
+
}
|
|
1882
|
+
recordBloomDiagnostics(options, sourceWidth, sourceHeight, hdr, mipCount) {
|
|
1883
|
+
const plan = resolveBloomPyramidPlan(sourceWidth, sourceHeight, options.quality, hdr);
|
|
1884
|
+
const pingPongBytes = sourceWidth * sourceHeight * 4 * (hdr ? 2 : 1) * 2;
|
|
1885
|
+
const bytesPerChannel = plan.halfFloat ? 2 : 1;
|
|
1886
|
+
const level0 = plan.mips[0];
|
|
1887
|
+
const accumulatorBytes = level0.width * level0.height * 4 * bytesPerChannel;
|
|
1888
|
+
this.lastBloomDiagnostics = {
|
|
1889
|
+
quality: options.quality,
|
|
1890
|
+
mipCount,
|
|
1891
|
+
targetCount: options.quality === "performance" ? 2 : plan.mips.length * 2 + 1,
|
|
1892
|
+
targetBytes: (options.quality === "performance" ? pingPongBytes : plan.targetBytes + accumulatorBytes + pingPongBytes),
|
|
1893
|
+
compositeGain: options.intensity * resolveBloomPyramidResponseGain(plan),
|
|
1894
|
+
threshold: options.threshold,
|
|
1895
|
+
intensity: options.intensity,
|
|
1896
|
+
softKnee: options.softKnee,
|
|
1897
|
+
shoulder: options.shoulder,
|
|
1898
|
+
halfFloat: hdr || plan.halfFloat,
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* PART U: every live GPU allocation with bytes + lane owner — registry
|
|
1903
|
+
* render targets (B1 shadow maps arrive labeled, B4 captures arrive as
|
|
1904
|
+
* `-mirror-target`/`-scene-color-target`) plus the raw-handle residents the
|
|
1905
|
+
* registry set cannot see: the A1 bloom pyramid (two targets per mip plus
|
|
1906
|
+
* two level-0 accumulators), the legacy bloom ping-pong pair, the A1/A3
|
|
1907
|
+
* lookup textures, and the motion-blur/taa history textures. The
|
|
1908
|
+
* depth-readback scratch pair is intentionally absent: it is allocated and
|
|
1909
|
+
* deleted inside one synchronous call, so it is never live at a diagnostics
|
|
1910
|
+
* boundary. A5 volumetric light, C4 decals, and the G1 SDF atlas own no GPU
|
|
1911
|
+
* target (forward uniforms + CPU kernel, CPU geometry, CPU atlas data), so
|
|
1912
|
+
* no entry is emitted for them; a target appearing under their owner would
|
|
1913
|
+
* be the signal that the lane grew one.
|
|
1914
|
+
*/
|
|
1915
|
+
describeGpuTargetInventory(liveRenderTargets) {
|
|
1916
|
+
const entries = liveRenderTargets.map((target) => ({
|
|
1917
|
+
label: target.label,
|
|
1918
|
+
kind: "render-target",
|
|
1919
|
+
bytes: target.colorTexture.byteLength + (target.depthTexture?.byteLength ?? 0),
|
|
1920
|
+
owner: resolveGpuTargetOwner(target.label)
|
|
1921
|
+
}));
|
|
1922
|
+
const pyramid = this.bloomPyramidResources;
|
|
1923
|
+
if (pyramid) {
|
|
1924
|
+
const bytesPerPixel = pyramid.hdr ? 8 : 4;
|
|
1925
|
+
pyramid.plan.mips.forEach((mip, index) => {
|
|
1926
|
+
for (const side of ["a", "b"]) {
|
|
1927
|
+
entries.push({
|
|
1928
|
+
label: `bloom-pyramid-mip${index}${side}`,
|
|
1929
|
+
kind: "pyramid-mip",
|
|
1930
|
+
bytes: mip.width * mip.height * bytesPerPixel,
|
|
1931
|
+
owner: "a1-bloom"
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
const level0 = pyramid.plan.mips[0];
|
|
1936
|
+
if (level0) {
|
|
1937
|
+
for (const side of ["a", "b"]) {
|
|
1938
|
+
entries.push({
|
|
1939
|
+
label: `bloom-pyramid-accumulator-${side}`,
|
|
1940
|
+
kind: "pyramid-accumulator",
|
|
1941
|
+
bytes: level0.width * level0.height * bytesPerPixel,
|
|
1942
|
+
owner: "a1-bloom"
|
|
1943
|
+
});
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
const pingPong = this.bloomPingPongResources;
|
|
1948
|
+
if (pingPong) {
|
|
1949
|
+
const bytesPerPixel = pingPong.hdr ? 8 : 4;
|
|
1950
|
+
for (const side of ["a", "b"]) {
|
|
1951
|
+
entries.push({
|
|
1952
|
+
label: `bloom-ping-pong-${side}`,
|
|
1953
|
+
kind: "ping-pong",
|
|
1954
|
+
bytes: pingPong.width * pingPong.height * bytesPerPixel,
|
|
1955
|
+
owner: "a1-bloom"
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1959
|
+
if (this.bloomBrightLutTexture) {
|
|
1960
|
+
entries.push({
|
|
1961
|
+
label: "bloom-bright-lut",
|
|
1962
|
+
kind: "lut",
|
|
1963
|
+
bytes: BLOOM_BRIGHT_LUT_WIDTH * BLOOM_BRIGHT_LUT_HEIGHT * 4,
|
|
1964
|
+
owner: "a1-bloom"
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
if (this.bloomCompositeLutTexture) {
|
|
1968
|
+
entries.push({
|
|
1969
|
+
label: "bloom-composite-lut",
|
|
1970
|
+
kind: "lut",
|
|
1971
|
+
bytes: BLOOM_COMPOSITE_LUT_SIZE * BLOOM_COMPOSITE_LUT_SIZE * 4,
|
|
1972
|
+
owner: "a1-bloom"
|
|
1973
|
+
});
|
|
1974
|
+
}
|
|
1975
|
+
if (this.outlineBlendLutTexture) {
|
|
1976
|
+
entries.push({ label: "outline-blend-lut", kind: "lut", bytes: OUTLINE_BLEND_LUT_WIDTH * 4, owner: "post" });
|
|
1977
|
+
}
|
|
1978
|
+
if (this.motionBlurVelocityTexture && this.motionBlurVelocitySize) {
|
|
1979
|
+
entries.push({
|
|
1980
|
+
label: "motion-blur-velocity",
|
|
1981
|
+
kind: "history",
|
|
1982
|
+
bytes: this.motionBlurVelocitySize.width * this.motionBlurVelocitySize.height * 8,
|
|
1983
|
+
owner: "post"
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
if (this.taaHistoryTexture && this.taaHistorySize) {
|
|
1987
|
+
entries.push({
|
|
1988
|
+
label: "taa-history",
|
|
1989
|
+
kind: "history",
|
|
1990
|
+
bytes: this.taaHistorySize.width * this.taaHistorySize.height * 4,
|
|
1991
|
+
owner: "post"
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
return entries;
|
|
1995
|
+
}
|
|
1996
|
+
disposeBloomPyramidResources() {
|
|
1997
|
+
if (!this.bloomPyramidResources)
|
|
1998
|
+
return;
|
|
1999
|
+
for (const texture of this.bloomPyramidResources.textures)
|
|
2000
|
+
this.gl.deleteTexture(texture);
|
|
2001
|
+
for (const framebuffer of this.bloomPyramidResources.framebuffers)
|
|
2002
|
+
this.gl.deleteFramebuffer(framebuffer);
|
|
2003
|
+
this.gl.deleteTexture(this.bloomPyramidResources.accumulatorTextureA);
|
|
2004
|
+
this.gl.deleteFramebuffer(this.bloomPyramidResources.accumulatorFramebufferA);
|
|
2005
|
+
this.gl.deleteTexture(this.bloomPyramidResources.accumulatorTextureB);
|
|
2006
|
+
this.gl.deleteFramebuffer(this.bloomPyramidResources.accumulatorFramebufferB);
|
|
2007
|
+
this.bloomPyramidResources = null;
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* Mip-chain targets for the balanced/cinematic bloom pyramid. Each level is a
|
|
2011
|
+
* single texture+framebuffer at the planned size with linear filtering so the
|
|
2012
|
+
* downsample and accumulate passes can sample across levels. Half-float backs
|
|
2013
|
+
* the chain when the source is HDR or the cinematic preset requests it.
|
|
2014
|
+
*/
|
|
2015
|
+
ensureBloomPyramidResources(plan, hdr) {
|
|
2016
|
+
const current = this.bloomPyramidResources;
|
|
2017
|
+
if (current && current.plan.quality === plan.quality && current.hdr === (hdr || plan.halfFloat) &&
|
|
2018
|
+
current.plan.mips.length === plan.mips.length &&
|
|
2019
|
+
current.plan.mips.every((mip, index) => mip.width === plan.mips[index].width && mip.height === plan.mips[index].height)) {
|
|
2020
|
+
return current;
|
|
2021
|
+
}
|
|
2022
|
+
this.disposeBloomPyramidResources();
|
|
2023
|
+
const textureUnit0 = this.captureTextureUnit0();
|
|
2024
|
+
const previousFramebuffer = this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING);
|
|
2025
|
+
const textures = [];
|
|
2026
|
+
const framebuffers = [];
|
|
2027
|
+
const useHalfFloat = hdr || plan.halfFloat;
|
|
2028
|
+
const allocateTarget = (width, height, label) => {
|
|
2029
|
+
const texture = this.gl.createTexture();
|
|
2030
|
+
const framebuffer = this.gl.createFramebuffer();
|
|
2031
|
+
if (!texture || !framebuffer) {
|
|
2032
|
+
if (texture)
|
|
2033
|
+
this.gl.deleteTexture(texture);
|
|
2034
|
+
if (framebuffer)
|
|
2035
|
+
this.gl.deleteFramebuffer(framebuffer);
|
|
2036
|
+
throw new RenderDeviceError(`Failed to allocate WebGL2 bloom pyramid ${label}`, "WEBGL_ALLOCATION_FAILED", { width, height });
|
|
2037
|
+
}
|
|
2038
|
+
textures.push(texture);
|
|
2039
|
+
framebuffers.push(framebuffer);
|
|
2040
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, texture);
|
|
2041
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
|
|
2042
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
|
|
2043
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
2044
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
2045
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, useHalfFloat ? this.gl.RGBA16F : this.gl.RGBA8, width, height, 0, this.gl.RGBA, useHalfFloat ? this.gl.HALF_FLOAT : this.gl.UNSIGNED_BYTE, null);
|
|
2046
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
|
|
2047
|
+
this.gl.framebufferTexture2D(this.gl.FRAMEBUFFER, this.gl.COLOR_ATTACHMENT0, this.gl.TEXTURE_2D, texture, 0);
|
|
2048
|
+
const status = this.gl.checkFramebufferStatus(this.gl.FRAMEBUFFER);
|
|
2049
|
+
if (status !== this.gl.FRAMEBUFFER_COMPLETE) {
|
|
2050
|
+
throw new RenderDeviceError("WebGL2 bloom pyramid framebuffer status is invalid", "FRAMEBUFFER_INVALID", { width, height, status });
|
|
2051
|
+
}
|
|
2052
|
+
return { texture, framebuffer };
|
|
2053
|
+
};
|
|
2054
|
+
try {
|
|
2055
|
+
// Two ping-pong targets per mip (separable blur needs distinct read/write
|
|
2056
|
+
// targets at the same size), plus two level-0-sized accumulators: the
|
|
2057
|
+
// weighted sum ping-pongs between them so no pass ever samples the
|
|
2058
|
+
// texture of its own bound framebuffer (feedback loop).
|
|
2059
|
+
for (const mip of plan.mips) {
|
|
2060
|
+
allocateTarget(mip.width, mip.height, "mip");
|
|
2061
|
+
allocateTarget(mip.width, mip.height, "mip");
|
|
2062
|
+
}
|
|
2063
|
+
const level0 = plan.mips[0];
|
|
2064
|
+
const accumulatorA = allocateTarget(level0.width, level0.height, "accumulator-a");
|
|
2065
|
+
const accumulatorB = allocateTarget(level0.width, level0.height, "accumulator-b");
|
|
2066
|
+
const finalResources = {
|
|
2067
|
+
plan,
|
|
2068
|
+
hdr: useHalfFloat,
|
|
2069
|
+
textures,
|
|
2070
|
+
framebuffers,
|
|
2071
|
+
accumulatorTextureA: accumulatorA.texture,
|
|
2072
|
+
accumulatorFramebufferA: accumulatorA.framebuffer,
|
|
2073
|
+
accumulatorTextureB: accumulatorB.texture,
|
|
2074
|
+
accumulatorFramebufferB: accumulatorB.framebuffer
|
|
2075
|
+
};
|
|
2076
|
+
this.bloomPyramidResources = finalResources;
|
|
2077
|
+
return finalResources;
|
|
2078
|
+
}
|
|
2079
|
+
catch (error) {
|
|
2080
|
+
for (const texture of textures)
|
|
2081
|
+
this.gl.deleteTexture(texture);
|
|
2082
|
+
for (const framebuffer of framebuffers)
|
|
2083
|
+
this.gl.deleteFramebuffer(framebuffer);
|
|
2084
|
+
throw error;
|
|
2085
|
+
}
|
|
2086
|
+
finally {
|
|
2087
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, previousFramebuffer);
|
|
2088
|
+
this.restoreTextureUnit0(textureUnit0);
|
|
2089
|
+
this.stateCache.invalidate();
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
captureTextureUnit0() {
|
|
2093
|
+
const activeTexture = this.gl.getParameter(this.gl.ACTIVE_TEXTURE);
|
|
2094
|
+
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
2095
|
+
return {
|
|
2096
|
+
activeTexture,
|
|
2097
|
+
texture2d: this.gl.getParameter(this.gl.TEXTURE_BINDING_2D),
|
|
2098
|
+
sampler: this.gl.getParameter(this.gl.SAMPLER_BINDING)
|
|
2099
|
+
};
|
|
2100
|
+
}
|
|
2101
|
+
restoreTextureUnit0(snapshot) {
|
|
2102
|
+
this.gl.activeTexture(this.gl.TEXTURE0);
|
|
2103
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, snapshot.texture2d);
|
|
2104
|
+
this.gl.bindSampler(0, snapshot.sampler);
|
|
2105
|
+
this.gl.activeTexture(snapshot.activeTexture);
|
|
2106
|
+
}
|
|
2107
|
+
captureTextureUnitBinding(unit) {
|
|
2108
|
+
this.gl.activeTexture(this.gl.TEXTURE0 + unit);
|
|
2109
|
+
return {
|
|
2110
|
+
texture2d: this.gl.getParameter(this.gl.TEXTURE_BINDING_2D),
|
|
2111
|
+
sampler: this.gl.getParameter(this.gl.SAMPLER_BINDING)
|
|
2112
|
+
};
|
|
2113
|
+
}
|
|
2114
|
+
restoreTextureUnitBinding(unit, snapshot) {
|
|
2115
|
+
this.gl.activeTexture(this.gl.TEXTURE0 + unit);
|
|
2116
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, snapshot.texture2d);
|
|
2117
|
+
this.gl.bindSampler(unit, snapshot.sampler);
|
|
2118
|
+
}
|
|
2119
|
+
prepareFullscreenPresentation(framebuffer, outputWidth, outputHeight) {
|
|
2120
|
+
const textureUnit0 = this.captureTextureUnit0();
|
|
2121
|
+
const textureUnit1 = this.captureTextureUnitBinding(1);
|
|
2122
|
+
const textureUnit2 = this.captureTextureUnitBinding(2);
|
|
2123
|
+
this.gl.activeTexture(textureUnit0.activeTexture);
|
|
2124
|
+
const snapshot = {
|
|
2125
|
+
framebuffer: this.gl.getParameter(this.gl.FRAMEBUFFER_BINDING),
|
|
2126
|
+
program: this.gl.getParameter(this.gl.CURRENT_PROGRAM),
|
|
2127
|
+
textureUnit0,
|
|
2128
|
+
textureUnit1,
|
|
2129
|
+
textureUnit2,
|
|
2130
|
+
vertexArray: this.gl.getParameter(this.gl.VERTEX_ARRAY_BINDING),
|
|
2131
|
+
viewport: this.gl.getParameter(this.gl.VIEWPORT),
|
|
2132
|
+
colorMask: this.gl.getParameter(this.gl.COLOR_WRITEMASK),
|
|
2133
|
+
depthTestEnabled: this.gl.isEnabled(this.gl.DEPTH_TEST),
|
|
2134
|
+
cullFaceEnabled: this.gl.isEnabled(this.gl.CULL_FACE),
|
|
2135
|
+
blendEnabled: this.gl.isEnabled(this.gl.BLEND),
|
|
2136
|
+
scissorTestEnabled: this.gl.isEnabled(this.gl.SCISSOR_TEST),
|
|
2137
|
+
stencilTestEnabled: this.gl.isEnabled(this.gl.STENCIL_TEST),
|
|
2138
|
+
polygonOffsetFillEnabled: this.gl.isEnabled(this.gl.POLYGON_OFFSET_FILL),
|
|
2139
|
+
depthMask: this.gl.getParameter(this.gl.DEPTH_WRITEMASK)
|
|
2140
|
+
};
|
|
2141
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, framebuffer);
|
|
2142
|
+
this.gl.viewport(0, 0, outputWidth, outputHeight);
|
|
2143
|
+
this.gl.colorMask(true, true, true, true);
|
|
2144
|
+
this.gl.disable(this.gl.SCISSOR_TEST);
|
|
2145
|
+
this.gl.disable(this.gl.STENCIL_TEST);
|
|
2146
|
+
this.gl.disable(this.gl.POLYGON_OFFSET_FILL);
|
|
2147
|
+
this.gl.disable(this.gl.DEPTH_TEST);
|
|
2148
|
+
this.gl.disable(this.gl.CULL_FACE);
|
|
2149
|
+
this.gl.disable(this.gl.BLEND);
|
|
2150
|
+
this.gl.depthMask(false);
|
|
2151
|
+
return snapshot;
|
|
2152
|
+
}
|
|
2153
|
+
restoreFullscreenPresentationState(snapshot, fallbackWidth, fallbackHeight) {
|
|
2154
|
+
this.gl.bindVertexArray(snapshot.vertexArray);
|
|
2155
|
+
this.restoreTextureUnitBinding(2, snapshot.textureUnit2);
|
|
2156
|
+
this.restoreTextureUnitBinding(1, snapshot.textureUnit1);
|
|
2157
|
+
this.restoreTextureUnit0(snapshot.textureUnit0);
|
|
2158
|
+
this.gl.useProgram(snapshot.program);
|
|
2159
|
+
if (snapshot.depthTestEnabled)
|
|
2160
|
+
this.gl.enable(this.gl.DEPTH_TEST);
|
|
2161
|
+
else
|
|
2162
|
+
this.gl.disable(this.gl.DEPTH_TEST);
|
|
2163
|
+
if (snapshot.cullFaceEnabled)
|
|
2164
|
+
this.gl.enable(this.gl.CULL_FACE);
|
|
2165
|
+
else
|
|
2166
|
+
this.gl.disable(this.gl.CULL_FACE);
|
|
2167
|
+
if (snapshot.blendEnabled)
|
|
2168
|
+
this.gl.enable(this.gl.BLEND);
|
|
2169
|
+
else
|
|
2170
|
+
this.gl.disable(this.gl.BLEND);
|
|
2171
|
+
if (snapshot.scissorTestEnabled)
|
|
2172
|
+
this.gl.enable(this.gl.SCISSOR_TEST);
|
|
2173
|
+
else
|
|
2174
|
+
this.gl.disable(this.gl.SCISSOR_TEST);
|
|
2175
|
+
if (snapshot.stencilTestEnabled)
|
|
2176
|
+
this.gl.enable(this.gl.STENCIL_TEST);
|
|
2177
|
+
else
|
|
2178
|
+
this.gl.disable(this.gl.STENCIL_TEST);
|
|
2179
|
+
if (snapshot.polygonOffsetFillEnabled)
|
|
2180
|
+
this.gl.enable(this.gl.POLYGON_OFFSET_FILL);
|
|
2181
|
+
else
|
|
2182
|
+
this.gl.disable(this.gl.POLYGON_OFFSET_FILL);
|
|
2183
|
+
this.gl.colorMask(snapshot.colorMask[0] ?? true, snapshot.colorMask[1] ?? true, snapshot.colorMask[2] ?? true, snapshot.colorMask[3] ?? true);
|
|
2184
|
+
this.gl.depthMask(snapshot.depthMask);
|
|
2185
|
+
this.gl.bindFramebuffer(this.gl.FRAMEBUFFER, snapshot.framebuffer);
|
|
2186
|
+
this.gl.viewport(snapshot.viewport[0] ?? 0, snapshot.viewport[1] ?? 0, snapshot.viewport[2] ?? fallbackWidth, snapshot.viewport[3] ?? fallbackHeight);
|
|
2187
|
+
this.stateCache.invalidate();
|
|
2188
|
+
}
|
|
2189
|
+
ensurePresentationVertexArray() {
|
|
2190
|
+
if (this.presentationVertexArray)
|
|
2191
|
+
return this.presentationVertexArray;
|
|
2192
|
+
const vertexArray = this.gl.createVertexArray();
|
|
2193
|
+
if (!vertexArray) {
|
|
2194
|
+
throw new RenderDeviceError("Failed to allocate WebGL presentation vertex array", "WEBGL_ALLOCATION_FAILED");
|
|
2195
|
+
}
|
|
2196
|
+
this.presentationVertexArray = vertexArray;
|
|
2197
|
+
return vertexArray;
|
|
2198
|
+
}
|
|
2199
|
+
ensureDepthReadbackProgram() {
|
|
2200
|
+
if (this.depthReadbackProgram) {
|
|
2201
|
+
return this.depthReadbackProgram;
|
|
2202
|
+
}
|
|
2203
|
+
const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
|
|
2204
|
+
precision highp float;
|
|
2205
|
+
out vec2 v_uv;
|
|
2206
|
+
void main() {
|
|
2207
|
+
vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
2208
|
+
v_uv = position;
|
|
2209
|
+
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
|
|
2210
|
+
}
|
|
2211
|
+
`, "webgl2-depth-readback");
|
|
2212
|
+
const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, `#version 300 es
|
|
2213
|
+
precision highp float;
|
|
2214
|
+
uniform sampler2D u_depth;
|
|
2215
|
+
uniform vec4 u_sourceRect;
|
|
2216
|
+
in vec2 v_uv;
|
|
2217
|
+
out vec4 outColor;
|
|
2218
|
+
vec3 packDepth24(float value) {
|
|
2219
|
+
value = clamp(value, 0.0, 1.0);
|
|
2220
|
+
vec3 encoded = fract(value * vec3(1.0, 255.0, 65025.0));
|
|
2221
|
+
encoded -= encoded.yzz * vec3(1.0 / 255.0, 1.0 / 255.0, 0.0);
|
|
2222
|
+
return encoded;
|
|
2223
|
+
}
|
|
2224
|
+
void main() {
|
|
2225
|
+
vec2 uv = u_sourceRect.xy + v_uv * u_sourceRect.zw;
|
|
2226
|
+
outColor = vec4(packDepth24(texture(u_depth, uv).r), 1.0);
|
|
2227
|
+
}
|
|
2228
|
+
`, "webgl2-depth-readback");
|
|
2229
|
+
const program = this.gl.createProgram();
|
|
2230
|
+
if (!program) {
|
|
2231
|
+
this.gl.deleteShader(vertex);
|
|
2232
|
+
this.gl.deleteShader(fragment);
|
|
2233
|
+
throw new RenderDeviceError("Failed to allocate WebGL2 depth readback shader", "WEBGL_ALLOCATION_FAILED");
|
|
2234
|
+
}
|
|
2235
|
+
this.gl.attachShader(program, vertex);
|
|
2236
|
+
this.gl.attachShader(program, fragment);
|
|
2237
|
+
this.gl.linkProgram(program);
|
|
2238
|
+
this.gl.deleteShader(vertex);
|
|
2239
|
+
this.gl.deleteShader(fragment);
|
|
2240
|
+
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
2241
|
+
const log = this.gl.getProgramInfoLog(program) ?? "Unknown depth readback shader link error";
|
|
2242
|
+
this.gl.deleteProgram(program);
|
|
2243
|
+
throw new RenderDeviceError("WebGL2 depth readback shader link failed", "SHADER_LINK_FAILED", { log });
|
|
2244
|
+
}
|
|
2245
|
+
this.depthReadbackProgram = program;
|
|
2246
|
+
return program;
|
|
2247
|
+
}
|
|
2248
|
+
ensurePresentationProgram() {
|
|
2249
|
+
if (this.presentationProgram) {
|
|
2250
|
+
return this.presentationProgram;
|
|
2251
|
+
}
|
|
2252
|
+
const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
|
|
2253
|
+
precision highp float;
|
|
2254
|
+
out vec2 v_uv;
|
|
2255
|
+
void main() {
|
|
2256
|
+
vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
2257
|
+
v_uv = position;
|
|
2258
|
+
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
|
|
2259
|
+
}
|
|
2260
|
+
`, "webgl2-present-render-target");
|
|
2261
|
+
const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, `#version 300 es
|
|
2262
|
+
precision highp float;
|
|
2263
|
+
uniform sampler2D u_source;
|
|
2264
|
+
in vec2 v_uv;
|
|
2265
|
+
out vec4 outColor;
|
|
2266
|
+
void main() {
|
|
2267
|
+
outColor = texture(u_source, v_uv);
|
|
2268
|
+
}
|
|
2269
|
+
`, "webgl2-present-render-target");
|
|
2270
|
+
const program = this.gl.createProgram();
|
|
2271
|
+
if (!program) {
|
|
2272
|
+
this.gl.deleteShader(vertex);
|
|
2273
|
+
this.gl.deleteShader(fragment);
|
|
2274
|
+
throw new RenderDeviceError("Failed to allocate WebGL presentation shader", "WEBGL_ALLOCATION_FAILED");
|
|
2275
|
+
}
|
|
2276
|
+
this.gl.attachShader(program, vertex);
|
|
2277
|
+
this.gl.attachShader(program, fragment);
|
|
2278
|
+
this.gl.linkProgram(program);
|
|
2279
|
+
this.gl.deleteShader(vertex);
|
|
2280
|
+
this.gl.deleteShader(fragment);
|
|
2281
|
+
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
2282
|
+
const log = this.gl.getProgramInfoLog(program) ?? "Unknown presentation shader link error";
|
|
2283
|
+
this.gl.deleteProgram(program);
|
|
2284
|
+
throw new RenderDeviceError("WebGL presentation shader link failed", "SHADER_LINK_FAILED", { log });
|
|
2285
|
+
}
|
|
2286
|
+
this.presentationProgram = program;
|
|
2287
|
+
return program;
|
|
2288
|
+
}
|
|
2289
|
+
ensureBloomBrightExtractProgram() {
|
|
2290
|
+
if (this.bloomBrightExtractProgram) {
|
|
2291
|
+
return this.bloomBrightExtractProgram;
|
|
2292
|
+
}
|
|
2293
|
+
this.bloomBrightExtractProgram = this.createFullscreenProgram(`#version 300 es
|
|
2294
|
+
precision highp float;
|
|
2295
|
+
precision highp int;
|
|
2296
|
+
uniform sampler2D u_source;
|
|
2297
|
+
uniform sampler2D u_brightLut;
|
|
2298
|
+
uniform int u_hdr;
|
|
2299
|
+
uniform float u_threshold;
|
|
2300
|
+
uniform float u_softKnee;
|
|
2301
|
+
out vec4 outColor;
|
|
2302
|
+
|
|
2303
|
+
uvec4 byteTexel(sampler2D source, ivec2 coordinate) {
|
|
2304
|
+
return uvec4(texelFetch(source, coordinate, 0) * 255.0 + 0.5);
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
void main() {
|
|
2308
|
+
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
|
2309
|
+
if (u_hdr == 1) {
|
|
2310
|
+
vec4 source = texelFetch(u_source, pixel, 0);
|
|
2311
|
+
float luma = dot(source.rgb, vec3(0.2126, 0.7152, 0.0722));
|
|
2312
|
+
float kneeLow = max(u_threshold - u_softKnee, 0.0);
|
|
2313
|
+
float kneeHigh = min(u_threshold + u_softKnee, 1.0);
|
|
2314
|
+
float weight = u_softKnee <= 0.0
|
|
2315
|
+
? (luma >= u_threshold ? 1.0 : 0.0)
|
|
2316
|
+
: smoothstep(kneeLow, kneeHigh, luma);
|
|
2317
|
+
outColor = source * weight;
|
|
2318
|
+
return;
|
|
2319
|
+
}
|
|
2320
|
+
uvec4 source = byteTexel(u_source, pixel);
|
|
2321
|
+
uint colorIndex = (source.r << 16u) | (source.g << 8u) | source.b;
|
|
2322
|
+
uint byteIndex = colorIndex >> 3u;
|
|
2323
|
+
uint texelIndex = byteIndex >> 2u;
|
|
2324
|
+
ivec2 lutCoordinate = ivec2(
|
|
2325
|
+
int(texelIndex % ${BLOOM_BRIGHT_LUT_WIDTH}u),
|
|
2326
|
+
int(texelIndex / ${BLOOM_BRIGHT_LUT_WIDTH}u)
|
|
2327
|
+
);
|
|
2328
|
+
uvec4 packed = byteTexel(u_brightLut, lutCoordinate);
|
|
2329
|
+
uint component = byteIndex & 3u;
|
|
2330
|
+
uint packedByte = component == 0u
|
|
2331
|
+
? packed.r
|
|
2332
|
+
: component == 1u
|
|
2333
|
+
? packed.g
|
|
2334
|
+
: component == 2u
|
|
2335
|
+
? packed.b
|
|
2336
|
+
: packed.a;
|
|
2337
|
+
bool bright = ((packedByte >> (colorIndex & 7u)) & 1u) == 1u;
|
|
2338
|
+
outColor = bright ? vec4(source) / 255.0 : vec4(0.0);
|
|
2339
|
+
}
|
|
2340
|
+
`, "webgl2-bloom-bright-extract");
|
|
2341
|
+
return this.bloomBrightExtractProgram;
|
|
2342
|
+
}
|
|
2343
|
+
ensureBloomBlurProgram() {
|
|
2344
|
+
if (this.bloomBlurProgram) {
|
|
2345
|
+
return this.bloomBlurProgram;
|
|
2346
|
+
}
|
|
2347
|
+
this.bloomBlurProgram = this.createFullscreenProgram(`#version 300 es
|
|
2348
|
+
precision highp float;
|
|
2349
|
+
precision highp int;
|
|
2350
|
+
uniform sampler2D u_source;
|
|
2351
|
+
uniform ivec2 u_size;
|
|
2352
|
+
uniform int u_radius;
|
|
2353
|
+
uniform int u_horizontal;
|
|
2354
|
+
uniform int u_hdr;
|
|
2355
|
+
out vec4 outColor;
|
|
2356
|
+
|
|
2357
|
+
vec4 sourcePixel(ivec2 coordinate) {
|
|
2358
|
+
return texelFetch(u_source, clamp(coordinate, ivec2(0), u_size - ivec2(1)), 0);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
void main() {
|
|
2362
|
+
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
|
2363
|
+
float sigma = max(float(u_radius) / 3.0, 0.5);
|
|
2364
|
+
float coefficient = 0.39894 / sigma;
|
|
2365
|
+
vec4 sum = sourcePixel(pixel) * coefficient;
|
|
2366
|
+
float weightSum = coefficient;
|
|
2367
|
+
for (int offset = 1; offset <= 16; offset += 1) {
|
|
2368
|
+
if (offset > u_radius) continue;
|
|
2369
|
+
float distance = float(offset);
|
|
2370
|
+
float weight = coefficient * exp(-0.5 * distance * distance / (sigma * sigma));
|
|
2371
|
+
ivec2 delta = u_horizontal == 1 ? ivec2(offset, 0) : ivec2(0, offset);
|
|
2372
|
+
sum += (sourcePixel(pixel + delta) + sourcePixel(pixel - delta)) * weight;
|
|
2373
|
+
weightSum += 2.0 * weight;
|
|
2374
|
+
}
|
|
2375
|
+
outColor = sum / max(weightSum, 0.000001);
|
|
2376
|
+
}
|
|
2377
|
+
`, "webgl2-bloom-blur");
|
|
2378
|
+
return this.bloomBlurProgram;
|
|
2379
|
+
}
|
|
2380
|
+
ensureBloomCompositeProgram() {
|
|
2381
|
+
if (this.bloomCompositeProgram) {
|
|
2382
|
+
return this.bloomCompositeProgram;
|
|
2383
|
+
}
|
|
2384
|
+
this.bloomCompositeProgram = this.createFullscreenProgram(`#version 300 es
|
|
2385
|
+
precision highp float;
|
|
2386
|
+
precision highp int;
|
|
2387
|
+
uniform sampler2D u_source;
|
|
2388
|
+
uniform sampler2D u_blurred;
|
|
2389
|
+
uniform sampler2D u_compositeLut;
|
|
2390
|
+
uniform int u_hdr;
|
|
2391
|
+
uniform float u_intensity;
|
|
2392
|
+
out vec4 outColor;
|
|
2393
|
+
|
|
2394
|
+
uvec4 byteTexel(sampler2D source, ivec2 coordinate) {
|
|
2395
|
+
return uvec4(texelFetch(source, coordinate, 0) * 255.0 + 0.5);
|
|
2396
|
+
}
|
|
2397
|
+
|
|
2398
|
+
float compositeChannel(uint source, uint blurred) {
|
|
2399
|
+
return texelFetch(u_compositeLut, ivec2(int(source), int(blurred)), 0).r;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
void main() {
|
|
2403
|
+
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
|
2404
|
+
if (u_hdr == 1) {
|
|
2405
|
+
vec4 source = texelFetch(u_source, pixel, 0);
|
|
2406
|
+
vec3 blurred = texelFetch(u_blurred, pixel, 0).rgb;
|
|
2407
|
+
outColor = vec4(source.rgb + blurred * u_intensity, source.a);
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
uvec4 source = byteTexel(u_source, pixel);
|
|
2411
|
+
uvec4 blurred = byteTexel(u_blurred, pixel);
|
|
2412
|
+
outColor = vec4(
|
|
2413
|
+
compositeChannel(source.r, blurred.r),
|
|
2414
|
+
compositeChannel(source.g, blurred.g),
|
|
2415
|
+
compositeChannel(source.b, blurred.b),
|
|
2416
|
+
float(source.a) / 255.0
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
`, "webgl2-bloom-composite");
|
|
2420
|
+
return this.bloomCompositeProgram;
|
|
2421
|
+
}
|
|
2422
|
+
ensureBloomDownsampleProgram() {
|
|
2423
|
+
if (this.bloomDownsampleProgram)
|
|
2424
|
+
return this.bloomDownsampleProgram;
|
|
2425
|
+
this.bloomDownsampleProgram = this.createFullscreenProgram(`#version 300 es
|
|
2426
|
+
precision highp float;
|
|
2427
|
+
uniform sampler2D u_source;
|
|
2428
|
+
uniform vec2 u_texelSize;
|
|
2429
|
+
uniform vec2 u_targetSize;
|
|
2430
|
+
out vec4 outColor;
|
|
2431
|
+
void main() {
|
|
2432
|
+
// gl_FragCoord is expressed in destination pixels. Normalize by the
|
|
2433
|
+
// destination size so every downsample covers the complete source image;
|
|
2434
|
+
// multiplying by source texel size cropped a half-size target to the
|
|
2435
|
+
// lower-left quarter of the preceding level.
|
|
2436
|
+
vec2 uv = gl_FragCoord.xy / u_targetSize;
|
|
2437
|
+
vec2 e = u_texelSize * 0.5;
|
|
2438
|
+
vec4 sum = texture(u_source, uv - e) + texture(u_source, uv + vec2(e.x, -e.y))
|
|
2439
|
+
+ texture(u_source, uv + vec2(-e.x, e.y)) + texture(u_source, uv + e);
|
|
2440
|
+
outColor = sum * 0.25;
|
|
2441
|
+
}
|
|
2442
|
+
`, "webgl2-bloom-downsample");
|
|
2443
|
+
return this.bloomDownsampleProgram;
|
|
2444
|
+
}
|
|
2445
|
+
ensureBloomAccumulateProgram() {
|
|
2446
|
+
if (this.bloomAccumulateProgram)
|
|
2447
|
+
return this.bloomAccumulateProgram;
|
|
2448
|
+
this.bloomAccumulateProgram = this.createFullscreenProgram(`#version 300 es
|
|
2449
|
+
precision highp float;
|
|
2450
|
+
uniform sampler2D u_base;
|
|
2451
|
+
uniform sampler2D u_bloom;
|
|
2452
|
+
uniform float u_weight;
|
|
2453
|
+
out vec4 outColor;
|
|
2454
|
+
void main() {
|
|
2455
|
+
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
|
2456
|
+
vec4 base = texelFetch(u_base, pixel, 0);
|
|
2457
|
+
vec2 uv = (vec2(pixel) + 0.5) / vec2(textureSize(u_base, 0));
|
|
2458
|
+
vec3 bloom = texture(u_bloom, uv).rgb;
|
|
2459
|
+
outColor = vec4(base.rgb + bloom * u_weight, base.a);
|
|
2460
|
+
}
|
|
2461
|
+
`, "webgl2-bloom-accumulate");
|
|
2462
|
+
return this.bloomAccumulateProgram;
|
|
2463
|
+
}
|
|
2464
|
+
ensureOutlineProgram() {
|
|
2465
|
+
if (this.outlineProgram)
|
|
2466
|
+
return this.outlineProgram;
|
|
2467
|
+
this.outlineProgram = this.createFullscreenProgram(`#version 300 es
|
|
2468
|
+
precision highp float;
|
|
2469
|
+
precision highp int;
|
|
2470
|
+
uniform sampler2D u_source;
|
|
2471
|
+
uniform sampler2D u_blendLut;
|
|
2472
|
+
uniform ivec2 u_size;
|
|
2473
|
+
uniform int u_width;
|
|
2474
|
+
uniform float u_boundHigh;
|
|
2475
|
+
uniform float u_boundLow;
|
|
2476
|
+
out vec4 outColor;
|
|
2477
|
+
|
|
2478
|
+
uvec4 sourceByte(ivec2 coordinate) {
|
|
2479
|
+
ivec2 bounded = clamp(coordinate, ivec2(0), u_size - ivec2(1));
|
|
2480
|
+
return uvec4(texelFetch(u_source, bounded, 0) * 255.0 + 0.5);
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
uint lumaNumerator(ivec2 coordinate) {
|
|
2484
|
+
uvec3 source = sourceByte(coordinate).rgb;
|
|
2485
|
+
return 2126u * source.r + 7152u * source.g + 722u * source.b;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
uvec2 squareWords(int signedValue) {
|
|
2489
|
+
uint value = uint(abs(signedValue));
|
|
2490
|
+
uint high = value >> 12u;
|
|
2491
|
+
uint low = value & 4095u;
|
|
2492
|
+
uint middle = 2u * high * low;
|
|
2493
|
+
uint lowWord = low * low + (middle & 4095u) * 4096u;
|
|
2494
|
+
uint highWord = high * high + (middle >> 12u);
|
|
2495
|
+
highWord += lowWord / ${OUTLINE_LIMB_RADIX}u;
|
|
2496
|
+
lowWord %= ${OUTLINE_LIMB_RADIX}u;
|
|
2497
|
+
return uvec2(highWord, lowWord);
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
bool edgeAt(ivec2 pixel) {
|
|
2501
|
+
int nw = int(lumaNumerator(pixel + ivec2(-1, -1)));
|
|
2502
|
+
int n = int(lumaNumerator(pixel + ivec2(0, -1)));
|
|
2503
|
+
int ne = int(lumaNumerator(pixel + ivec2(1, -1)));
|
|
2504
|
+
int w = int(lumaNumerator(pixel + ivec2(-1, 0)));
|
|
2505
|
+
int e = int(lumaNumerator(pixel + ivec2(1, 0)));
|
|
2506
|
+
int sw = int(lumaNumerator(pixel + ivec2(-1, 1)));
|
|
2507
|
+
int s = int(lumaNumerator(pixel + ivec2(0, 1)));
|
|
2508
|
+
int se = int(lumaNumerator(pixel + ivec2(1, 1)));
|
|
2509
|
+
int gx = -nw - 2 * w - sw + ne + 2 * e + se;
|
|
2510
|
+
int gy = -nw - 2 * n - ne + sw + 2 * s + se;
|
|
2511
|
+
uvec2 x = squareWords(gx);
|
|
2512
|
+
uvec2 y = squareWords(gy);
|
|
2513
|
+
uint low = x.y + y.y;
|
|
2514
|
+
uint high = x.x + y.x;
|
|
2515
|
+
if (low >= ${OUTLINE_LIMB_RADIX}u) {
|
|
2516
|
+
low -= ${OUTLINE_LIMB_RADIX}u;
|
|
2517
|
+
high += 1u;
|
|
2518
|
+
}
|
|
2519
|
+
uint boundHigh = uint(u_boundHigh);
|
|
2520
|
+
uint boundLow = uint(u_boundLow);
|
|
2521
|
+
return high != boundHigh ? high > boundHigh : low >= boundLow;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
void main() {
|
|
2525
|
+
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
|
2526
|
+
bool outlined = false;
|
|
2527
|
+
for (int offsetY = -6; offsetY <= 6 && !outlined; offsetY += 1) {
|
|
2528
|
+
for (int offsetX = -6; offsetX <= 6; offsetX += 1) {
|
|
2529
|
+
if (abs(offsetX) > u_width || abs(offsetY) > u_width) continue;
|
|
2530
|
+
if (offsetX * offsetX + offsetY * offsetY > u_width * u_width) continue;
|
|
2531
|
+
ivec2 candidate = clamp(pixel + ivec2(offsetX, offsetY), ivec2(0), u_size - ivec2(1));
|
|
2532
|
+
if (edgeAt(candidate)) {
|
|
2533
|
+
outlined = true;
|
|
2534
|
+
break;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
uvec4 source = sourceByte(pixel);
|
|
2539
|
+
if (!outlined) {
|
|
2540
|
+
outColor = vec4(source) / 255.0;
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
outColor = vec4(
|
|
2544
|
+
texelFetch(u_blendLut, ivec2(int(source.r), 0), 0).r,
|
|
2545
|
+
texelFetch(u_blendLut, ivec2(int(source.g), 0), 0).g,
|
|
2546
|
+
texelFetch(u_blendLut, ivec2(int(source.b), 0), 0).b,
|
|
2547
|
+
float(source.a) / 255.0
|
|
2548
|
+
);
|
|
2549
|
+
}
|
|
2550
|
+
`, "webgl2-outline");
|
|
2551
|
+
return this.outlineProgram;
|
|
2552
|
+
}
|
|
2553
|
+
ensureSsaoProgram() {
|
|
2554
|
+
if (this.ssaoProgram)
|
|
2555
|
+
return this.ssaoProgram;
|
|
2556
|
+
this.ssaoProgram = this.createFullscreenProgram(`#version 300 es
|
|
2557
|
+
precision highp float;
|
|
2558
|
+
precision highp int;
|
|
2559
|
+
uniform sampler2D u_source;
|
|
2560
|
+
uniform sampler2D u_depth;
|
|
2561
|
+
uniform ivec2 u_size;
|
|
2562
|
+
uniform int u_radius;
|
|
2563
|
+
uniform float u_intensity;
|
|
2564
|
+
uniform float u_bias;
|
|
2565
|
+
out vec4 outColor;
|
|
2566
|
+
|
|
2567
|
+
float depthAt(ivec2 coordinate) {
|
|
2568
|
+
return texelFetch(u_depth, clamp(coordinate, ivec2(0), u_size - ivec2(1)), 0).r;
|
|
2569
|
+
}
|
|
2570
|
+
|
|
2571
|
+
void main() {
|
|
2572
|
+
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
|
2573
|
+
float centerDepth = depthAt(pixel);
|
|
2574
|
+
float occlusion = 0.0;
|
|
2575
|
+
float samples = 0.0;
|
|
2576
|
+
for (int stepY = -1; stepY <= 1; stepY += 1) {
|
|
2577
|
+
for (int stepX = -1; stepX <= 1; stepX += 1) {
|
|
2578
|
+
if (stepX == 0 && stepY == 0) continue;
|
|
2579
|
+
float sampleDepth = depthAt(pixel + ivec2(stepX * u_radius, stepY * u_radius));
|
|
2580
|
+
occlusion += clamp(
|
|
2581
|
+
(centerDepth - sampleDepth - u_bias) / (0.14 + float(u_radius) * 0.04),
|
|
2582
|
+
0.0,
|
|
2583
|
+
1.0
|
|
2584
|
+
);
|
|
2585
|
+
samples += 1.0;
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
float factor = max(0.18, 1.0 - (occlusion / max(1.0, samples)) * u_intensity * 0.72);
|
|
2589
|
+
vec4 source = texelFetch(u_source, pixel, 0);
|
|
2590
|
+
outColor = vec4(source.rgb * factor, source.a);
|
|
2591
|
+
}
|
|
2592
|
+
`, "webgl2-ssao");
|
|
2593
|
+
return this.ssaoProgram;
|
|
2594
|
+
}
|
|
2595
|
+
ensureSsrProgram() {
|
|
2596
|
+
if (this.ssrProgram)
|
|
2597
|
+
return this.ssrProgram;
|
|
2598
|
+
this.ssrProgram = this.createFullscreenProgram(`#version 300 es
|
|
2599
|
+
precision highp float;
|
|
2600
|
+
precision highp int;
|
|
2601
|
+
uniform sampler2D u_source;
|
|
2602
|
+
uniform sampler2D u_depth;
|
|
2603
|
+
uniform ivec2 u_size;
|
|
2604
|
+
uniform float u_intensity;
|
|
2605
|
+
uniform int u_maxDistance;
|
|
2606
|
+
uniform float u_depthNear;
|
|
2607
|
+
uniform float u_depthFar;
|
|
2608
|
+
out vec4 outColor;
|
|
2609
|
+
|
|
2610
|
+
// Raw GL depth is nonlinear (0.1/1000 defaults park the play area past 0.97),
|
|
2611
|
+
// so every depth-gated comparison below runs on linearized depth. The CPU
|
|
2612
|
+
// byte kernel keeps fixture-unit semantics; the divergence is documented at
|
|
2613
|
+
// normalizeLdrDepthRange.
|
|
2614
|
+
float a3dSsrLinearDepth(float depth) {
|
|
2615
|
+
float viewZ = u_depthNear * u_depthFar / max(u_depthFar - depth * (u_depthFar - u_depthNear), 0.0001);
|
|
2616
|
+
return clamp((viewZ - u_depthNear) / max(u_depthFar - u_depthNear, 0.0001), 0.0, 1.0);
|
|
2617
|
+
}
|
|
2618
|
+
|
|
2619
|
+
void main() {
|
|
2620
|
+
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
|
2621
|
+
vec4 target = texelFetch(u_source, pixel, 0);
|
|
2622
|
+
float linearDepth = a3dSsrLinearDepth(texelFetch(u_depth, pixel, 0).r);
|
|
2623
|
+
int rayDistance = max(1, int(round((1.0 - linearDepth) * float(u_maxDistance))));
|
|
2624
|
+
int direction = pixel.x < u_size.x / 2 ? 1 : -1;
|
|
2625
|
+
ivec2 samplePixel = ivec2(
|
|
2626
|
+
clamp(pixel.x + direction * rayDistance, 0, u_size.x - 1),
|
|
2627
|
+
clamp(u_size.y - 1 - pixel.y, 0, u_size.y - 1)
|
|
2628
|
+
);
|
|
2629
|
+
vec3 reflected = texelFetch(u_source, samplePixel, 0).rgb;
|
|
2630
|
+
float sourceLuma = (reflected.r + reflected.g + reflected.b) / 3.0;
|
|
2631
|
+
if (sourceLuma < 0.18 || linearDepth > 0.995) {
|
|
2632
|
+
outColor = target;
|
|
2633
|
+
return;
|
|
2634
|
+
}
|
|
2635
|
+
float boost = sourceLuma * u_intensity * (1.0 - linearDepth);
|
|
2636
|
+
outColor = vec4(clamp(target.rgb + reflected * boost, 0.0, 1.0), target.a);
|
|
2637
|
+
}
|
|
2638
|
+
`, "webgl2-ssr");
|
|
2639
|
+
return this.ssrProgram;
|
|
2640
|
+
}
|
|
2641
|
+
ensureDepthOfFieldProgram() {
|
|
2642
|
+
if (this.depthOfFieldProgram)
|
|
2643
|
+
return this.depthOfFieldProgram;
|
|
2644
|
+
this.depthOfFieldProgram = this.createFullscreenProgram(`#version 300 es
|
|
2645
|
+
precision highp float;
|
|
2646
|
+
precision highp int;
|
|
2647
|
+
uniform sampler2D u_source;
|
|
2648
|
+
uniform sampler2D u_depth;
|
|
2649
|
+
uniform ivec2 u_size;
|
|
2650
|
+
uniform float u_focusDepth;
|
|
2651
|
+
uniform float u_focusRange;
|
|
2652
|
+
uniform int u_maxRadius;
|
|
2653
|
+
uniform float u_depthNear;
|
|
2654
|
+
uniform float u_depthFar;
|
|
2655
|
+
out vec4 outColor;
|
|
2656
|
+
|
|
2657
|
+
// Same linearization contract as the SSR program, except focus is authored
|
|
2658
|
+
// directly as a linear-distance fraction (0 = near plane, 1 = far plane):
|
|
2659
|
+
// buffer-unit focus values are unusable once depth is linearized, because the
|
|
2660
|
+
// projection curve parks the whole play area inside a sliver of buffer units.
|
|
2661
|
+
// The CPU byte kernel keeps buffer-unit semantics; the divergence is
|
|
2662
|
+
// documented at normalizeLdrDepthRange.
|
|
2663
|
+
float a3dDofLinearDepth(float depth) {
|
|
2664
|
+
float viewZ = u_depthNear * u_depthFar / max(u_depthFar - depth * (u_depthFar - u_depthNear), 0.0001);
|
|
2665
|
+
return clamp((viewZ - u_depthNear) / max(u_depthFar - u_depthNear, 0.0001), 0.0, 1.0);
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
void main() {
|
|
2669
|
+
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
|
2670
|
+
vec4 source = texelFetch(u_source, pixel, 0);
|
|
2671
|
+
float linearDepth = a3dDofLinearDepth(texelFetch(u_depth, pixel, 0).r);
|
|
2672
|
+
float normalizedBlur = max(0.0, abs(linearDepth - u_focusDepth) - u_focusRange)
|
|
2673
|
+
/ max(u_focusRange, 0.001) * float(u_maxRadius);
|
|
2674
|
+
int radius = min(u_maxRadius, int(round(normalizedBlur)));
|
|
2675
|
+
if (radius == 0) {
|
|
2676
|
+
outColor = source;
|
|
2677
|
+
return;
|
|
2678
|
+
}
|
|
2679
|
+
vec3 sum = vec3(0.0);
|
|
2680
|
+
float samples = 0.0;
|
|
2681
|
+
for (int offsetY = -8; offsetY <= 8; offsetY += 1) {
|
|
2682
|
+
for (int offsetX = -8; offsetX <= 8; offsetX += 1) {
|
|
2683
|
+
if (abs(offsetX) > radius || abs(offsetY) > radius) continue;
|
|
2684
|
+
if (offsetX * offsetX + offsetY * offsetY > radius * radius) continue;
|
|
2685
|
+
ivec2 samplePixel = clamp(pixel + ivec2(offsetX, offsetY), ivec2(0), u_size - ivec2(1));
|
|
2686
|
+
sum += texelFetch(u_source, samplePixel, 0).rgb;
|
|
2687
|
+
samples += 1.0;
|
|
2688
|
+
}
|
|
2689
|
+
}
|
|
2690
|
+
outColor = vec4(sum / max(1.0, samples), source.a);
|
|
2691
|
+
}
|
|
2692
|
+
`, "webgl2-depth-of-field");
|
|
2693
|
+
return this.depthOfFieldProgram;
|
|
2694
|
+
}
|
|
2695
|
+
ensureMotionBlurProgram() {
|
|
2696
|
+
if (this.motionBlurProgram)
|
|
2697
|
+
return this.motionBlurProgram;
|
|
2698
|
+
this.motionBlurProgram = this.createFullscreenProgram(`#version 300 es
|
|
2699
|
+
precision highp float;
|
|
2700
|
+
precision highp int;
|
|
2701
|
+
uniform sampler2D u_source;
|
|
2702
|
+
uniform sampler2D u_velocity;
|
|
2703
|
+
uniform ivec2 u_size;
|
|
2704
|
+
uniform int u_samples;
|
|
2705
|
+
uniform float u_scale;
|
|
2706
|
+
out vec4 outColor;
|
|
2707
|
+
|
|
2708
|
+
void main() {
|
|
2709
|
+
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
|
2710
|
+
vec4 source = texelFetch(u_source, pixel, 0);
|
|
2711
|
+
vec2 velocity = texelFetch(u_velocity, pixel, 0).rg * u_scale;
|
|
2712
|
+
if (length(velocity) < 0.01) {
|
|
2713
|
+
outColor = source;
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
vec3 sum = vec3(0.0);
|
|
2717
|
+
for (int sampleIndex = 0; sampleIndex < 16; sampleIndex += 1) {
|
|
2718
|
+
if (sampleIndex >= u_samples) continue;
|
|
2719
|
+
float t = float(sampleIndex) / float(u_samples - 1) - 0.5;
|
|
2720
|
+
ivec2 samplePixel = ivec2(floor(vec2(pixel) - velocity * t + vec2(0.5)));
|
|
2721
|
+
samplePixel = clamp(samplePixel, ivec2(0), u_size - ivec2(1));
|
|
2722
|
+
sum += texelFetch(u_source, samplePixel, 0).rgb;
|
|
2723
|
+
}
|
|
2724
|
+
outColor = vec4(sum / float(u_samples), source.a);
|
|
2725
|
+
}
|
|
2726
|
+
`, "webgl2-motion-blur");
|
|
2727
|
+
return this.motionBlurProgram;
|
|
2728
|
+
}
|
|
2729
|
+
ensureTaaProgram() {
|
|
2730
|
+
if (this.taaProgram)
|
|
2731
|
+
return this.taaProgram;
|
|
2732
|
+
this.taaProgram = this.createFullscreenProgram(`#version 300 es
|
|
2733
|
+
precision highp float;
|
|
2734
|
+
uniform sampler2D u_source;
|
|
2735
|
+
uniform sampler2D u_history;
|
|
2736
|
+
uniform float u_blend;
|
|
2737
|
+
out vec4 outColor;
|
|
2738
|
+
|
|
2739
|
+
void main() {
|
|
2740
|
+
ivec2 pixel = ivec2(gl_FragCoord.xy);
|
|
2741
|
+
vec4 source = texelFetch(u_source, pixel, 0);
|
|
2742
|
+
vec3 history = texelFetch(u_history, pixel, 0).rgb;
|
|
2743
|
+
outColor = vec4(mix(source.rgb, history, u_blend), source.a);
|
|
2744
|
+
}
|
|
2745
|
+
`, "webgl2-taa");
|
|
2746
|
+
return this.taaProgram;
|
|
2747
|
+
}
|
|
2748
|
+
createFullscreenProgram(fragmentSource, label) {
|
|
2749
|
+
const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
|
|
2750
|
+
precision highp float;
|
|
2751
|
+
void main() {
|
|
2752
|
+
vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
2753
|
+
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
|
|
2754
|
+
}
|
|
2755
|
+
`, label);
|
|
2756
|
+
const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, fragmentSource, label);
|
|
2757
|
+
const program = this.gl.createProgram();
|
|
2758
|
+
if (!program) {
|
|
2759
|
+
this.gl.deleteShader(vertex);
|
|
2760
|
+
this.gl.deleteShader(fragment);
|
|
2761
|
+
throw new RenderDeviceError("Failed to allocate WebGL2 fullscreen shader program", "WEBGL_ALLOCATION_FAILED", { label });
|
|
2762
|
+
}
|
|
2763
|
+
this.gl.attachShader(program, vertex);
|
|
2764
|
+
this.gl.attachShader(program, fragment);
|
|
2765
|
+
this.gl.linkProgram(program);
|
|
2766
|
+
this.gl.deleteShader(vertex);
|
|
2767
|
+
this.gl.deleteShader(fragment);
|
|
2768
|
+
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
2769
|
+
const log = this.gl.getProgramInfoLog(program) ?? "Unknown fullscreen shader link error";
|
|
2770
|
+
this.gl.deleteProgram(program);
|
|
2771
|
+
throw new RenderDeviceError("WebGL2 fullscreen shader link failed", "SHADER_LINK_FAILED", { label, log });
|
|
2772
|
+
}
|
|
2773
|
+
return program;
|
|
2774
|
+
}
|
|
2775
|
+
ensureLdrPostprocessProgram() {
|
|
2776
|
+
if (this.ldrPostprocessProgram) {
|
|
2777
|
+
return this.ldrPostprocessProgram;
|
|
2778
|
+
}
|
|
2779
|
+
const vertex = this.compileShader(this.gl.VERTEX_SHADER, `#version 300 es
|
|
2780
|
+
precision highp float;
|
|
2781
|
+
out vec2 v_uv;
|
|
2782
|
+
void main() {
|
|
2783
|
+
vec2 position = vec2(float((gl_VertexID << 1) & 2), float(gl_VertexID & 2));
|
|
2784
|
+
v_uv = position;
|
|
2785
|
+
gl_Position = vec4(position * 2.0 - 1.0, 0.0, 1.0);
|
|
2786
|
+
}
|
|
2787
|
+
`, "webgl2-ldr-postprocess");
|
|
2788
|
+
const fragment = this.compileShader(this.gl.FRAGMENT_SHADER, `#version 300 es
|
|
2789
|
+
precision highp float;
|
|
2790
|
+
uniform sampler2D u_source;
|
|
2791
|
+
uniform vec2 u_texelSize;
|
|
2792
|
+
uniform int u_hasToneMapping;
|
|
2793
|
+
uniform int u_toneOperator;
|
|
2794
|
+
uniform int u_inputColorSpace;
|
|
2795
|
+
uniform int u_outputColorSpace;
|
|
2796
|
+
uniform float u_exposure;
|
|
2797
|
+
uniform float u_whitePoint;
|
|
2798
|
+
uniform float u_gamma;
|
|
2799
|
+
uniform int u_hasColorGrade;
|
|
2800
|
+
uniform float u_contrast;
|
|
2801
|
+
uniform float u_temperature;
|
|
2802
|
+
uniform float u_tint;
|
|
2803
|
+
uniform float u_saturation;
|
|
2804
|
+
uniform float u_vibrance;
|
|
2805
|
+
uniform float u_vignette;
|
|
2806
|
+
uniform float u_sharpening;
|
|
2807
|
+
uniform int u_hasFxaa;
|
|
2808
|
+
uniform float u_edgeThreshold;
|
|
2809
|
+
uniform float u_subpixelBlend;
|
|
2810
|
+
in vec2 v_uv;
|
|
2811
|
+
out vec4 outColor;
|
|
2812
|
+
|
|
2813
|
+
float srgbToLinear(float value) {
|
|
2814
|
+
return value <= 0.04045 ? value / 12.92 : pow((value + 0.055) / 1.055, 2.4);
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
float linearToSrgb(float value) {
|
|
2818
|
+
return value <= 0.0031308 ? value * 12.92 : 1.055 * pow(value, 1.0 / 2.4) - 0.055;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
vec3 decodeColor(vec3 color) {
|
|
2822
|
+
return u_inputColorSpace == 1 ? vec3(srgbToLinear(color.r), srgbToLinear(color.g), srgbToLinear(color.b)) : color;
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2825
|
+
vec3 encodeColor(vec3 color) {
|
|
2826
|
+
vec3 linear = clamp(color, 0.0, 1.0);
|
|
2827
|
+
return u_outputColorSpace == 1 ? vec3(linearToSrgb(linear.r), linearToSrgb(linear.g), linearToSrgb(linear.b)) : linear;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
vec3 acesRrtAndOdtFit(vec3 value) {
|
|
2831
|
+
vec3 numerator = value * (value + 0.0245786) - 0.000090537;
|
|
2832
|
+
vec3 denominator = value * (0.983729 * value + 0.4329510) + 0.238081;
|
|
2833
|
+
return numerator / denominator;
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2836
|
+
vec3 acesFilmic(vec3 color) {
|
|
2837
|
+
// Match current Three.js ACESFilmicToneMapping. ACES is a coupled RGB
|
|
2838
|
+
// transform; a scalar approximation shifts hue and washes saturated colors.
|
|
2839
|
+
const mat3 acesInput = mat3(
|
|
2840
|
+
vec3(0.59719, 0.07600, 0.02840),
|
|
2841
|
+
vec3(0.35458, 0.90834, 0.13383),
|
|
2842
|
+
vec3(0.04823, 0.01566, 0.83777)
|
|
2843
|
+
);
|
|
2844
|
+
const mat3 acesOutput = mat3(
|
|
2845
|
+
vec3(1.60475, -0.10208, -0.00327),
|
|
2846
|
+
vec3(-0.53108, 1.10813, -0.07276),
|
|
2847
|
+
vec3(-0.07367, -0.00605, 1.07602)
|
|
2848
|
+
);
|
|
2849
|
+
vec3 acesColor = acesInput * (color / 0.6);
|
|
2850
|
+
acesColor = acesRrtAndOdtFit(acesColor);
|
|
2851
|
+
return clamp(acesOutput * acesColor, 0.0, 1.0);
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
float filmic(float value) {
|
|
2855
|
+
float x = max(0.0, value);
|
|
2856
|
+
float toe = max(0.0, x - 0.004);
|
|
2857
|
+
float curve = (toe * (6.2 * toe + 0.5)) / (toe * (6.2 * toe + 1.7) + 0.06);
|
|
2858
|
+
return min(curve, x * 1.08);
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
float uncharted2(float value) {
|
|
2862
|
+
float a = 0.15;
|
|
2863
|
+
float b = 0.5;
|
|
2864
|
+
float c = 0.1;
|
|
2865
|
+
float d = 0.2;
|
|
2866
|
+
float e = 0.02;
|
|
2867
|
+
float f = 0.3;
|
|
2868
|
+
float w = 11.2;
|
|
2869
|
+
float x = value * 2.0;
|
|
2870
|
+
float curve = ((x * (a * x + c * b) + d * e) / (x * (a * x + b) + d * f)) - e / f;
|
|
2871
|
+
float wx = w;
|
|
2872
|
+
float white = ((wx * (a * wx + c * b) + d * e) / (wx * (a * wx + b) + d * f)) - e / f;
|
|
2873
|
+
return clamp(curve / white, 0.0, 1.0);
|
|
2874
|
+
}
|
|
2875
|
+
|
|
2876
|
+
float agx(float value) {
|
|
2877
|
+
float x = max(0.0, value);
|
|
2878
|
+
float encoded = log2(1.0 + x) / log2(17.0);
|
|
2879
|
+
return clamp(encoded * encoded * (3.0 - 2.0 * encoded), 0.0, 1.0);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
float neutral(float value) {
|
|
2883
|
+
float x = max(0.0, value);
|
|
2884
|
+
return min(1.0, (x * (1.0 + x / 7.5)) / (1.0 + x));
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
float toneMapChannel(float value) {
|
|
2888
|
+
float exposed = max(0.0, value * u_exposure) / max(0.0001, u_whitePoint);
|
|
2889
|
+
if (u_toneOperator == 0) return min(1.0, exposed);
|
|
2890
|
+
if (u_toneOperator == 1) return exposed / (1.0 + exposed);
|
|
2891
|
+
if (u_toneOperator == 2) return exposed;
|
|
2892
|
+
if (u_toneOperator == 3) return filmic(exposed);
|
|
2893
|
+
if (u_toneOperator == 4) return uncharted2(exposed);
|
|
2894
|
+
if (u_toneOperator == 5) return agx(exposed);
|
|
2895
|
+
return neutral(exposed);
|
|
2896
|
+
}
|
|
2897
|
+
|
|
2898
|
+
vec3 applyToneMapping(vec3 color) {
|
|
2899
|
+
if (u_hasToneMapping == 0) return color;
|
|
2900
|
+
vec3 decoded = decodeColor(color);
|
|
2901
|
+
vec3 mapped = u_toneOperator == 2
|
|
2902
|
+
? acesFilmic(max(vec3(0.0), decoded * u_exposure) / max(0.0001, u_whitePoint))
|
|
2903
|
+
: vec3(toneMapChannel(decoded.r), toneMapChannel(decoded.g), toneMapChannel(decoded.b));
|
|
2904
|
+
return encodeColor(mapped);
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
vec3 applyColorGrade(vec3 color, vec2 uv) {
|
|
2908
|
+
if (u_hasColorGrade == 0) return color;
|
|
2909
|
+
float contrastOffset = 0.5 - 0.5 * u_contrast;
|
|
2910
|
+
float redShift = u_temperature * 0.08 - u_tint * 0.02;
|
|
2911
|
+
float greenShift = u_tint * 0.06;
|
|
2912
|
+
float blueShift = -u_temperature * 0.08 - u_tint * 0.02;
|
|
2913
|
+
float luma = dot(color, vec3(0.2126, 0.7152, 0.0722));
|
|
2914
|
+
vec3 graded = color * u_contrast + vec3(contrastOffset + redShift, contrastOffset + greenShift, contrastOffset + blueShift);
|
|
2915
|
+
float distanceFromLuma = min(1.0, abs(graded.r - luma) + abs(graded.g - luma) + abs(graded.b - luma));
|
|
2916
|
+
float vibranceBoost = u_vibrance == 0.0 ? 0.0 : u_vibrance * (1.0 - distanceFromLuma);
|
|
2917
|
+
float saturation = u_saturation + vibranceBoost;
|
|
2918
|
+
graded = vec3(luma) + (graded - vec3(luma)) * saturation;
|
|
2919
|
+
vec2 centered = uv * 2.0 - 1.0;
|
|
2920
|
+
float vignette = 1.0 - u_vignette * clamp((length(centered) - 0.28) / 1.12, 0.0, 1.0);
|
|
2921
|
+
return clamp(graded * vignette, 0.0, 1.0);
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
vec3 baseColorAt(vec2 uv) {
|
|
2925
|
+
vec2 clampedUv = clamp(uv, vec2(0.0), vec2(1.0));
|
|
2926
|
+
vec3 color = texture(u_source, clampedUv).rgb;
|
|
2927
|
+
return applyColorGrade(applyToneMapping(color), clampedUv);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
vec3 finalColorAt(vec2 uv) {
|
|
2931
|
+
vec3 center = baseColorAt(uv);
|
|
2932
|
+
if (u_hasColorGrade == 0 || u_sharpening <= 0.0) return center;
|
|
2933
|
+
vec3 blur = (
|
|
2934
|
+
baseColorAt(uv + vec2(-u_texelSize.x, 0.0)) +
|
|
2935
|
+
baseColorAt(uv + vec2(u_texelSize.x, 0.0)) +
|
|
2936
|
+
baseColorAt(uv + vec2(0.0, -u_texelSize.y)) +
|
|
2937
|
+
baseColorAt(uv + vec2(0.0, u_texelSize.y))
|
|
2938
|
+
) * 0.25;
|
|
2939
|
+
return clamp(center + (center - blur) * u_sharpening, 0.0, 1.0);
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
float luma(vec3 color) {
|
|
2943
|
+
return dot(color, vec3(0.2126, 0.7152, 0.0722));
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
void main() {
|
|
2947
|
+
vec3 center = finalColorAt(v_uv);
|
|
2948
|
+
float alpha = texture(u_source, clamp(v_uv, vec2(0.0), vec2(1.0))).a;
|
|
2949
|
+
if (u_hasFxaa == 0) {
|
|
2950
|
+
outColor = vec4(center, alpha);
|
|
2951
|
+
return;
|
|
2952
|
+
}
|
|
2953
|
+
vec3 north = finalColorAt(v_uv + vec2(0.0, -u_texelSize.y));
|
|
2954
|
+
vec3 south = finalColorAt(v_uv + vec2(0.0, u_texelSize.y));
|
|
2955
|
+
vec3 west = finalColorAt(v_uv + vec2(-u_texelSize.x, 0.0));
|
|
2956
|
+
vec3 east = finalColorAt(v_uv + vec2(u_texelSize.x, 0.0));
|
|
2957
|
+
float centerLuma = luma(center);
|
|
2958
|
+
float minLuma = min(centerLuma, min(min(luma(north), luma(south)), min(luma(west), luma(east))));
|
|
2959
|
+
float maxLuma = max(centerLuma, max(max(luma(north), luma(south)), max(luma(west), luma(east))));
|
|
2960
|
+
if (maxLuma - minLuma < u_edgeThreshold) {
|
|
2961
|
+
outColor = vec4(center, alpha);
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
vec3 average = (north + south + west + east) * 0.25;
|
|
2965
|
+
outColor = vec4(mix(center, average, clamp(u_subpixelBlend, 0.0, 1.0)), alpha);
|
|
2966
|
+
}
|
|
2967
|
+
`, "webgl2-ldr-postprocess");
|
|
2968
|
+
const program = this.gl.createProgram();
|
|
2969
|
+
if (!program) {
|
|
2970
|
+
this.gl.deleteShader(vertex);
|
|
2971
|
+
this.gl.deleteShader(fragment);
|
|
2972
|
+
throw new RenderDeviceError("Failed to allocate WebGL2 LDR postprocess shader", "WEBGL_ALLOCATION_FAILED");
|
|
2973
|
+
}
|
|
2974
|
+
this.gl.attachShader(program, vertex);
|
|
2975
|
+
this.gl.attachShader(program, fragment);
|
|
2976
|
+
this.gl.linkProgram(program);
|
|
2977
|
+
this.gl.deleteShader(vertex);
|
|
2978
|
+
this.gl.deleteShader(fragment);
|
|
2979
|
+
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
|
|
2980
|
+
const log = this.gl.getProgramInfoLog(program) ?? "Unknown LDR postprocess shader link error";
|
|
2981
|
+
this.gl.deleteProgram(program);
|
|
2982
|
+
throw new RenderDeviceError("WebGL2 LDR postprocess shader link failed", "SHADER_LINK_FAILED", { log });
|
|
2983
|
+
}
|
|
2984
|
+
this.ldrPostprocessProgram = program;
|
|
2985
|
+
return program;
|
|
2986
|
+
}
|
|
2987
|
+
compileShader(type, source, label) {
|
|
2988
|
+
const shader = this.gl.createShader(type);
|
|
2989
|
+
if (!shader) {
|
|
2990
|
+
throw new RenderDeviceError("Failed to allocate WebGL shader", "WEBGL_ALLOCATION_FAILED", { label });
|
|
2991
|
+
}
|
|
2992
|
+
this.gl.shaderSource(shader, source);
|
|
2993
|
+
this.gl.compileShader(shader);
|
|
2994
|
+
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
|
|
2995
|
+
const log = this.gl.getShaderInfoLog(shader) ?? "Unknown shader compile error";
|
|
2996
|
+
this.gl.deleteShader(shader);
|
|
2997
|
+
throw new RenderDeviceError("WebGL shader compile failed", "SHADER_COMPILE_FAILED", { label, log });
|
|
2998
|
+
}
|
|
2999
|
+
return shader;
|
|
3000
|
+
}
|
|
3001
|
+
reflectProgram(program) {
|
|
3002
|
+
const attributes = new Map();
|
|
3003
|
+
const uniforms = new Set();
|
|
3004
|
+
const attributeDetails = new Map();
|
|
3005
|
+
const uniformDetails = new Map();
|
|
3006
|
+
const activeAttributes = this.gl.getProgramParameter(program, this.gl.ACTIVE_ATTRIBUTES);
|
|
3007
|
+
const activeUniforms = this.gl.getProgramParameter(program, this.gl.ACTIVE_UNIFORMS);
|
|
3008
|
+
for (let i = 0; i < activeAttributes; i += 1) {
|
|
3009
|
+
const info = this.gl.getActiveAttrib(program, i);
|
|
3010
|
+
if (info) {
|
|
3011
|
+
const location = this.gl.getAttribLocation(program, info.name);
|
|
3012
|
+
attributes.set(info.name, location);
|
|
3013
|
+
attributeDetails.set(info.name, { name: info.name, type: String(info.type), location, source: "vertex", line: 0 });
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
for (let i = 0; i < activeUniforms; i += 1) {
|
|
3017
|
+
const info = this.gl.getActiveUniform(program, i);
|
|
3018
|
+
if (info) {
|
|
3019
|
+
const name = info.name.replace(/\[0\]$/, "");
|
|
3020
|
+
uniforms.add(name);
|
|
3021
|
+
uniformDetails.set(name, { name, type: String(info.type), arraySize: info.size > 1 ? info.size : null, source: "fragment", line: 0 });
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
return { attributes, uniforms, attributeDetails, uniformDetails };
|
|
3025
|
+
}
|
|
3026
|
+
uploadUniforms(shader, uniforms) {
|
|
3027
|
+
let textureUnit = 0;
|
|
3028
|
+
for (const [name, value] of uniforms) {
|
|
3029
|
+
if (!shader.reflection.uniforms.has(name)) {
|
|
3030
|
+
continue;
|
|
3031
|
+
}
|
|
3032
|
+
const location = this.getUniformLocation(shader, name);
|
|
3033
|
+
if (location === null) {
|
|
3034
|
+
throw new RenderDeviceError("Material tried to bind a missing shader uniform", "MISSING_UNIFORM", { name });
|
|
3035
|
+
}
|
|
3036
|
+
if (isTextureBinding(value)) {
|
|
3037
|
+
this.uploadTextureUniform(location, value, textureUnit);
|
|
3038
|
+
textureUnit += 1;
|
|
3039
|
+
}
|
|
3040
|
+
else if (typeof value === "number") {
|
|
3041
|
+
this.gl.uniform1f(location, value);
|
|
3042
|
+
}
|
|
3043
|
+
else if (Array.isArray(value) || ArrayBuffer.isView(value)) {
|
|
3044
|
+
const length = value.length;
|
|
3045
|
+
const floatData = value;
|
|
3046
|
+
if (length === 16 || (length > 16 && length % 16 === 0 && /(?:Matrix|Matrices)$/.test(name))) {
|
|
3047
|
+
this.gl.uniformMatrix4fv(location, false, floatData);
|
|
3048
|
+
}
|
|
3049
|
+
else if (length > 16 && length % 4 === 0) {
|
|
3050
|
+
this.gl.uniform4fv(location, floatData);
|
|
3051
|
+
}
|
|
3052
|
+
else if (length === 4) {
|
|
3053
|
+
this.gl.uniform4fv(location, floatData);
|
|
3054
|
+
}
|
|
3055
|
+
else if (length === 3) {
|
|
3056
|
+
this.gl.uniform3fv(location, floatData);
|
|
3057
|
+
}
|
|
3058
|
+
else if (length === 2) {
|
|
3059
|
+
this.gl.uniform2fv(location, floatData);
|
|
3060
|
+
}
|
|
3061
|
+
else {
|
|
3062
|
+
throw new RenderDeviceError("Unsupported uniform array length", "UNSUPPORTED_UNIFORM", { name, length });
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
else {
|
|
3066
|
+
throw new RenderDeviceError("Unsupported uniform value", "UNSUPPORTED_UNIFORM", { name, valueType: typeof value });
|
|
3067
|
+
}
|
|
3068
|
+
if (this.errorCheckMode === "strict") {
|
|
3069
|
+
const uniformError = this.readError();
|
|
3070
|
+
if (uniformError) {
|
|
3071
|
+
throw new RenderDeviceError(`WebGL2 uniform upload failed for ${name}: ${uniformError}`, "WEBGL_DRAW_FAILED", {
|
|
3072
|
+
name,
|
|
3073
|
+
stage: "uniform",
|
|
3074
|
+
error: uniformError
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
getUniformLocation(shader, name) {
|
|
3081
|
+
let cache = this.uniformLocationCache.get(shader);
|
|
3082
|
+
if (!cache) {
|
|
3083
|
+
cache = new Map();
|
|
3084
|
+
this.uniformLocationCache.set(shader, cache);
|
|
3085
|
+
}
|
|
3086
|
+
if (!cache.has(name)) {
|
|
3087
|
+
this.uniformLocationLookupCount += 1;
|
|
3088
|
+
cache.set(name, this.gl.getUniformLocation(shader.handle, name) ?? this.gl.getUniformLocation(shader.handle, `${name}[0]`));
|
|
3089
|
+
}
|
|
3090
|
+
return cache.get(name) ?? null;
|
|
3091
|
+
}
|
|
3092
|
+
uploadTextureUniform(location, binding, textureUnit) {
|
|
3093
|
+
const validation = binding.validate();
|
|
3094
|
+
if (!validation.ok) {
|
|
3095
|
+
throw new RenderDeviceError("Texture binding validation failed", "INVALID_TEXTURE_BINDING", {
|
|
3096
|
+
diagnostics: validation.diagnostics,
|
|
3097
|
+
name: binding.name
|
|
3098
|
+
});
|
|
3099
|
+
}
|
|
3100
|
+
this.activateTextureUnit(textureUnit);
|
|
3101
|
+
const dimension = binding.texture?.dimension ?? (binding.name.toLowerCase().includes("cubemap") || binding.name.toLowerCase().includes("cube") ? "cube" : "2d");
|
|
3102
|
+
const target = dimension === "cube" ? this.gl.TEXTURE_CUBE_MAP : this.gl.TEXTURE_2D;
|
|
3103
|
+
const handle = binding.texture ? this.getTextureHandle(binding.texture) : dimension === "cube" ? this.getFallbackCubeTextureHandle() : this.getFallbackTextureHandle();
|
|
3104
|
+
if (binding.texture) {
|
|
3105
|
+
const lowerName = binding.name.toLowerCase();
|
|
3106
|
+
if (lowerName.includes("environment"))
|
|
3107
|
+
this.nativeEnvironmentBindings += 1;
|
|
3108
|
+
if (lowerName.includes("shadow"))
|
|
3109
|
+
this.nativeShadowMapBindings += 1;
|
|
3110
|
+
}
|
|
3111
|
+
this.bindTextureForUnit(textureUnit, target, handle);
|
|
3112
|
+
const samplerHandle = this.getSamplerHandle(binding.sampler, target);
|
|
3113
|
+
this.stateCache.bindSampler(textureUnit, samplerHandle, () => this.gl.bindSampler(textureUnit, samplerHandle));
|
|
3114
|
+
this.gl.uniform1i(location, textureUnit);
|
|
3115
|
+
}
|
|
3116
|
+
activateTextureUnit(textureUnit) {
|
|
3117
|
+
if (this.activeTextureUnitIndex === textureUnit)
|
|
3118
|
+
return;
|
|
3119
|
+
this.stateCache.activeTexture(textureUnit, () => this.gl.activeTexture(this.gl.TEXTURE0 + textureUnit));
|
|
3120
|
+
this.activeTextureUnitIndex = textureUnit;
|
|
3121
|
+
}
|
|
3122
|
+
bindTextureForUnit(textureUnit, target, handle) {
|
|
3123
|
+
const key = `${textureUnit}:${target}`;
|
|
3124
|
+
if (this.textureUnitBindings.get(key) === handle)
|
|
3125
|
+
return;
|
|
3126
|
+
this.stateCache.bindTexture(target, handle, () => this.gl.bindTexture(target, handle));
|
|
3127
|
+
this.textureUnitBindings.set(key, handle);
|
|
3128
|
+
this.textureBindCount += 1;
|
|
3129
|
+
}
|
|
3130
|
+
setTextureParameterIfNeeded(handle, target, parameter, value) {
|
|
3131
|
+
let parameters = this.textureSamplerParameterCache.get(handle);
|
|
3132
|
+
if (!parameters) {
|
|
3133
|
+
parameters = new Map();
|
|
3134
|
+
this.textureSamplerParameterCache.set(handle, parameters);
|
|
3135
|
+
}
|
|
3136
|
+
if (parameters.get(parameter) === value)
|
|
3137
|
+
return;
|
|
3138
|
+
this.gl.texParameteri(target, parameter, value);
|
|
3139
|
+
parameters.set(parameter, value);
|
|
3140
|
+
this.samplerParameterUploadCount += 1;
|
|
3141
|
+
}
|
|
3142
|
+
applySamplerAnisotropy(maxAnisotropy, target = this.gl.TEXTURE_2D) {
|
|
3143
|
+
if (maxAnisotropy <= 1)
|
|
3144
|
+
return;
|
|
3145
|
+
if (!this.anisotropicFilteringExtension)
|
|
3146
|
+
return;
|
|
3147
|
+
this.gl.texParameterf(target, this.anisotropicFilteringExtension.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(Math.max(1, maxAnisotropy), this.maxTextureAnisotropy));
|
|
3148
|
+
this.samplerAnisotropyUploadCount += 1;
|
|
3149
|
+
}
|
|
3150
|
+
getSamplerHandle(sampler, target) {
|
|
3151
|
+
const key = this.samplerKey(sampler, target);
|
|
3152
|
+
const cached = this.samplerObjectCache.get(key);
|
|
3153
|
+
if (cached)
|
|
3154
|
+
return cached;
|
|
3155
|
+
const handle = this.gl.createSampler();
|
|
3156
|
+
if (!handle) {
|
|
3157
|
+
throw new RenderDeviceError("Failed to allocate WebGL sampler", "WEBGL_ALLOCATION_FAILED");
|
|
3158
|
+
}
|
|
3159
|
+
this.gl.samplerParameteri(handle, this.gl.TEXTURE_MIN_FILTER, this.minFilter(sampler.minFilter));
|
|
3160
|
+
this.gl.samplerParameteri(handle, this.gl.TEXTURE_MAG_FILTER, this.magFilter(sampler.magFilter));
|
|
3161
|
+
this.gl.samplerParameteri(handle, this.gl.TEXTURE_WRAP_S, this.addressMode(sampler.addressU));
|
|
3162
|
+
this.gl.samplerParameteri(handle, this.gl.TEXTURE_WRAP_T, this.addressMode(sampler.addressV));
|
|
3163
|
+
this.samplerParameterUploadCount += 4;
|
|
3164
|
+
if (target === this.gl.TEXTURE_CUBE_MAP) {
|
|
3165
|
+
this.gl.samplerParameteri(handle, this.gl.TEXTURE_WRAP_R, this.addressMode(sampler.addressV));
|
|
3166
|
+
this.samplerParameterUploadCount += 1;
|
|
3167
|
+
}
|
|
3168
|
+
if (sampler.maxAnisotropy > 1 && this.anisotropicFilteringExtension) {
|
|
3169
|
+
this.gl.samplerParameterf(handle, this.anisotropicFilteringExtension.TEXTURE_MAX_ANISOTROPY_EXT, Math.min(Math.max(1, sampler.maxAnisotropy), this.maxTextureAnisotropy));
|
|
3170
|
+
this.samplerAnisotropyUploadCount += 1;
|
|
3171
|
+
}
|
|
3172
|
+
this.samplerObjectCache.set(key, handle);
|
|
3173
|
+
return handle;
|
|
3174
|
+
}
|
|
3175
|
+
samplerKey(sampler, target) {
|
|
3176
|
+
return [
|
|
3177
|
+
sampler.minFilter,
|
|
3178
|
+
sampler.magFilter,
|
|
3179
|
+
sampler.addressU,
|
|
3180
|
+
sampler.addressV,
|
|
3181
|
+
sampler.maxAnisotropy,
|
|
3182
|
+
target === this.gl.TEXTURE_CUBE_MAP ? "cube" : "2d"
|
|
3183
|
+
].join("|");
|
|
3184
|
+
}
|
|
3185
|
+
getFallbackTextureHandle() {
|
|
3186
|
+
if (this.fallbackTexture) {
|
|
3187
|
+
return this.fallbackTexture;
|
|
3188
|
+
}
|
|
3189
|
+
const handle = this.gl.createTexture();
|
|
3190
|
+
if (!handle) {
|
|
3191
|
+
throw new RenderDeviceError("Failed to allocate WebGL fallback texture", "WEBGL_ALLOCATION_FAILED");
|
|
3192
|
+
}
|
|
3193
|
+
this.stateCache.invalidate();
|
|
3194
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, handle);
|
|
3195
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
|
|
3196
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
|
|
3197
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
3198
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
3199
|
+
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]));
|
|
3200
|
+
this.fallbackTexture = handle;
|
|
3201
|
+
return handle;
|
|
3202
|
+
}
|
|
3203
|
+
getFallbackCubeTextureHandle() {
|
|
3204
|
+
if (this.fallbackCubeTexture) {
|
|
3205
|
+
return this.fallbackCubeTexture;
|
|
3206
|
+
}
|
|
3207
|
+
const handle = this.gl.createTexture();
|
|
3208
|
+
if (!handle) {
|
|
3209
|
+
throw new RenderDeviceError("Failed to allocate WebGL fallback cube texture", "WEBGL_ALLOCATION_FAILED");
|
|
3210
|
+
}
|
|
3211
|
+
this.stateCache.invalidate();
|
|
3212
|
+
this.gl.bindTexture(this.gl.TEXTURE_CUBE_MAP, handle);
|
|
3213
|
+
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
|
|
3214
|
+
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
|
|
3215
|
+
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
3216
|
+
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
3217
|
+
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_R, this.gl.CLAMP_TO_EDGE);
|
|
3218
|
+
const pixel = new Uint8Array([255, 255, 255, 255]);
|
|
3219
|
+
for (const face of WEBGL_CUBE_FACES) {
|
|
3220
|
+
this.gl.texImage2D(this.cubeFaceTarget(face), 0, this.gl.RGBA, 1, 1, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, pixel);
|
|
3221
|
+
}
|
|
3222
|
+
this.fallbackCubeTexture = handle;
|
|
3223
|
+
return handle;
|
|
3224
|
+
}
|
|
3225
|
+
getTextureHandle(texture) {
|
|
3226
|
+
this.releaseDisposedTextureHandles();
|
|
3227
|
+
if (texture.disposed) {
|
|
3228
|
+
throw new RenderDeviceError("Texture is disposed", "DISPOSED_RESOURCE", { label: texture.label });
|
|
3229
|
+
}
|
|
3230
|
+
const cached = this.textures.get(texture);
|
|
3231
|
+
if (cached) {
|
|
3232
|
+
return cached;
|
|
3233
|
+
}
|
|
3234
|
+
const handle = this.gl.createTexture();
|
|
3235
|
+
if (!handle) {
|
|
3236
|
+
throw new RenderDeviceError("Failed to allocate WebGL texture", "WEBGL_ALLOCATION_FAILED", { label: texture.label });
|
|
3237
|
+
}
|
|
3238
|
+
this.stateCache.invalidate();
|
|
3239
|
+
this.gl.pixelStorei(this.gl.UNPACK_FLIP_Y_WEBGL, false);
|
|
3240
|
+
this.gl.pixelStorei(this.gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
|
|
3241
|
+
this.gl.pixelStorei(this.gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, this.gl.NONE);
|
|
3242
|
+
if (texture.dimension === "cube") {
|
|
3243
|
+
this.uploadCubeTexture(texture, handle);
|
|
3244
|
+
this.textureUploadModes.set(texture, "cube");
|
|
3245
|
+
this.textures.set(texture, handle);
|
|
3246
|
+
return handle;
|
|
3247
|
+
}
|
|
3248
|
+
this.gl.bindTexture(this.gl.TEXTURE_2D, handle);
|
|
3249
|
+
if (isCompressedTextureFormat(texture.format)) {
|
|
3250
|
+
const compressed = this.resolveCompressedTextureFormat(texture.format);
|
|
3251
|
+
if (compressed) {
|
|
3252
|
+
const uploadLevels = completeUploadLevels(texture.textureLevels);
|
|
3253
|
+
for (const [levelIndex, level] of uploadLevels.entries()) {
|
|
3254
|
+
this.gl.compressedTexImage2D(this.gl.TEXTURE_2D, levelIndex, compressed.internalFormat, level.width, level.height, 0, level.data);
|
|
3255
|
+
}
|
|
3256
|
+
const compressedUploadError = this.readError();
|
|
3257
|
+
if (!compressedUploadError) {
|
|
3258
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
|
|
3259
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, uploadLevels.length - 1);
|
|
3260
|
+
this.textureUploadModes.set(texture, "compressed");
|
|
3261
|
+
this.textures.set(texture, handle);
|
|
3262
|
+
return handle;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
const fallbackLevels = completeUploadLevels(texture.fallbackTextureLevels);
|
|
3266
|
+
if (fallbackLevels.length === 0) {
|
|
3267
|
+
this.gl.deleteTexture(handle);
|
|
3268
|
+
throw new RenderDeviceError("Compressed texture format is not supported and no RGBA8 fallback data was provided", "COMPRESSED_TEXTURE_UNSUPPORTED", {
|
|
3269
|
+
label: texture.label,
|
|
3270
|
+
format: texture.format
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
this.uploadRgba8FallbackTexture(texture, fallbackLevels);
|
|
3274
|
+
this.textureUploadModes.set(texture, "fallback");
|
|
3275
|
+
this.textures.set(texture, handle);
|
|
3276
|
+
return handle;
|
|
3277
|
+
}
|
|
3278
|
+
if (texture.format === "depth24") {
|
|
3279
|
+
throw new RenderDeviceError("Depth textures cannot be uploaded to WebGL2 color samplers", "UNSUPPORTED_TEXTURE_FORMAT", {
|
|
3280
|
+
label: texture.label,
|
|
3281
|
+
format: texture.format
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
if (texture.source) {
|
|
3285
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.rgba8TextureInternalFormat(texture), this.gl.RGBA, this.gl.UNSIGNED_BYTE, texture.source);
|
|
3286
|
+
this.gl.generateMipmap(this.gl.TEXTURE_2D);
|
|
3287
|
+
}
|
|
3288
|
+
else if (texture.mipLevels.length > 0) {
|
|
3289
|
+
const uploadFormat = this.textureUploadFormat(texture);
|
|
3290
|
+
const uploadLevels = completeUploadLevels(texture.textureLevels);
|
|
3291
|
+
for (const [levelIndex, level] of uploadLevels.entries()) {
|
|
3292
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, levelIndex, uploadFormat.internalFormat, level.width, level.height, 0, uploadFormat.format, uploadFormat.type, texturePixelUploadData(level.data, texture.format));
|
|
3293
|
+
}
|
|
3294
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
|
|
3295
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, uploadLevels.length - 1);
|
|
3296
|
+
}
|
|
3297
|
+
else {
|
|
3298
|
+
const uploadFormat = this.textureUploadFormat(texture);
|
|
3299
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, uploadFormat.internalFormat, texture.width, texture.height, 0, uploadFormat.format, uploadFormat.type, texture.data ? texturePixelUploadData(texture.data, texture.format) : null);
|
|
3300
|
+
// Float colour formats are not guaranteed mipmap-filterable in WebGL2:
|
|
3301
|
+
// RGBA32F/RGBA16F are only linear-filterable with OES_texture_float_linear, and
|
|
3302
|
+
// generateMipmap on a non-filterable format raises INVALID_OPERATION. Data
|
|
3303
|
+
// textures such as joint palettes are point-sampled with texelFetch and never
|
|
3304
|
+
// need mips, so skip generation and pin the level range instead.
|
|
3305
|
+
if (isFloatColorTextureFormat(texture.format)) {
|
|
3306
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
|
|
3307
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, 0);
|
|
3308
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.NEAREST);
|
|
3309
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.NEAREST);
|
|
3310
|
+
}
|
|
3311
|
+
else {
|
|
3312
|
+
this.gl.generateMipmap(this.gl.TEXTURE_2D);
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
this.textureUploadModes.set(texture, texture.format);
|
|
3316
|
+
this.textures.set(texture, handle);
|
|
3317
|
+
return handle;
|
|
3318
|
+
}
|
|
3319
|
+
uploadCubeTexture(texture, handle) {
|
|
3320
|
+
this.gl.bindTexture(this.gl.TEXTURE_CUBE_MAP, handle);
|
|
3321
|
+
const uploadFormat = this.textureUploadFormat(texture);
|
|
3322
|
+
for (const face of texture.cubeFaces) {
|
|
3323
|
+
const uploadLevels = completeUploadLevels(face.mipLevels);
|
|
3324
|
+
for (const [levelIndex, level] of uploadLevels.entries()) {
|
|
3325
|
+
this.gl.texImage2D(this.cubeFaceTarget(face.face), levelIndex, uploadFormat.internalFormat, level.width, level.height, 0, uploadFormat.format, uploadFormat.type, texturePixelUploadData(level.data, texture.format));
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
const firstFaceLevels = completeUploadLevels(texture.cubeFaces[0]?.mipLevels ?? []);
|
|
3329
|
+
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_BASE_LEVEL, 0);
|
|
3330
|
+
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_MAX_LEVEL, Math.max(0, firstFaceLevels.length - 1));
|
|
3331
|
+
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
|
|
3332
|
+
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
|
|
3333
|
+
this.gl.texParameteri(this.gl.TEXTURE_CUBE_MAP, this.gl.TEXTURE_WRAP_R, this.gl.CLAMP_TO_EDGE);
|
|
3334
|
+
}
|
|
3335
|
+
uploadRgba8FallbackTexture(texture, fallbackLevels) {
|
|
3336
|
+
const internalFormat = this.rgba8TextureInternalFormat(texture);
|
|
3337
|
+
for (const [levelIndex, level] of fallbackLevels.entries()) {
|
|
3338
|
+
this.gl.texImage2D(this.gl.TEXTURE_2D, levelIndex, internalFormat, level.width, level.height, 0, this.gl.RGBA, this.gl.UNSIGNED_BYTE, texturePixelUploadData(level.data, "rgba8"));
|
|
3339
|
+
}
|
|
3340
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_BASE_LEVEL, 0);
|
|
3341
|
+
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAX_LEVEL, fallbackLevels.length - 1);
|
|
3342
|
+
}
|
|
3343
|
+
cubeFaceTarget(face) {
|
|
3344
|
+
switch (face) {
|
|
3345
|
+
case "px": return this.gl.TEXTURE_CUBE_MAP_POSITIVE_X;
|
|
3346
|
+
case "nx": return this.gl.TEXTURE_CUBE_MAP_NEGATIVE_X;
|
|
3347
|
+
case "py": return this.gl.TEXTURE_CUBE_MAP_POSITIVE_Y;
|
|
3348
|
+
case "ny": return this.gl.TEXTURE_CUBE_MAP_NEGATIVE_Y;
|
|
3349
|
+
case "pz": return this.gl.TEXTURE_CUBE_MAP_POSITIVE_Z;
|
|
3350
|
+
case "nz": return this.gl.TEXTURE_CUBE_MAP_NEGATIVE_Z;
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
rgba8TextureInternalFormat(texture) {
|
|
3354
|
+
return texture.colorSpace === "srgb" ? this.gl.SRGB8_ALPHA8 : this.gl.RGBA;
|
|
3355
|
+
}
|
|
3356
|
+
textureUploadFormat(texture) {
|
|
3357
|
+
if (texture.format === "rgba8") {
|
|
3358
|
+
return { internalFormat: this.rgba8TextureInternalFormat(texture), format: this.gl.RGBA, type: this.gl.UNSIGNED_BYTE };
|
|
3359
|
+
}
|
|
3360
|
+
if (texture.colorSpace === "srgb") {
|
|
3361
|
+
throw new RenderDeviceError("Floating-point texture uploads must use linear colorSpace", "UNSUPPORTED_TEXTURE_FORMAT", {
|
|
3362
|
+
label: texture.label,
|
|
3363
|
+
format: texture.format,
|
|
3364
|
+
colorSpace: texture.colorSpace
|
|
3365
|
+
});
|
|
3366
|
+
}
|
|
3367
|
+
if (texture.format === "rgba16f") {
|
|
3368
|
+
return { internalFormat: this.gl.RGBA16F, format: this.gl.RGBA, type: this.gl.HALF_FLOAT };
|
|
3369
|
+
}
|
|
3370
|
+
if (texture.format === "rgba32f") {
|
|
3371
|
+
return { internalFormat: this.gl.RGBA32F, format: this.gl.RGBA, type: this.gl.FLOAT };
|
|
3372
|
+
}
|
|
3373
|
+
throw new RenderDeviceError("Unsupported texture upload format", "UNSUPPORTED_TEXTURE_FORMAT", {
|
|
3374
|
+
label: texture.label,
|
|
3375
|
+
format: texture.format
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
resolveRenderTargetFormat(format) {
|
|
3379
|
+
if (format === "rgba8") {
|
|
3380
|
+
return { internalFormat: this.gl.RGBA, type: this.gl.UNSIGNED_BYTE };
|
|
3381
|
+
}
|
|
3382
|
+
if (!this.gl.getExtension("EXT_color_buffer_float")) {
|
|
3383
|
+
throw new RenderDeviceError("Floating-point color render targets require EXT_color_buffer_float", "HDR_RENDER_TARGET_UNSUPPORTED", { format });
|
|
3384
|
+
}
|
|
3385
|
+
if (format === "rgba16f") {
|
|
3386
|
+
return { internalFormat: this.gl.RGBA16F, type: this.gl.HALF_FLOAT };
|
|
3387
|
+
}
|
|
3388
|
+
return { internalFormat: this.gl.RGBA32F, type: this.gl.FLOAT };
|
|
3389
|
+
}
|
|
3390
|
+
releaseDisposedTextureHandles() {
|
|
3391
|
+
for (const [texture, handle] of [...this.textures]) {
|
|
3392
|
+
if (texture.disposed) {
|
|
3393
|
+
this.gl.deleteTexture(handle);
|
|
3394
|
+
this.textures.delete(texture);
|
|
3395
|
+
this.textureUploadModes.delete(texture);
|
|
3396
|
+
this.releasedTextureHandles += 1;
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
resolveCompressedTextureFormat(format) {
|
|
3401
|
+
switch (format) {
|
|
3402
|
+
case "bc1-rgba-unorm": {
|
|
3403
|
+
const extension = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
|
|
3404
|
+
return extension ? { internalFormat: extension.COMPRESSED_RGBA_S3TC_DXT1_EXT } : null;
|
|
3405
|
+
}
|
|
3406
|
+
case "bc3-rgba-unorm": {
|
|
3407
|
+
const extension = this.gl.getExtension("WEBGL_compressed_texture_s3tc");
|
|
3408
|
+
return extension ? { internalFormat: extension.COMPRESSED_RGBA_S3TC_DXT5_EXT } : null;
|
|
3409
|
+
}
|
|
3410
|
+
case "etc2-rgba8unorm":
|
|
3411
|
+
return { internalFormat: 0x9278 };
|
|
3412
|
+
case "astc-4x4-rgba-unorm": {
|
|
3413
|
+
const extension = this.gl.getExtension("WEBGL_compressed_texture_astc");
|
|
3414
|
+
return extension ? { internalFormat: extension.COMPRESSED_RGBA_ASTC_4x4_KHR } : null;
|
|
3415
|
+
}
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
magFilter(filter) {
|
|
3419
|
+
return filter === "nearest" ? this.gl.NEAREST : this.gl.LINEAR;
|
|
3420
|
+
}
|
|
3421
|
+
minFilter(filter) {
|
|
3422
|
+
switch (filter) {
|
|
3423
|
+
case "nearest":
|
|
3424
|
+
return this.gl.NEAREST;
|
|
3425
|
+
case "linear":
|
|
3426
|
+
return this.gl.LINEAR;
|
|
3427
|
+
case "nearest-mipmap-nearest":
|
|
3428
|
+
return this.gl.NEAREST_MIPMAP_NEAREST;
|
|
3429
|
+
case "linear-mipmap-nearest":
|
|
3430
|
+
return this.gl.LINEAR_MIPMAP_NEAREST;
|
|
3431
|
+
case "nearest-mipmap-linear":
|
|
3432
|
+
return this.gl.NEAREST_MIPMAP_LINEAR;
|
|
3433
|
+
case "linear-mipmap-linear":
|
|
3434
|
+
return this.gl.LINEAR_MIPMAP_LINEAR;
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
addressMode(mode) {
|
|
3438
|
+
if (mode === "repeat")
|
|
3439
|
+
return this.gl.REPEAT;
|
|
3440
|
+
if (mode === "mirror-repeat")
|
|
3441
|
+
return this.gl.MIRRORED_REPEAT;
|
|
3442
|
+
return this.gl.CLAMP_TO_EDGE;
|
|
3443
|
+
}
|
|
3444
|
+
bindVertexFormat(shader, format) {
|
|
3445
|
+
const boundLocations = new Set();
|
|
3446
|
+
for (const attribute of format.attributes) {
|
|
3447
|
+
const location = this.resolveAttributeLocation(shader, attribute);
|
|
3448
|
+
if (location < 0) {
|
|
3449
|
+
continue;
|
|
3450
|
+
}
|
|
3451
|
+
if (attribute.type !== "float32") {
|
|
3452
|
+
throw new RenderDeviceError("Unsupported vertex attribute type", "UNSUPPORTED_VERTEX_ATTRIBUTE", {
|
|
3453
|
+
attribute: attribute.shaderName,
|
|
3454
|
+
type: attribute.type
|
|
3455
|
+
});
|
|
3456
|
+
}
|
|
3457
|
+
boundLocations.add(location);
|
|
3458
|
+
this.gl.enableVertexAttribArray(location);
|
|
3459
|
+
this.gl.vertexAttribDivisor(location, 0);
|
|
3460
|
+
this.gl.vertexAttribPointer(location, attribute.components, this.gl.FLOAT, attribute.normalized, format.stride, attribute.offset);
|
|
3461
|
+
}
|
|
3462
|
+
return boundLocations;
|
|
3463
|
+
}
|
|
3464
|
+
bindVertexArrayForCommand(command, shader, vertexBuffer) {
|
|
3465
|
+
const indexBuffer = command.indexBuffer ? this.requireBuffer(command.indexBuffer) : undefined;
|
|
3466
|
+
const key = this.vertexArrayCacheKey(command, shader, vertexBuffer, indexBuffer);
|
|
3467
|
+
const cached = this.vertexArrayCache.get(key);
|
|
3468
|
+
if (cached) {
|
|
3469
|
+
this.stateCache.bindVertexArray(cached.handle, () => this.gl.bindVertexArray(cached.handle));
|
|
3470
|
+
return cached;
|
|
3471
|
+
}
|
|
3472
|
+
const handle = this.gl.createVertexArray();
|
|
3473
|
+
if (!handle) {
|
|
3474
|
+
throw new RenderDeviceError("Failed to allocate WebGL vertex array", "WEBGL_ALLOCATION_FAILED");
|
|
3475
|
+
}
|
|
3476
|
+
this.vertexArrayCreateCount += 1;
|
|
3477
|
+
this.stateCache.bindVertexArray(handle, () => this.gl.bindVertexArray(handle));
|
|
3478
|
+
this.stateCache.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle, () => this.gl.bindBuffer(this.gl.ARRAY_BUFFER, vertexBuffer.handle));
|
|
3479
|
+
const boundLocations = this.bindVertexFormat(shader, command.vertexFormat);
|
|
3480
|
+
if (command.instanceAttributes && command.instanceAttributes.length > 0) {
|
|
3481
|
+
this.bindInstanceAttributes(shader, command.instanceAttributes, boundLocations);
|
|
3482
|
+
}
|
|
3483
|
+
if (indexBuffer) {
|
|
3484
|
+
this.stateCache.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle, () => this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, indexBuffer.handle));
|
|
3485
|
+
}
|
|
3486
|
+
this.disableUnboundVertexAttributes(boundLocations);
|
|
3487
|
+
this.applyDefaultAttributes(shader, boundLocations);
|
|
3488
|
+
const entry = { key, handle, boundLocations };
|
|
3489
|
+
this.vertexArrayCache.set(key, entry);
|
|
3490
|
+
return entry;
|
|
3491
|
+
}
|
|
3492
|
+
bindNoVertexArray() {
|
|
3493
|
+
this.stateCache.bindVertexArray(null, () => this.gl.bindVertexArray(null));
|
|
3494
|
+
}
|
|
3495
|
+
vertexArrayCacheKey(command, shader, vertexBuffer, indexBuffer) {
|
|
3496
|
+
const instanceKey = (command.instanceAttributes ?? []).map((attribute) => {
|
|
3497
|
+
const buffer = this.requireBuffer(attribute.buffer);
|
|
3498
|
+
return `${attribute.shaderName}:${buffer.id}:${attribute.components}:${attribute.offset}:${attribute.stride}:${attribute.normalized === true ? 1 : 0}:${attribute.divisor ?? 1}`;
|
|
3499
|
+
}).join(",");
|
|
3500
|
+
return [
|
|
3501
|
+
shader.id,
|
|
3502
|
+
vertexBuffer.id,
|
|
3503
|
+
this.vertexFormatId(command.vertexFormat),
|
|
3504
|
+
indexBuffer?.id ?? 0,
|
|
3505
|
+
instanceKey
|
|
3506
|
+
].join("|");
|
|
3507
|
+
}
|
|
3508
|
+
vertexFormatId(format) {
|
|
3509
|
+
const existing = this.vertexFormatIds.get(format);
|
|
3510
|
+
if (existing !== undefined)
|
|
3511
|
+
return existing;
|
|
3512
|
+
const next = this.nextVertexFormatId;
|
|
3513
|
+
this.nextVertexFormatId += 1;
|
|
3514
|
+
this.vertexFormatIds.set(format, next);
|
|
3515
|
+
return next;
|
|
3516
|
+
}
|
|
3517
|
+
bindInstanceAttributes(shader, attributes, boundLocations) {
|
|
3518
|
+
for (const attribute of attributes) {
|
|
3519
|
+
const location = shader.reflection.attributes.get(attribute.shaderName);
|
|
3520
|
+
if (location === undefined || location < 0) {
|
|
3521
|
+
continue;
|
|
3522
|
+
}
|
|
3523
|
+
if (![1, 2, 3, 4].includes(attribute.components)) {
|
|
3524
|
+
throw new RenderDeviceError("Instance vertex attribute components must be 1, 2, 3, or 4", "INVALID_DRAW_COMMAND", {
|
|
3525
|
+
attribute: attribute.shaderName,
|
|
3526
|
+
components: attribute.components
|
|
3527
|
+
});
|
|
3528
|
+
}
|
|
3529
|
+
if (!Number.isInteger(attribute.offset) || attribute.offset < 0 || attribute.offset % 4 !== 0) {
|
|
3530
|
+
throw new RenderDeviceError("Instance vertex attribute offset must be non-negative and 4-byte aligned", "INVALID_DRAW_COMMAND", {
|
|
3531
|
+
attribute: attribute.shaderName,
|
|
3532
|
+
offset: attribute.offset
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
if (!Number.isInteger(attribute.stride) || attribute.stride <= 0 || attribute.stride % 4 !== 0) {
|
|
3536
|
+
throw new RenderDeviceError("Instance vertex attribute stride must be positive and 4-byte aligned", "INVALID_DRAW_COMMAND", {
|
|
3537
|
+
attribute: attribute.shaderName,
|
|
3538
|
+
stride: attribute.stride
|
|
3539
|
+
});
|
|
3540
|
+
}
|
|
3541
|
+
if (attribute.offset + attribute.components * 4 > attribute.stride) {
|
|
3542
|
+
throw new RenderDeviceError("Instance vertex attribute range must fit inside its stride", "INVALID_DRAW_COMMAND", {
|
|
3543
|
+
attribute: attribute.shaderName,
|
|
3544
|
+
offset: attribute.offset,
|
|
3545
|
+
components: attribute.components,
|
|
3546
|
+
stride: attribute.stride
|
|
3547
|
+
});
|
|
3548
|
+
}
|
|
3549
|
+
const buffer = this.requireBuffer(attribute.buffer);
|
|
3550
|
+
boundLocations.add(location);
|
|
3551
|
+
this.stateCache.bindBuffer(this.gl.ARRAY_BUFFER, buffer.handle, () => this.gl.bindBuffer(this.gl.ARRAY_BUFFER, buffer.handle));
|
|
3552
|
+
this.gl.enableVertexAttribArray(location);
|
|
3553
|
+
this.gl.vertexAttribPointer(location, attribute.components, this.gl.FLOAT, attribute.normalized ?? false, attribute.stride, attribute.offset);
|
|
3554
|
+
this.gl.vertexAttribDivisor(location, attribute.divisor ?? 1);
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
resolveAttributeLocation(shader, attribute) {
|
|
3558
|
+
const reflected = shader.reflection.attributes.get(attribute.shaderName);
|
|
3559
|
+
if (reflected !== undefined) {
|
|
3560
|
+
return reflected;
|
|
3561
|
+
}
|
|
3562
|
+
return shader.reflection.attributes.get(attribute.semantic) ?? -1;
|
|
3563
|
+
}
|
|
3564
|
+
applyDefaultAttributes(shader, boundLocations) {
|
|
3565
|
+
const colorLocation = shader.reflection.attributes.get("a_color") ?? shader.reflection.attributes.get("color");
|
|
3566
|
+
if (colorLocation !== undefined && colorLocation >= 0 && !boundLocations.has(colorLocation)) {
|
|
3567
|
+
this.gl.disableVertexAttribArray(colorLocation);
|
|
3568
|
+
this.gl.vertexAttribDivisor(colorLocation, 0);
|
|
3569
|
+
this.gl.vertexAttrib4f(colorLocation, 1, 1, 1, 1);
|
|
3570
|
+
}
|
|
3571
|
+
const tangentLocation = shader.reflection.attributes.get("a_tangent") ?? shader.reflection.attributes.get("tangent");
|
|
3572
|
+
if (tangentLocation !== undefined && tangentLocation >= 0 && !boundLocations.has(tangentLocation)) {
|
|
3573
|
+
this.gl.disableVertexAttribArray(tangentLocation);
|
|
3574
|
+
this.gl.vertexAttribDivisor(tangentLocation, 0);
|
|
3575
|
+
this.gl.vertexAttrib4f(tangentLocation, 1, 0, 0, 1);
|
|
3576
|
+
}
|
|
3577
|
+
const uv1Location = shader.reflection.attributes.get("a_uv1") ?? shader.reflection.attributes.get("uv1");
|
|
3578
|
+
if (uv1Location !== undefined && uv1Location >= 0 && !boundLocations.has(uv1Location)) {
|
|
3579
|
+
this.gl.disableVertexAttribArray(uv1Location);
|
|
3580
|
+
this.gl.vertexAttribDivisor(uv1Location, 0);
|
|
3581
|
+
this.gl.vertexAttrib2f(uv1Location, 0, 0);
|
|
3582
|
+
}
|
|
3583
|
+
const instanceColorLocation = shader.reflection.attributes.get("a_instanceColor") ?? shader.reflection.attributes.get("instanceColor");
|
|
3584
|
+
if (instanceColorLocation !== undefined && instanceColorLocation >= 0 && !boundLocations.has(instanceColorLocation)) {
|
|
3585
|
+
this.gl.disableVertexAttribArray(instanceColorLocation);
|
|
3586
|
+
this.gl.vertexAttribDivisor(instanceColorLocation, 0);
|
|
3587
|
+
this.gl.vertexAttrib4f(instanceColorLocation, 1, 1, 1, 1);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
disableUnboundVertexAttributes(boundLocations) {
|
|
3591
|
+
for (let location = 0; location < this.maxVertexAttributes; location += 1) {
|
|
3592
|
+
if (!boundLocations.has(location)) {
|
|
3593
|
+
this.gl.disableVertexAttribArray(location);
|
|
3594
|
+
this.gl.vertexAttribDivisor(location, 0);
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
primitive(topology) {
|
|
3599
|
+
if (topology === "lines")
|
|
3600
|
+
return this.gl.LINES;
|
|
3601
|
+
if (topology === "points")
|
|
3602
|
+
return this.gl.POINTS;
|
|
3603
|
+
return this.gl.TRIANGLES;
|
|
3604
|
+
}
|
|
3605
|
+
applyRenderState(state) {
|
|
3606
|
+
const renderState = state ?? {
|
|
3607
|
+
depthTest: true,
|
|
3608
|
+
depthWrite: true,
|
|
3609
|
+
cullMode: "back",
|
|
3610
|
+
blend: false,
|
|
3611
|
+
depthCompare: "less-equal",
|
|
3612
|
+
colorWrite: [true, true, true, true],
|
|
3613
|
+
scissor: null,
|
|
3614
|
+
polygonOffset: null,
|
|
3615
|
+
stencil: null
|
|
3616
|
+
};
|
|
3617
|
+
this.stateCache.setEnabled(this.gl.DEPTH_TEST, renderState.depthTest, () => {
|
|
3618
|
+
if (renderState.depthTest)
|
|
3619
|
+
this.gl.enable(this.gl.DEPTH_TEST);
|
|
3620
|
+
else
|
|
3621
|
+
this.gl.disable(this.gl.DEPTH_TEST);
|
|
3622
|
+
});
|
|
3623
|
+
this.stateCache.depthMask(renderState.depthWrite, () => this.gl.depthMask(renderState.depthWrite));
|
|
3624
|
+
this.stateCache.depthFunc(renderState.depthCompare === "always" ? this.gl.ALWAYS : this.gl.LEQUAL, () => this.gl.depthFunc(renderState.depthCompare === "always" ? this.gl.ALWAYS : this.gl.LEQUAL));
|
|
3625
|
+
const colorWrite = renderState.colorWrite ?? [true, true, true, true];
|
|
3626
|
+
this.stateCache.colorMask(colorWrite[0], colorWrite[1], colorWrite[2], colorWrite[3], () => this.gl.colorMask(colorWrite[0], colorWrite[1], colorWrite[2], colorWrite[3]));
|
|
3627
|
+
if (renderState.scissor) {
|
|
3628
|
+
this.stateCache.setEnabled(this.gl.SCISSOR_TEST, true, () => this.gl.enable(this.gl.SCISSOR_TEST));
|
|
3629
|
+
this.stateCache.scissor(renderState.scissor.x, renderState.scissor.y, renderState.scissor.width, renderState.scissor.height, () => {
|
|
3630
|
+
this.gl.scissor(renderState.scissor.x, renderState.scissor.y, renderState.scissor.width, renderState.scissor.height);
|
|
3631
|
+
});
|
|
3632
|
+
}
|
|
3633
|
+
else {
|
|
3634
|
+
this.stateCache.setEnabled(this.gl.SCISSOR_TEST, false, () => this.gl.disable(this.gl.SCISSOR_TEST));
|
|
3635
|
+
}
|
|
3636
|
+
if (renderState.polygonOffset) {
|
|
3637
|
+
this.stateCache.setEnabled(this.gl.POLYGON_OFFSET_FILL, true, () => this.gl.enable(this.gl.POLYGON_OFFSET_FILL));
|
|
3638
|
+
this.stateCache.polygonOffset(renderState.polygonOffset.factor, renderState.polygonOffset.units, () => this.gl.polygonOffset(renderState.polygonOffset.factor, renderState.polygonOffset.units));
|
|
3639
|
+
}
|
|
3640
|
+
else {
|
|
3641
|
+
this.stateCache.setEnabled(this.gl.POLYGON_OFFSET_FILL, false, () => this.gl.disable(this.gl.POLYGON_OFFSET_FILL));
|
|
3642
|
+
}
|
|
3643
|
+
if (renderState.stencil) {
|
|
3644
|
+
const stencil = renderState.stencil;
|
|
3645
|
+
const compare = this.stencilCompare(stencil.compare ?? "always");
|
|
3646
|
+
const reference = stencil.reference ?? 0;
|
|
3647
|
+
const readMask = stencil.readMask ?? 0xff;
|
|
3648
|
+
const writeMask = stencil.writeMask ?? 0xff;
|
|
3649
|
+
const fail = this.stencilOperation(stencil.fail ?? "keep");
|
|
3650
|
+
const depthFail = this.stencilOperation(stencil.depthFail ?? "keep");
|
|
3651
|
+
const depthPass = this.stencilOperation(stencil.depthPass ?? "keep");
|
|
3652
|
+
this.stateCache.setEnabled(this.gl.STENCIL_TEST, true, () => this.gl.enable(this.gl.STENCIL_TEST));
|
|
3653
|
+
this.stateCache.stencilFunc(compare, reference, readMask, () => this.gl.stencilFunc(compare, reference, readMask));
|
|
3654
|
+
this.stateCache.stencilMask(writeMask, () => this.gl.stencilMask(writeMask));
|
|
3655
|
+
this.stateCache.stencilOp(fail, depthFail, depthPass, () => this.gl.stencilOp(fail, depthFail, depthPass));
|
|
3656
|
+
}
|
|
3657
|
+
else {
|
|
3658
|
+
this.stateCache.setEnabled(this.gl.STENCIL_TEST, false, () => this.gl.disable(this.gl.STENCIL_TEST));
|
|
3659
|
+
}
|
|
3660
|
+
if (renderState.cullMode === "none") {
|
|
3661
|
+
this.stateCache.setEnabled(this.gl.CULL_FACE, false, () => this.gl.disable(this.gl.CULL_FACE));
|
|
3662
|
+
}
|
|
3663
|
+
else {
|
|
3664
|
+
this.stateCache.setEnabled(this.gl.CULL_FACE, true, () => this.gl.enable(this.gl.CULL_FACE));
|
|
3665
|
+
this.stateCache.cullFace(renderState.cullMode === "front" ? this.gl.FRONT : this.gl.BACK, () => this.gl.cullFace(renderState.cullMode === "front" ? this.gl.FRONT : this.gl.BACK));
|
|
3666
|
+
}
|
|
3667
|
+
if (renderState.blend) {
|
|
3668
|
+
this.stateCache.setEnabled(this.gl.BLEND, true, () => this.gl.enable(this.gl.BLEND));
|
|
3669
|
+
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));
|
|
3670
|
+
}
|
|
3671
|
+
else {
|
|
3672
|
+
this.stateCache.setEnabled(this.gl.BLEND, false, () => this.gl.disable(this.gl.BLEND));
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
requireBuffer(buffer) {
|
|
3676
|
+
if (!(buffer instanceof WebGL2Buffer) || !this.buffers.has(buffer) || buffer.disposed) {
|
|
3677
|
+
throw new RenderDeviceError("Buffer is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
3678
|
+
bufferId: buffer.id
|
|
3679
|
+
});
|
|
3680
|
+
}
|
|
3681
|
+
return buffer;
|
|
3682
|
+
}
|
|
3683
|
+
stencilCompare(compare) {
|
|
3684
|
+
switch (compare) {
|
|
3685
|
+
case "never": return this.gl.NEVER;
|
|
3686
|
+
case "less": return this.gl.LESS;
|
|
3687
|
+
case "less-equal": return this.gl.LEQUAL;
|
|
3688
|
+
case "greater": return this.gl.GREATER;
|
|
3689
|
+
case "greater-equal": return this.gl.GEQUAL;
|
|
3690
|
+
case "equal": return this.gl.EQUAL;
|
|
3691
|
+
case "not-equal": return this.gl.NOTEQUAL;
|
|
3692
|
+
case "always": return this.gl.ALWAYS;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
stencilOperation(operation) {
|
|
3696
|
+
switch (operation) {
|
|
3697
|
+
case "keep": return this.gl.KEEP;
|
|
3698
|
+
case "zero": return this.gl.ZERO;
|
|
3699
|
+
case "replace": return this.gl.REPLACE;
|
|
3700
|
+
case "increment": return this.gl.INCR;
|
|
3701
|
+
case "decrement": return this.gl.DECR;
|
|
3702
|
+
case "invert": return this.gl.INVERT;
|
|
3703
|
+
case "increment-wrap": return this.gl.INCR_WRAP;
|
|
3704
|
+
case "decrement-wrap": return this.gl.DECR_WRAP;
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
requireShader(shader) {
|
|
3708
|
+
if (!(shader instanceof WebGL2ShaderProgram) || !this.shaders.has(shader) || shader.disposed) {
|
|
3709
|
+
throw new RenderDeviceError("Shader is not a live WebGL2 resource owned by this device", "INVALID_RESOURCE", {
|
|
3710
|
+
shaderId: shader.id
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3713
|
+
return shader;
|
|
3714
|
+
}
|
|
3715
|
+
assertAlive() {
|
|
3716
|
+
if (this.disposed) {
|
|
3717
|
+
throw new RenderDeviceError("Render device is disposed", "DISPOSED_DEVICE");
|
|
3718
|
+
}
|
|
3719
|
+
if (this.contextLost) {
|
|
3720
|
+
throw new RenderDeviceError("Render context is lost", "CONTEXT_LOST");
|
|
3721
|
+
}
|
|
3722
|
+
}
|
|
3723
|
+
assertFrame() {
|
|
3724
|
+
this.assertAlive();
|
|
3725
|
+
if (!this.frameActive) {
|
|
3726
|
+
throw new RenderDeviceError("No active frame", "NO_ACTIVE_FRAME");
|
|
3727
|
+
}
|
|
3728
|
+
}
|
|
3729
|
+
readError() {
|
|
3730
|
+
const error = this.gl.getError();
|
|
3731
|
+
return error === this.gl.NO_ERROR ? null : `0x${error.toString(16)}`;
|
|
3732
|
+
}
|
|
3733
|
+
}
|
|
3734
|
+
function numberOption(options, key, fallback) {
|
|
3735
|
+
const value = options[key];
|
|
3736
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
3737
|
+
}
|
|
3738
|
+
function normalizeNativeBloomOptions(options) {
|
|
3739
|
+
const threshold = bloomNumberOption(options, "threshold", 0.75);
|
|
3740
|
+
const intensity = bloomNumberOption(options, "intensity", 0.35);
|
|
3741
|
+
const radius = bloomNumberOption(options, "radius", 1);
|
|
3742
|
+
if (threshold < 0 || threshold > 1) {
|
|
3743
|
+
throw new RenderDeviceError("Bloom threshold must be finite and in [0, 1].", "INVALID_POSTPROCESS_OPTIONS", { threshold });
|
|
3744
|
+
}
|
|
3745
|
+
if (intensity < 0) {
|
|
3746
|
+
throw new RenderDeviceError("Bloom intensity must be finite and non-negative.", "INVALID_POSTPROCESS_OPTIONS", { intensity });
|
|
3747
|
+
}
|
|
3748
|
+
if (!Number.isInteger(radius) || radius < 0 || radius > 16) {
|
|
3749
|
+
throw new RenderDeviceError("Bloom radius must be an integer in [0, 16].", "INVALID_POSTPROCESS_OPTIONS", { radius });
|
|
3750
|
+
}
|
|
3751
|
+
const softKnee = bloomNumberOption(options, "softKnee", 0);
|
|
3752
|
+
if (!(softKnee >= 0) || softKnee > 0.5) {
|
|
3753
|
+
throw new RenderDeviceError("Bloom softKnee must be finite and in [0, 0.5].", "INVALID_POSTPROCESS_OPTIONS", { softKnee });
|
|
3754
|
+
}
|
|
3755
|
+
const shoulder = bloomNumberOption(options, "shoulder", 0);
|
|
3756
|
+
if (!(shoulder >= 0) || shoulder > 1) {
|
|
3757
|
+
throw new RenderDeviceError("Bloom shoulder must be finite and in [0, 1].", "INVALID_POSTPROCESS_OPTIONS", { shoulder });
|
|
3758
|
+
}
|
|
3759
|
+
let quality;
|
|
3760
|
+
try {
|
|
3761
|
+
quality = normalizeBloomQualityPreset(options.quality);
|
|
3762
|
+
}
|
|
3763
|
+
catch {
|
|
3764
|
+
throw new RenderDeviceError("Bloom quality must be one of performance|balanced|cinematic.", "INVALID_POSTPROCESS_OPTIONS", { quality: options.quality });
|
|
3765
|
+
}
|
|
3766
|
+
return { threshold, intensity, radius, quality, softKnee, shoulder };
|
|
3767
|
+
}
|
|
3768
|
+
function normalizeNativeOutlineOptions(options) {
|
|
3769
|
+
const width = outlineNumberOption(options, "width", 1);
|
|
3770
|
+
const threshold = outlineNumberOption(options, "threshold", 0.22);
|
|
3771
|
+
const opacity = outlineNumberOption(options, "opacity", 0.85);
|
|
3772
|
+
const value = options.color;
|
|
3773
|
+
const sourceColor = value === undefined ? [255, 188, 64, 255] : value;
|
|
3774
|
+
if (!Array.isArray(sourceColor)
|
|
3775
|
+
|| (sourceColor.length !== 3 && sourceColor.length !== 4)
|
|
3776
|
+
|| sourceColor.some((channel) => typeof channel !== "number" || !Number.isFinite(channel) || channel < 0 || channel > 255)) {
|
|
3777
|
+
throw new RenderDeviceError("Outline color must contain three or four finite channels in [0, 255].", "INVALID_POSTPROCESS_OPTIONS", { color: value });
|
|
3778
|
+
}
|
|
3779
|
+
if (!Number.isInteger(width) || width < 1 || width > 6) {
|
|
3780
|
+
throw new RenderDeviceError("Outline width must be an integer in [1, 6].", "INVALID_POSTPROCESS_OPTIONS", { width });
|
|
3781
|
+
}
|
|
3782
|
+
if (threshold < 0 || threshold > 4) {
|
|
3783
|
+
throw new RenderDeviceError("Outline threshold must be finite and in [0, 4].", "INVALID_POSTPROCESS_OPTIONS", { threshold });
|
|
3784
|
+
}
|
|
3785
|
+
if (opacity < 0 || opacity > 1) {
|
|
3786
|
+
throw new RenderDeviceError("Outline opacity must be finite and in [0, 1].", "INVALID_POSTPROCESS_OPTIONS", { opacity });
|
|
3787
|
+
}
|
|
3788
|
+
const color = [
|
|
3789
|
+
sourceColor[0],
|
|
3790
|
+
sourceColor[1],
|
|
3791
|
+
sourceColor[2],
|
|
3792
|
+
(sourceColor[3] ?? 255)
|
|
3793
|
+
];
|
|
3794
|
+
return { width, threshold, opacity, color };
|
|
3795
|
+
}
|
|
3796
|
+
function normalizeNativeSsaoOptions(options) {
|
|
3797
|
+
const radius = ssaoNumberOption(options, "radius", 2);
|
|
3798
|
+
const intensity = ssaoNumberOption(options, "intensity", 0.38);
|
|
3799
|
+
const bias = ssaoNumberOption(options, "bias", 0.015);
|
|
3800
|
+
if (!Number.isInteger(radius) || radius < 1 || radius > 8) {
|
|
3801
|
+
throw new RenderDeviceError("SSAO radius must be an integer in [1, 8].", "INVALID_POSTPROCESS_OPTIONS", { radius });
|
|
3802
|
+
}
|
|
3803
|
+
if (intensity < 0 || intensity > 2) {
|
|
3804
|
+
throw new RenderDeviceError("SSAO intensity must be finite and in [0, 2].", "INVALID_POSTPROCESS_OPTIONS", { intensity });
|
|
3805
|
+
}
|
|
3806
|
+
if (bias < 0 || bias > 0.25) {
|
|
3807
|
+
throw new RenderDeviceError("SSAO bias must be finite and in [0, 0.25].", "INVALID_POSTPROCESS_OPTIONS", { bias });
|
|
3808
|
+
}
|
|
3809
|
+
return { radius, intensity, bias };
|
|
3810
|
+
}
|
|
3811
|
+
/**
|
|
3812
|
+
* Camera depth range for linearizing sampleable GL depth in the native
|
|
3813
|
+
* SSR/depth-of-field programs (muse3jsparity-PRD A3). Missing or invalid
|
|
3814
|
+
* ranges fall back to the engine perspective defaults (0.1/1000) rather than
|
|
3815
|
+
* failing the frame: the range is a calibration hint, and every engine camera
|
|
3816
|
+
* builder defaults to exactly 0.1/1000. The CPU byte kernels intentionally
|
|
3817
|
+
* keep fixture-unit depth semantics (deterministic reference); the native
|
|
3818
|
+
* programs document their GL-depth contract in-shader.
|
|
3819
|
+
*/
|
|
3820
|
+
function normalizeLdrDepthRange(range) {
|
|
3821
|
+
const near = range && Number.isFinite(range.near) && range.near > 0 ? range.near : 0.1;
|
|
3822
|
+
const far = range && Number.isFinite(range.far) && range.far > near ? range.far : 1000;
|
|
3823
|
+
return { near, far };
|
|
3824
|
+
}
|
|
3825
|
+
function normalizeNativeSsrOptions(options) {
|
|
3826
|
+
const intensity = ssrNumberOption(options, "intensity", 0.32);
|
|
3827
|
+
const maxDistance = ssrNumberOption(options, "maxDistance", 16);
|
|
3828
|
+
if (intensity < 0 || intensity > 2) {
|
|
3829
|
+
throw new RenderDeviceError("SSR intensity must be finite and in [0, 2].", "INVALID_POSTPROCESS_OPTIONS", { intensity });
|
|
3830
|
+
}
|
|
3831
|
+
if (!Number.isInteger(maxDistance) || maxDistance < 1 || maxDistance > 64) {
|
|
3832
|
+
throw new RenderDeviceError("SSR maxDistance must be an integer in [1, 64].", "INVALID_POSTPROCESS_OPTIONS", { maxDistance });
|
|
3833
|
+
}
|
|
3834
|
+
return { intensity, maxDistance };
|
|
3835
|
+
}
|
|
3836
|
+
function normalizeNativeDepthOfFieldOptions(options) {
|
|
3837
|
+
const focusDepth = depthOfFieldNumberOption(options, "focusDepth", 0.5);
|
|
3838
|
+
const focusRange = depthOfFieldNumberOption(options, "focusRange", 0.12);
|
|
3839
|
+
const maxRadius = depthOfFieldNumberOption(options, "maxRadius", 2);
|
|
3840
|
+
if (focusDepth < 0 || focusDepth > 1) {
|
|
3841
|
+
throw new RenderDeviceError("Depth-of-field focusDepth must be finite and in [0, 1].", "INVALID_POSTPROCESS_OPTIONS", { focusDepth });
|
|
3842
|
+
}
|
|
3843
|
+
if (focusRange < 0.001 || focusRange > 1) {
|
|
3844
|
+
throw new RenderDeviceError("Depth-of-field focusRange must be finite and in [0.001, 1].", "INVALID_POSTPROCESS_OPTIONS", { focusRange });
|
|
3845
|
+
}
|
|
3846
|
+
if (!Number.isInteger(maxRadius) || maxRadius < 0 || maxRadius > 8) {
|
|
3847
|
+
throw new RenderDeviceError("Depth-of-field maxRadius must be an integer in [0, 8].", "INVALID_POSTPROCESS_OPTIONS", { maxRadius });
|
|
3848
|
+
}
|
|
3849
|
+
return { focusDepth, focusRange, maxRadius };
|
|
3850
|
+
}
|
|
3851
|
+
function normalizeNativeMotionBlurOptions(options, width, height) {
|
|
3852
|
+
const samples = motionBlurNumberOption(options, "samples", 5);
|
|
3853
|
+
const scale = motionBlurNumberOption(options, "scale", 1);
|
|
3854
|
+
const value = options.velocity;
|
|
3855
|
+
if (!(value instanceof Float32Array) && !Array.isArray(value)) {
|
|
3856
|
+
throw new RenderDeviceError("Motion blur requires a velocity Float32Array or number array.", "WEBGL_LDR_POSTPROCESS_VELOCITY_REQUIRED");
|
|
3857
|
+
}
|
|
3858
|
+
const velocity = value instanceof Float32Array ? value : new Float32Array(value);
|
|
3859
|
+
if (velocity.length !== width * height * 2) {
|
|
3860
|
+
throw new RenderDeviceError("Motion blur velocity must contain width * height * 2 float samples.", "INVALID_POSTPROCESS_OPTIONS", {
|
|
3861
|
+
expectedLength: width * height * 2,
|
|
3862
|
+
actualLength: velocity.length
|
|
3863
|
+
});
|
|
3864
|
+
}
|
|
3865
|
+
if (!Number.isInteger(samples) || samples < 2 || samples > 16) {
|
|
3866
|
+
throw new RenderDeviceError("Motion blur samples must be an integer in [2, 16].", "INVALID_POSTPROCESS_OPTIONS", { samples });
|
|
3867
|
+
}
|
|
3868
|
+
if (scale < 0 || scale > 8) {
|
|
3869
|
+
throw new RenderDeviceError("Motion blur scale must be finite and in [0, 8].", "INVALID_POSTPROCESS_OPTIONS", { scale });
|
|
3870
|
+
}
|
|
3871
|
+
return { samples, scale, velocity };
|
|
3872
|
+
}
|
|
3873
|
+
function normalizeNativeTaaOptions(options, width, height) {
|
|
3874
|
+
const blend = taaNumberOption(options, "blend", 0.18);
|
|
3875
|
+
const history = options.history;
|
|
3876
|
+
if (!(history instanceof Uint8Array)) {
|
|
3877
|
+
throw new RenderDeviceError("TAA requires a Uint8Array history buffer.", "WEBGL_LDR_POSTPROCESS_HISTORY_REQUIRED");
|
|
3878
|
+
}
|
|
3879
|
+
if (history.byteLength !== width * height * 4) {
|
|
3880
|
+
throw new RenderDeviceError("TAA history must contain width * height * 4 RGBA bytes.", "INVALID_POSTPROCESS_OPTIONS", {
|
|
3881
|
+
expectedLength: width * height * 4,
|
|
3882
|
+
actualLength: history.byteLength
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
if (blend < 0 || blend > 0.95) {
|
|
3886
|
+
throw new RenderDeviceError("TAA blend must be finite and in [0, 0.95].", "INVALID_POSTPROCESS_OPTIONS", { blend });
|
|
3887
|
+
}
|
|
3888
|
+
return { blend, history };
|
|
3889
|
+
}
|
|
3890
|
+
function bloomNumberOption(options, key, fallback) {
|
|
3891
|
+
const value = options[key];
|
|
3892
|
+
if (value === undefined)
|
|
3893
|
+
return fallback;
|
|
3894
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3895
|
+
throw new RenderDeviceError(`Bloom ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
|
|
3896
|
+
}
|
|
3897
|
+
return value;
|
|
3898
|
+
}
|
|
3899
|
+
function outlineNumberOption(options, key, fallback) {
|
|
3900
|
+
const value = options[key];
|
|
3901
|
+
if (value === undefined)
|
|
3902
|
+
return fallback;
|
|
3903
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3904
|
+
throw new RenderDeviceError(`Outline ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
|
|
3905
|
+
}
|
|
3906
|
+
return value;
|
|
3907
|
+
}
|
|
3908
|
+
function ssaoNumberOption(options, key, fallback) {
|
|
3909
|
+
const value = options[key];
|
|
3910
|
+
if (value === undefined)
|
|
3911
|
+
return fallback;
|
|
3912
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3913
|
+
throw new RenderDeviceError(`SSAO ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
|
|
3914
|
+
}
|
|
3915
|
+
return value;
|
|
3916
|
+
}
|
|
3917
|
+
function ssrNumberOption(options, key, fallback) {
|
|
3918
|
+
const value = options[key];
|
|
3919
|
+
if (value === undefined)
|
|
3920
|
+
return fallback;
|
|
3921
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3922
|
+
throw new RenderDeviceError(`SSR ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
|
|
3923
|
+
}
|
|
3924
|
+
return value;
|
|
3925
|
+
}
|
|
3926
|
+
function depthOfFieldNumberOption(options, key, fallback) {
|
|
3927
|
+
const value = options[key];
|
|
3928
|
+
if (value === undefined)
|
|
3929
|
+
return fallback;
|
|
3930
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3931
|
+
throw new RenderDeviceError(`Depth-of-field ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
|
|
3932
|
+
}
|
|
3933
|
+
return value;
|
|
3934
|
+
}
|
|
3935
|
+
function motionBlurNumberOption(options, key, fallback) {
|
|
3936
|
+
const value = options[key];
|
|
3937
|
+
if (value === undefined)
|
|
3938
|
+
return fallback;
|
|
3939
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3940
|
+
throw new RenderDeviceError(`Motion blur ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
|
|
3941
|
+
}
|
|
3942
|
+
return value;
|
|
3943
|
+
}
|
|
3944
|
+
function taaNumberOption(options, key, fallback) {
|
|
3945
|
+
const value = options[key];
|
|
3946
|
+
if (value === undefined)
|
|
3947
|
+
return fallback;
|
|
3948
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3949
|
+
throw new RenderDeviceError(`TAA ${key} must be a finite number.`, "INVALID_POSTPROCESS_OPTIONS", { key, value });
|
|
3950
|
+
}
|
|
3951
|
+
return value;
|
|
3952
|
+
}
|
|
3953
|
+
function stringOption(options, key, fallback) {
|
|
3954
|
+
const value = options[key];
|
|
3955
|
+
return typeof value === "string" ? value : fallback;
|
|
3956
|
+
}
|
|
3957
|
+
function colorSpaceId(value) {
|
|
3958
|
+
return value === "srgb" ? 1 : 0;
|
|
3959
|
+
}
|
|
3960
|
+
function toneMappingOperatorId(value) {
|
|
3961
|
+
if (value === "linear")
|
|
3962
|
+
return 0;
|
|
3963
|
+
if (value === "reinhard")
|
|
3964
|
+
return 1;
|
|
3965
|
+
if (value === "aces")
|
|
3966
|
+
return 2;
|
|
3967
|
+
if (value === "filmic")
|
|
3968
|
+
return 3;
|
|
3969
|
+
if (value === "uncharted2")
|
|
3970
|
+
return 4;
|
|
3971
|
+
if (value === "agx")
|
|
3972
|
+
return 5;
|
|
3973
|
+
return 6;
|
|
3974
|
+
}
|
|
3975
|
+
function completeUploadLevels(levels) {
|
|
3976
|
+
const uploadLevels = [];
|
|
3977
|
+
for (const level of levels) {
|
|
3978
|
+
if (uploadLevels.length === 0) {
|
|
3979
|
+
uploadLevels.push(level);
|
|
3980
|
+
continue;
|
|
3981
|
+
}
|
|
3982
|
+
const previous = uploadLevels[uploadLevels.length - 1];
|
|
3983
|
+
const expectedWidth = Math.max(1, Math.floor(previous.width / 2));
|
|
3984
|
+
const expectedHeight = Math.max(1, Math.floor(previous.height / 2));
|
|
3985
|
+
if (level.width !== expectedWidth || level.height !== expectedHeight) {
|
|
3986
|
+
break;
|
|
3987
|
+
}
|
|
3988
|
+
uploadLevels.push(level);
|
|
3989
|
+
}
|
|
3990
|
+
return uploadLevels;
|
|
3991
|
+
}
|
|
3992
|
+
function texturePixelUploadData(data, format) {
|
|
3993
|
+
if (format === "rgba16f" && data instanceof Uint16Array)
|
|
3994
|
+
return data;
|
|
3995
|
+
if (format === "rgba32f" && data instanceof Float32Array)
|
|
3996
|
+
return data;
|
|
3997
|
+
if (format === "rgba8" && (data instanceof Uint8Array || data instanceof Uint8ClampedArray))
|
|
3998
|
+
return data;
|
|
3999
|
+
throw new RenderDeviceError("Texture pixel data type does not match texture format", "UNSUPPORTED_TEXTURE_FORMAT", { format });
|
|
4000
|
+
}
|
|
4001
|
+
//# sourceMappingURL=LeanWebGL2Device.js.map
|