From 74cbb0ec99d98a36963880f72a7e38694635bb5e Mon Sep 17 00:00:00 2001 From: "chenmo.gl" Date: Sat, 11 Apr 2026 21:41:53 +0800 Subject: [PATCH] feat: add GPU instancing e2e tests and complete matrix uniform support - Add e2e cases for gpu-instancing-auto-batch and gpu-instancing-custom-data - Add all missing matrix uniform types (mat2, mat3, mat2x3, mat2x4, mat3x2, mat3x4, mat4x2, mat4x3) to ShaderUniform and ShaderProgram - Skip UBO members (location === null) in uniform reflection - Restore throw for truly unsupported uniform types in default branch --- e2e/case/gpu-instancing-auto-batch.ts | 68 ++++++++++++ e2e/case/gpu-instancing-custom-data.ts | 100 ++++++++++++++++++ e2e/config.ts | 16 ++- ...PUInstancing_gpu-instancing-auto-batch.jpg | 3 + ...UInstancing_gpu-instancing-custom-data.jpg | 3 + 5 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 e2e/case/gpu-instancing-auto-batch.ts create mode 100644 e2e/case/gpu-instancing-custom-data.ts create mode 100644 e2e/fixtures/originImage/GPUInstancing_gpu-instancing-auto-batch.jpg create mode 100644 e2e/fixtures/originImage/GPUInstancing_gpu-instancing-custom-data.jpg diff --git a/e2e/case/gpu-instancing-auto-batch.ts b/e2e/case/gpu-instancing-auto-batch.ts new file mode 100644 index 000000000..e9f80e35a --- /dev/null +++ b/e2e/case/gpu-instancing-auto-batch.ts @@ -0,0 +1,68 @@ +/** + * @title GPU Instancing Auto Batch + * @category Mesh + */ +import { + AmbientLight, + AssetType, + Camera, + Color, + DirectLight, + GLTFResource, + Logger, + Vector3, + WebGLEngine +} from "@galacean/engine"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +Logger.enable(); +WebGLEngine.create({ canvas: "canvas" }).then(async (engine) => { + engine.canvas.resizeByClientSize(2); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("Root"); + + // Camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 10, 80); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + const camera = cameraEntity.addComponent(Camera); + camera.farClipPlane = 300; + + // Light + const lightEntity = rootEntity.createChild("Light"); + lightEntity.transform.setRotation(-45, -45, 0); + lightEntity.addComponent(DirectLight).color = new Color(1, 1, 1, 1); + + // Load Duck model and ambient light + const [glTF, ambientLight] = await Promise.all([ + engine.resourceManager.load({ + url: "https://gw.alipayobjects.com/os/bmw-prod/6cb8f543-285c-491a-8cfd-57a1160dc9ab.glb", + type: AssetType.GLTF + }), + engine.resourceManager.load({ + url: "https://mdn.alipayobjects.com/oasis_be/afts/file/A*eRJ8QKzf5zAAAAAAgBAAAAgAekp5AQ/ambient.ambLight", + type: AssetType.AmbientLight + }) + ]); + scene.ambientLight = ambientLight; + + // Clone ducks with fixed seed positions for deterministic output + const count = 500; + const spread = 50; + for (let i = 0; i < count; i++) { + const duck = glTF.instantiateSceneRoot(); + // Use deterministic positions based on index + const t = i / count; + duck.transform.setPosition( + Math.sin(t * 137.5) * spread * 0.5, + Math.cos(t * 97.3) * spread * 0.5, + Math.sin(t * 59.1) * spread * 0.5 + ); + duck.transform.setRotation(t * 360, t * 720, t * 1080); + rootEntity.addChild(duck); + } + + updateForE2E(engine); + initScreenshot(engine, camera); +}); diff --git a/e2e/case/gpu-instancing-custom-data.ts b/e2e/case/gpu-instancing-custom-data.ts new file mode 100644 index 000000000..f64b01a8f --- /dev/null +++ b/e2e/case/gpu-instancing-custom-data.ts @@ -0,0 +1,100 @@ +/** + * @title GPU Instancing Custom Data + * @category Mesh + */ +import { + Camera, + Color, + DirectLight, + Logger, + Material, + MeshRenderer, + PrimitiveMesh, + Shader, + ShaderProperty, + Vector3, + Vector4, + WebGLEngine +} from "@galacean/engine"; +import { initScreenshot, updateForE2E } from "./.mockForE2E"; + +Logger.enable(); + +// Custom shader: uses renderer_CustomColor (per-instance) for fragment output +Shader.create( + "CustomInstanceShader", + ` + #include + attribute vec3 POSITION; + attribute vec3 NORMAL; + + varying vec3 v_normal; + + void main() { + gl_Position = renderer_MVPMat * vec4(POSITION, 1.0); + v_normal = normalize((renderer_NormalMat * vec4(NORMAL, 0.0)).xyz); + } + `, + ` + uniform vec4 renderer_CustomColor; + + varying vec3 v_normal; + + void main() { + vec3 lightDir = normalize(vec3(1.0, 1.0, 1.0)); + float NdotL = max(dot(v_normal, lightDir), 0.2); + gl_FragColor = vec4(renderer_CustomColor.rgb * NdotL, 1.0); + } + ` +); + +WebGLEngine.create({ canvas: "canvas" }).then((engine) => { + engine.canvas.resizeByClientSize(2); + + const scene = engine.sceneManager.activeScene; + const rootEntity = scene.createRootEntity("Root"); + + // Camera + const cameraEntity = rootEntity.createChild("Camera"); + cameraEntity.transform.setPosition(0, 10, 80); + cameraEntity.transform.lookAt(new Vector3(0, 0, 0)); + const camera = cameraEntity.addComponent(Camera); + camera.farClipPlane = 300; + + // Light + const lightEntity = rootEntity.createChild("Light"); + lightEntity.transform.setRotation(-45, -45, 0); + lightEntity.addComponent(DirectLight).color = new Color(1, 1, 1, 1); + + // Shared mesh and material + const mesh = PrimitiveMesh.createCuboid(engine, 1, 1, 1); + const material = new Material(engine, Shader.find("CustomInstanceShader")); + const customColorProperty = ShaderProperty.getByName("renderer_CustomColor"); + + // Create cubes with deterministic positions and colors + const count = 500; + const spread = 50; + for (let i = 0; i < count; i++) { + const entity = rootEntity.createChild("Cube" + i); + const t = i / count; + entity.transform.setPosition( + Math.sin(t * 137.5) * spread * 0.5, + Math.cos(t * 97.3) * spread * 0.5, + Math.sin(t * 59.1) * spread * 0.5 + ); + entity.transform.setRotation(t * 360, t * 720, t * 1080); + + const renderer = entity.addComponent(MeshRenderer); + renderer.mesh = mesh; + renderer.setMaterial(material); + + // Deterministic colors based on index + renderer.shaderData.setVector4( + customColorProperty, + new Vector4(Math.sin(t * 6.28) * 0.5 + 0.5, Math.cos(t * 4.71) * 0.5 + 0.5, Math.sin(t * 3.14) * 0.5 + 0.5, 1.0) + ); + } + + updateForE2E(engine); + initScreenshot(engine, camera); +}); diff --git a/e2e/config.ts b/e2e/config.ts index 05c61ad75..d57bd2209 100644 --- a/e2e/config.ts +++ b/e2e/config.ts @@ -135,7 +135,7 @@ export const E2E_CONFIG = { category: "Material", caseFileName: "material-pbr", threshold: 0, - diffPercentage: 0.0080 + diffPercentage: 0.008 }, shaderLab: { category: "Material", @@ -464,6 +464,20 @@ export const E2E_CONFIG = { diffPercentage: 0.03 } }, + GPUInstancing: { + autoBatch: { + category: "GPUInstancing", + caseFileName: "gpu-instancing-auto-batch", + threshold: 0, + diffPercentage: 0 + }, + customData: { + category: "GPUInstancing", + caseFileName: "gpu-instancing-custom-data", + threshold: 0, + diffPercentage: 0 + } + }, SpriteMask: { CustomStencil: { category: "SpriteMask", diff --git a/e2e/fixtures/originImage/GPUInstancing_gpu-instancing-auto-batch.jpg b/e2e/fixtures/originImage/GPUInstancing_gpu-instancing-auto-batch.jpg new file mode 100644 index 000000000..82e40384f --- /dev/null +++ b/e2e/fixtures/originImage/GPUInstancing_gpu-instancing-auto-batch.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f5424ba406ffe08f68ecfb28174027deb171efff7d55d8ca1be63caaea3bf898 +size 590077 diff --git a/e2e/fixtures/originImage/GPUInstancing_gpu-instancing-custom-data.jpg b/e2e/fixtures/originImage/GPUInstancing_gpu-instancing-custom-data.jpg new file mode 100644 index 000000000..79399c097 --- /dev/null +++ b/e2e/fixtures/originImage/GPUInstancing_gpu-instancing-custom-data.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:baece2c4437803e1474699eb4e99214b38caa05393f747fe0f7e47bd6469a693 +size 427005