第15.2章:着色器开发工作流

建立高效的着色器开发工作流是提升开发效率的关键。本教程将深入讲解如何设计和实施专业的着色器开发流程,包括项目结构、版本控制、测试流程等。

🎯 学习目标

  • 建立标准化的着色器项目结构
  • 掌握着色器版本控制最佳实�?- 学会实施自动化测试流�?- 了解团队协作和代码审查机�?

📁 项目结构设计

标准化目录结�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
cocos-shader-project/
├── assets/
�? └── shaders/
�? ├── common/ # 通用着色器
�? �? ├── includes/ # 公共Include文件
�? �? ├── utilities/ # 工具函数
�? �? └── templates/ # 着色器模板
�? ├── effects/ # 特效着色器
�? �? ├── particles/ # 粒子特效
�? �? ├── post-processing/ # 后处�?�? �? └── ui/ # UI特效
�? ├── materials/ # 材质着色器
�? �? ├── pbr/ # PBR材质
�? �? ├── toon/ # 卡通材�?�? �? └── custom/ # 自定义材�?�? └── experimental/ # 实验性着色器
├── tools/
�? ├── build/ # 构建工具
�? ├── validation/ # 验证工具
�? ├── optimization/ # 优化工具
�? └── documentation/ # 文档生成
├── tests/
�? ├── unit/ # 单元测试
�? ├── integration/ # 集成测试
�? └── performance/ # 性能测试
├── docs/
�? ├── guides/ # 开发指�?�? ├── api/ # API文档
�? └── examples/ # 示例代码
├── .vscode/ # VSCode配置
├── .github/ # GitHub Actions
├── package.json # 项目配置
├── tsconfig.json # TypeScript配置
├── .gitignore # Git忽略文件
└── README.md # 项目说明

项目配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// package.json
{
"name": "cocos-shader-project",
"version": "1.0.0",
"description": "专业的Cocos Creator着色器项目",
"scripts": {
"build": "node tools/build/build-shaders.js",
"test": "node tools/validation/run-tests.js",
"lint": "node tools/validation/lint-shaders.js",
"optimize": "node tools/optimization/optimize-shaders.js",
"docs": "node tools/documentation/generate-docs.js",
"watch": "node tools/build/watch-shaders.js",
"dev": "npm run watch & npm run test:watch",
"validate": "npm run lint && npm run test",
"release": "npm run validate && npm run build && npm run optimize",
"clean": "rimraf dist/ temp/ .cache/",
"setup": "node tools/setup/init-project.js"
},
"devDependencies": {
"@types/node": "^18.0.0",
"typescript": "^4.7.0",
"chokidar": "^3.5.0",
"yaml": "^2.1.0",
"chalk": "^4.1.0",
"fast-glob": "^3.2.0",
"rimraf": "^3.0.0",
"jest": "^28.0.0"
},
"engines": {
"node": ">=16.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/your-org/cocos-shader-project.git"
}
}

TypeScript配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./tools",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"baseUrl": ".",
"paths": {
"@tools/*": ["tools/*"],
"@assets/*": ["assets/*"],
"@tests/*": ["tests/*"]
}
},
"include": [
"tools/**/*",
"tests/**/*",
"types/**/*"
],
"exclude": [
"node_modules",
"dist",
"temp",
".cache"
]
}

🔄 开发工作流

着色器构建工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
// tools/build/build-shaders.ts
import * as fs from 'fs';
import * as path from 'path';
import * as yaml from 'yaml';
import { glob } from 'fast-glob';
import chalk from 'chalk';

interface ShaderBuildConfig {
inputDir: string;
outputDir: string;
optimization: boolean;
validation: boolean;
platforms: string[];
profiles: BuildProfile[];
}

interface BuildProfile {
name: string;
target: 'webgl1' | 'webgl2' | 'mobile' | 'desktop';
optimization: OptimizationSettings;
defines: Record<string, any>;
}

interface OptimizationSettings {
minify: boolean;
stripComments: boolean;
inlineIncludes: boolean;
optimizeConstants: boolean;
removeUnusedCode: boolean;
}

class ShaderBuilder {
private config: ShaderBuildConfig;
private includeCache: Map<string, string> = new Map();

constructor(configPath: string) {
this.config = this.loadConfig(configPath);
}

private loadConfig(configPath: string): ShaderBuildConfig {
const configContent = fs.readFileSync(configPath, 'utf-8');
return yaml.parse(configContent) as ShaderBuildConfig;
}

public async build(): Promise<void> {
console.log(chalk.blue('🚀 开始构建着色器...'));

try {
// 清理输出目录
await this.cleanOutputDir();

// 扫描着色器文件
const shaderFiles = await this.scanShaderFiles();
console.log(chalk.green(`📂 发现 ${shaderFiles.length} 个着色器文件`));

// 为每个构建配置文件构�? for (const profile of this.config.profiles) {
await this.buildProfile(profile, shaderFiles);
}

console.log(chalk.green('�?着色器构建完成'));
} catch (error) {
console.error(chalk.red('�?构建失败:'), error);
process.exit(1);
}
}

private async cleanOutputDir(): Promise<void> {
if (fs.existsSync(this.config.outputDir)) {
fs.rmSync(this.config.outputDir, { recursive: true, force: true });
}
fs.mkdirSync(this.config.outputDir, { recursive: true });
}

private async scanShaderFiles(): Promise<string[]> {
const patterns = [
`${this.config.inputDir}/**/*.effect`,
`${this.config.inputDir}/**/*.chunk`
];

return await glob(patterns, {
ignore: ['**/node_modules/**', '**/temp/**', '**/.*/**']
});
}

private async buildProfile(profile: BuildProfile, shaderFiles: string[]): Promise<void> {
console.log(chalk.yellow(`📦 构建配置: ${profile.name}`));

const outputDir = path.join(this.config.outputDir, profile.name);
fs.mkdirSync(outputDir, { recursive: true });

for (const filePath of shaderFiles) {
try {
await this.processShaderFile(filePath, profile, outputDir);
} catch (error) {
console.error(chalk.red(`�?处理文件失败 ${filePath}:`), error);
throw error;
}
}
}

private async processShaderFile(filePath: string, profile: BuildProfile, outputDir: string): Promise<void> {
const content = fs.readFileSync(filePath, 'utf-8');
let processedContent = content;

// 应用预处理器定义
processedContent = this.applyDefines(processedContent, profile.defines);

// 内联Include文件
if (profile.optimization.inlineIncludes) {
processedContent = await this.inlineIncludes(processedContent);
}

// 优化着色器代码
if (profile.optimization.optimizeConstants) {
processedContent = this.optimizeConstants(processedContent);
}

// 移除未使用的代码
if (profile.optimization.removeUnusedCode) {
processedContent = this.removeUnusedCode(processedContent);
}

// 压缩代码
if (profile.optimization.minify) {
processedContent = this.minifyShader(processedContent);
}

// 移除注释
if (profile.optimization.stripComments) {
processedContent = this.stripComments(processedContent);
}

// 验证着色器
if (this.config.validation) {
await this.validateShader(processedContent, filePath);
}

// 写入输出文件
const relativePath = path.relative(this.config.inputDir, filePath);
const outputPath = path.join(outputDir, relativePath);

fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, processedContent);

console.log(chalk.green(` �?${relativePath}`));
}

private applyDefines(content: string, defines: Record<string, any>): string {
let result = content;

for (const [key, value] of Object.entries(defines)) {
const definePattern = new RegExp(`#pragma\\s+define\\s+${key}\\s+.*`, 'g');
result = result.replace(definePattern, `#define ${key} ${value}`);
}

return result;
}

private async inlineIncludes(content: string): Promise<string> {
const includePattern = /#include\s+<([^>]+)>/g;
let result = content;
let match;

while ((match = includePattern.exec(content)) !== null) {
const includePath = match[1];
const includeContent = await this.loadIncludeFile(includePath);

result = result.replace(match[0], includeContent);
}

return result;
}

private async loadIncludeFile(includePath: string): Promise<string> {
if (this.includeCache.has(includePath)) {
return this.includeCache.get(includePath)!;
}

const fullPath = path.join(this.config.inputDir, 'common/includes', includePath);

if (!fs.existsSync(fullPath)) {
throw new Error(`Include文件不存�? ${includePath}`);
}

const content = fs.readFileSync(fullPath, 'utf-8');
this.includeCache.set(includePath, content);

return content;
}

private optimizeConstants(content: string): string {
// 常量折叠优化
let result = content;

// 简单的数学常量折叠
result = result.replace(/(\d+\.\d+)\s*\+\s*(\d+\.\d+)/g, (match, a, b) => {
return (parseFloat(a) + parseFloat(b)).toString();
});

result = result.replace(/(\d+\.\d+)\s*\*\s*(\d+\.\d+)/g, (match, a, b) => {
return (parseFloat(a) * parseFloat(b)).toString();
});

return result;
}

private removeUnusedCode(content: string): string {
// 移除未使用的函数和变�? // 这是一个简化的实现,实际需要更复杂的AST分析
const lines = content.split('\n');
const usedSymbols = new Set<string>();
const definedSymbols = new Map<string, number>();

// 第一遍:收集所有定义的符号
lines.forEach((line, index) => {
const functionMatch = line.match(/(?:float|vec2|vec3|vec4|mat3|mat4|bool|int)\s+(\w+)\s*\(/);
if (functionMatch) {
definedSymbols.set(functionMatch[1], index);
}

const variableMatch = line.match(/(?:uniform|varying|attribute)\s+\w+\s+(\w+);/);
if (variableMatch) {
definedSymbols.set(variableMatch[1], index);
}
});

// 第二遍:收集使用的符�? lines.forEach(line => {
for (const symbol of definedSymbols.keys()) {
if (line.includes(symbol) && !line.match(new RegExp(`\\b(?:float|vec2|vec3|vec4|mat3|mat4|bool|int)\\s+${symbol}\\s*[\\(;]`))) {
usedSymbols.add(symbol);
}
}
});

// 移除未使用的定义
return lines.filter((line, index) => {
for (const [symbol, defIndex] of definedSymbols.entries()) {
if (defIndex === index && !usedSymbols.has(symbol)) {
return false;
}
}
return true;
}).join('\n');
}

private minifyShader(content: string): string {
let result = content;

// 移除多余的空白字�? result = result.replace(/\s+/g, ' ');
result = result.replace(/\s*([{}();,])\s*/g, '$1');
result = result.replace(/^\s+|\s+$/gm, '');

// 移除空行
result = result.replace(/\n\s*\n/g, '\n');

return result;
}

private stripComments(content: string): string {
// 移除单行注释
let result = content.replace(/\/\/.*$/gm, '');

// 移除多行注释
result = result.replace(/\/\*[\s\S]*?\*\//g, '');

return result;
}

private async validateShader(content: string, filePath: string): Promise<void> {
// 基本的语法验�? const errors: string[] = [];

// 检查括号匹�? let braceCount = 0;
let parenCount = 0;

for (const char of content) {
switch (char) {
case '{': braceCount++; break;
case '}': braceCount--; break;
case '(': parenCount++; break;
case ')': parenCount--; break;
}
}

if (braceCount !== 0) {
errors.push('大括号不匹配');
}

if (parenCount !== 0) {
errors.push('小括号不匹配');
}

// 检查必需的函�? if (!content.includes('void main()') && !content.includes('void vert()') && !content.includes('void frag()')) {
errors.push('缺少主函�?);
}

if (errors.length > 0) {
throw new Error(`着色器验证失败 ${filePath}: ${errors.join(', ')}`);
}
}
}

// 使用示例
if (require.main === module) {
const builder = new ShaderBuilder('./tools/build/build-config.yaml');
builder.build().catch(console.error);
}

构建配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# tools/build/build-config.yaml
inputDir: "./assets/shaders"
outputDir: "./dist/shaders"
optimization: true
validation: true
platforms:
- webgl1
- webgl2
- mobile

profiles:
- name: "development"
target: "webgl2"
optimization:
minify: false
stripComments: false
inlineIncludes: false
optimizeConstants: false
removeUnusedCode: false
defines:
DEBUG: 1
QUALITY_LEVEL: 2

- name: "production"
target: "webgl2"
optimization:
minify: true
stripComments: true
inlineIncludes: true
optimizeConstants: true
removeUnusedCode: true
defines:
DEBUG: 0
QUALITY_LEVEL: 3

- name: "mobile"
target: "mobile"
optimization:
minify: true
stripComments: true
inlineIncludes: true
optimizeConstants: true
removeUnusedCode: true
defines:
DEBUG: 0
QUALITY_LEVEL: 1
MOBILE: 1
USE_LOWP: 1

🔍 自动化测�?

着色器测试框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
// tools/validation/shader-test-framework.ts
import * as fs from 'fs';
import * as path from 'path';
import chalk from 'chalk';

interface TestCase {
name: string;
shaderPath: string;
expectedOutput?: string;
expectedErrors?: string[];
timeout?: number;
platform?: string;
skipReason?: string;
}

interface TestResult {
testCase: TestCase;
passed: boolean;
error?: string;
duration: number;
output?: any;
}

interface TestSuite {
name: string;
description: string;
testCases: TestCase[];
setup?: () => Promise<void>;
teardown?: () => Promise<void>;
}

class ShaderTestRunner {
private testSuites: TestSuite[] = [];
private results: TestResult[] = [];

public addTestSuite(suite: TestSuite): void {
this.testSuites.push(suite);
}

public async runAllTests(): Promise<TestReport> {
console.log(chalk.blue('🧪 开始运行着色器测试...'));

const startTime = Date.now();
this.results = [];

for (const suite of this.testSuites) {
await this.runTestSuite(suite);
}

const endTime = Date.now();
const duration = endTime - startTime;

return this.generateReport(duration);
}

private async runTestSuite(suite: TestSuite): Promise<void> {
console.log(chalk.yellow(`📁 测试套件: ${suite.name}`));

// 执行设置
if (suite.setup) {
await suite.setup();
}

try {
for (const testCase of suite.testCases) {
const result = await this.runTestCase(testCase);
this.results.push(result);

const status = result.passed ? chalk.green('�?) : chalk.red('�?);
const duration = chalk.gray(`(${result.duration}ms)`);
console.log(` ${status} ${testCase.name} ${duration}`);

if (!result.passed && result.error) {
console.log(chalk.red(` 错误: ${result.error}`));
}
}
} finally {
// 执行清理
if (suite.teardown) {
await suite.teardown();
}
}
}

private async runTestCase(testCase: TestCase): Promise<TestResult> {
const startTime = Date.now();

try {
// 检查是否应该跳�? if (testCase.skipReason) {
return {
testCase,
passed: true,
duration: 0
};
}

// 运行测试
const output = await this.executeTest(testCase);

// 验证结果
const passed = this.validateTestResult(testCase, output);

return {
testCase,
passed,
duration: Date.now() - startTime,
output
};
} catch (error) {
return {
testCase,
passed: false,
error: error instanceof Error ? error.message : String(error),
duration: Date.now() - startTime
};
}
}

private async executeTest(testCase: TestCase): Promise<any> {
// 加载着色器文件
const shaderContent = fs.readFileSync(testCase.shaderPath, 'utf-8');

// 解析着色器
const shader = this.parseShader(shaderContent);

// 编译着色器
const compilationResult = await this.compileShader(shader, testCase.platform);

// 运行着色器(如果支持)
const runtimeResult = await this.runShader(compilationResult);

return {
compilation: compilationResult,
runtime: runtimeResult
};
}

private parseShader(content: string): ParsedShader {
// 解析CCEffect和CCProgram�? const effectMatch = content.match(/CCEffect\s*%\{([\s\S]*?)\}%/);
const programMatches = [...content.matchAll(/CCProgram\s+(\w+)\s*%\{([\s\S]*?)\}%/g)];

return {
effect: effectMatch ? effectMatch[1] : null,
programs: programMatches.map(match => ({
name: match[1],
source: match[2]
}))
};
}

private async compileShader(shader: ParsedShader, platform?: string): Promise<CompilationResult> {
const errors: string[] = [];
const warnings: string[] = [];

// 验证CCEffect语法
if (shader.effect) {
try {
const yaml = require('yaml');
yaml.parse(shader.effect);
} catch (error) {
errors.push(`CCEffect YAML语法错误: ${error.message}`);
}
}

// 验证GLSL语法
for (const program of shader.programs) {
const glslErrors = this.validateGLSL(program.source);
errors.push(...glslErrors);
}

return {
success: errors.length === 0,
errors,
warnings,
platform: platform || 'webgl2'
};
}

private validateGLSL(source: string): string[] {
const errors: string[] = [];

// 基本语法检�? let braceCount = 0;
let parenCount = 0;

for (const char of source) {
switch (char) {
case '{': braceCount++; break;
case '}': braceCount--; break;
case '(': parenCount++; break;
case ')': parenCount--; break;
}
}

if (braceCount !== 0) {
errors.push('大括号不匹配');
}

if (parenCount !== 0) {
errors.push('小括号不匹配');
}

// 检查必需的精度声�? if (source.includes('float') && !source.includes('precision')) {
errors.push('缺少精度声明');
}

return errors;
}

private async runShader(compilationResult: CompilationResult): Promise<RuntimeResult> {
// 简化的运行时测�? // 实际实现需要WebGL上下�? return {
executed: compilationResult.success,
renderTime: compilationResult.success ? Math.random() * 16 : 0,
memoryUsage: compilationResult.success ? Math.random() * 1024 : 0
};
}

private validateTestResult(testCase: TestCase, output: any): boolean {
// 检查编译是否成�? if (!output.compilation.success) {
// 如果期望有错误,检查错误是否匹�? if (testCase.expectedErrors) {
return testCase.expectedErrors.every(expectedError =>
output.compilation.errors.some((error: string) =>
error.includes(expectedError)
)
);
}
return false;
}

// 如果期望有错误但编译成功,测试失�? if (testCase.expectedErrors && testCase.expectedErrors.length > 0) {
return false;
}

// 检查预期输�? if (testCase.expectedOutput) {
// 这里可以添加更复杂的输出验证逻辑
return true;
}

return true;
}

private generateReport(duration: number): TestReport {
const total = this.results.length;
const passed = this.results.filter(r => r.passed).length;
const failed = total - passed;
const skipped = this.results.filter(r => r.testCase.skipReason).length;

return {
summary: {
total,
passed,
failed,
skipped,
duration,
passRate: total > 0 ? (passed / total) * 100 : 0
},
results: this.results,
timestamp: new Date().toISOString()
};
}
}

interface ParsedShader {
effect: string | null;
programs: Array<{
name: string;
source: string;
}>;
}

interface CompilationResult {
success: boolean;
errors: string[];
warnings: string[];
platform: string;
}

interface RuntimeResult {
executed: boolean;
renderTime: number;
memoryUsage: number;
}

interface TestReport {
summary: {
total: number;
passed: number;
failed: number;
skipped: number;
duration: number;
passRate: number;
};
results: TestResult[];
timestamp: string;
}

// 预定义测试套�?export const basicShaderTests: TestSuite = {
name: "基础着色器测试",
description: "测试基本的着色器功能",
testCases: [
{
name: "简单顶点着色器编译",
shaderPath: "tests/fixtures/simple-vertex.effect"
},
{
name: "简单片段着色器编译",
shaderPath: "tests/fixtures/simple-fragment.effect"
},
{
name: "Surface Shader编译",
shaderPath: "tests/fixtures/simple-surface.effect"
},
{
name: "语法错误检�?,
shaderPath: "tests/fixtures/syntax-error.effect",
expectedErrors: ["大括号不匹配"]
}
]
};

export const performanceTests: TestSuite = {
name: "性能测试",
description: "测试着色器性能特征",
testCases: [
{
name: "复杂片段着色器性能",
shaderPath: "tests/fixtures/complex-fragment.effect",
timeout: 5000
},
{
name: "大量纹理采样性能",
shaderPath: "tests/fixtures/many-textures.effect",
timeout: 3000
}
]
};

📝 本章小结

通过本教程,你应该掌握了�?

  1. 项目结构: 建立标准化的着色器项目结构
  2. 构建工具: 实现自动化的着色器构建流程
  3. 测试框架: 建立完整的着色器测试体系
  4. 工作流程: 掌握专业的着色器开发工作流

🚀 下一步学�?

继续学习着色器资源管理!📦✨