refactor(shader): remove raw GLSL shader path

All built-in shaders (including ParticleFeedback) now consume ShaderLab
via the precompiled instructions path. The legacy raw GLSL string
overload is unreachable production code, so drop it for a single
canonical path through ShaderPass.

- ShaderPass: keep only the (name, vertexInstructions,
  fragmentInstructions, platformTarget, tags?) constructor; drop
  _vertexSource / _fragmentSource fields and _compilePlatformSource
- Shader.create: drop the (name, vertexSource, fragmentSource) overload
- ShaderFactory: drop compilePlatformSource and parseCustomMacros (no
  remaining callers); registerInclude / unRegisterInclude /
  parseIncludes / convertTo300 stay (still used by ShaderLab includes)
- Tests: rewrite Shader.test custom-shader cases via SubShader /
  ShaderPass instructions overload; drop the GSP-vs-GLSL benchmark case
This commit is contained in:
chenmo.gl
2026-04-29 18:14:46 +08:00
parent 174751512c
commit e08af33d93
5 changed files with 35 additions and 312 deletions

View File

@@ -53,15 +53,6 @@ export class Shader implements IReferable {
*/
static create(shaderSource: string, platformTarget?: ShaderLanguage): Shader;
/**
* Create a shader.
* @param name - Name of the shader
* @param vertexSource - Vertex source code
* @param fragmentSource - Fragment source code
* @returns Shader
*/
static create(name: string, vertexSource: string, fragmentSource: string): Shader;
/**
* Create a shader.
* @param name - Name of the shader
@@ -80,17 +71,16 @@ export class Shader implements IReferable {
static create(
nameOrShaderSource: string,
vertexSourceOrShaderPassesOrSubShadersOrPlatformTarget?: ShaderLanguage | SubShader[] | ShaderPass[] | string,
fragmentSource?: string
shaderPassesOrSubShadersOrPlatformTarget?: ShaderLanguage | SubShader[] | ShaderPass[]
): Shader {
let shader: Shader;
const shaderMap = Shader._shaderMap;
if (vertexSourceOrShaderPassesOrSubShadersOrPlatformTarget == undefined) {
vertexSourceOrShaderPassesOrSubShadersOrPlatformTarget = ShaderLanguage.GLSLES100;
if (shaderPassesOrSubShadersOrPlatformTarget == undefined) {
shaderPassesOrSubShadersOrPlatformTarget = ShaderLanguage.GLSLES100;
}
if (typeof vertexSourceOrShaderPassesOrSubShadersOrPlatformTarget === "number") {
if (typeof shaderPassesOrSubShadersOrPlatformTarget === "number") {
const shaderCompiler = Shader._shaderCompiler;
if (!shaderCompiler) {
throw "ShaderCompiler has not been set up yet.";
@@ -112,7 +102,7 @@ export class Shader implements IReferable {
passSource.contents,
passSource.vertexEntry,
passSource.fragmentEntry,
vertexSourceOrShaderPassesOrSubShadersOrPlatformTarget
shaderPassesOrSubShadersOrPlatformTarget
);
if (!shaderPassSource) {
@@ -123,7 +113,7 @@ export class Shader implements IReferable {
passSource.name,
shaderPassSource.vertexShaderInstructions,
shaderPassSource.fragmentShaderInstructions,
vertexSourceOrShaderPassesOrSubShadersOrPlatformTarget as ShaderLanguage,
shaderPassesOrSubShadersOrPlatformTarget as ShaderLanguage,
passSource.tags
);
@@ -148,24 +138,16 @@ export class Shader implements IReferable {
console.error(`Shader named "${nameOrShaderSource}" already exists.`);
return;
}
if (typeof vertexSourceOrShaderPassesOrSubShadersOrPlatformTarget === "string") {
const shaderPass = new ShaderPass(vertexSourceOrShaderPassesOrSubShadersOrPlatformTarget, fragmentSource);
shader = new Shader(nameOrShaderSource, [new SubShader("Default", [shaderPass])]);
} else {
if (vertexSourceOrShaderPassesOrSubShadersOrPlatformTarget.length > 0) {
if (vertexSourceOrShaderPassesOrSubShadersOrPlatformTarget[0].constructor === ShaderPass) {
shader = new Shader(nameOrShaderSource, [
new SubShader("Default", <ShaderPass[]>vertexSourceOrShaderPassesOrSubShadersOrPlatformTarget)
]);
} else {
shader = new Shader(
nameOrShaderSource,
<SubShader[]>vertexSourceOrShaderPassesOrSubShadersOrPlatformTarget.slice()
);
}
if (shaderPassesOrSubShadersOrPlatformTarget.length > 0) {
if (shaderPassesOrSubShadersOrPlatformTarget[0].constructor === ShaderPass) {
shader = new Shader(nameOrShaderSource, [
new SubShader("Default", <ShaderPass[]>shaderPassesOrSubShadersOrPlatformTarget)
]);
} else {
throw "SubShader or ShaderPass count must large than 0.";
shader = new Shader(nameOrShaderSource, <SubShader[]>shaderPassesOrSubShadersOrPlatformTarget.slice());
}
} else {
throw "SubShader or ShaderPass count must large than 0.";
}
}

View File

@@ -45,15 +45,13 @@ export class ShaderPass extends ShaderPart {
[RenderStateElementKey.RenderQueueType]: ShaderProperty.getByName("renderQueueType")
};
/**
* @internal
*/
_platformTarget: ShaderLanguage | undefined;
/** @internal */
_platformTarget: ShaderLanguage;
/** @internal - Flat instruction array for vertex shader. */
_vertexShaderInstructions?: ShaderInstruction[];
_vertexShaderInstructions: ShaderInstruction[];
/** @internal */
_fragmentShaderInstructions?: ShaderInstruction[];
_fragmentShaderInstructions: ShaderInstruction[];
/** @internal */
_shaderPassId: number = 0;
@@ -72,34 +70,9 @@ export class ShaderPass extends ShaderPart {
/** @internal Transform feedback output varyings (WebGL2 only). */
_feedbackVaryings?: string[];
private _vertexSource?: string;
private _fragmentSource?: string;
private static _shaderMacroList: ShaderMacro[] = [];
private static _macroMap: Map<string, string> = new Map();
/**
* Create a shader pass.
* @param name - Shader pass name
* @param vertexSource - Vertex shader source
* @param fragmentSource - Fragment shader source
* @param tags - Tags
*/
constructor(
name: string,
vertexSource: string,
fragmentSource: string,
tags?: Record<string, number | string | boolean>
);
/**
* Create a shader pass.
* @param vertexSource - Vertex shader source
* @param fragmentSource - Fragment shader source
* @param tags - Tags
*/
constructor(vertexSource: string, fragmentSource: string, tags?: Record<string, number | string | boolean>);
/**
* Create a shader pass from precompiled instructions.
* @param name - Shader pass name
@@ -114,47 +87,18 @@ export class ShaderPass extends ShaderPart {
fragmentShaderInstructions: ShaderInstruction[],
platformTarget: ShaderLanguage,
tags?: Record<string, number | string | boolean>
);
constructor(
nameOrVertexSource: string,
vertexSourceOrFragmentSourceOrInstructions: string | ShaderInstruction[],
fragmentSourceOrTags?: string | ShaderInstruction[] | Record<string, number | string | boolean>,
tagsOrPlatformTarget?: Record<string, number | string | boolean> | ShaderLanguage,
tags?: Record<string, number | string | boolean>
) {
super();
this._shaderPassId = ShaderPass._shaderPassCounter++;
if (Array.isArray(vertexSourceOrFragmentSourceOrInstructions)) {
// Instructions overload: (name, vertexInst, fragInst, platformTarget, tags?)
this._name = nameOrVertexSource;
this._vertexShaderInstructions = vertexSourceOrFragmentSourceOrInstructions;
this._fragmentShaderInstructions = fragmentSourceOrTags as ShaderInstruction[];
this._platformTarget = tagsOrPlatformTarget as ShaderLanguage;
tags = { pipelineStage: PipelineStage.Forward, ...tags };
} else if (typeof fragmentSourceOrTags === "string") {
// Named overload: (name, vertexSource, fragmentSource, tags?)
this._name = nameOrVertexSource;
this._vertexSource = vertexSourceOrFragmentSourceOrInstructions;
this._fragmentSource = fragmentSourceOrTags;
tags = {
pipelineStage: PipelineStage.Forward,
...(tagsOrPlatformTarget as Record<string, number | string | boolean>)
};
} else {
// Unnamed overload: (vertexSource, fragmentSource, tags?)
this._name = "Default";
this._vertexSource = nameOrVertexSource;
this._fragmentSource = vertexSourceOrFragmentSourceOrInstructions as string;
tags = {
pipelineStage: PipelineStage.Forward,
...(fragmentSourceOrTags as Record<string, number | string | boolean>)
};
}
this._name = name;
this._vertexShaderInstructions = vertexShaderInstructions;
this._fragmentShaderInstructions = fragmentShaderInstructions;
this._platformTarget = platformTarget;
for (const key in tags) {
this.setTag(key, tags[key]);
const mergedTags = { pipelineStage: PipelineStage.Forward, ...tags };
for (const key in mergedTags) {
this.setTag(key, mergedTags[key]);
}
}
@@ -189,21 +133,10 @@ export class ShaderPass extends ShaderPart {
}
private _getCanonicalShaderProgram(engine: Engine, macroCollection: ShaderMacroCollection): ShaderProgram {
const { vertexSource, fragmentSource } =
this._platformTarget != undefined
? this._compileShaderSource(engine, macroCollection)
: this._compilePlatformSource(engine, macroCollection);
const { vertexSource, fragmentSource } = this._compileShaderSource(engine, macroCollection);
return new ShaderProgram(engine, vertexSource, fragmentSource, this._feedbackVaryings);
}
private _compilePlatformSource(
engine: Engine,
macroCollection: ShaderMacroCollection
): { vertexSource: string; fragmentSource: string } {
return ShaderFactory.compilePlatformSource(engine, macroCollection, this._vertexSource, this._fragmentSource);
}
private _compileShaderSource(
engine: Engine,
macroCollection: ShaderMacroCollection

View File

@@ -1,8 +1,4 @@
import { GLCapabilityType } from "../base/Constant";
import { Logger } from "../base/Logger";
import { Engine } from "../Engine";
import { ShaderMacro } from "../shader/ShaderMacro";
import { ShaderMacroCollection } from "../shader/ShaderMacroCollection";
import { ShaderLib } from "./ShaderLib";
export class ShaderFactory {
@@ -18,65 +14,6 @@ export class ShaderFactory {
private static readonly _has300OutInFragReg = /\bout\s+(?:\w+\s+)?(?:vec4)\s+(?:\w+)\s*;/; // [layout(location = 0)] out [highp] vec4 [color];
static parseCustomMacros(macros: ShaderMacro[]) {
return macros.map((m) => `#define ${m.value ? m.name + ` ` + m.value : m.name}\n`).join("");
}
/**
* @internal
* Compile vertex and fragment source with standard macros, includes, and version header.
* @param engine - Engine instance
* @param macroCollection - Current macro collection
* @param vertexSource - Raw vertex shader source (may contain #include)
* @param fragmentSource - Raw fragment shader source
* @returns Compiled { vertexSource, fragmentSource } ready for ShaderProgram
*/
static compilePlatformSource(
engine: Engine,
macroCollection: ShaderMacroCollection,
vertexSource: string,
fragmentSource: string
): { vertexSource: string; fragmentSource: string } {
const isWebGL2 = engine._hardwareRenderer.isWebGL2;
const shaderMacroList = new Array<ShaderMacro>();
ShaderMacro._getMacrosElements(macroCollection, shaderMacroList);
shaderMacroList.push(ShaderMacro.getByName(isWebGL2 ? "GRAPHICS_API_WEBGL2" : "GRAPHICS_API_WEBGL1"));
if (engine._hardwareRenderer.canIUse(GLCapabilityType.shaderTextureLod)) {
shaderMacroList.push(ShaderMacro.getByName("HAS_TEX_LOD"));
}
if (engine._hardwareRenderer.canIUse(GLCapabilityType.standardDerivatives)) {
shaderMacroList.push(ShaderMacro.getByName("HAS_DERIVATIVES"));
}
let noIncludeVertex = ShaderFactory.parseIncludes(vertexSource);
let noIncludeFrag = ShaderFactory.parseIncludes(fragmentSource);
const macroStr = ShaderFactory.parseCustomMacros(shaderMacroList);
noIncludeVertex = macroStr + noIncludeVertex;
noIncludeFrag = macroStr + noIncludeFrag;
if (isWebGL2) {
noIncludeVertex = ShaderFactory.convertTo300(noIncludeVertex);
noIncludeFrag = ShaderFactory.convertTo300(noIncludeFrag, true);
}
const versionStr = isWebGL2 ? "#version 300 es" : "#version 100";
const precisionStr = `
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
precision highp int;
#else
precision mediump float;
precision mediump int;
#endif
`;
return {
vertexSource: `${versionStr}\nprecision highp float;\n${noIncludeVertex}`,
fragmentSource: `${versionStr}\n${isWebGL2 ? "" : ShaderFactory._shaderExtension}${precisionStr}${noIncludeFrag}`
};
}
static registerInclude(includeName: string, includeSource: string) {
if (ShaderLib[includeName]) {
throw `The "${includeName}" shader include already exist`;

View File

@@ -19,18 +19,22 @@ import { ShaderCompiler } from "@galacean/engine-shader-compiler";
import { vi, describe, expect, it } from "vitest";
const shaderCompiler = new ShaderCompiler();
// @ts-ignore
Shader._shaderCompiler = shaderCompiler;
const makePass = (name = "Default") =>
new ShaderPass(name, [], [], ShaderLanguage.GLSLES100);
describe("Shader", () => {
describe("Custom Shader", () => {
it("Shader", () => {
// Create shader
let customShader = Shader.create("customByStringCreate", customVS, customFS);
customShader = Shader.create("customByPassCreate", [new ShaderPass(customVS, customFS)]);
customShader = Shader.create("custom", [new SubShader("Default", [new ShaderPass(customVS, customFS)])]);
// Create shader via ShaderPass[] / SubShader[] overloads
Shader.create("customByPassCreate", [makePass()]);
const customShader = Shader.create("custom", [new SubShader("Default", [makePass()])]);
// Create same name shader
const errorSpy = vi.spyOn(console, "error");
Shader.create("custom", [new SubShader("Default", [new ShaderPass(customVS, customFS)])]);
Shader.create("custom", [new SubShader("Default", [makePass()])]);
expect(errorSpy).toHaveBeenCalledWith('Shader named "custom" already exists.');
vi.resetAllMocks();
@@ -342,23 +346,6 @@ describe("Shader", () => {
});
});
const customVS = `
attribute vec3 POSITION;
uniform mat4 renderer_MVPMat;
void main() {
gl_Position = renderer_MVPMat * vec4(POSITION, 1.0);
}
`;
const customFS = `
uniform vec4 material_BaseColor;
void main() {
gl_FragColor = material_BaseColor;
}
`;
const testShaderCompilerCode = `
Shader "Test-Default" {
SubShader "Default" {

View File

@@ -357,120 +357,4 @@ describe("Precompile Benchmark", async () => {
});
});
// ═══════════════════════════════════════════════════════════
// 6. Variant switch breakdown: CPU / GPU / Total
//
// CPU measured via _compileShaderSource / compilePlatformSource
// Total measured via _getCanonicalShaderProgram (CPU + GPU)
// GPU = Total - CPU
//
// GSP CPU: buildMacroList + evaluateShaderInstructions + convertTo300 + assemble
// GLSL CPU: buildMacroList + parseIncludes + parseCustomMacros + convertTo300 + assemble
// GPU: new ShaderProgram(engine, vs, fs) — WebGL compile + link
// ═══════════════════════════════════════════════════════════
describe("6. Variant switch: CPU / GPU / Total (PBR)", () => {
it("precompiled (GSP) vs raw GLSL path", () => {
// @ts-ignore
Shader._shaderCompiler = shaderCompiler;
const precompiled = shaderCompiler._precompile(PBRSource, ShaderLanguage.GLSLES100);
const forwardPassData = precompiled.subShaders[0].passes.find((p) => !p.isUsePass)!;
// GSP ShaderPass (with instructions)
const gspShaderPass = new ShaderPass(
forwardPassData.name,
forwardPassData.vertexShaderInstructions!,
forwardPassData.fragmentShaderInstructions!,
ShaderLanguage.GLSLES100,
forwardPassData.tags
);
// GLSL ShaderPass (raw source, no instructions → compilePlatformSource path)
const parsed = shaderCompiler._parseShaderSource(PBRSource);
const livePassSource = parsed.subShaders[0].passes.find((p) => !p.isUsePass)!;
const liveProg = shaderCompiler._parseShaderPass(
livePassSource.contents,
livePassSource.vertexEntry,
livePassSource.fragmentEntry,
ShaderLanguage.GLSLES100
)!;
// Use original CodeGen GLSL (with all #ifdef branches preserved)
const glslShaderPass = new ShaderPass(
livePassSource.name,
liveProg.vertex,
liveProg.fragment,
livePassSource.tags
);
Logger.disable();
const scenarios: Array<{ label: string; macros: ShaderMacroCollection }> = [
{ label: "empty", macros: new ShaderMacroCollection() },
{ label: "base (11)", macros: buildMacroCollection(baseMacros) },
{ label: "full (18)", macros: buildMacroCollection([...baseMacros, ...materialVariantMacros]) }
];
console.log("\n=== Variant Switch Breakdown (PBR Forward Pass) ===");
console.log(
"| Scenario | GSP CPU (ms) | GLSL CPU (ms) | GSP GPU (ms) | GLSL GPU (ms) | GSP Total (ms) | GLSL Total (ms) | GSP Size | GLSL Size |"
);
console.log(
"|----------|-------------|--------------|-------------|--------------|---------------|----------------|----------|------------|"
);
// Split-timing bench: measure CPU and GPU within the same iteration
function benchSplit(
shaderPass: ShaderPass,
macroCollection: ShaderMacroCollection,
compileMethod: string,
runs: number,
warmup: number
): { cpu: number; gpu: number; total: number; vsLen: number; fsLen: number } {
// Warmup
for (let i = 0; i < warmup; i++) {
// @ts-ignore
shaderPass._getCanonicalShaderProgram(engine, macroCollection);
}
const cpuTimes: number[] = [];
const gpuTimes: number[] = [];
let vsLen = 0;
let fsLen = 0;
for (let i = 0; i < runs; i++) {
// CPU: compile source
const t0 = performance.now();
// @ts-ignore
const { vertexSource, fragmentSource } = shaderPass[compileMethod](engine, macroCollection);
const t1 = performance.now();
vsLen = vertexSource.length;
fsLen = fragmentSource.length;
// GPU: create ShaderProgram
// @ts-ignore
new ShaderProgram(engine, vertexSource, fragmentSource);
const t2 = performance.now();
cpuTimes.push(t1 - t0);
gpuTimes.push(t2 - t1);
}
cpuTimes.sort((a, b) => a - b);
gpuTimes.sort((a, b) => a - b);
const cpuAvg = cpuTimes.reduce((s, t) => s + t, 0) / cpuTimes.length;
const gpuAvg = gpuTimes.reduce((s, t) => s + t, 0) / gpuTimes.length;
return { cpu: cpuAvg, gpu: gpuAvg, total: cpuAvg + gpuAvg, vsLen, fsLen };
}
for (const { label, macros } of scenarios) {
const gsp = benchSplit(gspShaderPass, macros, "_compileShaderSource", 10, 3);
const glsl = benchSplit(glslShaderPass, macros, "_compilePlatformSource", 10, 3);
console.log(
`| ${label.padEnd(8)} | ${gsp.cpu.toFixed(3).padStart(11)} | ${glsl.cpu.toFixed(3).padStart(12)} | ${gsp.gpu.toFixed(2).padStart(11)} | ${glsl.gpu.toFixed(2).padStart(12)} | ${gsp.total.toFixed(2).padStart(13)} | ${glsl.total.toFixed(2).padStart(14)} | ${(gsp.vsLen + gsp.fsLen).toString().padStart(9)} | ${(glsl.vsLen + glsl.fsLen).toString().padStart(10)} |`
);
}
Logger.enable();
});
});
});