@aura3d/rendering 1.3.3 → 1.4.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/ForwardPass.js +7 -1
- package/dist/ForwardPass.js.map +1 -1
- package/dist/MorphTarget.d.ts +1 -0
- package/dist/MorphTarget.d.ts.map +1 -1
- package/dist/MorphTarget.js.map +1 -1
- package/dist/Renderer.js +7 -1
- package/dist/Renderer.js.map +1 -1
- package/package.json +38 -4
- package/src/ArchitecturalLightingFixtures.ts +0 -241
- package/src/ArchitecturalMaterialCatalog.ts +0 -130
- package/src/ArchitecturalMeasurementFixtures.ts +0 -155
- package/src/BRDFLut.ts +0 -47
- package/src/CameraFraming.ts +0 -200
- package/src/CanonicalSceneFixtures.ts +0 -95
- package/src/CascadedShadowMaps.ts +0 -449
- package/src/ColorManagement.ts +0 -127
- package/src/CullingFixtures.ts +0 -280
- package/src/DecalGeometry.ts +0 -26
- package/src/DepthPass.ts +0 -110
- package/src/EnvironmentBackgroundPass.ts +0 -194
- package/src/EnvironmentBackgroundResources.ts +0 -309
- package/src/EnvironmentLighting.ts +0 -96
- package/src/EnvironmentMapResources.ts +0 -1026
- package/src/EnvironmentPipeline.ts +0 -69
- package/src/EnvironmentPlatform.ts +0 -1306
- package/src/EnvironmentPreset.ts +0 -155
- package/src/Exposure.ts +0 -66
- package/src/ExternalParityRenderPreset.ts +0 -725
- package/src/ForwardPass.ts +0 -1663
- package/src/FrameVisualMetrics.ts +0 -301
- package/src/Geometry.ts +0 -461
- package/src/GeometryPrimitives.ts +0 -2
- package/src/HDRRenderPipeline.ts +0 -94
- package/src/IBL.ts +0 -86
- package/src/IndexBuffer.ts +0 -57
- package/src/InstancedPBRMaterial.ts +0 -186
- package/src/InstancedUnlitMaterial.ts +0 -60
- package/src/Instancing.ts +0 -19
- package/src/LightCollector.ts +0 -82
- package/src/LightUniforms.ts +0 -47
- package/src/LightingDebug.ts +0 -80
- package/src/LightingDefaults.ts +0 -79
- package/src/LightingRig.ts +0 -301
- package/src/LineGeometry.ts +0 -2
- package/src/Material.ts +0 -209
- package/src/MaterialBinding.ts +0 -131
- package/src/MaterialInstance.ts +0 -59
- package/src/MaterialPresets.ts +0 -272
- package/src/MorphTarget.ts +0 -123
- package/src/MorphTargetPlan.ts +0 -178
- package/src/MorphUnlitMaterial.ts +0 -60
- package/src/NormalMappedPBRMaterial.ts +0 -188
- package/src/OceanFixtures.ts +0 -318
- package/src/PBRLightingDefaults.ts +0 -12
- package/src/PBRMaterial.ts +0 -442
- package/src/PMREM.ts +0 -53
- package/src/PbrReference.ts +0 -681
- package/src/PostProcessPass.ts +0 -2507
- package/src/ProceduralTextureFixtures.ts +0 -393
- package/src/ProductTurntableFixtures.ts +0 -815
- package/src/Raycaster.ts +0 -21
- package/src/ReflectionProbe.ts +0 -13
- package/src/ReflectionSurfaces.ts +0 -219
- package/src/RenderBackend.ts +0 -43
- package/src/RenderDebugViews.ts +0 -138
- package/src/RenderDevice.ts +0 -836
- package/src/RenderGraph.ts +0 -194
- package/src/RenderPass.ts +0 -27
- package/src/RenderPipeline.ts +0 -101
- package/src/RenderQueue.ts +0 -12
- package/src/RenderState.ts +0 -12
- package/src/Renderer.ts +0 -2626
- package/src/RendererDebugOverlay.ts +0 -80
- package/src/RendererFeatureGates.ts +0 -150
- package/src/RendererPostprocessPlan.ts +0 -279
- package/src/RendererTiming.ts +0 -251
- package/src/RendererVisualPipelineReport.ts +0 -836
- package/src/ResourceLifecycle.ts +0 -21
- package/src/Sampler.ts +0 -37
- package/src/SceneOptimization.ts +0 -518
- package/src/ShaderChunks.ts +0 -393
- package/src/ShaderLibrary.ts +0 -2766
- package/src/ShaderModule.ts +0 -36
- package/src/ShaderPreprocessor.ts +0 -260
- package/src/ShaderReflection.ts +0 -201
- package/src/ShadowMap.ts +0 -237
- package/src/ShadowPass.ts +0 -198
- package/src/ShadowProjection.ts +0 -115
- package/src/SkinnedLitMaterial.ts +0 -397
- package/src/SkinnedUnlitMaterial.ts +0 -63
- package/src/SkinningBounds.ts +0 -131
- package/src/SpaceEnvironmentFixtures.ts +0 -200
- package/src/SpriteGeometry.ts +0 -21
- package/src/StereoCameraRig.ts +0 -127
- package/src/StereoEffects.ts +0 -274
- package/src/TerrainFixtures.ts +0 -180
- package/src/Texture.ts +0 -307
- package/src/TextureBinding.ts +0 -91
- package/src/TexturedPBRMaterial.ts +0 -1112
- package/src/TexturedUnlitMaterial.ts +0 -67
- package/src/ToneMapping.ts +0 -105
- package/src/UniformBinder.ts +0 -16
- package/src/UniformLayout.ts +0 -129
- package/src/UnlitMaterial.ts +0 -75
- package/src/VegetationFixtures.ts +0 -314
- package/src/VertexBuffer.ts +0 -120
- package/src/VertexFormat.ts +0 -171
- package/src/VoxelWorldFixtures.ts +0 -280
- package/src/WeatherFixtures.ts +0 -223
- package/src/WebGL2Device.ts +0 -2280
- package/src/WebGL2StateCache.ts +0 -330
- package/src/WebGPUDevice.ts +0 -2877
- package/src/WebGPURenderToTextureProof.ts +0 -139
- package/src/advanced-runtime/AdvancedRenderer.ts +0 -73
- package/src/advanced-runtime/index.ts +0 -9
- package/src/animation/AnimationMaterialStyle.ts +0 -32
- package/src/animation/AnimationRenderPreset.ts +0 -45
- package/src/animation/AnimationToonMaterial.ts +0 -387
- package/src/animation/AnimationVisualQuality.ts +0 -71
- package/src/animation/applyAnimationRenderPreset.ts +0 -180
- package/src/animation/index.ts +0 -5
- package/src/cinematic/BloomPass.ts +0 -26
- package/src/cinematic/CinematicDepthComposition.ts +0 -30
- package/src/cinematic/CinematicEvidence.ts +0 -123
- package/src/cinematic/CinematicLightingRig.ts +0 -144
- package/src/cinematic/CinematicMaterialPresets.ts +0 -130
- package/src/cinematic/CinematicPostProcess.ts +0 -83
- package/src/cinematic/DepthHazePass.ts +0 -30
- package/src/cinematic/EmissivePracticalLightSystem.ts +0 -45
- package/src/cinematic/FilmGrainPass.ts +0 -24
- package/src/cinematic/FogVolumeSystem.ts +0 -30
- package/src/cinematic/GlowCardSystem.ts +0 -44
- package/src/cinematic/RainParticleSystem.ts +0 -151
- package/src/cinematic/VignettePass.ts +0 -25
- package/src/cinematic/WetReflectionApproximation.ts +0 -27
- package/src/cinematic/index.ts +0 -14
- package/src/effects/CollisionModule.ts +0 -64
- package/src/effects/ColorModule.ts +0 -60
- package/src/effects/ForceModule.ts +0 -25
- package/src/effects/GPUParticleBackend.ts +0 -569
- package/src/effects/Particle.ts +0 -108
- package/src/effects/ParticleDiagnostics.ts +0 -224
- package/src/effects/ParticleEffectPresets.ts +0 -147
- package/src/effects/ParticleEmitter.ts +0 -326
- package/src/effects/ParticleModule.ts +0 -26
- package/src/effects/ParticleRenderPass.ts +0 -117
- package/src/effects/ParticleRenderer.ts +0 -132
- package/src/effects/ParticleSystem.ts +0 -333
- package/src/effects/SizeModule.ts +0 -56
- package/src/effects/TrailModule.ts +0 -72
- package/src/effects/VelocityModule.ts +0 -75
- package/src/effects/index.ts +0 -15
- package/src/index.ts +0 -925
- package/src/materials/AlphaSorting.ts +0 -30
- package/src/materials/MaterialExtensions.ts +0 -42
- package/src/materials/PhysicalMaterial.ts +0 -133
- package/src/materials/TransmissionPass.ts +0 -51
- package/src/performance/BVH.ts +0 -18
- package/src/performance/Batcher.ts +0 -2
- package/src/performance/FrustumCuller.ts +0 -9
- package/src/performance/LOD.ts +0 -20
- package/src/performance/Octree.ts +0 -10
- package/src/performance/RenderItemSorting.ts +0 -170
- package/src/performance/RendererStats.ts +0 -43
- package/src/performance/ResourceBudget.ts +0 -44
- package/src/postprocess/BloomPass.ts +0 -28
- package/src/postprocess/CinematicDiagnostics.ts +0 -642
- package/src/postprocess/ColorGradingPass.ts +0 -24
- package/src/postprocess/DepthOfFieldPass.ts +0 -12
- package/src/postprocess/EffectComposer.ts +0 -349
- package/src/postprocess/SSAOPass.ts +0 -20
- package/src/postprocess/index.ts +0 -34
- package/src/production-runtime/AnimationControlsPipeline.ts +0 -77
- package/src/production-runtime/PBRHDRPipeline.ts +0 -381
- package/src/production-runtime/ProductionEffectsPipeline.ts +0 -85
- package/src/production-runtime/ProductionRendererTypes.ts +0 -136
- package/src/production-runtime/ProductionRuntimeRenderer.ts +0 -172
- package/src/production-runtime/ProductionWebGL2Renderer.ts +0 -313
- package/src/production-runtime/ProductionWebGPURenderer.ts +0 -396
- package/src/production-runtime/TransmissionBackdropCapture.ts +0 -130
- package/src/production-runtime/animation/MorphTargetRenderer.ts +0 -2
- package/src/production-runtime/animation/SkinningRenderer.ts +0 -2
- package/src/production-runtime/backends/RendererBackend.ts +0 -17
- package/src/production-runtime/backends/WebGL2RendererBackend.ts +0 -30
- package/src/production-runtime/backends/WebGPURendererBackend.ts +0 -24
- package/src/production-runtime/backends/webgl2/WebGL2Buffer.ts +0 -2
- package/src/production-runtime/backends/webgl2/WebGL2Capabilities.ts +0 -2
- package/src/production-runtime/backends/webgl2/WebGL2RenderTarget.ts +0 -2
- package/src/production-runtime/backends/webgl2/WebGL2Shader.ts +0 -2
- package/src/production-runtime/backends/webgl2/WebGL2StateCache.ts +0 -1
- package/src/production-runtime/backends/webgl2/WebGL2Texture.ts +0 -2
- package/src/production-runtime/backends/webgpu/WebGPUBuffer.ts +0 -2
- package/src/production-runtime/backends/webgpu/WebGPUCapabilities.ts +0 -2
- package/src/production-runtime/backends/webgpu/WebGPURenderTarget.ts +0 -2
- package/src/production-runtime/backends/webgpu/WebGPUShader.ts +0 -2
- package/src/production-runtime/backends/webgpu/WebGPUTexture.ts +0 -2
- package/src/production-runtime/color/ColorManagement.ts +0 -3
- package/src/production-runtime/color/ToneMapping.ts +0 -7
- package/src/production-runtime/diagnostics/FrameCapture.ts +0 -2
- package/src/production-runtime/diagnostics/GPUCapabilities.ts +0 -2
- package/src/production-runtime/diagnostics/RendererStats.ts +0 -2
- package/src/production-runtime/environment/EnvironmentMap.ts +0 -1
- package/src/production-runtime/environment/HDRLoader.ts +0 -57
- package/src/production-runtime/environment/PMREMGenerator.ts +0 -713
- package/src/production-runtime/framegraph/FrameGraph.ts +0 -18
- package/src/production-runtime/framegraph/RenderPass.ts +0 -15
- package/src/production-runtime/geometry/ProjectedDecalGeometry.ts +0 -439
- package/src/production-runtime/index.ts +0 -127
- package/src/production-runtime/lights/LightManager.ts +0 -7
- package/src/production-runtime/lights/ShadowMapRenderer.ts +0 -5
- package/src/production-runtime/materials/GLTFMaterialAdapter.ts +0 -6
- package/src/production-runtime/materials/GLTFPBRMaterialAdapter.ts +0 -2
- package/src/production-runtime/materials/MaterialCompiler.ts +0 -7
- package/src/production-runtime/materials/MaterialTextureBindings.ts +0 -5
- package/src/production-runtime/materials/PBRMaterial.ts +0 -2
- package/src/production-runtime/materials/PBRShaderFeatures.ts +0 -2
- package/src/production-runtime/passes/ContactShadowPass.ts +0 -184
- package/src/production-runtime/passes/DepthPrepass.ts +0 -7
- package/src/production-runtime/passes/OpaquePass.ts +0 -7
- package/src/production-runtime/passes/ShadowPass.ts +0 -7
- package/src/production-runtime/passes/SkyboxPass.ts +0 -7
- package/src/production-runtime/passes/ToneMappingPass.ts +0 -7
- package/src/production-runtime/passes/TransparentPass.ts +0 -7
- package/src/production-runtime/postprocess/BloomPass.ts +0 -51
- package/src/production-runtime/postprocess/ColorGradingPass.ts +0 -49
- package/src/production-runtime/postprocess/DOFPass.ts +0 -47
- package/src/production-runtime/postprocess/FXAAPass.ts +0 -53
- package/src/production-runtime/postprocess/ProductionEffectComposer.ts +0 -78
- package/src/production-runtime/postprocess/ProductionPostProcessTypes.ts +0 -174
- package/src/production-runtime/postprocess/SSAOPass.ts +0 -45
- package/src/production-runtime/resources/GPUBuffer.ts +0 -2
- package/src/production-runtime/resources/GPUTexture.ts +0 -2
- package/src/production-runtime/resources/RenderTarget.ts +0 -3
- package/src/production-runtime/resources/ResourceCache.ts +0 -7
- package/src/production-runtime/scene/Camera.ts +0 -1
- package/src/production-runtime/scene/Lights.ts +0 -2
- package/src/production-runtime/scene/RenderableMesh.ts +0 -2
- package/src/production-runtime/scene/RenderablePrimitive.ts +0 -1
- package/src/production-runtime/scene/RenderableScene.ts +0 -4
- package/src/production-runtime/shaders/ShaderProgramLibrary.ts +0 -7
- package/src/production-runtime/shaders/chunks/brdf.glsl +0 -2
- package/src/production-runtime/shaders/chunks/ibl.glsl +0 -3
- package/src/production-runtime/shaders/chunks/pbr.frag.glsl +0 -47
- package/src/production-runtime/shaders/chunks/pbr.vert.glsl +0 -8
- package/src/production-runtime/shaders/chunks/shadows.glsl +0 -2
- package/src/production-runtime/shaders/wgsl/pbr.wgsl +0 -3
- package/src/production-runtime/shaders/wgsl/postprocess.wgsl +0 -2
- package/src/production-runtime/shaders/wgsl/skybox.wgsl +0 -2
- package/src/shaders/depth.frag.glsl +0 -5
- package/src/shaders/depth.vert.glsl +0 -8
- package/src/shaders/pbr-direct.frag.glsl +0 -687
- package/src/shaders/pbr-direct.vert.glsl +0 -18
- package/src/shaders/unlit.frag.glsl +0 -18
- package/src/shaders/unlit.vert.glsl +0 -13
- package/src/shadows/CascadedShadowPipeline.ts +0 -53
- package/src/shadows/ContactShadows.ts +0 -106
- package/src/shadows/ShadowDebugViews.ts +0 -46
- package/src/threejs-compatibility/InstancingSystem.ts +0 -17
- package/src/threejs-compatibility/LightingSystem.ts +0 -22
- package/src/threejs-compatibility/MaterialSystem.ts +0 -9
- package/src/threejs-compatibility/RenderTargetSystem.ts +0 -35
- package/src/threejs-compatibility/RendererDiagnostics.ts +0 -61
- package/src/threejs-compatibility/SceneRenderer.ts +0 -34
- package/src/threejs-compatibility/ShadowSystem.ts +0 -19
- package/src/threejs-compatibility/TextureSystem.ts +0 -19
- package/src/threejs-compatibility/ThreeCompatRenderer.ts +0 -90
- package/src/threejs-compatibility/TransparencySystem.ts +0 -19
- package/src/threejs-compatibility/index.ts +0 -29
- package/src/threejs-compatibility/performance/BVH.ts +0 -14
- package/src/threejs-compatibility/performance/FrustumCulling.ts +0 -10
- package/src/threejs-compatibility/performance/Instancing.ts +0 -4
- package/src/threejs-compatibility/performance/LODSystem.ts +0 -16
- package/src/threejs-compatibility/performance/OcclusionCulling.ts +0 -6
- package/src/threejs-compatibility/performance/RaycastAcceleration.ts +0 -12
- package/src/threejs-compatibility/performance/RendererProfiler.ts +0 -19
- package/src/threejs-compatibility/performance/TextureStreaming.ts +0 -4
- package/src/threejs-compatibility/performance/index.ts +0 -12
- package/src/threejs-compatibility/postprocess/BloomPass.ts +0 -16
- package/src/threejs-compatibility/postprocess/ColorGradingPass.ts +0 -16
- package/src/threejs-compatibility/postprocess/DepthOfFieldPass.ts +0 -21
- package/src/threejs-compatibility/postprocess/EffectComposer.ts +0 -14
- package/src/threejs-compatibility/postprocess/FXAAPass.ts +0 -15
- package/src/threejs-compatibility/postprocess/MotionBlurPass.ts +0 -20
- package/src/threejs-compatibility/postprocess/OutlinePass.ts +0 -21
- package/src/threejs-compatibility/postprocess/PostProcessTypes.ts +0 -169
- package/src/threejs-compatibility/postprocess/RenderPass.ts +0 -9
- package/src/threejs-compatibility/postprocess/SMAAPass.ts +0 -15
- package/src/threejs-compatibility/postprocess/SSAOPass.ts +0 -21
- package/src/threejs-compatibility/postprocess/ShaderPass.ts +0 -7
- package/src/threejs-compatibility/postprocess/TAAPass.ts +0 -15
- package/src/threejs-compatibility/postprocess/VignettePass.ts +0 -16
- package/src/threejs-compatibility/postprocess/index.ts +0 -15
- package/src/threejs-compatibility/shaders/NodeMaterial.ts +0 -21
- package/src/threejs-compatibility/shaders/RawShaderMaterial.ts +0 -6
- package/src/threejs-compatibility/shaders/ShaderDiagnostics.ts +0 -17
- package/src/threejs-compatibility/shaders/ShaderMaterial.ts +0 -18
- package/src/threejs-compatibility/shaders/ThreeCompatShaderChunks.ts +0 -6
- package/src/threejs-compatibility/shaders/Uniforms.ts +0 -18
- package/src/threejs-compatibility/shaders/index.ts +0 -9
- package/src/threejs-compatibility/vfx/GPUPointCloud.ts +0 -7
- package/src/threejs-compatibility/vfx/LineRenderer.ts +0 -13
- package/src/threejs-compatibility/vfx/ParticleSystem.ts +0 -23
- package/src/threejs-compatibility/vfx/SpriteSystem.ts +0 -14
- package/src/threejs-compatibility/vfx/TrailRenderer.ts +0 -8
- package/src/threejs-compatibility/vfx/VFXDiagnostics.ts +0 -23
- package/src/threejs-compatibility/vfx/index.ts +0 -9
- package/src/threejs-example-parity/index.ts +0 -205
- package/src/webgpu/WebGPUBuffer.ts +0 -1
- package/src/webgpu/WebGPUCompute.ts +0 -2
- package/src/webgpu/WebGPUPipelineCache.ts +0 -15
- package/src/webgpu/WebGPUPostProcess.ts +0 -2
- package/src/webgpu/WebGPUTexture.ts +0 -2
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -20
package/src/WebGPUDevice.ts
DELETED
|
@@ -1,2877 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type BufferUsage,
|
|
3
|
-
type DrawCommand,
|
|
4
|
-
type RenderBuffer,
|
|
5
|
-
type RenderDevice,
|
|
6
|
-
RenderDeviceError,
|
|
7
|
-
type RenderDeviceDiagnostics,
|
|
8
|
-
type RenderDeviceInfo,
|
|
9
|
-
type RenderShaderProgram,
|
|
10
|
-
type RenderTarget,
|
|
11
|
-
type RenderTargetDescriptor,
|
|
12
|
-
type ShaderReflection,
|
|
13
|
-
type ShaderSources,
|
|
14
|
-
type UniformValue,
|
|
15
|
-
viewBytes
|
|
16
|
-
} from "./RenderDevice";
|
|
17
|
-
import { reflectShaderSources } from "./ShaderReflection";
|
|
18
|
-
import type { Sampler, TextureMagFilter, TextureMinFilter } from "./Sampler";
|
|
19
|
-
import { Texture, bytesPerPixel, isCompressedTextureFormat, type TextureFormat } from "./Texture";
|
|
20
|
-
import { TextureBinding } from "./TextureBinding";
|
|
21
|
-
import { type VertexFormat } from "./VertexFormat";
|
|
22
|
-
|
|
23
|
-
const BUFFER_USAGE = {
|
|
24
|
-
MAP_READ: 0x0001,
|
|
25
|
-
COPY_SRC: 0x0004,
|
|
26
|
-
COPY_DST: 0x0008,
|
|
27
|
-
INDEX: 0x0010,
|
|
28
|
-
VERTEX: 0x0020,
|
|
29
|
-
UNIFORM: 0x0040
|
|
30
|
-
} as const;
|
|
31
|
-
const MAP_MODE = {
|
|
32
|
-
READ: 0x0001
|
|
33
|
-
} as const;
|
|
34
|
-
|
|
35
|
-
const TEXTURE_USAGE = {
|
|
36
|
-
RENDER_ATTACHMENT: 0x0010,
|
|
37
|
-
COPY_SRC: 0x0001,
|
|
38
|
-
COPY_DST: 0x0002,
|
|
39
|
-
TEXTURE_BINDING: 0x0004
|
|
40
|
-
} as const;
|
|
41
|
-
const DEPTH_TEXTURE_FORMAT = "depth24plus";
|
|
42
|
-
|
|
43
|
-
interface ForwardShadowUniforms {
|
|
44
|
-
readonly texture: TextureBinding;
|
|
45
|
-
readonly matrix: readonly number[];
|
|
46
|
-
readonly strength: number;
|
|
47
|
-
readonly bias: number;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface WebGPULike {
|
|
51
|
-
requestAdapter(): Promise<WebGPUAdapterLike | null>;
|
|
52
|
-
getPreferredCanvasFormat?(): string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface WebGPUAdapterLike {
|
|
56
|
-
requestDevice(): Promise<WebGPUDeviceLike>;
|
|
57
|
-
readonly name?: string;
|
|
58
|
-
readonly info?: {
|
|
59
|
-
readonly vendor?: string;
|
|
60
|
-
readonly architecture?: string;
|
|
61
|
-
readonly device?: string;
|
|
62
|
-
readonly description?: string;
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface WebGPUDeviceLike {
|
|
67
|
-
readonly queue: WebGPUQueueLike;
|
|
68
|
-
readonly lost?: Promise<WebGPUDeviceLostInfoLike>;
|
|
69
|
-
createBuffer(descriptor: WebGPUBufferDescriptorLike): WebGPUBufferLike;
|
|
70
|
-
createShaderModule?(descriptor: { readonly label?: string; readonly code: string }): unknown;
|
|
71
|
-
createRenderPipeline?(descriptor: WebGPURenderPipelineDescriptorLike): WebGPURenderPipelineLike;
|
|
72
|
-
createBindGroup?(descriptor: WebGPUBindGroupDescriptorLike): WebGPUBindGroupLike;
|
|
73
|
-
createTexture?(descriptor: WebGPUTextureDescriptorLike): WebGPUTextureLike;
|
|
74
|
-
createSampler?(descriptor?: WebGPUSamplerDescriptorLike): WebGPUSamplerLike;
|
|
75
|
-
createCommandEncoder?(descriptor?: { readonly label?: string }): WebGPUCommandEncoderLike;
|
|
76
|
-
destroy?(): void;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface WebGPUDeviceLostInfoLike {
|
|
80
|
-
readonly reason?: string;
|
|
81
|
-
readonly message?: string;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export interface WebGPUQueueLike {
|
|
85
|
-
writeBuffer(buffer: WebGPUBufferLike, offset: number, data: ArrayBuffer | ArrayBufferView, dataOffset?: number, size?: number): void;
|
|
86
|
-
writeTexture?(destination: WebGPUImageCopyTextureLike, data: ArrayBuffer | ArrayBufferView, dataLayout: WebGPUImageDataLayoutLike, size: WebGPUExtent3DLike): void;
|
|
87
|
-
copyExternalImageToTexture?(source: WebGPUImageCopyExternalImageLike, destination: WebGPUImageCopyTextureLike, size: WebGPUExtent3DLike): void;
|
|
88
|
-
submit(commands: readonly unknown[]): void;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface WebGPUBufferDescriptorLike {
|
|
92
|
-
readonly label?: string;
|
|
93
|
-
readonly size: number;
|
|
94
|
-
readonly usage: number;
|
|
95
|
-
readonly mappedAtCreation?: boolean;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export interface WebGPUBufferLike {
|
|
99
|
-
mapAsync?(mode: number): Promise<void>;
|
|
100
|
-
getMappedRange?(): ArrayBuffer;
|
|
101
|
-
unmap?(): void;
|
|
102
|
-
destroy(): void;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export interface WebGPUTextureDescriptorLike {
|
|
106
|
-
readonly label?: string;
|
|
107
|
-
readonly size: readonly [number, number, number?] | { readonly width: number; readonly height: number; readonly depthOrArrayLayers?: number };
|
|
108
|
-
readonly format: string;
|
|
109
|
-
readonly usage: number;
|
|
110
|
-
readonly mipLevelCount?: number;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface WebGPUTextureLike {
|
|
114
|
-
createView(): unknown;
|
|
115
|
-
destroy(): void;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export interface WebGPUSamplerDescriptorLike {
|
|
119
|
-
readonly minFilter?: "nearest" | "linear";
|
|
120
|
-
readonly magFilter?: "nearest" | "linear";
|
|
121
|
-
readonly mipmapFilter?: "nearest" | "linear";
|
|
122
|
-
readonly addressModeU?: "clamp-to-edge" | "repeat" | "mirror-repeat";
|
|
123
|
-
readonly addressModeV?: "clamp-to-edge" | "repeat" | "mirror-repeat";
|
|
124
|
-
readonly maxAnisotropy?: number;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export interface WebGPUSamplerLike {}
|
|
128
|
-
|
|
129
|
-
export interface WebGPUCanvasContextLike {
|
|
130
|
-
configure(configuration: WebGPUCanvasConfigurationLike): void;
|
|
131
|
-
getCurrentTexture(): WebGPUTextureLike;
|
|
132
|
-
unconfigure?(): void;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export interface WebGPUCanvasConfigurationLike {
|
|
136
|
-
readonly device: WebGPUDeviceLike;
|
|
137
|
-
readonly format: string;
|
|
138
|
-
readonly alphaMode?: "opaque" | "premultiplied";
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export interface WebGPURenderPipelineDescriptorLike {
|
|
142
|
-
readonly label?: string;
|
|
143
|
-
readonly layout: "auto";
|
|
144
|
-
readonly vertex: {
|
|
145
|
-
readonly module: unknown;
|
|
146
|
-
readonly entryPoint: string;
|
|
147
|
-
readonly buffers?: readonly WebGPUVertexBufferLayoutLike[];
|
|
148
|
-
};
|
|
149
|
-
readonly fragment?: {
|
|
150
|
-
readonly module: unknown;
|
|
151
|
-
readonly entryPoint: string;
|
|
152
|
-
readonly targets: readonly [{ readonly format: string }];
|
|
153
|
-
};
|
|
154
|
-
readonly primitive: {
|
|
155
|
-
readonly topology: "triangle-list" | "line-list" | "point-list";
|
|
156
|
-
};
|
|
157
|
-
readonly depthStencil?: {
|
|
158
|
-
readonly format: string;
|
|
159
|
-
readonly depthWriteEnabled: boolean;
|
|
160
|
-
readonly depthCompare: "less-equal" | "always";
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export interface WebGPUVertexBufferLayoutLike {
|
|
165
|
-
readonly arrayStride: number;
|
|
166
|
-
readonly attributes: readonly WebGPUVertexAttributeLike[];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export interface WebGPUVertexAttributeLike {
|
|
170
|
-
readonly shaderLocation: number;
|
|
171
|
-
readonly offset: number;
|
|
172
|
-
readonly format: "float32" | "float32x2" | "float32x3" | "float32x4";
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
export interface WebGPUBindGroupDescriptorLike {
|
|
176
|
-
readonly label?: string;
|
|
177
|
-
readonly layout: unknown;
|
|
178
|
-
readonly entries: readonly WebGPUBindGroupEntryLike[];
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export interface WebGPUBindGroupEntryLike {
|
|
182
|
-
readonly binding: number;
|
|
183
|
-
readonly resource: unknown;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export interface WebGPUBindGroupLike {}
|
|
187
|
-
|
|
188
|
-
export interface WebGPURenderPipelineLike {
|
|
189
|
-
getBindGroupLayout?(index: number): unknown;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export interface WebGPUCommandEncoderLike {
|
|
193
|
-
beginRenderPass(descriptor: WebGPURenderPassDescriptorLike): WebGPURenderPassEncoderLike;
|
|
194
|
-
copyTextureToBuffer?(source: WebGPUImageCopyTextureLike, destination: WebGPUImageCopyBufferLike, size: WebGPUExtent3DLike): void;
|
|
195
|
-
finish(): unknown;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export interface WebGPUImageCopyTextureLike {
|
|
199
|
-
readonly texture: WebGPUTextureLike;
|
|
200
|
-
readonly mipLevel?: number;
|
|
201
|
-
readonly origin?: {
|
|
202
|
-
readonly x?: number;
|
|
203
|
-
readonly y?: number;
|
|
204
|
-
readonly z?: number;
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export interface WebGPUImageCopyExternalImageLike {
|
|
209
|
-
readonly source: TexImageSource;
|
|
210
|
-
readonly origin?: {
|
|
211
|
-
readonly x?: number;
|
|
212
|
-
readonly y?: number;
|
|
213
|
-
};
|
|
214
|
-
readonly flipY?: boolean;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export interface WebGPUImageCopyBufferLike {
|
|
218
|
-
readonly buffer: WebGPUBufferLike;
|
|
219
|
-
readonly bytesPerRow: number;
|
|
220
|
-
readonly rowsPerImage?: number;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export interface WebGPUImageDataLayoutLike {
|
|
224
|
-
readonly offset?: number;
|
|
225
|
-
readonly bytesPerRow: number;
|
|
226
|
-
readonly rowsPerImage?: number;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
export type WebGPUExtent3DLike =
|
|
230
|
-
| readonly [number, number, number?]
|
|
231
|
-
| {
|
|
232
|
-
readonly width: number;
|
|
233
|
-
readonly height: number;
|
|
234
|
-
readonly depthOrArrayLayers?: number;
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
export interface WebGPURenderPassDescriptorLike {
|
|
238
|
-
readonly label?: string;
|
|
239
|
-
readonly colorAttachments: readonly [{
|
|
240
|
-
readonly view: unknown;
|
|
241
|
-
readonly clearValue?: readonly [number, number, number, number];
|
|
242
|
-
readonly loadOp: "clear" | "load";
|
|
243
|
-
readonly storeOp: "store";
|
|
244
|
-
}];
|
|
245
|
-
readonly depthStencilAttachment?: {
|
|
246
|
-
readonly view: unknown;
|
|
247
|
-
readonly depthClearValue?: number;
|
|
248
|
-
readonly depthLoadOp: "clear" | "load";
|
|
249
|
-
readonly depthStoreOp: "store";
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export interface WebGPURenderPassEncoderLike {
|
|
254
|
-
setPipeline(pipeline: WebGPURenderPipelineLike): void;
|
|
255
|
-
setVertexBuffer(slot: number, buffer: WebGPUBufferLike): void;
|
|
256
|
-
setBindGroup?(index: number, bindGroup: WebGPUBindGroupLike): void;
|
|
257
|
-
setIndexBuffer?(buffer: WebGPUBufferLike, indexFormat: "uint16" | "uint32"): void;
|
|
258
|
-
draw(vertexCount: number, instanceCount?: number, firstVertex?: number): void;
|
|
259
|
-
drawIndexed?(indexCount: number, instanceCount?: number, firstIndex?: number): void;
|
|
260
|
-
end(): void;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export interface WebGPUDeviceOptions {
|
|
264
|
-
readonly gpu?: WebGPULike;
|
|
265
|
-
readonly adapter?: WebGPUAdapterLike;
|
|
266
|
-
readonly device?: WebGPUDeviceLike;
|
|
267
|
-
readonly canvas?: HTMLCanvasElement | OffscreenCanvas;
|
|
268
|
-
readonly presentationFormat?: string;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
class WebGPURenderBuffer implements RenderBuffer {
|
|
272
|
-
public disposed = false;
|
|
273
|
-
public readonly bytes: Uint8Array;
|
|
274
|
-
|
|
275
|
-
constructor(
|
|
276
|
-
public readonly id: number,
|
|
277
|
-
public readonly usage: BufferUsage,
|
|
278
|
-
public readonly byteLength: number,
|
|
279
|
-
public readonly nativeByteLength: number,
|
|
280
|
-
public readonly handle: WebGPUBufferLike,
|
|
281
|
-
initialData?: ArrayBufferView
|
|
282
|
-
) {
|
|
283
|
-
this.bytes = new Uint8Array(byteLength);
|
|
284
|
-
if (initialData) this.bytes.set(viewBytes(initialData), 0);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
dispose(): void {
|
|
288
|
-
if (!this.disposed) {
|
|
289
|
-
this.handle.destroy();
|
|
290
|
-
this.disposed = true;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
type NativeUniformLayout =
|
|
296
|
-
| "generated-basic"
|
|
297
|
-
| "generated-texture"
|
|
298
|
-
| "generated-pbr"
|
|
299
|
-
| "generated-textured-pbr"
|
|
300
|
-
| "generated-instanced-pbr"
|
|
301
|
-
| "generated-skinned-unlit"
|
|
302
|
-
| "generated-morph-unlit"
|
|
303
|
-
| "passthrough";
|
|
304
|
-
|
|
305
|
-
class WebGPUShaderProgram implements RenderShaderProgram {
|
|
306
|
-
public disposed = false;
|
|
307
|
-
public readonly renderPipelines = new Map<string, WebGPURenderPipelineLike>();
|
|
308
|
-
|
|
309
|
-
constructor(
|
|
310
|
-
public readonly id: number,
|
|
311
|
-
public readonly label: string,
|
|
312
|
-
public readonly marker: string,
|
|
313
|
-
public readonly reflection: ShaderReflection,
|
|
314
|
-
public readonly modules: readonly unknown[],
|
|
315
|
-
public readonly entryPoints: readonly [string, string],
|
|
316
|
-
public readonly nativeUniformLayout: NativeUniformLayout
|
|
317
|
-
) {}
|
|
318
|
-
|
|
319
|
-
dispose(): void {
|
|
320
|
-
this.disposed = true;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
class WebGPURenderTarget implements RenderTarget {
|
|
325
|
-
public disposed = false;
|
|
326
|
-
public nativeNeedsClear = true;
|
|
327
|
-
public nativeClearColor: readonly [number, number, number, number] = [0, 0, 0, 1];
|
|
328
|
-
public readonly colorPixels: Uint8Array;
|
|
329
|
-
public readonly colorFloatPixels: Float32Array | null;
|
|
330
|
-
public readonly depthPixels: Float32Array | null;
|
|
331
|
-
|
|
332
|
-
constructor(
|
|
333
|
-
public readonly id: number,
|
|
334
|
-
public readonly width: number,
|
|
335
|
-
public readonly height: number,
|
|
336
|
-
public readonly label: string,
|
|
337
|
-
public readonly colorTexture: Texture,
|
|
338
|
-
public readonly depthTexture: Texture | undefined,
|
|
339
|
-
public readonly nativeTexture: WebGPUTextureLike | null,
|
|
340
|
-
public readonly nativeView: unknown | null,
|
|
341
|
-
public readonly nativeDepthTexture: WebGPUTextureLike | null,
|
|
342
|
-
public readonly nativeDepthView: unknown | null,
|
|
343
|
-
public readonly hasDepth: boolean
|
|
344
|
-
) {
|
|
345
|
-
this.colorPixels = new Uint8Array(width * height * 4);
|
|
346
|
-
this.colorFloatPixels = colorTexture.format === "rgba16f" || colorTexture.format === "rgba32f"
|
|
347
|
-
? new Float32Array(width * height * 4)
|
|
348
|
-
: null;
|
|
349
|
-
this.depthPixels = hasDepth ? new Float32Array(width * height).fill(1) : null;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
dispose(): void {
|
|
353
|
-
if (!this.disposed) {
|
|
354
|
-
this.nativeTexture?.destroy();
|
|
355
|
-
this.nativeDepthTexture?.destroy();
|
|
356
|
-
this.colorTexture.dispose();
|
|
357
|
-
this.depthTexture?.dispose();
|
|
358
|
-
this.disposed = true;
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/** Joint-palette capacity of the WebGPU skinning path — parity with the WebGL2 `u_jointMatrices[96]`. */
|
|
364
|
-
export const MAX_WEBGPU_SKINNING_JOINTS = 96;
|
|
365
|
-
|
|
366
|
-
export class WebGPUDevice implements RenderDevice {
|
|
367
|
-
public readonly kind = "webgpu";
|
|
368
|
-
public readonly info: RenderDeviceInfo;
|
|
369
|
-
public disposed = false;
|
|
370
|
-
public contextLost = false;
|
|
371
|
-
|
|
372
|
-
private nextId = 1;
|
|
373
|
-
private frameActive = false;
|
|
374
|
-
private drawCalls = 0;
|
|
375
|
-
private viewportWidth = 0;
|
|
376
|
-
private viewportHeight = 0;
|
|
377
|
-
private clearColor: readonly [number, number, number, number] = [0, 0, 0, 1];
|
|
378
|
-
private lastError: string | null = null;
|
|
379
|
-
private nativeSubmissions = 0;
|
|
380
|
-
private nativeTextureBindings = 0;
|
|
381
|
-
private nativeGeneratedBasicSubmissions = 0;
|
|
382
|
-
private nativeGeneratedTextureSubmissions = 0;
|
|
383
|
-
private nativePassthroughSubmissions = 0;
|
|
384
|
-
private nativePbrSubmissions = 0;
|
|
385
|
-
private nativeInstancedSubmissions = 0;
|
|
386
|
-
private nativeSkinnedSubmissions = 0;
|
|
387
|
-
private nativeMorphSubmissions = 0;
|
|
388
|
-
private nativeEnvironmentBindings = 0;
|
|
389
|
-
private nativeShadowMapBindings = 0;
|
|
390
|
-
private canvasSubmissions = 0;
|
|
391
|
-
private readonly buffers = new Set<WebGPURenderBuffer>();
|
|
392
|
-
private readonly shaders = new Set<WebGPUShaderProgram>();
|
|
393
|
-
private readonly renderTargets = new Set<WebGPURenderTarget>();
|
|
394
|
-
private readonly nativeSampledTextures = new Map<Texture, { readonly texture: WebGPUTextureLike; readonly view: unknown }>();
|
|
395
|
-
private activeRenderTarget: WebGPURenderTarget | null = null;
|
|
396
|
-
private backbufferPixels: Uint8Array | null = null;
|
|
397
|
-
private backbufferWidth = 0;
|
|
398
|
-
private backbufferHeight = 0;
|
|
399
|
-
private canvasDepthTexture: WebGPUTextureLike | null = null;
|
|
400
|
-
private canvasDepthView: unknown | null = null;
|
|
401
|
-
private canvasDepthWidth = 0;
|
|
402
|
-
private canvasDepthHeight = 0;
|
|
403
|
-
|
|
404
|
-
static async create(options: WebGPUDeviceOptions = {}): Promise<WebGPUDevice> {
|
|
405
|
-
const gpu = options.gpu ?? readWebGPU(globalThis);
|
|
406
|
-
const adapter = options.adapter ?? (await requestAdapter(gpu));
|
|
407
|
-
if (!adapter) {
|
|
408
|
-
throw new RenderDeviceError("WebGPU adapter request did not return a device owner", "WEBGPU_ADAPTER_MISSING");
|
|
409
|
-
}
|
|
410
|
-
const device = validateWebGPUDevice(await requestDevice(adapter, options.device));
|
|
411
|
-
const canvasContext = options.canvas ? acquireWebGPUCanvasContext(options.canvas) : null;
|
|
412
|
-
const presentationFormat = options.presentationFormat ?? gpu?.getPreferredCanvasFormat?.() ?? "bgra8unorm";
|
|
413
|
-
if (canvasContext) {
|
|
414
|
-
canvasContext.configure({
|
|
415
|
-
device,
|
|
416
|
-
format: presentationFormat,
|
|
417
|
-
alphaMode: "opaque"
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
return new WebGPUDevice(adapter, device, canvasContext, presentationFormat);
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
private constructor(
|
|
424
|
-
private readonly adapter: WebGPUAdapterLike,
|
|
425
|
-
private readonly device: WebGPUDeviceLike,
|
|
426
|
-
private readonly canvasContext: WebGPUCanvasContextLike | null,
|
|
427
|
-
private readonly presentationFormat: string
|
|
428
|
-
) {
|
|
429
|
-
const supportsNativeRenderPipeline = hasNativeRenderPipeline(device);
|
|
430
|
-
const supportsNativeTextureReadback = hasNativeTextureReadback(device);
|
|
431
|
-
const supportsCanvasSurface = supportsNativeRenderPipeline && canvasContext !== null;
|
|
432
|
-
this.info = {
|
|
433
|
-
backend: "webgpu",
|
|
434
|
-
vendor: adapter.info?.vendor ?? "webgpu",
|
|
435
|
-
renderer: describeAdapter(adapter) ?? "webgpu-render-device",
|
|
436
|
-
capabilities: [
|
|
437
|
-
"buffers",
|
|
438
|
-
"buffer-readback",
|
|
439
|
-
"shader-validation",
|
|
440
|
-
"render-targets",
|
|
441
|
-
"pixel-readback",
|
|
442
|
-
"postprocess-presentation",
|
|
443
|
-
"hdr-render-targets",
|
|
444
|
-
"float-readback",
|
|
445
|
-
"draw-validation",
|
|
446
|
-
"rasterization",
|
|
447
|
-
"depth-render-targets",
|
|
448
|
-
"spot-shadow-maps",
|
|
449
|
-
"point-shadow-maps",
|
|
450
|
-
"hdr-image-based-lighting",
|
|
451
|
-
...(supportsNativeRenderPipeline ? ["native-render-pipeline" as const] : []),
|
|
452
|
-
...(hasNativeSampledTextureBinding(device) ? ["native-sampled-textures" as const] : []),
|
|
453
|
-
...(supportsNativeTextureReadback ? ["native-texture-readback" as const] : []),
|
|
454
|
-
...(supportsCanvasSurface ? ["canvas-surface" as const] : [])
|
|
455
|
-
],
|
|
456
|
-
limitations: [
|
|
457
|
-
"Synchronous readPixels remains CPU-shadowed for deterministic tests; use readPixelsAsync for native WebGPU texture-to-buffer readback when available.",
|
|
458
|
-
"Renderer.render() stays synchronous and uses CPU-shadowed readback for deterministic compatibility; use Renderer.renderAsync() when renderer-owned WebGPU postprocess must read native render targets.",
|
|
459
|
-
"Sampleable WebGPU depth render-target textures are not advertised until native texture binding and depth readback evidence exists.",
|
|
460
|
-
...(supportsNativeRenderPipeline ? [] : ["native WebGPU render-pipeline submission requires createRenderPipeline, createTexture, and createCommandEncoder."]),
|
|
461
|
-
...(hasNativeSampledTextureBinding(device) ? [] : ["native WebGPU sampled texture binding requires createSampler, createTexture, and queue.writeTexture."]),
|
|
462
|
-
...(supportsNativeTextureReadback ? [] : ["native WebGPU texture readback requires copyTextureToBuffer plus mappable readback buffers."]),
|
|
463
|
-
...(canvasContext ? [] : ["Canvas presentation requires a canvas that exposes a webgpu context."])
|
|
464
|
-
]
|
|
465
|
-
};
|
|
466
|
-
this.observeDeviceLost();
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
createBuffer(usage: BufferUsage, byteLength: number, initialData?: ArrayBufferView): RenderBuffer {
|
|
470
|
-
this.assertAlive();
|
|
471
|
-
if (byteLength <= 0 || !Number.isInteger(byteLength)) {
|
|
472
|
-
throw new RenderDeviceError("Buffer byteLength must be a positive integer", "INVALID_BUFFER_SIZE", { byteLength });
|
|
473
|
-
}
|
|
474
|
-
if (initialData && initialData.byteLength > byteLength) {
|
|
475
|
-
throw new RenderDeviceError("Initial data exceeds buffer size", "BUFFER_OVERFLOW", {
|
|
476
|
-
byteLength,
|
|
477
|
-
dataByteLength: initialData.byteLength
|
|
478
|
-
});
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const handle = this.device.createBuffer({
|
|
482
|
-
label: `aura3d-${usage}-${this.nextId}`,
|
|
483
|
-
size: alignTo(byteLength, 4),
|
|
484
|
-
usage: bufferUsageFlags(usage)
|
|
485
|
-
});
|
|
486
|
-
const buffer = new WebGPURenderBuffer(this.nextId++, usage, byteLength, alignTo(byteLength, 4), handle, initialData);
|
|
487
|
-
this.buffers.add(buffer);
|
|
488
|
-
if (initialData) writeAlignedQueueBuffer(this.device.queue, handle, 0, initialData, buffer.nativeByteLength);
|
|
489
|
-
return buffer;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
updateBuffer(buffer: RenderBuffer, byteOffset: number, data: ArrayBufferView): void {
|
|
493
|
-
this.assertAlive();
|
|
494
|
-
const webgpuBuffer = this.requireBuffer(buffer);
|
|
495
|
-
if (byteOffset < 0 || byteOffset + data.byteLength > webgpuBuffer.byteLength) {
|
|
496
|
-
throw new RenderDeviceError("Buffer update range is out of bounds", "BUFFER_RANGE_OUT_OF_BOUNDS", {
|
|
497
|
-
byteOffset,
|
|
498
|
-
dataByteLength: data.byteLength,
|
|
499
|
-
byteLength: webgpuBuffer.byteLength
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
webgpuBuffer.bytes.set(viewBytes(data), byteOffset);
|
|
503
|
-
writeAlignedQueueBuffer(this.device.queue, webgpuBuffer.handle, byteOffset, webgpuBuffer.bytes.subarray(byteOffset, byteOffset + data.byteLength), webgpuBuffer.nativeByteLength);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
readBuffer(buffer: RenderBuffer, byteOffset = 0, byteLength = buffer.byteLength - byteOffset): Uint8Array {
|
|
507
|
-
this.assertAlive();
|
|
508
|
-
const webgpuBuffer = this.requireBuffer(buffer);
|
|
509
|
-
if (byteOffset < 0 || byteLength < 0 || byteOffset + byteLength > webgpuBuffer.byteLength) {
|
|
510
|
-
throw new RenderDeviceError("Buffer read range is out of bounds", "BUFFER_RANGE_OUT_OF_BOUNDS", {
|
|
511
|
-
byteOffset,
|
|
512
|
-
byteLength,
|
|
513
|
-
bufferByteLength: webgpuBuffer.byteLength
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
return webgpuBuffer.bytes.slice(byteOffset, byteOffset + byteLength);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
createShaderProgram(sources: ShaderSources): RenderShaderProgram {
|
|
520
|
-
this.assertAlive();
|
|
521
|
-
if (!sources.vertex.includes(sources.marker) || !sources.fragment.includes(sources.marker)) {
|
|
522
|
-
throw new RenderDeviceError("Shader source marker is missing from compiled sources", "SHADER_MARKER_MISSING", {
|
|
523
|
-
label: sources.label,
|
|
524
|
-
marker: sources.marker
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
const nativeSources = createNativeShaderSources(sources);
|
|
528
|
-
const modules = this.device.createShaderModule
|
|
529
|
-
? [
|
|
530
|
-
this.device.createShaderModule({ label: `${sources.label}-vertex`, code: nativeSources.vertex }),
|
|
531
|
-
this.device.createShaderModule({ label: `${sources.label}-fragment`, code: nativeSources.fragment })
|
|
532
|
-
]
|
|
533
|
-
: [];
|
|
534
|
-
const shader = new WebGPUShaderProgram(
|
|
535
|
-
this.nextId++,
|
|
536
|
-
sources.label,
|
|
537
|
-
sources.marker,
|
|
538
|
-
reflectShaderSources(sources),
|
|
539
|
-
modules,
|
|
540
|
-
nativeSources.entryPoints,
|
|
541
|
-
nativeSources.uniformLayout
|
|
542
|
-
);
|
|
543
|
-
this.shaders.add(shader);
|
|
544
|
-
return shader;
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
createRenderTarget(descriptor: RenderTargetDescriptor): RenderTarget {
|
|
548
|
-
this.assertAlive();
|
|
549
|
-
if (
|
|
550
|
-
!Number.isInteger(descriptor.width) ||
|
|
551
|
-
descriptor.width <= 0 ||
|
|
552
|
-
!Number.isInteger(descriptor.height) ||
|
|
553
|
-
descriptor.height <= 0
|
|
554
|
-
) {
|
|
555
|
-
throw new RenderDeviceError("Render target dimensions must be positive integers", "INVALID_RENDER_TARGET_SIZE", {
|
|
556
|
-
width: descriptor.width,
|
|
557
|
-
height: descriptor.height,
|
|
558
|
-
label: descriptor.label
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
const nativeTexture = this.device.createTexture?.({
|
|
562
|
-
label: descriptor.label ?? "aura3d-webgpu-render-target",
|
|
563
|
-
size: [descriptor.width, descriptor.height],
|
|
564
|
-
format: webgpuTextureFormat(descriptor.format ?? "rgba8"),
|
|
565
|
-
usage: TEXTURE_USAGE.RENDER_ATTACHMENT | TEXTURE_USAGE.COPY_SRC | TEXTURE_USAGE.COPY_DST | TEXTURE_USAGE.TEXTURE_BINDING
|
|
566
|
-
}) ?? null;
|
|
567
|
-
const hasDepth = descriptor.depth !== false;
|
|
568
|
-
const hasSampleableDepth = descriptor.depth === "texture";
|
|
569
|
-
const nativeDepthTexture = hasDepth
|
|
570
|
-
? this.device.createTexture?.({
|
|
571
|
-
label: `${descriptor.label ?? "aura3d-webgpu-render-target"}-depth`,
|
|
572
|
-
size: [descriptor.width, descriptor.height],
|
|
573
|
-
format: DEPTH_TEXTURE_FORMAT,
|
|
574
|
-
usage: TEXTURE_USAGE.RENDER_ATTACHMENT | (hasSampleableDepth ? TEXTURE_USAGE.TEXTURE_BINDING : 0)
|
|
575
|
-
}) ?? null
|
|
576
|
-
: null;
|
|
577
|
-
const depthTexture = hasSampleableDepth
|
|
578
|
-
? new Texture({ width: descriptor.width, height: descriptor.height, format: "depth24", label: `${descriptor.label ?? "render-target"}-depth` })
|
|
579
|
-
: undefined;
|
|
580
|
-
const target = new WebGPURenderTarget(
|
|
581
|
-
this.nextId++,
|
|
582
|
-
descriptor.width,
|
|
583
|
-
descriptor.height,
|
|
584
|
-
descriptor.label ?? "render-target",
|
|
585
|
-
new Texture({ width: descriptor.width, height: descriptor.height, format: descriptor.format ?? "rgba8", label: descriptor.label ?? "render-target-color" }),
|
|
586
|
-
depthTexture,
|
|
587
|
-
nativeTexture,
|
|
588
|
-
nativeTexture?.createView() ?? null,
|
|
589
|
-
nativeDepthTexture,
|
|
590
|
-
nativeDepthTexture?.createView() ?? null,
|
|
591
|
-
hasDepth
|
|
592
|
-
);
|
|
593
|
-
this.renderTargets.add(target);
|
|
594
|
-
return target;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
setRenderTarget(target: RenderTarget | null): void {
|
|
598
|
-
this.assertAlive();
|
|
599
|
-
if (target === null) {
|
|
600
|
-
this.activeRenderTarget = null;
|
|
601
|
-
return;
|
|
602
|
-
}
|
|
603
|
-
if (!(target instanceof WebGPURenderTarget) || !this.renderTargets.has(target) || target.disposed) {
|
|
604
|
-
throw new RenderDeviceError("Render target is not a live WebGPU resource owned by this device", "INVALID_RESOURCE", {
|
|
605
|
-
targetId: target.id
|
|
606
|
-
});
|
|
607
|
-
}
|
|
608
|
-
this.activeRenderTarget = target;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
writeRenderTargetPixels(target: RenderTarget, pixels: Uint8Array): void {
|
|
612
|
-
this.assertAlive();
|
|
613
|
-
const webgpuTarget = this.requireRenderTarget(target);
|
|
614
|
-
const expectedLength = webgpuTarget.width * webgpuTarget.height * 4;
|
|
615
|
-
if (pixels.length !== expectedLength) {
|
|
616
|
-
throw new RenderDeviceError("Render target pixel upload length does not match target dimensions", "INVALID_RENDER_TARGET_PIXELS", {
|
|
617
|
-
targetId: target.id,
|
|
618
|
-
expectedLength,
|
|
619
|
-
actualLength: pixels.length
|
|
620
|
-
});
|
|
621
|
-
}
|
|
622
|
-
webgpuTarget.colorPixels.set(pixels);
|
|
623
|
-
if (webgpuTarget.colorFloatPixels) {
|
|
624
|
-
for (let index = 0; index < pixels.length; index += 1) {
|
|
625
|
-
webgpuTarget.colorFloatPixels[index] = pixels[index]! / 255;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
if (webgpuTarget.nativeTexture && webgpuTarget.colorTexture.format === "rgba8" && hasNativeTextureUpload(this.device)) {
|
|
629
|
-
this.device.queue.writeTexture(
|
|
630
|
-
{ texture: webgpuTarget.nativeTexture },
|
|
631
|
-
pixels,
|
|
632
|
-
{ bytesPerRow: webgpuTarget.width * 4, rowsPerImage: webgpuTarget.height },
|
|
633
|
-
{ width: webgpuTarget.width, height: webgpuTarget.height, depthOrArrayLayers: 1 }
|
|
634
|
-
);
|
|
635
|
-
webgpuTarget.nativeNeedsClear = false;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
presentRenderTarget(source: RenderTarget): void {
|
|
640
|
-
this.assertAlive();
|
|
641
|
-
const target = this.requireRenderTarget(source);
|
|
642
|
-
if (target.colorTexture.format !== "rgba8") {
|
|
643
|
-
throw new RenderDeviceError("WebGPU backbuffer presentation currently requires rgba8 source pixels", "PRESENT_FORMAT_UNSUPPORTED", {
|
|
644
|
-
source: source.label,
|
|
645
|
-
format: target.colorTexture.format
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
this.backbufferPixels = target.colorPixels.slice();
|
|
649
|
-
this.backbufferWidth = target.width;
|
|
650
|
-
this.backbufferHeight = target.height;
|
|
651
|
-
this.activeRenderTarget = null;
|
|
652
|
-
this.canvasSubmissions += 1;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
readPixels(x: number, y: number, width: number, height: number): Uint8Array {
|
|
656
|
-
this.assertAlive();
|
|
657
|
-
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
|
|
658
|
-
throw new RenderDeviceError("Readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
|
|
659
|
-
}
|
|
660
|
-
const boundsWidth = this.activeRenderTarget?.width ?? (this.backbufferPixels ? this.backbufferWidth : this.viewportWidth);
|
|
661
|
-
const boundsHeight = this.activeRenderTarget?.height ?? (this.backbufferPixels ? this.backbufferHeight : this.viewportHeight);
|
|
662
|
-
if (x + width > boundsWidth || y + height > boundsHeight) {
|
|
663
|
-
throw new RenderDeviceError("Readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
|
|
664
|
-
x,
|
|
665
|
-
y,
|
|
666
|
-
width,
|
|
667
|
-
height,
|
|
668
|
-
boundsWidth,
|
|
669
|
-
boundsHeight
|
|
670
|
-
});
|
|
671
|
-
}
|
|
672
|
-
const output = new Uint8Array(width * height * 4);
|
|
673
|
-
const target = this.activeRenderTarget;
|
|
674
|
-
if (!target) {
|
|
675
|
-
if (this.backbufferPixels) {
|
|
676
|
-
for (let row = 0; row < height; row += 1) {
|
|
677
|
-
const sourceOffset = ((y + row) * this.backbufferWidth + x) * 4;
|
|
678
|
-
const destOffset = row * width * 4;
|
|
679
|
-
output.set(this.backbufferPixels.subarray(sourceOffset, sourceOffset + width * 4), destOffset);
|
|
680
|
-
}
|
|
681
|
-
return output;
|
|
682
|
-
}
|
|
683
|
-
const bytes = rgbaBytes(this.clearColor);
|
|
684
|
-
for (let index = 0; index < output.length; index += 4) output.set(bytes, index);
|
|
685
|
-
return output;
|
|
686
|
-
}
|
|
687
|
-
for (let row = 0; row < height; row += 1) {
|
|
688
|
-
const sourceOffset = ((y + row) * target.width + x) * 4;
|
|
689
|
-
const destOffset = row * width * 4;
|
|
690
|
-
output.set(target.colorPixels.subarray(sourceOffset, sourceOffset + width * 4), destOffset);
|
|
691
|
-
}
|
|
692
|
-
return output;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
async readPixelsAsync(x: number, y: number, width: number, height: number): Promise<Uint8Array> {
|
|
696
|
-
this.assertAlive();
|
|
697
|
-
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
|
|
698
|
-
throw new RenderDeviceError("Readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
|
|
699
|
-
}
|
|
700
|
-
const target = this.activeRenderTarget;
|
|
701
|
-
if (!target || !target.nativeTexture) {
|
|
702
|
-
throw new RenderDeviceError("Native WebGPU readback requires an active native render target", "NATIVE_READBACK_TARGET_MISSING", {
|
|
703
|
-
x,
|
|
704
|
-
y,
|
|
705
|
-
width,
|
|
706
|
-
height,
|
|
707
|
-
renderTarget: target?.label ?? null
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
if (target.colorTexture.format !== "rgba8") {
|
|
711
|
-
throw new RenderDeviceError("Native WebGPU readback currently supports rgba8 render targets", "NATIVE_READBACK_FORMAT_UNSUPPORTED", {
|
|
712
|
-
format: target.colorTexture.format,
|
|
713
|
-
renderTarget: target.label
|
|
714
|
-
});
|
|
715
|
-
}
|
|
716
|
-
if (x + width > target.width || y + height > target.height) {
|
|
717
|
-
throw new RenderDeviceError("Readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
|
|
718
|
-
x,
|
|
719
|
-
y,
|
|
720
|
-
width,
|
|
721
|
-
height,
|
|
722
|
-
boundsWidth: target.width,
|
|
723
|
-
boundsHeight: target.height
|
|
724
|
-
});
|
|
725
|
-
}
|
|
726
|
-
if (!hasNativeTextureReadback(this.device)) {
|
|
727
|
-
throw new RenderDeviceError("WebGPU device does not expose native texture-to-buffer readback", "NATIVE_READBACK_UNSUPPORTED");
|
|
728
|
-
}
|
|
729
|
-
this.flushNativeRenderTargetClear(target);
|
|
730
|
-
|
|
731
|
-
const bytesPerPixel = 4;
|
|
732
|
-
const unpaddedBytesPerRow = width * bytesPerPixel;
|
|
733
|
-
const bytesPerRow = alignTo(unpaddedBytesPerRow, 256);
|
|
734
|
-
const bufferSize = bytesPerRow * height;
|
|
735
|
-
const readback = this.device.createBuffer({
|
|
736
|
-
label: `${target.label}-native-readback`,
|
|
737
|
-
size: bufferSize,
|
|
738
|
-
usage: BUFFER_USAGE.MAP_READ | BUFFER_USAGE.COPY_DST
|
|
739
|
-
});
|
|
740
|
-
const encoder = this.device.createCommandEncoder({ label: `${target.label}-native-readback-encoder` });
|
|
741
|
-
const copyTextureToBuffer = encoder.copyTextureToBuffer;
|
|
742
|
-
if (!copyTextureToBuffer) {
|
|
743
|
-
readback.destroy();
|
|
744
|
-
throw new RenderDeviceError("WebGPU command encoder does not expose texture-to-buffer copy", "NATIVE_READBACK_UNSUPPORTED");
|
|
745
|
-
}
|
|
746
|
-
copyTextureToBuffer.call(
|
|
747
|
-
encoder,
|
|
748
|
-
{ texture: target.nativeTexture, origin: { x, y, z: 0 } },
|
|
749
|
-
{ buffer: readback, bytesPerRow, rowsPerImage: height },
|
|
750
|
-
{ width, height, depthOrArrayLayers: 1 }
|
|
751
|
-
);
|
|
752
|
-
this.device.queue.submit([encoder.finish()]);
|
|
753
|
-
|
|
754
|
-
try {
|
|
755
|
-
const mapAsync = readback.mapAsync;
|
|
756
|
-
const getMappedRange = readback.getMappedRange;
|
|
757
|
-
const unmap = readback.unmap;
|
|
758
|
-
if (!mapAsync || !getMappedRange || !unmap) {
|
|
759
|
-
throw new RenderDeviceError("WebGPU readback buffer does not expose mapped readback APIs", "NATIVE_READBACK_UNSUPPORTED");
|
|
760
|
-
}
|
|
761
|
-
await mapAsync.call(readback, MAP_MODE.READ);
|
|
762
|
-
const mapped = new Uint8Array(getMappedRange.call(readback));
|
|
763
|
-
const output = new Uint8Array(width * height * bytesPerPixel);
|
|
764
|
-
for (let row = 0; row < height; row += 1) {
|
|
765
|
-
const sourceOffset = row * bytesPerRow;
|
|
766
|
-
const destOffset = (height - 1 - row) * unpaddedBytesPerRow;
|
|
767
|
-
output.set(mapped.subarray(sourceOffset, sourceOffset + unpaddedBytesPerRow), destOffset);
|
|
768
|
-
}
|
|
769
|
-
unmap.call(readback);
|
|
770
|
-
return output;
|
|
771
|
-
} finally {
|
|
772
|
-
readback.destroy();
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
async readFloatPixelsAsync(x: number, y: number, width: number, height: number): Promise<Float32Array> {
|
|
777
|
-
this.assertAlive();
|
|
778
|
-
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
|
|
779
|
-
throw new RenderDeviceError("Float readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
|
|
780
|
-
}
|
|
781
|
-
const target = this.activeRenderTarget;
|
|
782
|
-
if (!target || !target.nativeTexture) {
|
|
783
|
-
throw new RenderDeviceError("Native WebGPU float readback requires an active native render target", "NATIVE_READBACK_TARGET_MISSING", {
|
|
784
|
-
x,
|
|
785
|
-
y,
|
|
786
|
-
width,
|
|
787
|
-
height,
|
|
788
|
-
renderTarget: target?.label ?? null
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
if (x + width > target.width || y + height > target.height) {
|
|
792
|
-
throw new RenderDeviceError("Float readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
|
|
793
|
-
x,
|
|
794
|
-
y,
|
|
795
|
-
width,
|
|
796
|
-
height,
|
|
797
|
-
boundsWidth: target.width,
|
|
798
|
-
boundsHeight: target.height
|
|
799
|
-
});
|
|
800
|
-
}
|
|
801
|
-
if (!hasNativeTextureReadback(this.device)) {
|
|
802
|
-
throw new RenderDeviceError("WebGPU device does not expose native texture-to-buffer readback", "NATIVE_READBACK_UNSUPPORTED");
|
|
803
|
-
}
|
|
804
|
-
this.flushNativeRenderTargetClear(target);
|
|
805
|
-
|
|
806
|
-
const format = target.colorTexture.format;
|
|
807
|
-
const bytesPerPixel = format === "rgba32f" ? 16 : format === "rgba16f" ? 8 : 4;
|
|
808
|
-
const unpaddedBytesPerRow = width * bytesPerPixel;
|
|
809
|
-
const bytesPerRow = alignTo(unpaddedBytesPerRow, 256);
|
|
810
|
-
const bufferSize = bytesPerRow * height;
|
|
811
|
-
const readback = this.device.createBuffer({
|
|
812
|
-
label: `${target.label}-native-float-readback`,
|
|
813
|
-
size: bufferSize,
|
|
814
|
-
usage: BUFFER_USAGE.MAP_READ | BUFFER_USAGE.COPY_DST
|
|
815
|
-
});
|
|
816
|
-
const encoder = this.device.createCommandEncoder({ label: `${target.label}-native-float-readback-encoder` });
|
|
817
|
-
const copyTextureToBuffer = encoder.copyTextureToBuffer;
|
|
818
|
-
if (!copyTextureToBuffer) {
|
|
819
|
-
readback.destroy();
|
|
820
|
-
throw new RenderDeviceError("WebGPU command encoder does not expose texture-to-buffer copy", "NATIVE_READBACK_UNSUPPORTED");
|
|
821
|
-
}
|
|
822
|
-
copyTextureToBuffer.call(
|
|
823
|
-
encoder,
|
|
824
|
-
{ texture: target.nativeTexture, origin: { x, y, z: 0 } },
|
|
825
|
-
{ buffer: readback, bytesPerRow, rowsPerImage: height },
|
|
826
|
-
{ width, height, depthOrArrayLayers: 1 }
|
|
827
|
-
);
|
|
828
|
-
this.device.queue.submit([encoder.finish()]);
|
|
829
|
-
|
|
830
|
-
try {
|
|
831
|
-
const mapAsync = readback.mapAsync;
|
|
832
|
-
const getMappedRange = readback.getMappedRange;
|
|
833
|
-
const unmap = readback.unmap;
|
|
834
|
-
if (!mapAsync || !getMappedRange || !unmap) {
|
|
835
|
-
throw new RenderDeviceError("WebGPU readback buffer does not expose mapped readback APIs", "NATIVE_READBACK_UNSUPPORTED");
|
|
836
|
-
}
|
|
837
|
-
await mapAsync.call(readback, MAP_MODE.READ);
|
|
838
|
-
const mapped = new Uint8Array(getMappedRange.call(readback));
|
|
839
|
-
const view = new DataView(mapped.buffer, mapped.byteOffset, mapped.byteLength);
|
|
840
|
-
const output = new Float32Array(width * height * 4);
|
|
841
|
-
for (let row = 0; row < height; row += 1) {
|
|
842
|
-
const destRow = height - 1 - row;
|
|
843
|
-
for (let column = 0; column < width; column += 1) {
|
|
844
|
-
const sourceOffset = row * bytesPerRow + column * bytesPerPixel;
|
|
845
|
-
const destOffset = (destRow * width + column) * 4;
|
|
846
|
-
if (format === "rgba32f") {
|
|
847
|
-
output[destOffset] = view.getFloat32(sourceOffset, true);
|
|
848
|
-
output[destOffset + 1] = view.getFloat32(sourceOffset + 4, true);
|
|
849
|
-
output[destOffset + 2] = view.getFloat32(sourceOffset + 8, true);
|
|
850
|
-
output[destOffset + 3] = view.getFloat32(sourceOffset + 12, true);
|
|
851
|
-
} else if (format === "rgba16f") {
|
|
852
|
-
output[destOffset] = halfFloatToNumber(view.getUint16(sourceOffset, true));
|
|
853
|
-
output[destOffset + 1] = halfFloatToNumber(view.getUint16(sourceOffset + 2, true));
|
|
854
|
-
output[destOffset + 2] = halfFloatToNumber(view.getUint16(sourceOffset + 4, true));
|
|
855
|
-
output[destOffset + 3] = halfFloatToNumber(view.getUint16(sourceOffset + 6, true));
|
|
856
|
-
} else {
|
|
857
|
-
output[destOffset] = (mapped[sourceOffset] ?? 0) / 255;
|
|
858
|
-
output[destOffset + 1] = (mapped[sourceOffset + 1] ?? 0) / 255;
|
|
859
|
-
output[destOffset + 2] = (mapped[sourceOffset + 2] ?? 0) / 255;
|
|
860
|
-
output[destOffset + 3] = (mapped[sourceOffset + 3] ?? 0) / 255;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
864
|
-
unmap.call(readback);
|
|
865
|
-
return output;
|
|
866
|
-
} finally {
|
|
867
|
-
readback.destroy();
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
readFloatPixels(x: number, y: number, width: number, height: number): Float32Array {
|
|
872
|
-
this.assertAlive();
|
|
873
|
-
if (![x, y, width, height].every(Number.isInteger) || x < 0 || y < 0 || width <= 0 || height <= 0) {
|
|
874
|
-
throw new RenderDeviceError("Float readback rectangle must be positive and in bounds", "INVALID_READBACK_RECT", { x, y, width, height });
|
|
875
|
-
}
|
|
876
|
-
const boundsWidth = this.activeRenderTarget?.width ?? (this.backbufferPixels ? this.backbufferWidth : this.viewportWidth);
|
|
877
|
-
const boundsHeight = this.activeRenderTarget?.height ?? (this.backbufferPixels ? this.backbufferHeight : this.viewportHeight);
|
|
878
|
-
if (x + width > boundsWidth || y + height > boundsHeight) {
|
|
879
|
-
throw new RenderDeviceError("Float readback rectangle exceeds framebuffer bounds", "READBACK_OUT_OF_BOUNDS", {
|
|
880
|
-
x,
|
|
881
|
-
y,
|
|
882
|
-
width,
|
|
883
|
-
height,
|
|
884
|
-
boundsWidth,
|
|
885
|
-
boundsHeight
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
const output = new Float32Array(width * height * 4);
|
|
889
|
-
const target = this.activeRenderTarget;
|
|
890
|
-
if (!target) {
|
|
891
|
-
if (this.backbufferPixels) {
|
|
892
|
-
const bytes = this.readPixels(x, y, width, height);
|
|
893
|
-
for (let index = 0; index < bytes.length; index += 1) output[index] = bytes[index]! / 255;
|
|
894
|
-
return output;
|
|
895
|
-
}
|
|
896
|
-
for (let index = 0; index < output.length; index += 4) {
|
|
897
|
-
output[index] = this.clearColor[0];
|
|
898
|
-
output[index + 1] = this.clearColor[1];
|
|
899
|
-
output[index + 2] = this.clearColor[2];
|
|
900
|
-
output[index + 3] = this.clearColor[3];
|
|
901
|
-
}
|
|
902
|
-
return output;
|
|
903
|
-
}
|
|
904
|
-
if (target.colorFloatPixels) {
|
|
905
|
-
for (let row = 0; row < height; row += 1) {
|
|
906
|
-
const sourceOffset = ((y + row) * target.width + x) * 4;
|
|
907
|
-
const destOffset = row * width * 4;
|
|
908
|
-
output.set(target.colorFloatPixels.subarray(sourceOffset, sourceOffset + width * 4), destOffset);
|
|
909
|
-
}
|
|
910
|
-
return output;
|
|
911
|
-
}
|
|
912
|
-
const bytes = this.readPixels(x, y, width, height);
|
|
913
|
-
for (let index = 0; index < bytes.length; index += 1) output[index] = bytes[index]! / 255;
|
|
914
|
-
return output;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
beginFrame(width: number, height: number): void {
|
|
918
|
-
this.assertAlive();
|
|
919
|
-
if (this.frameActive) {
|
|
920
|
-
throw new RenderDeviceError("Frame is already active", "FRAME_ALREADY_ACTIVE");
|
|
921
|
-
}
|
|
922
|
-
if (width <= 0 || height <= 0) {
|
|
923
|
-
throw new RenderDeviceError("Frame dimensions must be positive", "INVALID_FRAME_SIZE", { width, height });
|
|
924
|
-
}
|
|
925
|
-
this.frameActive = true;
|
|
926
|
-
this.drawCalls = 0;
|
|
927
|
-
this.lastError = null;
|
|
928
|
-
this.viewportWidth = width;
|
|
929
|
-
this.viewportHeight = height;
|
|
930
|
-
this.ensureCanvasDepthAttachment(width, height);
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
clear(color: readonly [number, number, number, number]): void {
|
|
934
|
-
this.assertFrame();
|
|
935
|
-
this.clearColor = color;
|
|
936
|
-
if (this.activeRenderTarget) {
|
|
937
|
-
this.clearActiveRenderTarget(color);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
clearRenderTarget(color: readonly [number, number, number, number]): void {
|
|
942
|
-
this.assertFrame();
|
|
943
|
-
if (!this.activeRenderTarget) {
|
|
944
|
-
this.clearColor = color;
|
|
945
|
-
return;
|
|
946
|
-
}
|
|
947
|
-
this.clearActiveRenderTarget(color);
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
draw(command: DrawCommand): void {
|
|
951
|
-
this.assertFrame();
|
|
952
|
-
const vertexBuffer = this.requireBuffer(command.vertexBuffer);
|
|
953
|
-
if (vertexBuffer.disposed) {
|
|
954
|
-
throw new RenderDeviceError("Cannot draw with a disposed vertex buffer", "DISPOSED_RESOURCE", {
|
|
955
|
-
bufferId: command.vertexBuffer.id
|
|
956
|
-
});
|
|
957
|
-
}
|
|
958
|
-
if (command.indexBuffer) {
|
|
959
|
-
const indexBuffer = this.requireBuffer(command.indexBuffer);
|
|
960
|
-
if (indexBuffer.disposed) {
|
|
961
|
-
throw new RenderDeviceError("Cannot draw with a disposed index buffer", "DISPOSED_RESOURCE", {
|
|
962
|
-
bufferId: command.indexBuffer.id
|
|
963
|
-
});
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
if (command.shader) this.requireShader(command.shader);
|
|
967
|
-
if (command.vertexCount <= 0) {
|
|
968
|
-
throw new RenderDeviceError("Draw command must have a positive vertex count", "INVALID_DRAW_COMMAND", {
|
|
969
|
-
vertexCount: command.vertexCount,
|
|
970
|
-
label: command.label
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
if (command.firstVertex !== undefined && (!Number.isInteger(command.firstVertex) || command.firstVertex < 0)) {
|
|
974
|
-
throw new RenderDeviceError("Draw command firstVertex must be a non-negative integer", "INVALID_DRAW_COMMAND", {
|
|
975
|
-
firstVertex: command.firstVertex,
|
|
976
|
-
label: command.label
|
|
977
|
-
});
|
|
978
|
-
}
|
|
979
|
-
if (command.indexBuffer && (command.indexCount === undefined || command.indexCount <= 0)) {
|
|
980
|
-
throw new RenderDeviceError("Indexed draw command must have a positive index count", "INVALID_DRAW_COMMAND", {
|
|
981
|
-
indexCount: command.indexCount,
|
|
982
|
-
label: command.label
|
|
983
|
-
});
|
|
984
|
-
}
|
|
985
|
-
if (command.firstIndex !== undefined && (!Number.isInteger(command.firstIndex) || command.firstIndex < 0)) {
|
|
986
|
-
throw new RenderDeviceError("Draw command firstIndex must be a non-negative integer", "INVALID_DRAW_COMMAND", {
|
|
987
|
-
firstIndex: command.firstIndex,
|
|
988
|
-
label: command.label
|
|
989
|
-
});
|
|
990
|
-
}
|
|
991
|
-
if (command.instanceCount !== undefined && (!Number.isInteger(command.instanceCount) || command.instanceCount <= 0)) {
|
|
992
|
-
throw new RenderDeviceError("Draw command instanceCount must be a positive integer", "INVALID_DRAW_COMMAND", {
|
|
993
|
-
instanceCount: command.instanceCount,
|
|
994
|
-
label: command.label
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
this.submitNativeRenderPass(command, vertexBuffer);
|
|
998
|
-
this.rasterizeDraw(command, vertexBuffer);
|
|
999
|
-
this.drawCalls += 1;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
endFrame(): void {
|
|
1003
|
-
this.assertFrame();
|
|
1004
|
-
this.frameActive = false;
|
|
1005
|
-
this.device.queue.submit([]);
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
captureState(): ReadonlyMap<string, string | number | boolean | null> {
|
|
1009
|
-
return new Map<string, string | number | boolean | null>([
|
|
1010
|
-
["backend", this.kind],
|
|
1011
|
-
["disposed", this.disposed],
|
|
1012
|
-
["contextLost", this.contextLost],
|
|
1013
|
-
["frameActive", this.frameActive],
|
|
1014
|
-
["viewportWidth", this.viewportWidth],
|
|
1015
|
-
["viewportHeight", this.viewportHeight],
|
|
1016
|
-
["renderTarget", this.activeRenderTarget?.label ?? null],
|
|
1017
|
-
["presentationFormat", this.presentationFormat],
|
|
1018
|
-
["drawCalls", this.drawCalls],
|
|
1019
|
-
["nativeSubmissions", this.nativeSubmissions],
|
|
1020
|
-
["nativeTextureBindings", this.nativeTextureBindings],
|
|
1021
|
-
["nativeGeneratedBasicSubmissions", this.nativeGeneratedBasicSubmissions],
|
|
1022
|
-
["nativeGeneratedTextureSubmissions", this.nativeGeneratedTextureSubmissions],
|
|
1023
|
-
["nativePassthroughSubmissions", this.nativePassthroughSubmissions],
|
|
1024
|
-
["nativePbrSubmissions", this.nativePbrSubmissions],
|
|
1025
|
-
["nativeInstancedSubmissions", this.nativeInstancedSubmissions],
|
|
1026
|
-
["nativeSkinnedSubmissions", this.nativeSkinnedSubmissions],
|
|
1027
|
-
["nativeMorphSubmissions", this.nativeMorphSubmissions],
|
|
1028
|
-
["nativeEnvironmentBindings", this.nativeEnvironmentBindings],
|
|
1029
|
-
["nativeShadowMapBindings", this.nativeShadowMapBindings],
|
|
1030
|
-
["canvasSubmissions", this.canvasSubmissions]
|
|
1031
|
-
]);
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
getDiagnostics(): RenderDeviceDiagnostics {
|
|
1035
|
-
const liveRenderTargets = [...this.renderTargets].filter((target) => !target.disposed);
|
|
1036
|
-
const bufferBytes = [...this.buffers].filter((buffer) => !buffer.disposed).reduce((total, buffer) => total + buffer.byteLength, 0);
|
|
1037
|
-
const textureBytes = liveRenderTargets.reduce((total, target) => total + target.colorTexture.byteLength + (target.depthTexture?.byteLength ?? 0), 0);
|
|
1038
|
-
return {
|
|
1039
|
-
drawCalls: this.drawCalls,
|
|
1040
|
-
buffers: [...this.buffers].filter((buffer) => !buffer.disposed).length,
|
|
1041
|
-
shaders: [...this.shaders].filter((shader) => !shader.disposed).length,
|
|
1042
|
-
renderTargets: liveRenderTargets.length,
|
|
1043
|
-
textures: liveRenderTargets.length,
|
|
1044
|
-
bufferBytes,
|
|
1045
|
-
textureBytes,
|
|
1046
|
-
approximateGpuMemoryBytes: bufferBytes + textureBytes,
|
|
1047
|
-
compressedTextures: 0,
|
|
1048
|
-
compressedTextureBytes: 0,
|
|
1049
|
-
textureFallbacks: 0,
|
|
1050
|
-
textureFallbackBytes: 0,
|
|
1051
|
-
disposedBuffers: [...this.buffers].filter((buffer) => buffer.disposed).length,
|
|
1052
|
-
disposedShaders: [...this.shaders].filter((shader) => shader.disposed).length,
|
|
1053
|
-
disposedRenderTargets: [...this.renderTargets].filter((target) => target.disposed).length,
|
|
1054
|
-
disposedTextures: [...this.renderTargets].reduce((total, target) => total + (target.colorTexture.disposed ? 1 : 0) + (target.depthTexture?.disposed ? 1 : 0), 0),
|
|
1055
|
-
nativeSubmissions: this.nativeSubmissions,
|
|
1056
|
-
nativeTextureBindings: this.nativeTextureBindings,
|
|
1057
|
-
nativeGeneratedBasicSubmissions: this.nativeGeneratedBasicSubmissions,
|
|
1058
|
-
nativeGeneratedTextureSubmissions: this.nativeGeneratedTextureSubmissions,
|
|
1059
|
-
nativePassthroughSubmissions: this.nativePassthroughSubmissions,
|
|
1060
|
-
nativePbrSubmissions: this.nativePbrSubmissions,
|
|
1061
|
-
nativeInstancedSubmissions: this.nativeInstancedSubmissions,
|
|
1062
|
-
nativeSkinnedSubmissions: this.nativeSkinnedSubmissions,
|
|
1063
|
-
nativeMorphSubmissions: this.nativeMorphSubmissions,
|
|
1064
|
-
nativeEnvironmentBindings: this.nativeEnvironmentBindings,
|
|
1065
|
-
nativeShadowMapBindings: this.nativeShadowMapBindings,
|
|
1066
|
-
lastError: this.lastError,
|
|
1067
|
-
contextLost: this.contextLost
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
dispose(): void {
|
|
1072
|
-
if (this.disposed) return;
|
|
1073
|
-
for (const buffer of this.buffers) buffer.dispose();
|
|
1074
|
-
for (const shader of this.shaders) shader.dispose();
|
|
1075
|
-
for (const target of this.renderTargets) target.dispose();
|
|
1076
|
-
for (const resource of this.nativeSampledTextures.values()) resource.texture.destroy();
|
|
1077
|
-
this.nativeSampledTextures.clear();
|
|
1078
|
-
this.canvasDepthTexture?.destroy();
|
|
1079
|
-
this.canvasDepthTexture = null;
|
|
1080
|
-
this.canvasDepthView = null;
|
|
1081
|
-
this.canvasContext?.unconfigure?.();
|
|
1082
|
-
this.device.destroy?.();
|
|
1083
|
-
this.frameActive = false;
|
|
1084
|
-
this.disposed = true;
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
private assertAlive(): void {
|
|
1088
|
-
if (this.disposed) {
|
|
1089
|
-
throw new RenderDeviceError("Render device is disposed", "DISPOSED_DEVICE");
|
|
1090
|
-
}
|
|
1091
|
-
if (this.contextLost) {
|
|
1092
|
-
throw new RenderDeviceError("Render context is lost", "CONTEXT_LOST");
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
|
|
1096
|
-
private assertFrame(): void {
|
|
1097
|
-
this.assertAlive();
|
|
1098
|
-
if (!this.frameActive) {
|
|
1099
|
-
throw new RenderDeviceError("No active frame", "NO_ACTIVE_FRAME");
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
private observeDeviceLost(): void {
|
|
1104
|
-
const lost = this.device.lost;
|
|
1105
|
-
if (!lost || typeof lost.then !== "function") return;
|
|
1106
|
-
void lost.then((info) => {
|
|
1107
|
-
if (this.disposed) return;
|
|
1108
|
-
this.contextLost = true;
|
|
1109
|
-
const detail = [info.reason, info.message].filter(Boolean).join(": ");
|
|
1110
|
-
this.lastError = detail ? `WebGPU device lost: ${detail}` : "WebGPU device lost";
|
|
1111
|
-
}).catch((error: unknown) => {
|
|
1112
|
-
if (this.disposed) return;
|
|
1113
|
-
this.contextLost = true;
|
|
1114
|
-
this.lastError = `WebGPU device lost promise rejected: ${error instanceof Error ? error.message : String(error)}`;
|
|
1115
|
-
});
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
private requireBuffer(buffer: RenderBuffer): WebGPURenderBuffer {
|
|
1119
|
-
if (!(buffer instanceof WebGPURenderBuffer) || !this.buffers.has(buffer)) {
|
|
1120
|
-
throw new RenderDeviceError("Buffer was not created by this WebGPU device", "FOREIGN_RESOURCE", {
|
|
1121
|
-
bufferId: buffer.id
|
|
1122
|
-
});
|
|
1123
|
-
}
|
|
1124
|
-
if (buffer.disposed) {
|
|
1125
|
-
throw new RenderDeviceError("Cannot use a disposed buffer", "DISPOSED_RESOURCE", { bufferId: buffer.id });
|
|
1126
|
-
}
|
|
1127
|
-
return buffer;
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
private requireShader(shader: RenderShaderProgram): WebGPUShaderProgram {
|
|
1131
|
-
if (!(shader instanceof WebGPUShaderProgram) || !this.shaders.has(shader)) {
|
|
1132
|
-
throw new RenderDeviceError("Shader was not created by this WebGPU device", "FOREIGN_RESOURCE", {
|
|
1133
|
-
shaderId: shader.id
|
|
1134
|
-
});
|
|
1135
|
-
}
|
|
1136
|
-
if (shader.disposed) {
|
|
1137
|
-
throw new RenderDeviceError("Cannot use a disposed shader", "DISPOSED_RESOURCE", { shaderId: shader.id });
|
|
1138
|
-
}
|
|
1139
|
-
return shader;
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
private requireRenderTarget(target: RenderTarget): WebGPURenderTarget {
|
|
1143
|
-
if (!(target instanceof WebGPURenderTarget) || !this.renderTargets.has(target)) {
|
|
1144
|
-
throw new RenderDeviceError("Render target was not created by this WebGPU device", "FOREIGN_RESOURCE", {
|
|
1145
|
-
targetId: target.id
|
|
1146
|
-
});
|
|
1147
|
-
}
|
|
1148
|
-
if (target.disposed) {
|
|
1149
|
-
throw new RenderDeviceError("Cannot use a disposed render target", "DISPOSED_RESOURCE", { targetId: target.id });
|
|
1150
|
-
}
|
|
1151
|
-
return target;
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
private rasterizeDraw(command: DrawCommand, vertexBuffer: WebGPURenderBuffer): void {
|
|
1155
|
-
if (!this.activeRenderTarget || !command.vertexFormat?.hasAttribute("position")) return;
|
|
1156
|
-
const position = command.vertexFormat.getAttribute("position");
|
|
1157
|
-
const colorAttribute = command.vertexFormat.hasAttribute("color") ? command.vertexFormat.getAttribute("color") : null;
|
|
1158
|
-
const uvAttribute = command.vertexFormat.hasAttribute("uv") ? command.vertexFormat.getAttribute("uv") : null;
|
|
1159
|
-
if (position.components < 2) return;
|
|
1160
|
-
const indices = this.indicesFor(command);
|
|
1161
|
-
const color = uniformColor(command.uniforms);
|
|
1162
|
-
const texture = uniformBaseColorTextureBinding(command.uniforms);
|
|
1163
|
-
const shadow = uniformForwardShadow(command.uniforms);
|
|
1164
|
-
const instanceMatrices = uniformRasterMatrices(command.uniforms, command.instanceCount ?? 1);
|
|
1165
|
-
const modelMatrices = uniformRasterModelMatrices(command.uniforms, command.instanceCount ?? 1);
|
|
1166
|
-
const depthTest = command.renderState?.depthTest !== false && this.activeRenderTarget.depthPixels !== null;
|
|
1167
|
-
const depthWrite = command.renderState?.depthWrite !== false;
|
|
1168
|
-
// Full joint-palette skinning (up to 96 joints, WebGL2 parity). readLocal applies morph then skin.
|
|
1169
|
-
const jointPalette = jointPaletteFor(command.uniforms);
|
|
1170
|
-
const format = command.vertexFormat;
|
|
1171
|
-
const readLocal = (index: number): readonly [number, number, number] =>
|
|
1172
|
-
skinLocalPosition(
|
|
1173
|
-
readMorphedPosition(vertexBuffer.bytes, format.stride, position.offset, index, command.uniforms),
|
|
1174
|
-
vertexBuffer.bytes,
|
|
1175
|
-
format,
|
|
1176
|
-
index,
|
|
1177
|
-
jointPalette
|
|
1178
|
-
);
|
|
1179
|
-
for (let instanceIndex = 0; instanceIndex < instanceMatrices.length; instanceIndex += 1) {
|
|
1180
|
-
const matrix = instanceMatrices[instanceIndex] ?? identityMatrix();
|
|
1181
|
-
const modelMatrix = modelMatrices[instanceIndex] ?? identityMatrix();
|
|
1182
|
-
if (command.topology === "triangles") {
|
|
1183
|
-
for (let offset = 0; offset + 2 < indices.length; offset += 3) {
|
|
1184
|
-
const indexA = indices[offset]!;
|
|
1185
|
-
const indexB = indices[offset + 1]!;
|
|
1186
|
-
const indexC = indices[offset + 2]!;
|
|
1187
|
-
const localA = readLocal(indexA);
|
|
1188
|
-
const localB = readLocal(indexB);
|
|
1189
|
-
const localC = readLocal(indexC);
|
|
1190
|
-
const a = transformPosition(localA, matrix);
|
|
1191
|
-
const b = transformPosition(localB, matrix);
|
|
1192
|
-
const c = transformPosition(localC, matrix);
|
|
1193
|
-
const worldA = transformPosition(localA, modelMatrix);
|
|
1194
|
-
const worldB = transformPosition(localB, modelMatrix);
|
|
1195
|
-
const worldC = transformPosition(localC, modelMatrix);
|
|
1196
|
-
const colorA = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexA);
|
|
1197
|
-
const colorB = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexB);
|
|
1198
|
-
const colorC = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexC);
|
|
1199
|
-
const uvA = readVertexUv(vertexBuffer.bytes, command.vertexFormat.stride, uvAttribute?.offset, indexA);
|
|
1200
|
-
const uvB = readVertexUv(vertexBuffer.bytes, command.vertexFormat.stride, uvAttribute?.offset, indexB);
|
|
1201
|
-
const uvC = readVertexUv(vertexBuffer.bytes, command.vertexFormat.stride, uvAttribute?.offset, indexC);
|
|
1202
|
-
rasterizeTriangle(this.activeRenderTarget, a, b, c, worldA, worldB, worldC, color, colorA, colorB, colorC, uvA, uvB, uvC, texture, shadow, depthTest, depthWrite);
|
|
1203
|
-
}
|
|
1204
|
-
} else if (command.topology === "lines") {
|
|
1205
|
-
for (let offset = 0; offset + 1 < indices.length; offset += 2) {
|
|
1206
|
-
const indexA = indices[offset]!;
|
|
1207
|
-
const indexB = indices[offset + 1]!;
|
|
1208
|
-
const a = transformPosition(readLocal(indexA), matrix);
|
|
1209
|
-
const b = transformPosition(readLocal(indexB), matrix);
|
|
1210
|
-
const colorA = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexA);
|
|
1211
|
-
const colorB = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, indexB);
|
|
1212
|
-
rasterizeLine(this.activeRenderTarget, a, b, color, colorA, colorB, depthTest, depthWrite);
|
|
1213
|
-
}
|
|
1214
|
-
} else {
|
|
1215
|
-
for (const index of indices) {
|
|
1216
|
-
const point = transformPosition(readLocal(index), matrix);
|
|
1217
|
-
const vertexColor = readVertexColor(vertexBuffer.bytes, command.vertexFormat.stride, colorAttribute?.offset, index);
|
|
1218
|
-
rasterizePoint(this.activeRenderTarget, point, multiplyColor(color, vertexColor), depthTest, depthWrite);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
private submitNativeRenderPass(command: DrawCommand, vertexBuffer: WebGPURenderBuffer): void {
|
|
1225
|
-
const nativeView = this.activeRenderTarget?.nativeView ?? this.currentCanvasView();
|
|
1226
|
-
if (!nativeView || !command.shader) return;
|
|
1227
|
-
if (!hasNativeRenderPipeline(this.device)) return;
|
|
1228
|
-
const shader = this.requireShader(command.shader);
|
|
1229
|
-
const vertexModule = shader.modules[0];
|
|
1230
|
-
const fragmentModule = shader.modules[1] ?? vertexModule;
|
|
1231
|
-
if (!vertexModule || !fragmentModule) return;
|
|
1232
|
-
const topology = command.topology === "lines" ? "line-list" : command.topology === "points" ? "point-list" : "triangle-list";
|
|
1233
|
-
const targetFormat = this.activeRenderTarget ? webgpuTextureFormat(this.activeRenderTarget.colorTexture.format) : this.presentationFormat;
|
|
1234
|
-
const vertexBuffers = command.vertexFormat ? [vertexBufferLayout(command.vertexFormat)] : undefined;
|
|
1235
|
-
const shouldClear = this.activeRenderTarget ? this.activeRenderTarget.nativeNeedsClear : this.drawCalls === 0;
|
|
1236
|
-
const depthView = this.activeRenderTarget?.nativeDepthView ?? this.canvasDepthView;
|
|
1237
|
-
const depthEnabled = depthView !== null &&
|
|
1238
|
-
depthView !== undefined &&
|
|
1239
|
-
command.renderState?.depthTest !== false;
|
|
1240
|
-
const depthWriteEnabled = command.renderState?.depthWrite ?? true;
|
|
1241
|
-
const depthCompare = command.renderState?.depthCompare ?? "less-equal";
|
|
1242
|
-
const blendEnabled = command.renderState?.blend === true;
|
|
1243
|
-
const pipelineKey = `${shader.nativeUniformLayout}:${topology}:${targetFormat}:${blendEnabled ? "blend" : "opaque"}:${depthEnabled ? `${depthWriteEnabled}:${depthCompare}` : "nodepth"}:${command.vertexFormat?.stride ?? 0}:${command.vertexFormat?.attributes.map((attribute) => `${attribute.shaderLocation}/${attribute.offset}/${attribute.components}`).join(",") ?? "none"}`;
|
|
1244
|
-
let pipeline = shader.renderPipelines.get(pipelineKey);
|
|
1245
|
-
if (!pipeline) {
|
|
1246
|
-
pipeline = this.device.createRenderPipeline({
|
|
1247
|
-
label: `${shader.label}-pipeline`,
|
|
1248
|
-
layout: "auto",
|
|
1249
|
-
vertex: { module: vertexModule, entryPoint: shader.entryPoints[0], ...(vertexBuffers ? { buffers: vertexBuffers } : {}) },
|
|
1250
|
-
fragment: {
|
|
1251
|
-
module: fragmentModule,
|
|
1252
|
-
entryPoint: shader.entryPoints[1],
|
|
1253
|
-
targets: [{
|
|
1254
|
-
format: targetFormat,
|
|
1255
|
-
...(blendEnabled
|
|
1256
|
-
? {
|
|
1257
|
-
blend: {
|
|
1258
|
-
color: { srcFactor: "src-alpha", dstFactor: "one-minus-src-alpha", operation: "add" },
|
|
1259
|
-
alpha: { srcFactor: "one", dstFactor: "one-minus-src-alpha", operation: "add" }
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
: {})
|
|
1263
|
-
}]
|
|
1264
|
-
},
|
|
1265
|
-
primitive: { topology },
|
|
1266
|
-
...(depthEnabled
|
|
1267
|
-
? {
|
|
1268
|
-
depthStencil: {
|
|
1269
|
-
format: DEPTH_TEXTURE_FORMAT,
|
|
1270
|
-
depthWriteEnabled,
|
|
1271
|
-
depthCompare
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
: {})
|
|
1275
|
-
});
|
|
1276
|
-
shader.renderPipelines.set(pipelineKey, pipeline);
|
|
1277
|
-
}
|
|
1278
|
-
const encoder = this.device.createCommandEncoder({ label: `${command.label ?? "draw"}-encoder` });
|
|
1279
|
-
const uniformBuffer = usesNativeDrawUniforms(shader.nativeUniformLayout) ? this.createNativeDrawUniformBuffer(command) : null;
|
|
1280
|
-
const sampledTexture = shader.nativeUniformLayout === "generated-texture" ? this.createNativeSampledTextureBinding(uniformBaseColorTextureBinding(command.uniforms)) : null;
|
|
1281
|
-
const materialTextures = usesNativePbrTextureBindings(shader.nativeUniformLayout) ? this.createNativePbrTextureBindings(command) : null;
|
|
1282
|
-
if (shader.nativeUniformLayout === "generated-texture" && !sampledTexture) {
|
|
1283
|
-
uniformBuffer?.destroy();
|
|
1284
|
-
return;
|
|
1285
|
-
}
|
|
1286
|
-
if (usesNativePbrTextureBindings(shader.nativeUniformLayout) && !materialTextures) {
|
|
1287
|
-
uniformBuffer?.destroy();
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
const bindGroup = uniformBuffer && pipeline.getBindGroupLayout && this.device.createBindGroup
|
|
1291
|
-
? this.device.createBindGroup({
|
|
1292
|
-
label: `${command.label ?? "draw"}-draw-bind-group`,
|
|
1293
|
-
layout: pipeline.getBindGroupLayout(0),
|
|
1294
|
-
entries: [
|
|
1295
|
-
{ binding: 0, resource: { buffer: uniformBuffer } },
|
|
1296
|
-
...(sampledTexture ? [
|
|
1297
|
-
{ binding: 1, resource: sampledTexture.sampler },
|
|
1298
|
-
{ binding: 2, resource: sampledTexture.view }
|
|
1299
|
-
] : []),
|
|
1300
|
-
...(materialTextures ? [
|
|
1301
|
-
{ binding: 1, resource: materialTextures.base.sampler },
|
|
1302
|
-
{ binding: 2, resource: materialTextures.base.view },
|
|
1303
|
-
{ binding: 3, resource: materialTextures.environment.sampler },
|
|
1304
|
-
{ binding: 4, resource: materialTextures.environment.view },
|
|
1305
|
-
{ binding: 5, resource: materialTextures.brdf.sampler },
|
|
1306
|
-
{ binding: 6, resource: materialTextures.brdf.view },
|
|
1307
|
-
{ binding: 7, resource: materialTextures.shadow.sampler },
|
|
1308
|
-
{ binding: 8, resource: materialTextures.shadow.view },
|
|
1309
|
-
{ binding: 9, resource: materialTextures.normal.sampler },
|
|
1310
|
-
{ binding: 10, resource: materialTextures.normal.view },
|
|
1311
|
-
{ binding: 11, resource: materialTextures.metallicRoughness.sampler },
|
|
1312
|
-
{ binding: 12, resource: materialTextures.metallicRoughness.view },
|
|
1313
|
-
{ binding: 13, resource: materialTextures.occlusion.sampler },
|
|
1314
|
-
{ binding: 14, resource: materialTextures.occlusion.view }
|
|
1315
|
-
] : [])
|
|
1316
|
-
]
|
|
1317
|
-
})
|
|
1318
|
-
: null;
|
|
1319
|
-
const pass = encoder.beginRenderPass({
|
|
1320
|
-
label: `${command.label ?? "draw"}-pass`,
|
|
1321
|
-
colorAttachments: [{
|
|
1322
|
-
view: nativeView,
|
|
1323
|
-
clearValue: this.activeRenderTarget?.nativeClearColor ?? this.clearColor,
|
|
1324
|
-
loadOp: shouldClear ? "clear" : "load",
|
|
1325
|
-
storeOp: "store"
|
|
1326
|
-
}],
|
|
1327
|
-
...(depthEnabled
|
|
1328
|
-
? {
|
|
1329
|
-
depthStencilAttachment: {
|
|
1330
|
-
view: depthView,
|
|
1331
|
-
depthClearValue: 1,
|
|
1332
|
-
depthLoadOp: shouldClear ? "clear" as const : "load" as const,
|
|
1333
|
-
depthStoreOp: "store" as const
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
: {})
|
|
1337
|
-
});
|
|
1338
|
-
pass.setPipeline(pipeline);
|
|
1339
|
-
pass.setVertexBuffer(0, vertexBuffer.handle);
|
|
1340
|
-
if (bindGroup) pass.setBindGroup?.(0, bindGroup);
|
|
1341
|
-
if (command.indexBuffer) {
|
|
1342
|
-
if (!pass.setIndexBuffer || !pass.drawIndexed) {
|
|
1343
|
-
pass.end();
|
|
1344
|
-
uniformBuffer?.destroy();
|
|
1345
|
-
this.lastError = "Native WebGPU indexed draw skipped because the render-pass encoder does not expose indexed draw APIs.";
|
|
1346
|
-
return;
|
|
1347
|
-
}
|
|
1348
|
-
const indexBuffer = this.requireBuffer(command.indexBuffer);
|
|
1349
|
-
pass.setIndexBuffer(indexBuffer.handle, command.indexType ?? "uint16");
|
|
1350
|
-
pass.drawIndexed(command.indexCount ?? this.indicesFor(command).length, command.instanceCount ?? 1, command.firstIndex ?? 0);
|
|
1351
|
-
} else {
|
|
1352
|
-
pass.draw(command.vertexCount, command.instanceCount ?? 1, command.firstVertex ?? 0);
|
|
1353
|
-
}
|
|
1354
|
-
pass.end();
|
|
1355
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1356
|
-
if (this.activeRenderTarget) this.activeRenderTarget.nativeNeedsClear = false;
|
|
1357
|
-
uniformBuffer?.destroy();
|
|
1358
|
-
this.nativeSubmissions += 1;
|
|
1359
|
-
if (sampledTexture) this.nativeTextureBindings += 1;
|
|
1360
|
-
if (shader.nativeUniformLayout === "generated-basic") this.nativeGeneratedBasicSubmissions += 1;
|
|
1361
|
-
if (shader.nativeUniformLayout === "generated-texture") this.nativeGeneratedTextureSubmissions += 1;
|
|
1362
|
-
if (shader.nativeUniformLayout === "passthrough") this.nativePassthroughSubmissions += 1;
|
|
1363
|
-
if (shader.nativeUniformLayout === "generated-pbr" || shader.nativeUniformLayout === "generated-textured-pbr" || shader.nativeUniformLayout === "generated-instanced-pbr") this.nativePbrSubmissions += 1;
|
|
1364
|
-
if (shader.nativeUniformLayout === "generated-instanced-pbr") this.nativeInstancedSubmissions += 1;
|
|
1365
|
-
if (shader.nativeUniformLayout === "generated-skinned-unlit") this.nativeSkinnedSubmissions += 1;
|
|
1366
|
-
if (shader.nativeUniformLayout === "generated-morph-unlit") this.nativeMorphSubmissions += 1;
|
|
1367
|
-
if (materialTextures?.actualBaseColor) this.nativeTextureBindings += 1;
|
|
1368
|
-
if (materialTextures?.actualNormal) this.nativeTextureBindings += 1;
|
|
1369
|
-
if (materialTextures?.actualMetallicRoughness) this.nativeTextureBindings += 1;
|
|
1370
|
-
if (materialTextures?.actualOcclusion) this.nativeTextureBindings += 1;
|
|
1371
|
-
if (materialTextures?.actualEnvironment) this.nativeEnvironmentBindings += 1;
|
|
1372
|
-
if (materialTextures?.actualBrdf) this.nativeEnvironmentBindings += 1;
|
|
1373
|
-
if (materialTextures?.actualShadow) this.nativeShadowMapBindings += 1;
|
|
1374
|
-
if (!this.activeRenderTarget) this.canvasSubmissions += 1;
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
private flushNativeRenderTargetClear(target: WebGPURenderTarget): void {
|
|
1378
|
-
if (!target.nativeNeedsClear || !target.nativeView || !this.device.createCommandEncoder) return;
|
|
1379
|
-
const encoder = this.device.createCommandEncoder({ label: `${target.label}-native-clear` });
|
|
1380
|
-
const pass = encoder.beginRenderPass({
|
|
1381
|
-
label: `${target.label}-native-clear-pass`,
|
|
1382
|
-
colorAttachments: [{
|
|
1383
|
-
view: target.nativeView,
|
|
1384
|
-
clearValue: target.nativeClearColor,
|
|
1385
|
-
loadOp: "clear",
|
|
1386
|
-
storeOp: "store"
|
|
1387
|
-
}],
|
|
1388
|
-
...(target.nativeDepthView
|
|
1389
|
-
? {
|
|
1390
|
-
depthStencilAttachment: {
|
|
1391
|
-
view: target.nativeDepthView,
|
|
1392
|
-
depthClearValue: 1,
|
|
1393
|
-
depthLoadOp: "clear" as const,
|
|
1394
|
-
depthStoreOp: "store" as const
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
: {})
|
|
1398
|
-
});
|
|
1399
|
-
pass.end();
|
|
1400
|
-
this.device.queue.submit([encoder.finish()]);
|
|
1401
|
-
target.nativeNeedsClear = false;
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
private createNativeSampledTextureBinding(binding: TextureBinding | null): { readonly view: unknown; readonly sampler: WebGPUSamplerLike; readonly actual: boolean } | null {
|
|
1405
|
-
if (!binding?.texture || binding.texture.disposed || !binding.validate().ok || !hasNativeSampledTextureBinding(this.device)) return null;
|
|
1406
|
-
const texture = binding.texture;
|
|
1407
|
-
let resource = this.nativeSampledTextures.get(texture);
|
|
1408
|
-
if (!resource) {
|
|
1409
|
-
if (texture.source) {
|
|
1410
|
-
const sourceLevels = nativeTextureSourceLevels(texture);
|
|
1411
|
-
if (sourceLevels.length === 0) return null;
|
|
1412
|
-
const nativeTexture = this.device.createTexture({
|
|
1413
|
-
label: texture.label,
|
|
1414
|
-
size: { width: texture.width, height: texture.height, depthOrArrayLayers: 1 },
|
|
1415
|
-
format: webgpuSampledTextureFormat(texture, "rgba8"),
|
|
1416
|
-
usage: TEXTURE_USAGE.TEXTURE_BINDING | TEXTURE_USAGE.COPY_DST,
|
|
1417
|
-
mipLevelCount: sourceLevels.length
|
|
1418
|
-
});
|
|
1419
|
-
const writeTexture = this.device.queue.writeTexture;
|
|
1420
|
-
if (!writeTexture) return null;
|
|
1421
|
-
sourceLevels.forEach((level, mipLevel) => {
|
|
1422
|
-
writeTexture.call(
|
|
1423
|
-
this.device.queue,
|
|
1424
|
-
{ texture: nativeTexture, mipLevel },
|
|
1425
|
-
level.data,
|
|
1426
|
-
{ bytesPerRow: level.bytesPerRow, rowsPerImage: level.height },
|
|
1427
|
-
{ width: level.width, height: level.height, depthOrArrayLayers: 1 }
|
|
1428
|
-
);
|
|
1429
|
-
});
|
|
1430
|
-
resource = { texture: nativeTexture, view: nativeTexture.createView() };
|
|
1431
|
-
} else {
|
|
1432
|
-
const sampledTexture = nativeSampledTextureLevels(texture);
|
|
1433
|
-
if (!sampledTexture) return null;
|
|
1434
|
-
const { levels, format } = sampledTexture;
|
|
1435
|
-
const baseLevel = levels[0];
|
|
1436
|
-
if (!baseLevel) return null;
|
|
1437
|
-
const levelBytesPerPixel = bytesPerPixel(format);
|
|
1438
|
-
if (levels.some((level) => level.data.byteLength < level.width * level.height * levelBytesPerPixel)) return null;
|
|
1439
|
-
const nativeTexture = this.device.createTexture({
|
|
1440
|
-
label: texture.label,
|
|
1441
|
-
size: { width: baseLevel.width, height: baseLevel.height, depthOrArrayLayers: 1 },
|
|
1442
|
-
format: webgpuSampledTextureFormat(texture, format),
|
|
1443
|
-
usage: TEXTURE_USAGE.TEXTURE_BINDING | TEXTURE_USAGE.COPY_DST,
|
|
1444
|
-
mipLevelCount: levels.length
|
|
1445
|
-
});
|
|
1446
|
-
for (const [mipLevel, level] of levels.entries()) {
|
|
1447
|
-
this.device.queue.writeTexture(
|
|
1448
|
-
{ texture: nativeTexture, mipLevel },
|
|
1449
|
-
level.data,
|
|
1450
|
-
{ bytesPerRow: level.width * levelBytesPerPixel, rowsPerImage: level.height },
|
|
1451
|
-
{ width: level.width, height: level.height, depthOrArrayLayers: 1 }
|
|
1452
|
-
);
|
|
1453
|
-
}
|
|
1454
|
-
resource = { texture: nativeTexture, view: nativeTexture.createView() };
|
|
1455
|
-
}
|
|
1456
|
-
this.nativeSampledTextures.set(texture, resource);
|
|
1457
|
-
}
|
|
1458
|
-
return {
|
|
1459
|
-
view: resource.view,
|
|
1460
|
-
sampler: this.device.createSampler(webgpuSamplerDescriptor(binding.sampler)),
|
|
1461
|
-
actual: true
|
|
1462
|
-
};
|
|
1463
|
-
}
|
|
1464
|
-
|
|
1465
|
-
private createNativePbrTextureBindings(command: DrawCommand): {
|
|
1466
|
-
readonly base: { readonly view: unknown; readonly sampler: WebGPUSamplerLike; readonly actual: boolean };
|
|
1467
|
-
readonly environment: { readonly view: unknown; readonly sampler: WebGPUSamplerLike; readonly actual: boolean };
|
|
1468
|
-
readonly brdf: { readonly view: unknown; readonly sampler: WebGPUSamplerLike; readonly actual: boolean };
|
|
1469
|
-
readonly shadow: { readonly view: unknown; readonly sampler: WebGPUSamplerLike; readonly actual: boolean };
|
|
1470
|
-
readonly normal: { readonly view: unknown; readonly sampler: WebGPUSamplerLike; readonly actual: boolean };
|
|
1471
|
-
readonly metallicRoughness: { readonly view: unknown; readonly sampler: WebGPUSamplerLike; readonly actual: boolean };
|
|
1472
|
-
readonly occlusion: { readonly view: unknown; readonly sampler: WebGPUSamplerLike; readonly actual: boolean };
|
|
1473
|
-
readonly actualBaseColor: boolean;
|
|
1474
|
-
readonly actualEnvironment: boolean;
|
|
1475
|
-
readonly actualBrdf: boolean;
|
|
1476
|
-
readonly actualShadow: boolean;
|
|
1477
|
-
readonly actualNormal: boolean;
|
|
1478
|
-
readonly actualMetallicRoughness: boolean;
|
|
1479
|
-
readonly actualOcclusion: boolean;
|
|
1480
|
-
} | null {
|
|
1481
|
-
if (!hasNativeSampledTextureBinding(this.device)) return null;
|
|
1482
|
-
const baseBinding = uniformBaseColorTextureBinding(command.uniforms);
|
|
1483
|
-
const environmentBinding = uniformTextureBinding(command.uniforms, "u_environmentMapTexture");
|
|
1484
|
-
const brdfBinding = uniformTextureBinding(command.uniforms, "u_environmentBrdfLutTexture");
|
|
1485
|
-
const shadowBinding = uniformTextureBinding(command.uniforms, "u_shadowMapTexture");
|
|
1486
|
-
const normalBinding = uniformTextureBinding(command.uniforms, "u_normalTexture");
|
|
1487
|
-
const metallicRoughnessBinding = uniformTextureBinding(command.uniforms, "u_metallicRoughnessTexture");
|
|
1488
|
-
const occlusionBinding = uniformTextureBinding(command.uniforms, "u_occlusionTexture");
|
|
1489
|
-
const base = this.createNativeSampledTextureBinding(baseBinding) ?? this.createNativeFallbackSampledTextureBinding("white-srgb", [255, 255, 255, 255], "srgb");
|
|
1490
|
-
const environment = this.createNativeSampledTextureBinding(environmentBinding) ?? this.createNativeFallbackSampledTextureBinding("environment-srgb", [92, 116, 156, 255], "srgb");
|
|
1491
|
-
const brdf = this.createNativeSampledTextureBinding(brdfBinding) ?? this.createNativeFallbackSampledTextureBinding("brdf-linear", [180, 180, 255, 255], "linear");
|
|
1492
|
-
const shadow = this.createNativeSampledTextureBinding(shadowBinding) ?? this.createNativeFallbackSampledTextureBinding("shadow-lit-linear", [255, 255, 255, 255], "linear");
|
|
1493
|
-
const normal = this.createNativeSampledTextureBinding(normalBinding) ?? this.createNativeFallbackSampledTextureBinding("flat-normal-linear", [128, 128, 255, 255], "linear");
|
|
1494
|
-
const metallicRoughness = this.createNativeSampledTextureBinding(metallicRoughnessBinding) ?? this.createNativeFallbackSampledTextureBinding("metallic-roughness-linear", [255, 255, 255, 255], "linear");
|
|
1495
|
-
const occlusion = this.createNativeSampledTextureBinding(occlusionBinding) ?? this.createNativeFallbackSampledTextureBinding("occlusion-linear", [255, 255, 255, 255], "linear");
|
|
1496
|
-
if (!base || !environment || !brdf || !shadow || !normal || !metallicRoughness || !occlusion) return null;
|
|
1497
|
-
return {
|
|
1498
|
-
base,
|
|
1499
|
-
environment,
|
|
1500
|
-
brdf,
|
|
1501
|
-
shadow,
|
|
1502
|
-
normal,
|
|
1503
|
-
metallicRoughness,
|
|
1504
|
-
occlusion,
|
|
1505
|
-
actualBaseColor: baseBinding !== null && base.actual,
|
|
1506
|
-
actualEnvironment: environmentBinding !== null && environment.actual,
|
|
1507
|
-
actualBrdf: brdfBinding !== null && brdf.actual,
|
|
1508
|
-
actualShadow: shadowBinding !== null && shadow.actual,
|
|
1509
|
-
actualNormal: normalBinding !== null && normal.actual,
|
|
1510
|
-
actualMetallicRoughness: metallicRoughnessBinding !== null && metallicRoughness.actual,
|
|
1511
|
-
actualOcclusion: occlusionBinding !== null && occlusion.actual
|
|
1512
|
-
};
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
private createNativeFallbackSampledTextureBinding(key: string, rgba: readonly [number, number, number, number], colorSpace: Texture["colorSpace"]): { readonly view: unknown; readonly sampler: WebGPUSamplerLike; readonly actual: boolean } | null {
|
|
1516
|
-
if (!hasNativeSampledTextureBinding(this.device)) return null;
|
|
1517
|
-
let resource = this.nativeSampledTextures.get(`fallback:${key}` as unknown as Texture);
|
|
1518
|
-
if (!resource) {
|
|
1519
|
-
const nativeTexture = this.device.createTexture({
|
|
1520
|
-
label: `aura3d-native-${key}`,
|
|
1521
|
-
size: { width: 1, height: 1, depthOrArrayLayers: 1 },
|
|
1522
|
-
format: colorSpace === "srgb" ? "rgba8unorm-srgb" : "rgba8unorm",
|
|
1523
|
-
usage: TEXTURE_USAGE.TEXTURE_BINDING | TEXTURE_USAGE.COPY_DST
|
|
1524
|
-
});
|
|
1525
|
-
this.device.queue.writeTexture(
|
|
1526
|
-
{ texture: nativeTexture },
|
|
1527
|
-
new Uint8Array(rgba),
|
|
1528
|
-
{ bytesPerRow: 4, rowsPerImage: 1 },
|
|
1529
|
-
{ width: 1, height: 1, depthOrArrayLayers: 1 }
|
|
1530
|
-
);
|
|
1531
|
-
resource = { texture: nativeTexture, view: nativeTexture.createView() };
|
|
1532
|
-
this.nativeSampledTextures.set(`fallback:${key}` as unknown as Texture, resource);
|
|
1533
|
-
}
|
|
1534
|
-
return {
|
|
1535
|
-
view: resource.view,
|
|
1536
|
-
sampler: this.device.createSampler({
|
|
1537
|
-
minFilter: "nearest",
|
|
1538
|
-
magFilter: "nearest",
|
|
1539
|
-
addressModeU: "clamp-to-edge",
|
|
1540
|
-
addressModeV: "clamp-to-edge"
|
|
1541
|
-
}),
|
|
1542
|
-
actual: false
|
|
1543
|
-
};
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
private createNativeDrawUniformBuffer(command: DrawCommand): WebGPUBufferLike | null {
|
|
1547
|
-
if (!this.device.createBindGroup) return null;
|
|
1548
|
-
// 188 floats for the scalar/matrix fields + 96 joint matrices (16 floats each) appended at 188.
|
|
1549
|
-
const data = new Float32Array(188 + MAX_WEBGPU_SKINNING_JOINTS * 16);
|
|
1550
|
-
const baseColorBinding = uniformBaseColorTextureBinding(command.uniforms);
|
|
1551
|
-
const environmentBinding = uniformTextureBinding(command.uniforms, "u_environmentMapTexture");
|
|
1552
|
-
const normalBinding = uniformTextureBinding(command.uniforms, "u_normalTexture");
|
|
1553
|
-
const metallicRoughnessBinding = uniformTextureBinding(command.uniforms, "u_metallicRoughnessTexture");
|
|
1554
|
-
const occlusionBinding = uniformTextureBinding(command.uniforms, "u_occlusionTexture");
|
|
1555
|
-
data.set(uniformMat4(command.uniforms?.get("u_modelViewProjection")) ?? identityMatrix(), 0);
|
|
1556
|
-
data.set(uniformColor(command.uniforms), 16);
|
|
1557
|
-
data[20] = uniformNumber(command.uniforms?.get("u_metallic"), 0);
|
|
1558
|
-
data[21] = uniformNumber(command.uniforms?.get("u_roughness"), 0.5);
|
|
1559
|
-
data[22] = uniformNumber(command.uniforms?.get("u_environmentIntensity"), 0);
|
|
1560
|
-
data[23] = uniformNumber(command.uniforms?.get("u_environmentMapTextureIntensity"), 0);
|
|
1561
|
-
data[24] = uniformNumber(command.uniforms?.get("u_baseColorTextureEnabled"), baseColorBinding ? 1 : 0);
|
|
1562
|
-
data[25] = uniformNumber(command.uniforms?.get("u_shadowMapEnabled"), 0);
|
|
1563
|
-
data[26] = uniformNumber(command.uniforms?.get("u_shadowMapStrength"), 0.65);
|
|
1564
|
-
data[27] = uniformNumber(command.uniforms?.get("u_environmentMapTextureEnabled"), environmentBinding ? 1 : 0);
|
|
1565
|
-
const morphWeights = command.uniforms?.get("u_morphWeights");
|
|
1566
|
-
data[28] = morphWeights instanceof Float32Array || Array.isArray(morphWeights) ? Number(morphWeights[0] ?? 0) : 0;
|
|
1567
|
-
data[29] = uniformNumber(command.uniforms?.get("u_morphTargetCount"), 0);
|
|
1568
|
-
data[30] = uniformNumber(command.uniforms?.get("u_environmentMapTextureMipCount"), 1);
|
|
1569
|
-
data[31] = uniformNumber(command.uniforms?.get("u_environmentMapTextureSpecularIntensity"), 0)
|
|
1570
|
-
* uniformNumber(command.uniforms?.get("u_materialEnvironmentSpecularScale"), 1);
|
|
1571
|
-
data[128] = uniformNumber(command.uniforms?.get("u_normalTextureEnabled"), normalBinding ? 1 : 0);
|
|
1572
|
-
data[129] = uniformNumber(command.uniforms?.get("u_normalScale"), 1);
|
|
1573
|
-
data[130] = uniformNumber(command.uniforms?.get("u_occlusionStrength"), 1);
|
|
1574
|
-
data[160] = uniformNumber(command.uniforms?.get("u_alphaCutoff"), 0);
|
|
1575
|
-
data[161] = uniformNumber(command.uniforms?.get("u_transmissionFactor"), 0);
|
|
1576
|
-
data[162] = uniformNumber(command.uniforms?.get("u_diffuseTransmissionFactor"), 0);
|
|
1577
|
-
data.set(uniformVec3(command.uniforms?.get("u_cameraPosition")) ?? [0, 0, 1], 164);
|
|
1578
|
-
data[167] = 1;
|
|
1579
|
-
data[168] = uniformNumber(command.uniforms?.get("u_metallicRoughnessTextureEnabled"), metallicRoughnessBinding ? 1 : 0);
|
|
1580
|
-
data[169] = uniformNumber(command.uniforms?.get("u_occlusionTextureEnabled"), occlusionBinding ? 1 : 0);
|
|
1581
|
-
data[170] = uniformNumber(command.uniforms?.get("u_productColorSmoothing"), 0);
|
|
1582
|
-
data.set(uniformMat4(command.uniforms?.get("u_normalMatrix")) ?? identityMatrix(), 172);
|
|
1583
|
-
const modelMatrix = uniformMat4(command.uniforms?.get("u_modelMatrix")) ?? identityMatrix();
|
|
1584
|
-
const instanceMatrixValue = command.uniforms?.get("u_instanceMatrices");
|
|
1585
|
-
const instanceMatrixNumbers = instanceMatrixValue instanceof Float32Array || Array.isArray(instanceMatrixValue) ? Array.from(instanceMatrixValue) : [];
|
|
1586
|
-
const hasInstanceMatrices = instanceMatrixNumbers.length >= 16 && instanceMatrixNumbers.slice(0, 16).every(Number.isFinite);
|
|
1587
|
-
const instanceMatrices = hasInstanceMatrices ? uniformMat4Array(instanceMatrixValue, 4) : [];
|
|
1588
|
-
for (let index = 0; index < 4; index += 1) {
|
|
1589
|
-
data.set(instanceMatrices[index] ?? (index === 0 ? modelMatrix : identityMatrix()), 32 + index * 16);
|
|
1590
|
-
}
|
|
1591
|
-
// Legacy 2-slot joint fields (offsets 96/112) kept as identity for layout stability.
|
|
1592
|
-
data.set(identityMatrix(), 96);
|
|
1593
|
-
data.set(identityMatrix(), 112);
|
|
1594
|
-
// Full 96-joint palette (WebGL2 parity), appended after normalMatrix at offset 188.
|
|
1595
|
-
const jointMatrices = uniformMat4Array(command.uniforms?.get("u_jointMatrices"), MAX_WEBGPU_SKINNING_JOINTS);
|
|
1596
|
-
for (let index = 0; index < MAX_WEBGPU_SKINNING_JOINTS; index += 1) {
|
|
1597
|
-
data.set(jointMatrices[index] ?? identityMatrix(), 188 + index * 16);
|
|
1598
|
-
}
|
|
1599
|
-
const morphDeltas = command.uniforms?.get("u_morphPositionDeltas");
|
|
1600
|
-
const morphNumbers = morphDeltas instanceof Float32Array || Array.isArray(morphDeltas) ? Array.from(morphDeltas).slice(0, 32) : [];
|
|
1601
|
-
if (morphNumbers.length > 0) data.set(morphNumbers, 128);
|
|
1602
|
-
const buffer = this.device.createBuffer({
|
|
1603
|
-
label: `${command.label ?? "draw"}-draw-uniforms`,
|
|
1604
|
-
size: data.byteLength,
|
|
1605
|
-
usage: BUFFER_USAGE.UNIFORM | BUFFER_USAGE.COPY_DST
|
|
1606
|
-
});
|
|
1607
|
-
this.device.queue.writeBuffer(buffer, 0, data);
|
|
1608
|
-
return buffer;
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
private currentCanvasView(): unknown | null {
|
|
1612
|
-
if (!this.canvasContext) return null;
|
|
1613
|
-
return this.canvasContext.getCurrentTexture().createView();
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
private ensureCanvasDepthAttachment(width: number, height: number): void {
|
|
1617
|
-
if (!this.canvasContext || !this.device.createTexture) return;
|
|
1618
|
-
if (
|
|
1619
|
-
this.canvasDepthTexture &&
|
|
1620
|
-
this.canvasDepthView &&
|
|
1621
|
-
this.canvasDepthWidth === width &&
|
|
1622
|
-
this.canvasDepthHeight === height
|
|
1623
|
-
) {
|
|
1624
|
-
return;
|
|
1625
|
-
}
|
|
1626
|
-
this.canvasDepthTexture?.destroy();
|
|
1627
|
-
this.canvasDepthTexture = this.device.createTexture({
|
|
1628
|
-
label: "aura3d-webgpu-canvas-depth",
|
|
1629
|
-
size: [width, height],
|
|
1630
|
-
format: DEPTH_TEXTURE_FORMAT,
|
|
1631
|
-
usage: TEXTURE_USAGE.RENDER_ATTACHMENT
|
|
1632
|
-
});
|
|
1633
|
-
this.canvasDepthView = this.canvasDepthTexture.createView();
|
|
1634
|
-
this.canvasDepthWidth = width;
|
|
1635
|
-
this.canvasDepthHeight = height;
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
private clearActiveRenderTarget(color: readonly [number, number, number, number]): void {
|
|
1639
|
-
if (!this.activeRenderTarget) return;
|
|
1640
|
-
const bytes = rgbaBytes(color);
|
|
1641
|
-
for (let index = 0; index < this.activeRenderTarget.colorPixels.length; index += 4) {
|
|
1642
|
-
this.activeRenderTarget.colorPixels.set(bytes, index);
|
|
1643
|
-
}
|
|
1644
|
-
this.activeRenderTarget.nativeNeedsClear = true;
|
|
1645
|
-
this.activeRenderTarget.nativeClearColor = color;
|
|
1646
|
-
this.activeRenderTarget.depthPixels?.fill(1);
|
|
1647
|
-
if (this.activeRenderTarget.colorFloatPixels) {
|
|
1648
|
-
for (let index = 0; index < this.activeRenderTarget.colorFloatPixels.length; index += 4) {
|
|
1649
|
-
this.activeRenderTarget.colorFloatPixels[index] = color[0];
|
|
1650
|
-
this.activeRenderTarget.colorFloatPixels[index + 1] = color[1];
|
|
1651
|
-
this.activeRenderTarget.colorFloatPixels[index + 2] = color[2];
|
|
1652
|
-
this.activeRenderTarget.colorFloatPixels[index + 3] = color[3];
|
|
1653
|
-
}
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
private indicesFor(command: DrawCommand): readonly number[] {
|
|
1658
|
-
if (!command.indexBuffer) {
|
|
1659
|
-
const firstVertex = command.firstVertex ?? 0;
|
|
1660
|
-
return Array.from({ length: command.vertexCount }, (_, index) => firstVertex + index);
|
|
1661
|
-
}
|
|
1662
|
-
const indexBuffer = this.requireBuffer(command.indexBuffer);
|
|
1663
|
-
const indexCount = command.indexCount ?? Math.floor(indexBuffer.byteLength / (command.indexType === "uint32" ? 4 : 2));
|
|
1664
|
-
const indices: number[] = [];
|
|
1665
|
-
const view = new DataView(indexBuffer.bytes.buffer, indexBuffer.bytes.byteOffset, indexBuffer.bytes.byteLength);
|
|
1666
|
-
const firstIndex = command.firstIndex ?? 0;
|
|
1667
|
-
for (let index = 0; index < indexCount; index += 1) {
|
|
1668
|
-
const byteOffset = (firstIndex + index) * (command.indexType === "uint32" ? 4 : 2);
|
|
1669
|
-
indices.push(command.indexType === "uint32" ? view.getUint32(byteOffset, true) : view.getUint16(byteOffset, true));
|
|
1670
|
-
}
|
|
1671
|
-
return indices;
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
async function requestAdapter(gpu: WebGPULike | undefined): Promise<WebGPUAdapterLike | null> {
|
|
1676
|
-
if (!gpu) {
|
|
1677
|
-
throw new RenderDeviceError("WebGPU runtime is missing from the current environment", "WEBGPU_RUNTIME_MISSING");
|
|
1678
|
-
}
|
|
1679
|
-
return gpu.requestAdapter();
|
|
1680
|
-
}
|
|
1681
|
-
|
|
1682
|
-
async function requestDevice(adapter: WebGPUAdapterLike, injectedDevice?: WebGPUDeviceLike): Promise<WebGPUDeviceLike> {
|
|
1683
|
-
if (injectedDevice) {
|
|
1684
|
-
return injectedDevice;
|
|
1685
|
-
}
|
|
1686
|
-
try {
|
|
1687
|
-
return await adapter.requestDevice();
|
|
1688
|
-
} catch (error) {
|
|
1689
|
-
throw new RenderDeviceError("WebGPU adapter failed to create a device", "WEBGPU_DEVICE_REQUEST_FAILED", {
|
|
1690
|
-
adapter: describeAdapter(adapter),
|
|
1691
|
-
reason: error instanceof Error ? error.message : String(error)
|
|
1692
|
-
});
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
function validateWebGPUDevice(device: WebGPUDeviceLike): WebGPUDeviceLike {
|
|
1697
|
-
const missing: string[] = [];
|
|
1698
|
-
if (!device || typeof device !== "object") missing.push("device");
|
|
1699
|
-
if (typeof device?.createBuffer !== "function") missing.push("createBuffer");
|
|
1700
|
-
if (typeof device?.queue?.writeBuffer !== "function") missing.push("queue.writeBuffer");
|
|
1701
|
-
if (typeof device?.queue?.submit !== "function") missing.push("queue.submit");
|
|
1702
|
-
|
|
1703
|
-
if (missing.length > 0) {
|
|
1704
|
-
throw new RenderDeviceError("WebGPU device is missing required render-device capabilities", "WEBGPU_DEVICE_INVALID", {
|
|
1705
|
-
missing
|
|
1706
|
-
});
|
|
1707
|
-
}
|
|
1708
|
-
return device;
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
function hasNativeRenderPipeline(device: WebGPUDeviceLike): device is WebGPUDeviceLike & Required<Pick<WebGPUDeviceLike, "createBindGroup" | "createCommandEncoder" | "createRenderPipeline" | "createTexture">> {
|
|
1712
|
-
return typeof device.createCommandEncoder === "function" &&
|
|
1713
|
-
typeof device.createRenderPipeline === "function" &&
|
|
1714
|
-
typeof device.createBindGroup === "function" &&
|
|
1715
|
-
typeof device.createTexture === "function";
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
function hasNativeSampledTextureBinding(device: WebGPUDeviceLike): device is WebGPUDeviceLike & Required<Pick<WebGPUDeviceLike, "createTexture" | "createSampler">> & { readonly queue: WebGPUQueueLike & Required<Pick<WebGPUQueueLike, "writeTexture">> } {
|
|
1719
|
-
return typeof device.createTexture === "function" &&
|
|
1720
|
-
typeof device.createSampler === "function" &&
|
|
1721
|
-
typeof device.queue.writeTexture === "function";
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
function hasNativeTextureUpload(device: WebGPUDeviceLike): device is WebGPUDeviceLike & { readonly queue: WebGPUQueueLike & Required<Pick<WebGPUQueueLike, "writeTexture">> } {
|
|
1725
|
-
return typeof device.queue.writeTexture === "function";
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
function hasNativeTextureReadback(device: WebGPUDeviceLike): device is WebGPUDeviceLike & Required<Pick<WebGPUDeviceLike, "createCommandEncoder" | "createTexture">> {
|
|
1729
|
-
if (typeof device.createCommandEncoder !== "function" || typeof device.createTexture !== "function") return false;
|
|
1730
|
-
const probe = device.createBuffer({
|
|
1731
|
-
label: "aura3d-native-readback-capability-probe",
|
|
1732
|
-
size: 4,
|
|
1733
|
-
usage: BUFFER_USAGE.MAP_READ | BUFFER_USAGE.COPY_DST
|
|
1734
|
-
});
|
|
1735
|
-
const supported = typeof probe.mapAsync === "function" &&
|
|
1736
|
-
typeof probe.getMappedRange === "function" &&
|
|
1737
|
-
typeof probe.unmap === "function" &&
|
|
1738
|
-
typeof device.createCommandEncoder({ label: "aura3d-native-readback-capability-probe" }).copyTextureToBuffer === "function";
|
|
1739
|
-
probe.destroy();
|
|
1740
|
-
return supported;
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
function alignTo(value: number, alignment: number): number {
|
|
1744
|
-
return Math.ceil(value / alignment) * alignment;
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
function readWebGPU(globalScope: Pick<typeof globalThis, "navigator">): WebGPULike | undefined {
|
|
1748
|
-
return (globalScope.navigator as Navigator & { gpu?: WebGPULike } | undefined)?.gpu;
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
function acquireWebGPUCanvasContext(canvas: HTMLCanvasElement | OffscreenCanvas): WebGPUCanvasContextLike {
|
|
1752
|
-
const getContext = (canvas as HTMLCanvasElement & {
|
|
1753
|
-
getContext(type: "webgpu"): WebGPUCanvasContextLike | null;
|
|
1754
|
-
}).getContext;
|
|
1755
|
-
if (typeof getContext !== "function") {
|
|
1756
|
-
throw new RenderDeviceError("WebGPU canvas presentation requires getContext(\"webgpu\")", "WEBGPU_CANVAS_CONTEXT_MISSING");
|
|
1757
|
-
}
|
|
1758
|
-
const context = getContext.call(canvas, "webgpu");
|
|
1759
|
-
if (!context || typeof context.configure !== "function" || typeof context.getCurrentTexture !== "function") {
|
|
1760
|
-
throw new RenderDeviceError("Canvas did not provide a usable WebGPU presentation context", "WEBGPU_CANVAS_CONTEXT_INVALID");
|
|
1761
|
-
}
|
|
1762
|
-
return context;
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
|
-
function describeAdapter(adapter: WebGPUAdapterLike): string | undefined {
|
|
1766
|
-
if (adapter.name) return adapter.name;
|
|
1767
|
-
const parts = [adapter.info?.vendor, adapter.info?.architecture, adapter.info?.device, adapter.info?.description].filter(Boolean);
|
|
1768
|
-
return parts.length > 0 ? parts.join(" ") : undefined;
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
function bufferUsageFlags(usage: BufferUsage): number {
|
|
1772
|
-
if (usage === "vertex") return BUFFER_USAGE.VERTEX | BUFFER_USAGE.COPY_DST | BUFFER_USAGE.COPY_SRC;
|
|
1773
|
-
if (usage === "index") return BUFFER_USAGE.INDEX | BUFFER_USAGE.COPY_DST | BUFFER_USAGE.COPY_SRC;
|
|
1774
|
-
if (usage === "uniform") return BUFFER_USAGE.UNIFORM | BUFFER_USAGE.COPY_DST | BUFFER_USAGE.COPY_SRC;
|
|
1775
|
-
return BUFFER_USAGE.MAP_READ | BUFFER_USAGE.COPY_DST;
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
function webgpuTextureFormat(format: "rgba8" | "rgba16f" | "rgba32f" | string): string {
|
|
1779
|
-
if (format === "rgba16f") return "rgba16float";
|
|
1780
|
-
if (format === "rgba32f") return "rgba32float";
|
|
1781
|
-
return "rgba8unorm";
|
|
1782
|
-
}
|
|
1783
|
-
|
|
1784
|
-
function nativeSampledTextureLevels(texture: Texture): { readonly levels: readonly { readonly width: number; readonly height: number; readonly data: ArrayBufferView }[]; readonly format: TextureFormat } | null {
|
|
1785
|
-
if (isCompressedTextureFormat(texture.format)) {
|
|
1786
|
-
const fallback = texture.fallbackTextureLevels;
|
|
1787
|
-
return fallback.length > 0 ? { levels: fallback, format: "rgba8" } : null;
|
|
1788
|
-
}
|
|
1789
|
-
if (texture.format === "depth24") return null;
|
|
1790
|
-
const levels = texture.textureLevels;
|
|
1791
|
-
return levels.length > 0 ? { levels, format: texture.format } : null;
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
function nativeTextureSourceLevels(texture: Texture): readonly {
|
|
1795
|
-
readonly width: number;
|
|
1796
|
-
readonly height: number;
|
|
1797
|
-
readonly bytesPerRow: number;
|
|
1798
|
-
readonly data: Uint8Array;
|
|
1799
|
-
}[] {
|
|
1800
|
-
const source = texture.source;
|
|
1801
|
-
if (!source || typeof globalThis === "undefined") return [];
|
|
1802
|
-
const width = Math.max(1, Math.round(texture.width));
|
|
1803
|
-
const height = Math.max(1, Math.round(texture.height));
|
|
1804
|
-
const mipCount = Math.max(1, Math.floor(Math.log2(Math.max(width, height))) + 1);
|
|
1805
|
-
const levels: {
|
|
1806
|
-
readonly width: number;
|
|
1807
|
-
readonly height: number;
|
|
1808
|
-
readonly bytesPerRow: number;
|
|
1809
|
-
readonly data: Uint8Array;
|
|
1810
|
-
}[] = [];
|
|
1811
|
-
for (let level = 0; level < mipCount; level += 1) {
|
|
1812
|
-
const levelWidth = Math.max(1, width >> level);
|
|
1813
|
-
const levelHeight = Math.max(1, height >> level);
|
|
1814
|
-
const canvas = createTextureReadbackCanvas(levelWidth, levelHeight);
|
|
1815
|
-
if (!canvas) return levels;
|
|
1816
|
-
const context = canvas.getContext("2d", { willReadFrequently: true }) as CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D | null;
|
|
1817
|
-
if (!context) return levels;
|
|
1818
|
-
context.clearRect(0, 0, levelWidth, levelHeight);
|
|
1819
|
-
context.drawImage(source as CanvasImageSource, 0, 0, levelWidth, levelHeight);
|
|
1820
|
-
const pixels = context.getImageData(0, 0, levelWidth, levelHeight).data;
|
|
1821
|
-
const sourceBytesPerRow = levelWidth * 4;
|
|
1822
|
-
const bytesPerRow = alignTo(sourceBytesPerRow, 256);
|
|
1823
|
-
if (bytesPerRow === sourceBytesPerRow) {
|
|
1824
|
-
levels.push({ width: levelWidth, height: levelHeight, bytesPerRow, data: new Uint8Array(pixels.buffer.slice(pixels.byteOffset, pixels.byteOffset + pixels.byteLength)) });
|
|
1825
|
-
continue;
|
|
1826
|
-
}
|
|
1827
|
-
const data = new Uint8Array(bytesPerRow * levelHeight);
|
|
1828
|
-
for (let row = 0; row < levelHeight; row += 1) {
|
|
1829
|
-
data.set(pixels.subarray(row * sourceBytesPerRow, (row + 1) * sourceBytesPerRow), row * bytesPerRow);
|
|
1830
|
-
}
|
|
1831
|
-
levels.push({ width: levelWidth, height: levelHeight, bytesPerRow, data });
|
|
1832
|
-
}
|
|
1833
|
-
return levels;
|
|
1834
|
-
}
|
|
1835
|
-
|
|
1836
|
-
function createTextureReadbackCanvas(width: number, height: number): OffscreenCanvas | HTMLCanvasElement | null {
|
|
1837
|
-
const offscreenCanvas = (globalThis as unknown as { OffscreenCanvas?: typeof OffscreenCanvas }).OffscreenCanvas;
|
|
1838
|
-
if (offscreenCanvas) return new offscreenCanvas(width, height);
|
|
1839
|
-
if (typeof document === "undefined") return null;
|
|
1840
|
-
const canvas = document.createElement("canvas");
|
|
1841
|
-
canvas.width = width;
|
|
1842
|
-
canvas.height = height;
|
|
1843
|
-
return canvas;
|
|
1844
|
-
}
|
|
1845
|
-
|
|
1846
|
-
function webgpuSampledTextureFormat(texture: Texture, format: TextureFormat): string {
|
|
1847
|
-
if (format === "rgba16f") return "rgba16float";
|
|
1848
|
-
if (format === "rgba32f") return "rgba32float";
|
|
1849
|
-
return texture.colorSpace === "srgb" ? "rgba8unorm-srgb" : "rgba8unorm";
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
function webgpuMinFilter(filter: TextureMinFilter): TextureMagFilter {
|
|
1853
|
-
if (filter === "nearest" || filter === "nearest-mipmap-nearest" || filter === "nearest-mipmap-linear") return "nearest";
|
|
1854
|
-
return "linear";
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
function webgpuMagFilter(filter: TextureMagFilter): TextureMagFilter {
|
|
1858
|
-
return filter;
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
function webgpuMipmapFilter(filter: TextureMinFilter): TextureMagFilter | undefined {
|
|
1862
|
-
if (filter === "nearest-mipmap-nearest" || filter === "linear-mipmap-nearest") return "nearest";
|
|
1863
|
-
if (filter === "nearest-mipmap-linear" || filter === "linear-mipmap-linear") return "linear";
|
|
1864
|
-
return undefined;
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
function webgpuSamplerDescriptor(sampler: Sampler): WebGPUSamplerDescriptorLike {
|
|
1868
|
-
const minFilter = webgpuMinFilter(sampler.minFilter);
|
|
1869
|
-
const magFilter = webgpuMagFilter(sampler.magFilter);
|
|
1870
|
-
const mipmapFilter = webgpuMipmapFilter(sampler.minFilter);
|
|
1871
|
-
const descriptor: WebGPUSamplerDescriptorLike = {
|
|
1872
|
-
minFilter,
|
|
1873
|
-
magFilter,
|
|
1874
|
-
...(mipmapFilter ? { mipmapFilter } : {}),
|
|
1875
|
-
addressModeU: sampler.addressU,
|
|
1876
|
-
addressModeV: sampler.addressV
|
|
1877
|
-
};
|
|
1878
|
-
const maxAnisotropy = webgpuMaxAnisotropy(sampler.maxAnisotropy, minFilter, magFilter, mipmapFilter);
|
|
1879
|
-
return maxAnisotropy === undefined ? descriptor : { ...descriptor, maxAnisotropy };
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
function webgpuMaxAnisotropy(
|
|
1883
|
-
maxAnisotropy: number,
|
|
1884
|
-
minFilter: TextureMagFilter,
|
|
1885
|
-
magFilter: TextureMagFilter,
|
|
1886
|
-
mipmapFilter: TextureMagFilter | undefined
|
|
1887
|
-
): number | undefined {
|
|
1888
|
-
if (maxAnisotropy <= 1) return undefined;
|
|
1889
|
-
if (minFilter !== "linear" || magFilter !== "linear" || mipmapFilter !== "linear") return undefined;
|
|
1890
|
-
return Math.min(16, Math.max(1, Math.floor(maxAnisotropy)));
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
function usesNativeDrawUniforms(layout: NativeUniformLayout): boolean {
|
|
1894
|
-
return layout !== "passthrough";
|
|
1895
|
-
}
|
|
1896
|
-
|
|
1897
|
-
function usesNativePbrTextureBindings(layout: NativeUniformLayout): boolean {
|
|
1898
|
-
return layout === "generated-pbr" ||
|
|
1899
|
-
layout === "generated-textured-pbr" ||
|
|
1900
|
-
layout === "generated-instanced-pbr";
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
function writeAlignedQueueBuffer(
|
|
1904
|
-
queue: WebGPUQueueLike,
|
|
1905
|
-
buffer: WebGPUBufferLike,
|
|
1906
|
-
byteOffset: number,
|
|
1907
|
-
data: ArrayBufferView,
|
|
1908
|
-
nativeByteLength: number
|
|
1909
|
-
): void {
|
|
1910
|
-
const bytes = viewBytes(data);
|
|
1911
|
-
if (byteOffset % 4 === 0 && bytes.byteLength % 4 === 0) {
|
|
1912
|
-
queue.writeBuffer(buffer, byteOffset, bytes);
|
|
1913
|
-
return;
|
|
1914
|
-
}
|
|
1915
|
-
const alignedOffset = Math.floor(byteOffset / 4) * 4;
|
|
1916
|
-
const alignedEnd = alignTo(byteOffset + bytes.byteLength, 4);
|
|
1917
|
-
if (alignedEnd > nativeByteLength) {
|
|
1918
|
-
throw new RenderDeviceError("Aligned WebGPU buffer write exceeds native buffer allocation", "BUFFER_RANGE_OUT_OF_BOUNDS", {
|
|
1919
|
-
byteOffset,
|
|
1920
|
-
dataByteLength: bytes.byteLength,
|
|
1921
|
-
alignedOffset,
|
|
1922
|
-
alignedEnd,
|
|
1923
|
-
nativeByteLength
|
|
1924
|
-
});
|
|
1925
|
-
}
|
|
1926
|
-
const padded = new Uint8Array(alignedEnd - alignedOffset);
|
|
1927
|
-
padded.set(bytes, byteOffset - alignedOffset);
|
|
1928
|
-
queue.writeBuffer(buffer, alignedOffset, padded);
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
function rgbaBytes(color: readonly [number, number, number, number]): Uint8Array {
|
|
1932
|
-
return new Uint8Array(color.map((channel) => Math.round(Math.max(0, Math.min(1, channel)) * 255)));
|
|
1933
|
-
}
|
|
1934
|
-
|
|
1935
|
-
function halfFloatToNumber(bits: number): number {
|
|
1936
|
-
const sign = (bits & 0x8000) ? -1 : 1;
|
|
1937
|
-
const exponent = (bits >> 10) & 0x1f;
|
|
1938
|
-
const fraction = bits & 0x03ff;
|
|
1939
|
-
if (exponent === 0) {
|
|
1940
|
-
return sign * 2 ** -14 * (fraction / 1024);
|
|
1941
|
-
}
|
|
1942
|
-
if (exponent === 0x1f) {
|
|
1943
|
-
return fraction === 0 ? sign * Infinity : Number.NaN;
|
|
1944
|
-
}
|
|
1945
|
-
return sign * 2 ** (exponent - 15) * (1 + fraction / 1024);
|
|
1946
|
-
}
|
|
1947
|
-
|
|
1948
|
-
function uniformColor(uniforms: DrawCommand["uniforms"]): readonly [number, number, number, number] {
|
|
1949
|
-
const value = uniforms?.get("u_baseColor") ?? uniforms?.get("u_color") ?? uniforms?.get("color");
|
|
1950
|
-
const components = value instanceof Float32Array || Array.isArray(value) ? Array.from(value).slice(0, 4) : [];
|
|
1951
|
-
if (components.length >= 3 && components.every((component) => Number.isFinite(component))) {
|
|
1952
|
-
return [
|
|
1953
|
-
components[0] ?? 1,
|
|
1954
|
-
components[1] ?? 1,
|
|
1955
|
-
components[2] ?? 1,
|
|
1956
|
-
components[3] ?? 1
|
|
1957
|
-
];
|
|
1958
|
-
}
|
|
1959
|
-
return [1, 1, 1, 1];
|
|
1960
|
-
}
|
|
1961
|
-
|
|
1962
|
-
function uniformBaseColorTextureBinding(uniforms: DrawCommand["uniforms"]): TextureBinding | null {
|
|
1963
|
-
if (!uniforms) return null;
|
|
1964
|
-
const direct = uniforms.get("u_texture") ?? uniforms.get("u_baseColorTexture") ?? uniforms.get("baseColorTexture");
|
|
1965
|
-
if (direct instanceof TextureBinding && direct.texture && !direct.texture.disposed && direct.validate().ok) {
|
|
1966
|
-
return direct;
|
|
1967
|
-
}
|
|
1968
|
-
return null;
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
function uniformTextureBinding(uniforms: DrawCommand["uniforms"], name: string): TextureBinding | null {
|
|
1972
|
-
const value = uniforms?.get(name);
|
|
1973
|
-
return value instanceof TextureBinding && value.texture && !value.texture.disposed && value.validate().ok ? value : null;
|
|
1974
|
-
}
|
|
1975
|
-
|
|
1976
|
-
function uniformNumber(value: UniformValue | undefined, fallback: number): number {
|
|
1977
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1978
|
-
}
|
|
1979
|
-
|
|
1980
|
-
function uniformVec3(value: UniformValue | undefined): readonly [number, number, number] | null {
|
|
1981
|
-
const components = value instanceof Float32Array || Array.isArray(value) ? Array.from(value).slice(0, 3) : [];
|
|
1982
|
-
return components.length === 3 && components.every(Number.isFinite)
|
|
1983
|
-
? [components[0]!, components[1]!, components[2]!]
|
|
1984
|
-
: null;
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
function uniformMat4Array(value: UniformValue | undefined, count: number): readonly (readonly number[])[] {
|
|
1988
|
-
const numbers = value instanceof Float32Array || Array.isArray(value) ? Array.from(value) : [];
|
|
1989
|
-
return Array.from({ length: count }, (_, index) => {
|
|
1990
|
-
const start = index * 16;
|
|
1991
|
-
const matrix = numbers.slice(start, start + 16);
|
|
1992
|
-
return matrix.length === 16 && matrix.every(Number.isFinite) ? matrix : identityMatrix();
|
|
1993
|
-
});
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
function uniformRasterMatrices(uniforms: DrawCommand["uniforms"], instanceCount: number): readonly (readonly number[])[] {
|
|
1997
|
-
const modelViewProjection = uniformMat4(uniforms?.get("u_modelViewProjection")) ?? identityMatrix();
|
|
1998
|
-
const value = uniforms?.get("u_instanceMatrices");
|
|
1999
|
-
const numbers = value instanceof Float32Array || Array.isArray(value) ? Array.from(value) : [];
|
|
2000
|
-
if (numbers.length < instanceCount * 16 || !numbers.slice(0, instanceCount * 16).every(Number.isFinite)) {
|
|
2001
|
-
return Array.from({ length: instanceCount }, () => modelViewProjection);
|
|
2002
|
-
}
|
|
2003
|
-
return Array.from({ length: instanceCount }, (_, index) => multiplyMat4(modelViewProjection, numbers.slice(index * 16, index * 16 + 16)));
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
function uniformRasterModelMatrices(uniforms: DrawCommand["uniforms"], instanceCount: number): readonly (readonly number[])[] {
|
|
2007
|
-
const modelMatrix = uniformMat4(uniforms?.get("u_modelMatrix")) ?? identityMatrix();
|
|
2008
|
-
const value = uniforms?.get("u_instanceMatrices");
|
|
2009
|
-
const numbers = value instanceof Float32Array || Array.isArray(value) ? Array.from(value) : [];
|
|
2010
|
-
if (numbers.length < instanceCount * 16 || !numbers.slice(0, instanceCount * 16).every(Number.isFinite)) {
|
|
2011
|
-
return Array.from({ length: instanceCount }, () => modelMatrix);
|
|
2012
|
-
}
|
|
2013
|
-
return Array.from({ length: instanceCount }, (_, index) => multiplyMat4(modelMatrix, numbers.slice(index * 16, index * 16 + 16)));
|
|
2014
|
-
}
|
|
2015
|
-
|
|
2016
|
-
function uniformForwardShadow(uniforms: DrawCommand["uniforms"]): ForwardShadowUniforms | null {
|
|
2017
|
-
if (!uniforms) return null;
|
|
2018
|
-
const enabled = uniforms.get("u_shadowMapEnabled");
|
|
2019
|
-
if (typeof enabled !== "number" || enabled < 0.5) return null;
|
|
2020
|
-
const texture = uniforms.get("u_shadowMapTexture");
|
|
2021
|
-
const matrix = uniformMat4(uniforms.get("u_shadowMapMatrix")) ?? identityMatrix();
|
|
2022
|
-
if (!(texture instanceof TextureBinding) || !texture.texture || texture.texture.disposed || !texture.validate().ok) {
|
|
2023
|
-
return null;
|
|
2024
|
-
}
|
|
2025
|
-
const strengthValue = uniforms.get("u_shadowMapStrength");
|
|
2026
|
-
const biasValue = uniforms.get("u_shadowMapBias");
|
|
2027
|
-
const strength = typeof strengthValue === "number" && Number.isFinite(strengthValue) ? Math.max(0, Math.min(1, strengthValue)) : 0.65;
|
|
2028
|
-
const bias = typeof biasValue === "number" && Number.isFinite(biasValue) ? Math.max(0, biasValue) : 0.001;
|
|
2029
|
-
return { texture, matrix, strength, bias };
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
function uniformMat4(value: UniformValue | undefined): readonly number[] | null {
|
|
2033
|
-
const numbers = value instanceof Float32Array || Array.isArray(value) ? Array.from(value) : [];
|
|
2034
|
-
return numbers.length === 16 && numbers.every(Number.isFinite) ? numbers : null;
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
function multiplyMat4(left: readonly number[], right: readonly number[]): readonly number[] {
|
|
2038
|
-
const out = new Array<number>(16);
|
|
2039
|
-
for (let row = 0; row < 4; row += 1) {
|
|
2040
|
-
for (let column = 0; column < 4; column += 1) {
|
|
2041
|
-
out[column * 4 + row] =
|
|
2042
|
-
(left[0 * 4 + row] ?? 0) * (right[column * 4 + 0] ?? 0) +
|
|
2043
|
-
(left[1 * 4 + row] ?? 0) * (right[column * 4 + 1] ?? 0) +
|
|
2044
|
-
(left[2 * 4 + row] ?? 0) * (right[column * 4 + 2] ?? 0) +
|
|
2045
|
-
(left[3 * 4 + row] ?? 0) * (right[column * 4 + 3] ?? 0);
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
return out;
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
function transformPosition(position: readonly [number, number, number], matrix: readonly number[]): readonly [number, number, number] {
|
|
2052
|
-
const x = (matrix[0] ?? 1) * position[0] + (matrix[4] ?? 0) * position[1] + (matrix[8] ?? 0) * position[2] + (matrix[12] ?? 0);
|
|
2053
|
-
const y = (matrix[1] ?? 0) * position[0] + (matrix[5] ?? 1) * position[1] + (matrix[9] ?? 0) * position[2] + (matrix[13] ?? 0);
|
|
2054
|
-
const z = (matrix[2] ?? 0) * position[0] + (matrix[6] ?? 0) * position[1] + (matrix[10] ?? 1) * position[2] + (matrix[14] ?? 0);
|
|
2055
|
-
const w = (matrix[3] ?? 0) * position[0] + (matrix[7] ?? 0) * position[1] + (matrix[11] ?? 0) * position[2] + (matrix[15] ?? 1);
|
|
2056
|
-
const inverseW = Math.abs(w) > 1e-8 ? 1 / w : 1;
|
|
2057
|
-
return [
|
|
2058
|
-
x * inverseW,
|
|
2059
|
-
y * inverseW,
|
|
2060
|
-
z * inverseW
|
|
2061
|
-
];
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
function toDepth(ndcZ: number): number {
|
|
2065
|
-
return Math.max(0, Math.min(1, ndcZ * 0.5 + 0.5));
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
function passesDepthTest(target: WebGPURenderTarget, pixelIndex: number, depth: number, enabled: boolean, write: boolean): boolean {
|
|
2069
|
-
if (!enabled || !target.depthPixels) return true;
|
|
2070
|
-
if (depth > (target.depthPixels[pixelIndex] ?? 1) + 1e-6) return false;
|
|
2071
|
-
if (write) target.depthPixels[pixelIndex] = depth;
|
|
2072
|
-
return true;
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
function readPosition(bytes: Uint8Array, stride: number, offset: number, vertexIndex: number): readonly [number, number, number] {
|
|
2076
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2077
|
-
const base = vertexIndex * stride + offset;
|
|
2078
|
-
return [
|
|
2079
|
-
view.getFloat32(base, true),
|
|
2080
|
-
view.getFloat32(base + 4, true),
|
|
2081
|
-
view.getFloat32(base + 8, true)
|
|
2082
|
-
];
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
function readMorphedPosition(bytes: Uint8Array, stride: number, offset: number, vertexIndex: number, uniforms: DrawCommand["uniforms"]): readonly [number, number, number] {
|
|
2086
|
-
const basePosition = readPosition(bytes, stride, offset, vertexIndex);
|
|
2087
|
-
const packed = uniforms?.get("u_morphPositionDeltas");
|
|
2088
|
-
const weights = uniforms?.get("u_morphWeights");
|
|
2089
|
-
const targetCountValue = uniforms?.get("u_morphTargetCount");
|
|
2090
|
-
if (!(packed instanceof Float32Array) || !(weights instanceof Float32Array) || typeof targetCountValue !== "number") {
|
|
2091
|
-
return basePosition;
|
|
2092
|
-
}
|
|
2093
|
-
const targetCount = Math.max(0, Math.min(weights.length, Math.floor(targetCountValue)));
|
|
2094
|
-
let x = basePosition[0];
|
|
2095
|
-
let y = basePosition[1];
|
|
2096
|
-
let z = basePosition[2];
|
|
2097
|
-
for (let target = 0; target < targetCount; target += 1) {
|
|
2098
|
-
const weight = weights[target] ?? 0;
|
|
2099
|
-
const deltaOffset = (target * 64 + vertexIndex) * 4;
|
|
2100
|
-
x += (packed[deltaOffset] ?? 0) * weight;
|
|
2101
|
-
y += (packed[deltaOffset + 1] ?? 0) * weight;
|
|
2102
|
-
z += (packed[deltaOffset + 2] ?? 0) * weight;
|
|
2103
|
-
}
|
|
2104
|
-
return [x, y, z];
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
function toPixel(position: readonly [number, number, number], width: number, height: number): readonly [number, number, number] {
|
|
2108
|
-
return [
|
|
2109
|
-
(position[0] * 0.5 + 0.5) * (width - 1),
|
|
2110
|
-
(1 - (position[1] * 0.5 + 0.5)) * (height - 1),
|
|
2111
|
-
toDepth(position[2])
|
|
2112
|
-
];
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2115
|
-
function identityMatrix(): readonly number[] {
|
|
2116
|
-
return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
function readVec4Attribute(bytes: Uint8Array, stride: number, offset: number, vertexIndex: number): readonly [number, number, number, number] {
|
|
2120
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2121
|
-
const base = vertexIndex * stride + offset;
|
|
2122
|
-
return [
|
|
2123
|
-
view.getFloat32(base, true),
|
|
2124
|
-
view.getFloat32(base + 4, true),
|
|
2125
|
-
view.getFloat32(base + 8, true),
|
|
2126
|
-
view.getFloat32(base + 12, true)
|
|
2127
|
-
];
|
|
2128
|
-
}
|
|
2129
|
-
|
|
2130
|
-
// CPU skinning for the WebGPU emulation rasterizer: blend the joint palette (up to the full
|
|
2131
|
-
// WebGL2-parity 96-joint count) by the vertex's 4 joint indices + weights, then transform the local
|
|
2132
|
-
// position by the blended matrix. Mirrors the WebGL2 skinning path so a skinned character deforms
|
|
2133
|
-
// identically on both backends. `palette` may hold up to 96 matrices (parity with WebGL2).
|
|
2134
|
-
function skinLocalPosition(
|
|
2135
|
-
local: readonly [number, number, number],
|
|
2136
|
-
bytes: Uint8Array,
|
|
2137
|
-
format: VertexFormat,
|
|
2138
|
-
vertexIndex: number,
|
|
2139
|
-
palette: readonly (readonly number[])[]
|
|
2140
|
-
): readonly [number, number, number] {
|
|
2141
|
-
if (palette.length === 0 || !format.hasAttribute("joints") || !format.hasAttribute("weights")) {
|
|
2142
|
-
return local;
|
|
2143
|
-
}
|
|
2144
|
-
const joints = readVec4Attribute(bytes, format.stride, format.getAttribute("joints").offset, vertexIndex);
|
|
2145
|
-
const weights = readVec4Attribute(bytes, format.stride, format.getAttribute("weights").offset, vertexIndex);
|
|
2146
|
-
const weightSum = weights[0] + weights[1] + weights[2] + weights[3];
|
|
2147
|
-
if (weightSum <= 1e-4) return local;
|
|
2148
|
-
const blended = new Array<number>(16).fill(0);
|
|
2149
|
-
const maxJoint = palette.length - 1;
|
|
2150
|
-
for (let influence = 0; influence < 4; influence += 1) {
|
|
2151
|
-
const weight = weights[influence] ?? 0;
|
|
2152
|
-
if (weight === 0) continue;
|
|
2153
|
-
const jointIndex = Math.max(0, Math.min(maxJoint, Math.round(joints[influence] ?? 0)));
|
|
2154
|
-
const jointMatrix = palette[jointIndex] ?? identityMatrix();
|
|
2155
|
-
for (let k = 0; k < 16; k += 1) blended[k] += (jointMatrix[k] ?? 0) * weight;
|
|
2156
|
-
}
|
|
2157
|
-
return transformPosition(local, blended);
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
function jointPaletteFor(uniforms: DrawCommand["uniforms"]): readonly (readonly number[])[] {
|
|
2161
|
-
const value = uniforms?.get("u_jointMatrices");
|
|
2162
|
-
const length = value instanceof Float32Array || Array.isArray(value) ? (value as ArrayLike<number>).length : 0;
|
|
2163
|
-
const count = Math.min(MAX_WEBGPU_SKINNING_JOINTS, Math.floor(length / 16));
|
|
2164
|
-
return count > 0 ? uniformMat4Array(value, count) : [];
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
function readVertexColor(bytes: Uint8Array, stride: number, offset: number | undefined, vertexIndex: number): readonly [number, number, number, number] {
|
|
2168
|
-
if (offset === undefined) return [1, 1, 1, 1];
|
|
2169
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2170
|
-
const base = vertexIndex * stride + offset;
|
|
2171
|
-
return [
|
|
2172
|
-
view.getFloat32(base, true),
|
|
2173
|
-
view.getFloat32(base + 4, true),
|
|
2174
|
-
view.getFloat32(base + 8, true),
|
|
2175
|
-
view.getFloat32(base + 12, true)
|
|
2176
|
-
];
|
|
2177
|
-
}
|
|
2178
|
-
|
|
2179
|
-
function readVertexUv(bytes: Uint8Array, stride: number, offset: number | undefined, vertexIndex: number): readonly [number, number] {
|
|
2180
|
-
if (offset === undefined) return [0, 0];
|
|
2181
|
-
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
2182
|
-
const base = vertexIndex * stride + offset;
|
|
2183
|
-
return [view.getFloat32(base, true), view.getFloat32(base + 4, true)];
|
|
2184
|
-
}
|
|
2185
|
-
|
|
2186
|
-
function rasterizeTriangle(
|
|
2187
|
-
target: WebGPURenderTarget,
|
|
2188
|
-
a: readonly [number, number, number],
|
|
2189
|
-
b: readonly [number, number, number],
|
|
2190
|
-
c: readonly [number, number, number],
|
|
2191
|
-
worldA: readonly [number, number, number],
|
|
2192
|
-
worldB: readonly [number, number, number],
|
|
2193
|
-
worldC: readonly [number, number, number],
|
|
2194
|
-
color: readonly [number, number, number, number],
|
|
2195
|
-
colorA: readonly [number, number, number, number],
|
|
2196
|
-
colorB: readonly [number, number, number, number],
|
|
2197
|
-
colorC: readonly [number, number, number, number],
|
|
2198
|
-
uvA: readonly [number, number],
|
|
2199
|
-
uvB: readonly [number, number],
|
|
2200
|
-
uvC: readonly [number, number],
|
|
2201
|
-
texture: TextureBinding | null,
|
|
2202
|
-
shadow: ForwardShadowUniforms | null,
|
|
2203
|
-
depthTest: boolean,
|
|
2204
|
-
depthWrite: boolean
|
|
2205
|
-
): void {
|
|
2206
|
-
const pa = toPixel(a, target.width, target.height);
|
|
2207
|
-
const pb = toPixel(b, target.width, target.height);
|
|
2208
|
-
const pc = toPixel(c, target.width, target.height);
|
|
2209
|
-
const minX = clampInt(Math.floor(Math.min(pa[0], pb[0], pc[0])), 0, target.width - 1);
|
|
2210
|
-
const maxX = clampInt(Math.ceil(Math.max(pa[0], pb[0], pc[0])), 0, target.width - 1);
|
|
2211
|
-
const minY = clampInt(Math.floor(Math.min(pa[1], pb[1], pc[1])), 0, target.height - 1);
|
|
2212
|
-
const maxY = clampInt(Math.ceil(Math.max(pa[1], pb[1], pc[1])), 0, target.height - 1);
|
|
2213
|
-
const area = edge(pa, pb, pc);
|
|
2214
|
-
if (Math.abs(area) < 1e-6) return;
|
|
2215
|
-
for (let y = minY; y <= maxY; y += 1) {
|
|
2216
|
-
for (let x = minX; x <= maxX; x += 1) {
|
|
2217
|
-
const p = [x + 0.5, y + 0.5] as const;
|
|
2218
|
-
const w0 = edge(pb, pc, p);
|
|
2219
|
-
const w1 = edge(pc, pa, p);
|
|
2220
|
-
const w2 = edge(pa, pb, p);
|
|
2221
|
-
if ((w0 >= 0 && w1 >= 0 && w2 >= 0) || (w0 <= 0 && w1 <= 0 && w2 <= 0)) {
|
|
2222
|
-
const invArea = 1 / area;
|
|
2223
|
-
const wa = w0 * invArea;
|
|
2224
|
-
const wb = w1 * invArea;
|
|
2225
|
-
const wc = w2 * invArea;
|
|
2226
|
-
const uv: readonly [number, number] = [
|
|
2227
|
-
uvA[0] * wa + uvB[0] * wb + uvC[0] * wc,
|
|
2228
|
-
uvA[1] * wa + uvB[1] * wb + uvC[1] * wc
|
|
2229
|
-
];
|
|
2230
|
-
const worldPosition: readonly [number, number, number] = [
|
|
2231
|
-
worldA[0] * wa + worldB[0] * wb + worldC[0] * wc,
|
|
2232
|
-
worldA[1] * wa + worldB[1] * wb + worldC[1] * wc,
|
|
2233
|
-
worldA[2] * wa + worldB[2] * wb + worldC[2] * wc
|
|
2234
|
-
];
|
|
2235
|
-
const pixelIndex = y * target.width + x;
|
|
2236
|
-
const depth = pa[2] * wa + pb[2] * wb + pc[2] * wc;
|
|
2237
|
-
if (!passesDepthTest(target, pixelIndex, depth, depthTest, depthWrite)) continue;
|
|
2238
|
-
const shadedColor = multiplyColor(
|
|
2239
|
-
multiplyColor(color, interpolateColor(colorA, colorB, colorC, wa, wb, wc)),
|
|
2240
|
-
texture ? sampleTextureBinding(texture, uv) : [1, 1, 1, 1]
|
|
2241
|
-
);
|
|
2242
|
-
target.colorPixels.set(rgbaBytes(multiplyColor(shadedColor, shadowFactor(shadow, worldPosition))), pixelIndex * 4);
|
|
2243
|
-
}
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
|
-
}
|
|
2247
|
-
|
|
2248
|
-
function rasterizeLine(
|
|
2249
|
-
target: WebGPURenderTarget,
|
|
2250
|
-
a: readonly [number, number, number],
|
|
2251
|
-
b: readonly [number, number, number],
|
|
2252
|
-
color: readonly [number, number, number, number],
|
|
2253
|
-
colorA: readonly [number, number, number, number],
|
|
2254
|
-
colorB: readonly [number, number, number, number],
|
|
2255
|
-
depthTest: boolean,
|
|
2256
|
-
depthWrite: boolean
|
|
2257
|
-
): void {
|
|
2258
|
-
const pa = toPixel(a, target.width, target.height);
|
|
2259
|
-
const pb = toPixel(b, target.width, target.height);
|
|
2260
|
-
const x0 = clampInt(Math.round(pa[0]), 0, target.width - 1);
|
|
2261
|
-
const y0 = clampInt(Math.round(pa[1]), 0, target.height - 1);
|
|
2262
|
-
const x1 = clampInt(Math.round(pb[0]), 0, target.width - 1);
|
|
2263
|
-
const y1 = clampInt(Math.round(pb[1]), 0, target.height - 1);
|
|
2264
|
-
const dx = Math.abs(x1 - x0);
|
|
2265
|
-
const dy = Math.abs(y1 - y0);
|
|
2266
|
-
const steps = Math.max(dx, dy, 1);
|
|
2267
|
-
const shadedA = multiplyColor(color, colorA);
|
|
2268
|
-
const shadedB = multiplyColor(color, colorB);
|
|
2269
|
-
for (let step = 0; step <= steps; step += 1) {
|
|
2270
|
-
const t = step / steps;
|
|
2271
|
-
const x = clampInt(Math.round(x0 + (x1 - x0) * t), 0, target.width - 1);
|
|
2272
|
-
const y = clampInt(Math.round(y0 + (y1 - y0) * t), 0, target.height - 1);
|
|
2273
|
-
const pixelIndex = y * target.width + x;
|
|
2274
|
-
const depth = pa[2] + (pb[2] - pa[2]) * t;
|
|
2275
|
-
if (!passesDepthTest(target, pixelIndex, depth, depthTest, depthWrite)) continue;
|
|
2276
|
-
const shaded = lerpColor(shadedA, shadedB, t);
|
|
2277
|
-
target.colorPixels.set(rgbaBytes(shaded), pixelIndex * 4);
|
|
2278
|
-
}
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
function rasterizePoint(
|
|
2282
|
-
target: WebGPURenderTarget,
|
|
2283
|
-
point: readonly [number, number, number],
|
|
2284
|
-
color: readonly [number, number, number, number],
|
|
2285
|
-
depthTest: boolean,
|
|
2286
|
-
depthWrite: boolean
|
|
2287
|
-
): void {
|
|
2288
|
-
const pixel = toPixel(point, target.width, target.height);
|
|
2289
|
-
const centerX = clampInt(Math.round(pixel[0]), 0, target.width - 1);
|
|
2290
|
-
const centerY = clampInt(Math.round(pixel[1]), 0, target.height - 1);
|
|
2291
|
-
const bytes = rgbaBytes(color);
|
|
2292
|
-
for (let y = centerY - 2; y <= centerY + 2; y += 1) {
|
|
2293
|
-
if (y < 0 || y >= target.height) continue;
|
|
2294
|
-
for (let x = centerX - 2; x <= centerX + 2; x += 1) {
|
|
2295
|
-
if (x < 0 || x >= target.width) continue;
|
|
2296
|
-
const pixelIndex = y * target.width + x;
|
|
2297
|
-
if (!passesDepthTest(target, pixelIndex, pixel[2], depthTest, depthWrite)) continue;
|
|
2298
|
-
target.colorPixels.set(bytes, pixelIndex * 4);
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
function sampleTextureBinding(binding: TextureBinding, uv: readonly [number, number]): readonly [number, number, number, number] {
|
|
2304
|
-
const texture = binding.texture;
|
|
2305
|
-
if (!texture || texture.disposed) return [1, 1, 1, 1];
|
|
2306
|
-
const level = texture.textureLevels[0] ?? texture.fallbackTextureLevels[0];
|
|
2307
|
-
if (!level || level.data.length < level.width * level.height * 4) return [1, 1, 1, 1];
|
|
2308
|
-
const transformed = binding.transformUV(uv);
|
|
2309
|
-
const u = addressCoordinate(transformed[0], binding.sampler.addressU);
|
|
2310
|
-
const v = addressCoordinate(transformed[1], binding.sampler.addressV);
|
|
2311
|
-
const x = clampInt(Math.round(u * (level.width - 1)), 0, level.width - 1);
|
|
2312
|
-
const y = clampInt(Math.round(v * (level.height - 1)), 0, level.height - 1);
|
|
2313
|
-
const offset = (y * level.width + x) * 4;
|
|
2314
|
-
const r = (level.data[offset] ?? 255) / 255;
|
|
2315
|
-
const g = (level.data[offset + 1] ?? 255) / 255;
|
|
2316
|
-
const b = (level.data[offset + 2] ?? 255) / 255;
|
|
2317
|
-
return [
|
|
2318
|
-
texture.colorSpace === "srgb" ? srgbToLinear(r) : r,
|
|
2319
|
-
texture.colorSpace === "srgb" ? srgbToLinear(g) : g,
|
|
2320
|
-
texture.colorSpace === "srgb" ? srgbToLinear(b) : b,
|
|
2321
|
-
(level.data[offset + 3] ?? 255) / 255
|
|
2322
|
-
];
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
function srgbToLinear(value: number): number {
|
|
2326
|
-
return value <= 0.04045 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
function shadowFactor(shadow: ForwardShadowUniforms | null, worldPosition: readonly [number, number, number]): readonly [number, number, number, number] {
|
|
2330
|
-
if (!shadow) return [1, 1, 1, 1];
|
|
2331
|
-
const projected = transformPosition(worldPosition, shadow.matrix);
|
|
2332
|
-
if (projected[0] < -1 || projected[0] > 1 || projected[1] < -1 || projected[1] > 1 || projected[2] < -1 || projected[2] > 1) {
|
|
2333
|
-
return [1, 1, 1, 1];
|
|
2334
|
-
}
|
|
2335
|
-
const uv: readonly [number, number] = [projected[0] * 0.5 + 0.5, projected[1] * 0.5 + 0.5];
|
|
2336
|
-
const storedDepth = sampleTextureBinding(shadow.texture, uv)[0];
|
|
2337
|
-
const receiverDepth = projected[2] * 0.5 + 0.5 - shadow.bias;
|
|
2338
|
-
const visibility = receiverDepth > storedDepth ? 1 - shadow.strength : 1;
|
|
2339
|
-
return [visibility, visibility, visibility, 1];
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
function addressCoordinate(value: number, mode: "clamp-to-edge" | "repeat" | "mirror-repeat"): number {
|
|
2343
|
-
if (mode === "repeat") return value - Math.floor(value);
|
|
2344
|
-
if (mode === "mirror-repeat") {
|
|
2345
|
-
const repeated = value - Math.floor(value);
|
|
2346
|
-
return Math.floor(value) % 2 === 0 ? repeated : 1 - repeated;
|
|
2347
|
-
}
|
|
2348
|
-
return Math.min(1, Math.max(0, value));
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
|
-
function lerpColor(
|
|
2352
|
-
a: readonly [number, number, number, number],
|
|
2353
|
-
b: readonly [number, number, number, number],
|
|
2354
|
-
t: number
|
|
2355
|
-
): readonly [number, number, number, number] {
|
|
2356
|
-
return [
|
|
2357
|
-
a[0] + (b[0] - a[0]) * t,
|
|
2358
|
-
a[1] + (b[1] - a[1]) * t,
|
|
2359
|
-
a[2] + (b[2] - a[2]) * t,
|
|
2360
|
-
a[3] + (b[3] - a[3]) * t
|
|
2361
|
-
];
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
function interpolateColor(
|
|
2365
|
-
a: readonly [number, number, number, number],
|
|
2366
|
-
b: readonly [number, number, number, number],
|
|
2367
|
-
c: readonly [number, number, number, number],
|
|
2368
|
-
wa: number,
|
|
2369
|
-
wb: number,
|
|
2370
|
-
wc: number
|
|
2371
|
-
): readonly [number, number, number, number] {
|
|
2372
|
-
return [
|
|
2373
|
-
a[0] * wa + b[0] * wb + c[0] * wc,
|
|
2374
|
-
a[1] * wa + b[1] * wb + c[1] * wc,
|
|
2375
|
-
a[2] * wa + b[2] * wb + c[2] * wc,
|
|
2376
|
-
a[3] * wa + b[3] * wb + c[3] * wc
|
|
2377
|
-
];
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
|
-
function multiplyColor(
|
|
2381
|
-
a: readonly [number, number, number, number],
|
|
2382
|
-
b: readonly [number, number, number, number]
|
|
2383
|
-
): readonly [number, number, number, number] {
|
|
2384
|
-
return [a[0] * b[0], a[1] * b[1], a[2] * b[2], a[3] * b[3]];
|
|
2385
|
-
}
|
|
2386
|
-
|
|
2387
|
-
function edge(a: readonly number[], b: readonly number[], c: readonly number[]): number {
|
|
2388
|
-
return (c[0] - a[0]) * (b[1] - a[1]) - (c[1] - a[1]) * (b[0] - a[0]);
|
|
2389
|
-
}
|
|
2390
|
-
|
|
2391
|
-
function clampInt(value: number, min: number, max: number): number {
|
|
2392
|
-
return Math.min(max, Math.max(min, value));
|
|
2393
|
-
}
|
|
2394
|
-
|
|
2395
|
-
function createNativeShaderSources(sources: ShaderSources): {
|
|
2396
|
-
readonly vertex: string;
|
|
2397
|
-
readonly fragment: string;
|
|
2398
|
-
readonly entryPoints: readonly [string, string];
|
|
2399
|
-
readonly uniformLayout: NativeUniformLayout;
|
|
2400
|
-
} {
|
|
2401
|
-
const vertexEntry = inferWGSLVertexEntryPoint(sources.vertex) ?? "vs_main";
|
|
2402
|
-
const fragmentEntry = inferWGSLFragmentEntryPoint(sources.fragment) ?? "fs_main";
|
|
2403
|
-
if (/\@(vertex|fragment|compute)\b/.test(sources.vertex) || /\@(vertex|fragment|compute)\b/.test(sources.fragment)) {
|
|
2404
|
-
return { vertex: sources.vertex, fragment: sources.fragment, entryPoints: [vertexEntry, fragmentEntry], uniformLayout: "passthrough" };
|
|
2405
|
-
}
|
|
2406
|
-
if (sources.marker.includes("instanced-pbr")) {
|
|
2407
|
-
return { ...nativeInstancedPbrShader(vertexEntry, fragmentEntry, sources.marker), entryPoints: [vertexEntry, fragmentEntry], uniformLayout: "generated-instanced-pbr" };
|
|
2408
|
-
}
|
|
2409
|
-
if (sources.marker.includes("pbr-textured")) {
|
|
2410
|
-
return { ...nativeTexturedPbrShader(vertexEntry, fragmentEntry, sources.marker, nativeShaderUsesTangent(sources.vertex)), entryPoints: [vertexEntry, fragmentEntry], uniformLayout: "generated-textured-pbr" };
|
|
2411
|
-
}
|
|
2412
|
-
if (sources.marker.includes("pbr-direct")) {
|
|
2413
|
-
return sources.fragment.includes("u_baseColorTexture")
|
|
2414
|
-
? { ...nativeTexturedPbrShader(vertexEntry, fragmentEntry, sources.marker, nativeShaderUsesTangent(sources.vertex)), entryPoints: [vertexEntry, fragmentEntry], uniformLayout: "generated-textured-pbr" }
|
|
2415
|
-
: { ...nativePbrShader(vertexEntry, fragmentEntry, sources.marker), entryPoints: [vertexEntry, fragmentEntry], uniformLayout: "generated-pbr" };
|
|
2416
|
-
}
|
|
2417
|
-
if (sources.marker.includes("skinned-unlit")) {
|
|
2418
|
-
return { ...nativeSkinnedUnlitShader(vertexEntry, fragmentEntry, sources.marker), entryPoints: [vertexEntry, fragmentEntry], uniformLayout: "generated-skinned-unlit" };
|
|
2419
|
-
}
|
|
2420
|
-
if (sources.marker.includes("morph-unlit")) {
|
|
2421
|
-
return { ...nativeMorphUnlitShader(vertexEntry, fragmentEntry, sources.marker), entryPoints: [vertexEntry, fragmentEntry], uniformLayout: "generated-morph-unlit" };
|
|
2422
|
-
}
|
|
2423
|
-
if (/sampler2D/.test(sources.fragment) && /layout\s*\(\s*location\s*=\s*2\s*\)\s*in\s+vec2/.test(sources.vertex)) {
|
|
2424
|
-
return {
|
|
2425
|
-
vertex: `// ${sources.marker}\nstruct DrawUniforms {\n modelViewProjection: mat4x4<f32>,\n color: vec4<f32>,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@group(0) @binding(0) var<uniform> u_draw: DrawUniforms;\n\n@vertex\nfn ${vertexEntry}(@location(0) position: vec3<f32>, @location(2) uv: vec2<f32>) -> VertexOutput {\n var output: VertexOutput;\n let clipPosition = u_draw.modelViewProjection * vec4<f32>(position, 1.0);
|
|
2426
|
-
output.position = vec4<f32>(clipPosition.x, clipPosition.y, clipPosition.z * 0.5 + clipPosition.w * 0.5, clipPosition.w);\n output.uv = uv;\n return output;\n}\n`,
|
|
2427
|
-
fragment: `// ${sources.marker}\nstruct DrawUniforms {\n modelViewProjection: mat4x4<f32>,\n color: vec4<f32>,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n @location(0) uv: vec2<f32>,\n};\n\n@group(0) @binding(0) var<uniform> u_draw: DrawUniforms;\n@group(0) @binding(1) var u_textureSampler: sampler;\n@group(0) @binding(2) var u_texture: texture_2d<f32>;\n\n@fragment\nfn ${fragmentEntry}(input: VertexOutput) -> @location(0) vec4<f32> {\n return u_draw.color * textureSample(u_texture, u_textureSampler, input.uv);\n}\n`,
|
|
2428
|
-
entryPoints: [vertexEntry, fragmentEntry],
|
|
2429
|
-
uniformLayout: "generated-texture"
|
|
2430
|
-
};
|
|
2431
|
-
}
|
|
2432
|
-
return {
|
|
2433
|
-
vertex: `// ${sources.marker}\nstruct DrawUniforms {\n modelViewProjection: mat4x4<f32>,\n color: vec4<f32>,\n};\n\nstruct VertexOutput {\n @builtin(position) position: vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> u_draw: DrawUniforms;\n\n@vertex\nfn ${vertexEntry}(@location(0) position: vec3<f32>) -> VertexOutput {\n var output: VertexOutput;\n let clipPosition = u_draw.modelViewProjection * vec4<f32>(position, 1.0);
|
|
2434
|
-
output.position = vec4<f32>(clipPosition.x, clipPosition.y, clipPosition.z * 0.5 + clipPosition.w * 0.5, clipPosition.w);\n return output;\n}\n`,
|
|
2435
|
-
fragment: `// ${sources.marker}\nstruct DrawUniforms {\n modelViewProjection: mat4x4<f32>,\n color: vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> u_draw: DrawUniforms;\n\n@fragment\nfn ${fragmentEntry}() -> @location(0) vec4<f32> {\n return u_draw.color;\n}\n`,
|
|
2436
|
-
entryPoints: [vertexEntry, fragmentEntry],
|
|
2437
|
-
uniformLayout: "generated-basic"
|
|
2438
|
-
};
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
function nativeUniformStruct(): string {
|
|
2442
|
-
return `struct DrawUniforms {
|
|
2443
|
-
modelViewProjection: mat4x4<f32>,
|
|
2444
|
-
color: vec4<f32>,
|
|
2445
|
-
params: vec4<f32>,
|
|
2446
|
-
flags: vec4<f32>,
|
|
2447
|
-
reserved0: vec4<f32>,
|
|
2448
|
-
instance0: mat4x4<f32>,
|
|
2449
|
-
instance1: mat4x4<f32>,
|
|
2450
|
-
instance2: mat4x4<f32>,
|
|
2451
|
-
instance3: mat4x4<f32>,
|
|
2452
|
-
joint0: mat4x4<f32>,
|
|
2453
|
-
joint1: mat4x4<f32>,
|
|
2454
|
-
morph0: vec4<f32>,
|
|
2455
|
-
// (joint palette appended at struct end below as \`joints\` for 96-joint WebGL2 parity)
|
|
2456
|
-
morph1: vec4<f32>,
|
|
2457
|
-
morph2: vec4<f32>,
|
|
2458
|
-
morph3: vec4<f32>,
|
|
2459
|
-
morph4: vec4<f32>,
|
|
2460
|
-
morph5: vec4<f32>,
|
|
2461
|
-
morph6: vec4<f32>,
|
|
2462
|
-
morph7: vec4<f32>,
|
|
2463
|
-
material: vec4<f32>,
|
|
2464
|
-
camera: vec4<f32>,
|
|
2465
|
-
materialFlags: vec4<f32>,
|
|
2466
|
-
normalMatrix: mat4x4<f32>,
|
|
2467
|
-
// 96-joint skinning palette (WebGL2 \`u_jointMatrices[96]\` parity). Appended at the struct end so
|
|
2468
|
-
// the existing field offsets are unchanged.
|
|
2469
|
-
joints: array<mat4x4<f32>, 96>,
|
|
2470
|
-
};
|
|
2471
|
-
|
|
2472
|
-
@group(0) @binding(0) var<uniform> u_draw: DrawUniforms;
|
|
2473
|
-
|
|
2474
|
-
fn a3dWebGPUClipPosition(clipPosition: vec4<f32>) -> vec4<f32> {
|
|
2475
|
-
return vec4<f32>(clipPosition.x, clipPosition.y, clipPosition.z * 0.5 + clipPosition.w * 0.5, clipPosition.w);
|
|
2476
|
-
}
|
|
2477
|
-
`;
|
|
2478
|
-
}
|
|
2479
|
-
|
|
2480
|
-
function nativeShaderUsesTangent(vertexSource: string): boolean {
|
|
2481
|
-
return /\ba_tangent\b/.test(vertexSource) || /layout\s*\(\s*location\s*=\s*3\s*\)\s*in\s+vec4/.test(vertexSource);
|
|
2482
|
-
}
|
|
2483
|
-
|
|
2484
|
-
function nativePbrFragmentPrelude(marker: string): string {
|
|
2485
|
-
return `// ${marker}
|
|
2486
|
-
${nativeUniformStruct()}
|
|
2487
|
-
@group(0) @binding(1) var u_baseSampler: sampler;
|
|
2488
|
-
@group(0) @binding(2) var u_baseTexture: texture_2d<f32>;
|
|
2489
|
-
@group(0) @binding(3) var u_environmentSampler: sampler;
|
|
2490
|
-
@group(0) @binding(4) var u_environmentTexture: texture_2d<f32>;
|
|
2491
|
-
@group(0) @binding(5) var u_brdfSampler: sampler;
|
|
2492
|
-
@group(0) @binding(6) var u_brdfTexture: texture_2d<f32>;
|
|
2493
|
-
@group(0) @binding(7) var u_shadowSampler: sampler;
|
|
2494
|
-
@group(0) @binding(8) var u_shadowTexture: texture_2d<f32>;
|
|
2495
|
-
@group(0) @binding(9) var u_normalSampler: sampler;
|
|
2496
|
-
@group(0) @binding(10) var u_normalTexture: texture_2d<f32>;
|
|
2497
|
-
@group(0) @binding(11) var u_metallicRoughnessSampler: sampler;
|
|
2498
|
-
@group(0) @binding(12) var u_metallicRoughnessTexture: texture_2d<f32>;
|
|
2499
|
-
@group(0) @binding(13) var u_occlusionSampler: sampler;
|
|
2500
|
-
@group(0) @binding(14) var u_occlusionTexture: texture_2d<f32>;
|
|
2501
|
-
|
|
2502
|
-
struct VertexOutput {
|
|
2503
|
-
@builtin(position) position: vec4<f32>,
|
|
2504
|
-
@location(0) normal: vec3<f32>,
|
|
2505
|
-
@location(1) uv: vec2<f32>,
|
|
2506
|
-
@location(2) worldPosition: vec3<f32>,
|
|
2507
|
-
@location(3) tangent: vec4<f32>,
|
|
2508
|
-
};
|
|
2509
|
-
|
|
2510
|
-
fn fresnelSchlick(cosTheta: f32, f0: vec3<f32>) -> vec3<f32> {
|
|
2511
|
-
return f0 + (vec3<f32>(1.0, 1.0, 1.0) - f0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
|
|
2512
|
-
}
|
|
2513
|
-
|
|
2514
|
-
fn fresnelSchlickRoughness(cosTheta: f32, f0: vec3<f32>, roughness: f32) -> vec3<f32> {
|
|
2515
|
-
let smoothness = 1.0 - clamp(roughness, 0.0, 1.0);
|
|
2516
|
-
return f0 + (max(vec3<f32>(smoothness, smoothness, smoothness), f0) - f0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
|
-
fn ggxDistribution(nDotH: f32, roughness: f32) -> f32 {
|
|
2520
|
-
let a = max(roughness * roughness, 0.045);
|
|
2521
|
-
let a2 = a * a;
|
|
2522
|
-
let denom = max((nDotH * nDotH) * (a2 - 1.0) + 1.0, 0.001);
|
|
2523
|
-
return a2 / max(3.14159265 * denom * denom, 0.001);
|
|
2524
|
-
}
|
|
2525
|
-
|
|
2526
|
-
fn ggxVisibilitySmithCorrelated(nDotV: f32, nDotL: f32, roughness: f32) -> f32 {
|
|
2527
|
-
let a = max(roughness * roughness, 0.045);
|
|
2528
|
-
let a2 = a * a;
|
|
2529
|
-
let lambdaV = nDotL * sqrt(max((nDotV - a2 * nDotV) * nDotV + a2, 0.00001));
|
|
2530
|
-
let lambdaL = nDotV * sqrt(max((nDotL - a2 * nDotL) * nDotL + a2, 0.00001));
|
|
2531
|
-
return 0.5 / max(lambdaV + lambdaL, 0.00001);
|
|
2532
|
-
}
|
|
2533
|
-
|
|
2534
|
-
fn diffuseBurley(nDotV: f32, nDotL: f32, lDotH: f32, roughness: f32) -> f32 {
|
|
2535
|
-
let energyBias = mix(0.0, 0.5, roughness);
|
|
2536
|
-
let energyFactor = mix(1.0, 1.0 / 1.51, roughness);
|
|
2537
|
-
let fd90 = energyBias + 2.0 * lDotH * lDotH * roughness;
|
|
2538
|
-
let lightScatter = 1.0 + (fd90 - 1.0) * pow(clamp(1.0 - nDotL, 0.0, 1.0), 5.0);
|
|
2539
|
-
let viewScatter = 1.0 + (fd90 - 1.0) * pow(clamp(1.0 - nDotV, 0.0, 1.0), 5.0);
|
|
2540
|
-
return lightScatter * viewScatter * energyFactor;
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
fn encodePbrOutput(linearColor: vec3<f32>) -> vec3<f32> {
|
|
2544
|
-
let color = max(linearColor, vec3<f32>(0.0, 0.0, 0.0));
|
|
2545
|
-
let filmic = clamp((color * (2.51 * color + vec3<f32>(0.03, 0.03, 0.03))) / (color * (2.43 * color + vec3<f32>(0.59, 0.59, 0.59)) + vec3<f32>(0.14, 0.14, 0.14)), vec3<f32>(0.0, 0.0, 0.0), vec3<f32>(1.0, 1.0, 1.0));
|
|
2546
|
-
return pow(filmic, vec3<f32>(1.0 / 2.2, 1.0 / 2.2, 1.0 / 2.2));
|
|
2547
|
-
}
|
|
2548
|
-
|
|
2549
|
-
fn productPropBodyGate(baseColor: vec3<f32>, strength: f32) -> f32 {
|
|
2550
|
-
let maxChannel = max(max(baseColor.r, baseColor.g), baseColor.b);
|
|
2551
|
-
let minChannel = min(min(baseColor.r, baseColor.g), baseColor.b);
|
|
2552
|
-
let normalized = baseColor / max(maxChannel, 0.001);
|
|
2553
|
-
let warm = clamp(strength, 0.0, 1.0)
|
|
2554
|
-
* smoothstep(0.18, 0.42, maxChannel)
|
|
2555
|
-
* smoothstep(0.04, 0.2, maxChannel - minChannel)
|
|
2556
|
-
* smoothstep(0.64, 0.84, normalized.r)
|
|
2557
|
-
* (1.0 - smoothstep(0.12, 0.32, normalized.b));
|
|
2558
|
-
return warm * smoothstep(0.46, 0.64, normalized.g) * (1.0 - smoothstep(0.74, 0.94, normalized.b));
|
|
2559
|
-
}
|
|
2560
|
-
|
|
2561
|
-
fn productPropOrangeGate(baseColor: vec3<f32>, strength: f32) -> f32 {
|
|
2562
|
-
let maxChannel = max(max(baseColor.r, baseColor.g), baseColor.b);
|
|
2563
|
-
let minChannel = min(min(baseColor.r, baseColor.g), baseColor.b);
|
|
2564
|
-
let normalized = baseColor / max(maxChannel, 0.001);
|
|
2565
|
-
let warm = clamp(strength, 0.0, 1.0)
|
|
2566
|
-
* smoothstep(0.32, 0.58, maxChannel)
|
|
2567
|
-
* smoothstep(0.05, 0.24, maxChannel - minChannel)
|
|
2568
|
-
* smoothstep(0.68, 0.88, normalized.r)
|
|
2569
|
-
* (1.0 - smoothstep(0.1, 0.28, normalized.b));
|
|
2570
|
-
return warm * (1.0 - smoothstep(0.34, 0.58, normalized.g)) * smoothstep(0.012, 0.08, baseColor.g);
|
|
2571
|
-
}
|
|
2572
|
-
|
|
2573
|
-
fn productPropAlbedo(baseColor: vec3<f32>, strength: f32) -> vec3<f32> {
|
|
2574
|
-
let orangeGate = productPropOrangeGate(baseColor, strength);
|
|
2575
|
-
let bodyGate = productPropBodyGate(baseColor, strength) * (1.0 - orangeGate * 0.92);
|
|
2576
|
-
let maxChannel = max(max(baseColor.r, baseColor.g), baseColor.b);
|
|
2577
|
-
let bodyLuma = mix(0.98, 1.03, smoothstep(0.24, 0.9, maxChannel));
|
|
2578
|
-
let beakLuma = mix(0.94, 1.02, smoothstep(0.2, 0.82, maxChannel));
|
|
2579
|
-
var color = mix(baseColor, vec3<f32>(1.0, 0.88, 0.012) * bodyLuma, clamp(bodyGate * 0.995, 0.0, 0.995));
|
|
2580
|
-
color = mix(color, vec3<f32>(1.0, 0.24, 0.018) * beakLuma, clamp(orangeGate * 0.99, 0.0, 0.99));
|
|
2581
|
-
return color;
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
|
-
fn perturbNormal(normalInput: vec3<f32>, tangentFrame: vec4<f32>, normalSample: vec3<f32>, normalScale: f32) -> vec3<f32> {
|
|
2585
|
-
let n = normalize(normalInput);
|
|
2586
|
-
let tangent = normalize(tangentFrame.xyz);
|
|
2587
|
-
let bitangent = normalize(cross(n, tangent) * tangentFrame.w);
|
|
2588
|
-
let mapped = vec3<f32>((normalSample.xy * 2.0 - vec2<f32>(1.0, 1.0)) * max(normalScale, 0.0), normalSample.z * 2.0 - 1.0);
|
|
2589
|
-
return normalize(tangent * mapped.x + bitangent * mapped.y + n * max(mapped.z, 0.001));
|
|
2590
|
-
}
|
|
2591
|
-
|
|
2592
|
-
fn shadePbr(normalInput: vec3<f32>, tangentFrame: vec4<f32>, uv: vec2<f32>, worldPosition: vec3<f32>) -> vec4<f32> {
|
|
2593
|
-
var normal = normalize(normalInput);
|
|
2594
|
-
if (u_draw.morph0.x > 0.5) {
|
|
2595
|
-
normal = perturbNormal(normal, tangentFrame, textureSample(u_normalTexture, u_normalSampler, uv).rgb, u_draw.morph0.y);
|
|
2596
|
-
}
|
|
2597
|
-
let viewDirection = normalize(u_draw.camera.xyz - worldPosition);
|
|
2598
|
-
let lightDirection = normalize(vec3<f32>(0.36, 0.52, 0.78));
|
|
2599
|
-
let halfVector = normalize(lightDirection + viewDirection);
|
|
2600
|
-
var baseColor = u_draw.color.rgb;
|
|
2601
|
-
var materialAlpha = u_draw.color.a;
|
|
2602
|
-
if (u_draw.flags.x > 0.5) {
|
|
2603
|
-
let baseSample = textureSample(u_baseTexture, u_baseSampler, uv);
|
|
2604
|
-
baseColor = baseColor * baseSample.rgb;
|
|
2605
|
-
materialAlpha = materialAlpha * baseSample.a;
|
|
2606
|
-
}
|
|
2607
|
-
let sourceProductBaseColor = baseColor;
|
|
2608
|
-
let productOrangeGate = productPropOrangeGate(sourceProductBaseColor, u_draw.materialFlags.z);
|
|
2609
|
-
let productBodyGate = productPropBodyGate(sourceProductBaseColor, u_draw.materialFlags.z) * (1.0 - productOrangeGate * 0.92);
|
|
2610
|
-
let productSurfaceGate = clamp(productBodyGate + productOrangeGate * 0.72, 0.0, 1.0);
|
|
2611
|
-
baseColor = productPropAlbedo(sourceProductBaseColor, u_draw.materialFlags.z);
|
|
2612
|
-
if (materialAlpha < u_draw.material.x) {
|
|
2613
|
-
discard;
|
|
2614
|
-
}
|
|
2615
|
-
let transmission = clamp(max(u_draw.material.y, u_draw.material.z), 0.0, 1.0);
|
|
2616
|
-
let metallicRoughnessSample = textureSample(u_metallicRoughnessTexture, u_metallicRoughnessSampler, uv);
|
|
2617
|
-
let metallicRoughnessEnabled = step(0.5, u_draw.materialFlags.x);
|
|
2618
|
-
let occlusionEnabled = step(0.5, u_draw.materialFlags.y);
|
|
2619
|
-
let sampledMetallic = clamp(u_draw.params.x * metallicRoughnessSample.b, 0.0, 1.0);
|
|
2620
|
-
let sampledRoughness = clamp(u_draw.params.y * metallicRoughnessSample.g, 0.045, 1.0);
|
|
2621
|
-
let sampledOcclusion = mix(1.0, textureSample(u_occlusionTexture, u_occlusionSampler, uv).r, clamp(u_draw.morph0.z, 0.0, 1.0));
|
|
2622
|
-
let metallic = mix(clamp(u_draw.params.x, 0.0, 1.0), sampledMetallic, metallicRoughnessEnabled);
|
|
2623
|
-
let roughness = mix(clamp(u_draw.params.y, 0.045, 1.0), sampledRoughness, metallicRoughnessEnabled);
|
|
2624
|
-
let occlusion = mix(1.0, sampledOcclusion, occlusionEnabled);
|
|
2625
|
-
let smoothedProductNormal = normalize(vec3<f32>(normal.x * 0.42, max(normal.y, 0.28), normal.z * 0.42));
|
|
2626
|
-
normal = normalize(mix(normal, smoothedProductNormal, productBodyGate * 0.46 + productOrangeGate * 0.84));
|
|
2627
|
-
let nDotL = max(dot(normal, lightDirection), 0.0);
|
|
2628
|
-
let nDotV = max(dot(normal, viewDirection), 0.001);
|
|
2629
|
-
let nDotH = max(dot(normal, halfVector), 0.001);
|
|
2630
|
-
let vDotH = max(dot(viewDirection, halfVector), 0.001);
|
|
2631
|
-
let f0 = mix(vec3<f32>(0.04, 0.04, 0.04), baseColor, metallic);
|
|
2632
|
-
let fresnel = fresnelSchlick(vDotH, f0);
|
|
2633
|
-
let distribution = ggxDistribution(nDotH, roughness);
|
|
2634
|
-
let visibility = ggxVisibilitySmithCorrelated(nDotV, nDotL, roughness);
|
|
2635
|
-
let lDotH = max(dot(lightDirection, halfVector), 0.0);
|
|
2636
|
-
let kd = (vec3<f32>(1.0, 1.0, 1.0) - fresnel) * (1.0 - metallic);
|
|
2637
|
-
let specular = fresnel * distribution * visibility;
|
|
2638
|
-
let diffuse = kd * baseColor * diffuseBurley(nDotV, nDotL, lDotH, roughness) / 3.14159265;
|
|
2639
|
-
var environment = baseColor * u_draw.params.z * (0.28 + 0.72 * clamp(normal.y * 0.5 + 0.5, 0.0, 1.0)) * occlusion;
|
|
2640
|
-
var environmentSpecularContribution = vec3<f32>(0.0, 0.0, 0.0);
|
|
2641
|
-
if (u_draw.flags.w > 0.5) {
|
|
2642
|
-
let reflectionDirection = reflect(-viewDirection, normal);
|
|
2643
|
-
let diffuseUv = vec2<f32>(fract(atan2(normal.z, normal.x) / 6.2831853 + 0.5), acos(clamp(normal.y, -1.0, 1.0)) / 3.14159265);
|
|
2644
|
-
let specularUv = vec2<f32>(fract(atan2(reflectionDirection.z, reflectionDirection.x) / 6.2831853 + 0.5), acos(clamp(reflectionDirection.y, -1.0, 1.0)) / 3.14159265);
|
|
2645
|
-
let environmentMipCount = max(u_draw.reserved0.z, 1.0);
|
|
2646
|
-
let diffuseEnv = textureSampleLevel(u_environmentTexture, u_environmentSampler, diffuseUv, max(environmentMipCount - 1.0, 0.0)).rgb;
|
|
2647
|
-
let specularEnv = textureSampleLevel(u_environmentTexture, u_environmentSampler, specularUv, roughness * max(environmentMipCount - 1.0, 0.0)).rgb;
|
|
2648
|
-
let brdf = textureSampleLevel(u_brdfTexture, u_brdfSampler, vec2<f32>(nDotV, roughness), 0.0).rg;
|
|
2649
|
-
let environmentFresnel = fresnelSchlickRoughness(nDotV, f0, roughness);
|
|
2650
|
-
let environmentDiffuse = (vec3<f32>(1.0, 1.0, 1.0) - environmentFresnel) * (1.0 - metallic) * diffuseEnv * baseColor * u_draw.params.w * occlusion;
|
|
2651
|
-
let environmentSpecular = specularEnv * (f0 * brdf.x + vec3<f32>(brdf.y, brdf.y, brdf.y)) * max(u_draw.reserved0.w, 0.0);
|
|
2652
|
-
environmentSpecularContribution = environmentSpecular;
|
|
2653
|
-
environment = environment + environmentDiffuse + environmentSpecular;
|
|
2654
|
-
}
|
|
2655
|
-
var shadow = 1.0;
|
|
2656
|
-
if (u_draw.flags.y > 0.5) {
|
|
2657
|
-
let shadowDepth = textureSampleLevel(u_shadowTexture, u_shadowSampler, vec2<f32>(0.5, 0.5), 0.0).r;
|
|
2658
|
-
shadow = mix(1.0, shadowDepth, clamp(u_draw.flags.z, 0.0, 1.0));
|
|
2659
|
-
}
|
|
2660
|
-
let litOpaqueLinearColor = environment + (diffuse + specular) * nDotL * 2.25 * shadow;
|
|
2661
|
-
let smoothedBodyColor = mix(baseColor, vec3<f32>(1.0, 0.88, 0.012), productBodyGate * 0.32);
|
|
2662
|
-
let smoothedBeakColor = mix(baseColor, vec3<f32>(1.0, 0.24, 0.018), productOrangeGate * 0.82);
|
|
2663
|
-
let softBodyProduct = smoothedBodyColor * (1.5 + 0.06 * nDotL) + specular * nDotL * 0.018;
|
|
2664
|
-
let softBeakProduct = smoothedBeakColor * (1.08 + 0.05 * nDotL) + specular * nDotL * 0.018;
|
|
2665
|
-
var opaqueLinearColor = mix(litOpaqueLinearColor, softBodyProduct, productBodyGate * 0.78);
|
|
2666
|
-
opaqueLinearColor = mix(opaqueLinearColor, softBeakProduct, productOrangeGate * 0.95);
|
|
2667
|
-
let transmittedTint = baseColor * u_draw.params.w * (0.18 + 0.42 * clamp(1.0 - roughness, 0.0, 1.0)) + environmentSpecularContribution * 1.35;
|
|
2668
|
-
let linearColor = mix(opaqueLinearColor, opaqueLinearColor * 0.22 + transmittedTint, transmission);
|
|
2669
|
-
let outputAlpha = mix(materialAlpha, min(materialAlpha, 0.22), transmission);
|
|
2670
|
-
return vec4<f32>(encodePbrOutput(linearColor), outputAlpha);
|
|
2671
|
-
}
|
|
2672
|
-
`;
|
|
2673
|
-
}
|
|
2674
|
-
|
|
2675
|
-
function nativePbrShader(vertexEntry: string, fragmentEntry: string, marker: string): { readonly vertex: string; readonly fragment: string } {
|
|
2676
|
-
return {
|
|
2677
|
-
vertex: `// ${marker}
|
|
2678
|
-
${nativeUniformStruct()}
|
|
2679
|
-
struct VertexOutput {
|
|
2680
|
-
@builtin(position) position: vec4<f32>,
|
|
2681
|
-
@location(0) normal: vec3<f32>,
|
|
2682
|
-
@location(1) uv: vec2<f32>,
|
|
2683
|
-
@location(2) worldPosition: vec3<f32>,
|
|
2684
|
-
@location(3) tangent: vec4<f32>,
|
|
2685
|
-
};
|
|
2686
|
-
|
|
2687
|
-
@vertex
|
|
2688
|
-
fn ${vertexEntry}(@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>) -> VertexOutput {
|
|
2689
|
-
var output: VertexOutput;
|
|
2690
|
-
let worldPosition = (u_draw.instance0 * vec4<f32>(position, 1.0)).xyz;
|
|
2691
|
-
output.position = a3dWebGPUClipPosition(u_draw.modelViewProjection * vec4<f32>(position, 1.0));
|
|
2692
|
-
output.normal = normalize((u_draw.normalMatrix * vec4<f32>(normal, 0.0)).xyz);
|
|
2693
|
-
output.uv = vec2<f32>(0.5, 0.5);
|
|
2694
|
-
output.worldPosition = worldPosition;
|
|
2695
|
-
output.tangent = vec4<f32>(1.0, 0.0, 0.0, 1.0);
|
|
2696
|
-
return output;
|
|
2697
|
-
}
|
|
2698
|
-
`,
|
|
2699
|
-
fragment: `${nativePbrFragmentPrelude(marker)}
|
|
2700
|
-
@fragment
|
|
2701
|
-
fn ${fragmentEntry}(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
2702
|
-
return shadePbr(input.normal, input.tangent, input.uv, input.worldPosition);
|
|
2703
|
-
}
|
|
2704
|
-
`
|
|
2705
|
-
};
|
|
2706
|
-
}
|
|
2707
|
-
|
|
2708
|
-
function nativeTexturedPbrShader(vertexEntry: string, fragmentEntry: string, marker: string, usesTangent: boolean): { readonly vertex: string; readonly fragment: string } {
|
|
2709
|
-
return {
|
|
2710
|
-
vertex: `// ${marker}
|
|
2711
|
-
${nativeUniformStruct()}
|
|
2712
|
-
struct VertexOutput {
|
|
2713
|
-
@builtin(position) position: vec4<f32>,
|
|
2714
|
-
@location(0) normal: vec3<f32>,
|
|
2715
|
-
@location(1) uv: vec2<f32>,
|
|
2716
|
-
@location(2) worldPosition: vec3<f32>,
|
|
2717
|
-
@location(3) tangent: vec4<f32>,
|
|
2718
|
-
};
|
|
2719
|
-
|
|
2720
|
-
@vertex
|
|
2721
|
-
fn ${vertexEntry}(@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>, @location(2) uv: vec2<f32>${usesTangent ? ", @location(3) tangent: vec4<f32>" : ""}) -> VertexOutput {
|
|
2722
|
-
var output: VertexOutput;
|
|
2723
|
-
let worldPosition = (u_draw.instance0 * vec4<f32>(position, 1.0)).xyz;
|
|
2724
|
-
output.position = a3dWebGPUClipPosition(u_draw.modelViewProjection * vec4<f32>(position, 1.0));
|
|
2725
|
-
output.normal = normalize((u_draw.normalMatrix * vec4<f32>(normal, 0.0)).xyz);
|
|
2726
|
-
output.uv = uv;
|
|
2727
|
-
output.worldPosition = worldPosition;
|
|
2728
|
-
${usesTangent ? "output.tangent = vec4<f32>(normalize((u_draw.normalMatrix * vec4<f32>(tangent.xyz, 0.0)).xyz), tangent.w);" : "output.tangent = vec4<f32>(1.0, 0.0, 0.0, 1.0);"}
|
|
2729
|
-
return output;
|
|
2730
|
-
}
|
|
2731
|
-
`,
|
|
2732
|
-
fragment: `${nativePbrFragmentPrelude(marker)}
|
|
2733
|
-
@fragment
|
|
2734
|
-
fn ${fragmentEntry}(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
2735
|
-
return shadePbr(input.normal, input.tangent, input.uv, input.worldPosition);
|
|
2736
|
-
}
|
|
2737
|
-
`
|
|
2738
|
-
};
|
|
2739
|
-
}
|
|
2740
|
-
|
|
2741
|
-
function nativeInstancedPbrShader(vertexEntry: string, fragmentEntry: string, marker: string): { readonly vertex: string; readonly fragment: string } {
|
|
2742
|
-
return {
|
|
2743
|
-
vertex: `// ${marker}
|
|
2744
|
-
${nativeUniformStruct()}
|
|
2745
|
-
struct VertexOutput {
|
|
2746
|
-
@builtin(position) position: vec4<f32>,
|
|
2747
|
-
@location(0) normal: vec3<f32>,
|
|
2748
|
-
@location(1) uv: vec2<f32>,
|
|
2749
|
-
@location(2) worldPosition: vec3<f32>,
|
|
2750
|
-
@location(3) tangent: vec4<f32>,
|
|
2751
|
-
};
|
|
2752
|
-
|
|
2753
|
-
fn instanceMatrix(index: u32) -> mat4x4<f32> {
|
|
2754
|
-
if (index == 1u) { return u_draw.instance1; }
|
|
2755
|
-
if (index == 2u) { return u_draw.instance2; }
|
|
2756
|
-
if (index == 3u) { return u_draw.instance3; }
|
|
2757
|
-
return u_draw.instance0;
|
|
2758
|
-
}
|
|
2759
|
-
|
|
2760
|
-
@vertex
|
|
2761
|
-
fn ${vertexEntry}(@location(0) position: vec3<f32>, @location(1) normal: vec3<f32>, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
|
|
2762
|
-
var output: VertexOutput;
|
|
2763
|
-
let model = instanceMatrix(instanceIndex);
|
|
2764
|
-
let worldPosition = (model * vec4<f32>(position, 1.0)).xyz;
|
|
2765
|
-
output.position = a3dWebGPUClipPosition(u_draw.modelViewProjection * model * vec4<f32>(position, 1.0));
|
|
2766
|
-
output.normal = normalize((model * vec4<f32>(normal, 0.0)).xyz);
|
|
2767
|
-
output.uv = vec2<f32>(0.5, 0.5);
|
|
2768
|
-
output.worldPosition = worldPosition;
|
|
2769
|
-
output.tangent = vec4<f32>(1.0, 0.0, 0.0, 1.0);
|
|
2770
|
-
return output;
|
|
2771
|
-
}
|
|
2772
|
-
`,
|
|
2773
|
-
fragment: `${nativePbrFragmentPrelude(marker)}
|
|
2774
|
-
@fragment
|
|
2775
|
-
fn ${fragmentEntry}(input: VertexOutput) -> @location(0) vec4<f32> {
|
|
2776
|
-
return shadePbr(input.normal, input.tangent, input.uv, input.worldPosition);
|
|
2777
|
-
}
|
|
2778
|
-
`
|
|
2779
|
-
};
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
|
-
function nativeSkinnedUnlitShader(vertexEntry: string, fragmentEntry: string, marker: string): { readonly vertex: string; readonly fragment: string } {
|
|
2783
|
-
return {
|
|
2784
|
-
vertex: `// ${marker}
|
|
2785
|
-
${nativeUniformStruct()}
|
|
2786
|
-
struct VertexOutput {
|
|
2787
|
-
@builtin(position) position: vec4<f32>,
|
|
2788
|
-
};
|
|
2789
|
-
|
|
2790
|
-
fn jointMatrix(index: f32) -> mat4x4<f32> {
|
|
2791
|
-
let i = clamp(i32(index + 0.5), 0, 95);
|
|
2792
|
-
return u_draw.joints[i];
|
|
2793
|
-
}
|
|
2794
|
-
|
|
2795
|
-
@vertex
|
|
2796
|
-
fn ${vertexEntry}(@location(0) position: vec3<f32>, @location(5) joints: vec4<f32>, @location(6) weights: vec4<f32>) -> VertexOutput {
|
|
2797
|
-
var output: VertexOutput;
|
|
2798
|
-
let skin = jointMatrix(joints.x) * weights.x + jointMatrix(joints.y) * weights.y + jointMatrix(joints.z) * weights.z + jointMatrix(joints.w) * weights.w;
|
|
2799
|
-
let weightSum = weights.x + weights.y + weights.z + weights.w;
|
|
2800
|
-
let skinned = select(vec4<f32>(position, 1.0), skin * vec4<f32>(position, 1.0), weightSum > 0.0001);
|
|
2801
|
-
output.position = a3dWebGPUClipPosition(u_draw.modelViewProjection * skinned);
|
|
2802
|
-
return output;
|
|
2803
|
-
}
|
|
2804
|
-
`,
|
|
2805
|
-
fragment: `// ${marker}
|
|
2806
|
-
${nativeUniformStruct()}
|
|
2807
|
-
@fragment
|
|
2808
|
-
fn ${fragmentEntry}() -> @location(0) vec4<f32> {
|
|
2809
|
-
return u_draw.color;
|
|
2810
|
-
}
|
|
2811
|
-
`
|
|
2812
|
-
};
|
|
2813
|
-
}
|
|
2814
|
-
|
|
2815
|
-
function nativeMorphUnlitShader(vertexEntry: string, fragmentEntry: string, marker: string): { readonly vertex: string; readonly fragment: string } {
|
|
2816
|
-
return {
|
|
2817
|
-
vertex: `// ${marker}
|
|
2818
|
-
${nativeUniformStruct()}
|
|
2819
|
-
struct VertexOutput {
|
|
2820
|
-
@builtin(position) position: vec4<f32>,
|
|
2821
|
-
};
|
|
2822
|
-
|
|
2823
|
-
fn morphDelta(index: u32) -> vec3<f32> {
|
|
2824
|
-
if (index == 0u) { return u_draw.morph0.xyz; }
|
|
2825
|
-
if (index == 1u) { return u_draw.morph1.xyz; }
|
|
2826
|
-
if (index == 2u) { return u_draw.morph2.xyz; }
|
|
2827
|
-
if (index == 3u) { return u_draw.morph3.xyz; }
|
|
2828
|
-
if (index == 4u) { return u_draw.morph4.xyz; }
|
|
2829
|
-
if (index == 5u) { return u_draw.morph5.xyz; }
|
|
2830
|
-
if (index == 6u) { return u_draw.morph6.xyz; }
|
|
2831
|
-
return u_draw.morph7.xyz;
|
|
2832
|
-
}
|
|
2833
|
-
|
|
2834
|
-
@vertex
|
|
2835
|
-
fn ${vertexEntry}(@location(0) position: vec3<f32>, @builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
|
|
2836
|
-
var output: VertexOutput;
|
|
2837
|
-
let enabled = min(u_draw.reserved0.y, 1.0);
|
|
2838
|
-
let morphed = position + morphDelta(min(vertexIndex, 7u)) * u_draw.reserved0.x * enabled;
|
|
2839
|
-
output.position = a3dWebGPUClipPosition(u_draw.modelViewProjection * vec4<f32>(morphed, 1.0));
|
|
2840
|
-
return output;
|
|
2841
|
-
}
|
|
2842
|
-
`,
|
|
2843
|
-
fragment: `// ${marker}
|
|
2844
|
-
${nativeUniformStruct()}
|
|
2845
|
-
@fragment
|
|
2846
|
-
fn ${fragmentEntry}() -> @location(0) vec4<f32> {
|
|
2847
|
-
return u_draw.color;
|
|
2848
|
-
}
|
|
2849
|
-
`
|
|
2850
|
-
};
|
|
2851
|
-
}
|
|
2852
|
-
|
|
2853
|
-
function inferWGSLVertexEntryPoint(source: string): string | undefined {
|
|
2854
|
-
return /@vertex\s+fn\s+([A-Za-z_]\w*)/.exec(source)?.[1];
|
|
2855
|
-
}
|
|
2856
|
-
|
|
2857
|
-
function inferWGSLFragmentEntryPoint(source: string): string | undefined {
|
|
2858
|
-
return /@fragment\s+fn\s+([A-Za-z_]\w*)/.exec(source)?.[1];
|
|
2859
|
-
}
|
|
2860
|
-
|
|
2861
|
-
function vertexBufferLayout(format: VertexFormat): WebGPUVertexBufferLayoutLike {
|
|
2862
|
-
return {
|
|
2863
|
-
arrayStride: format.stride,
|
|
2864
|
-
attributes: format.attributes.map((attribute) => ({
|
|
2865
|
-
shaderLocation: attribute.shaderLocation,
|
|
2866
|
-
offset: attribute.offset,
|
|
2867
|
-
format: vertexAttributeFormat(attribute.components)
|
|
2868
|
-
}))
|
|
2869
|
-
};
|
|
2870
|
-
}
|
|
2871
|
-
|
|
2872
|
-
function vertexAttributeFormat(components: 1 | 2 | 3 | 4): WebGPUVertexAttributeLike["format"] {
|
|
2873
|
-
if (components === 1) return "float32";
|
|
2874
|
-
if (components === 2) return "float32x2";
|
|
2875
|
-
if (components === 3) return "float32x3";
|
|
2876
|
-
return "float32x4";
|
|
2877
|
-
}
|