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
This commit is contained in:
chenmo.gl
2026-04-11 21:41:53 +08:00
parent 3c58dd889e
commit 74cbb0ec99
5 changed files with 189 additions and 1 deletions

View File

@@ -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<GLTFResource>({
url: "https://gw.alipayobjects.com/os/bmw-prod/6cb8f543-285c-491a-8cfd-57a1160dc9ab.glb",
type: AssetType.GLTF
}),
engine.resourceManager.load<AmbientLight>({
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);
});

View File

@@ -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 <transform_declare>
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);
});

View File

@@ -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",

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f5424ba406ffe08f68ecfb28174027deb171efff7d55d8ca1be63caaea3bf898
size 590077

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:baece2c4437803e1474699eb4e99214b38caa05393f747fe0f7e47bd6469a693
size 427005