331 lines
10 KiB
TypeScript
331 lines
10 KiB
TypeScript
import { EDITOR } from 'cc/env';
|
|
import { ShaderNode } from '../../base';
|
|
import {
|
|
ConcretePrecisionType,
|
|
TextureConcretePrecision,
|
|
NormalSpace,
|
|
NormalMapSpace,
|
|
ViewDirSpace,
|
|
PositionSpace,
|
|
SlotDefine,
|
|
INodeDataDefine,
|
|
} from '../../type';
|
|
import { ensureEnumDefines, fs, getEnumDefine, getEnumNames, path } from '../../utils';
|
|
import { ShaderSlot } from '../../slot';
|
|
import { ShaderProperty, ShaderPropertyType } from '../../property';
|
|
import { shaderContext } from '../../context';
|
|
import { Asset, Material, Texture2D, assetManager } from 'cc';
|
|
|
|
export enum MasterSlotType {
|
|
Vertex,
|
|
Fragment,
|
|
}
|
|
|
|
export declare class MasterSlotDefine extends SlotDefine {
|
|
slotType: MasterSlotType;
|
|
codeChunk: number;
|
|
}
|
|
|
|
function findConnectNodes(slot: ShaderSlot, nodes: ShaderNode[]) {
|
|
if (!slot.connectSlot) return;
|
|
|
|
const connectNode = slot.connectNode;
|
|
if (connectNode) {
|
|
if (nodes.includes(connectNode)) {
|
|
return;
|
|
}
|
|
|
|
connectNode.inputs.forEach(slot => {
|
|
findConnectNodes(slot, nodes);
|
|
});
|
|
|
|
nodes.push(connectNode);
|
|
}
|
|
}
|
|
|
|
export default class MasterNode extends ShaderNode {
|
|
|
|
// vsSlotIndices: string[] = [];
|
|
// fsSlotIndices: string[] = [];
|
|
|
|
get templatePath() {
|
|
return '';
|
|
}
|
|
|
|
isMasterNode = true;
|
|
concretePrecisionType = ConcretePrecisionType.Fixed;
|
|
|
|
properties: ShaderProperty[] = [];
|
|
|
|
data: INodeDataDefine = {
|
|
inputs: [],
|
|
};
|
|
|
|
getConnectNodes(slots: ShaderSlot[]) {
|
|
const nodes: ShaderNode[] = [];
|
|
slots.forEach(slot => {
|
|
findConnectNodes(slot, nodes);
|
|
});
|
|
|
|
nodes.sort((a, b) => b.priority - a.priority);
|
|
return nodes;
|
|
}
|
|
|
|
generatePropertiesCode() {
|
|
let uniform = '\n';
|
|
let mtl = '\n';
|
|
let uniformSampler = '';
|
|
|
|
const properties = shaderContext.properties;
|
|
properties.sort((a, b) => {
|
|
return b.concretePrecision - a.concretePrecision;
|
|
});
|
|
|
|
let blockUniformCount = 0;
|
|
|
|
properties.forEach(p => {
|
|
let precision = '';
|
|
let mtlValue = '';
|
|
|
|
const value = p.value;
|
|
const isColor = p.type === ShaderPropertyType.Color;
|
|
const x = value.x;
|
|
const y = value.y;
|
|
const z = value.z;
|
|
const w = value.w;
|
|
|
|
if (p.type === ShaderPropertyType.Texture2D) {
|
|
precision = 'sampler2D';
|
|
mtlValue = 'white';
|
|
}
|
|
else if (p.type === ShaderPropertyType.TextureCube) {
|
|
precision = 'samplerCube';
|
|
mtlValue = 'white';
|
|
}
|
|
else {
|
|
const concretePrecision = p.concretePrecision;
|
|
if (concretePrecision === 1) {
|
|
precision = 'float';
|
|
mtlValue = `${value}`;
|
|
}
|
|
else if (concretePrecision === 2) {
|
|
precision = 'vec2';
|
|
mtlValue = `[${x}, ${y}]`;
|
|
}
|
|
else if (concretePrecision === 3) {
|
|
precision = 'vec4';
|
|
mtlValue = `[${x}, ${y}, ${z}, 0]`;
|
|
}
|
|
else if (concretePrecision === 4) {
|
|
precision = 'vec4';
|
|
mtlValue = `[${x}, ${y}, ${z}, ${w}]`;
|
|
}
|
|
}
|
|
|
|
const editorStr = isColor ? `, editor: { type: color }` : '';
|
|
|
|
if (!p.isTexture()) {
|
|
uniform += ` ${precision} ${p.name};\n`;
|
|
blockUniformCount++;
|
|
}
|
|
else {
|
|
uniformSampler += ` uniform ${precision} ${p.name};\n`;
|
|
}
|
|
mtl += ` ${p.name}: { value: ${mtlValue} ${editorStr}}\n`;
|
|
});
|
|
|
|
if (blockUniformCount === 0) {
|
|
uniform += ' vec4 empty_value;\n';
|
|
}
|
|
|
|
return {
|
|
uniform,
|
|
uniformSampler,
|
|
mtl,
|
|
};
|
|
}
|
|
|
|
replaceChunks(code: string) {
|
|
const depChunks: string[] = ['common'];
|
|
const allNodes = shaderContext.allNodes;
|
|
allNodes.forEach(node => {
|
|
for (let k = 0; k < node.depChunks.length; k++) {
|
|
if (!depChunks.includes(node.depChunks[k])) {
|
|
depChunks.push(node.depChunks[k]);
|
|
}
|
|
}
|
|
});
|
|
|
|
let chunkIncludes = '\n';
|
|
let chunks = '\n';
|
|
depChunks.forEach(chunkName => {
|
|
const chunkPath = path.join(shaderContext.shaderTemplatesDir, `chunks/${chunkName}.chunk`);
|
|
const chunk = fs.readFileSync(chunkPath, 'utf-8');
|
|
if (!chunk) {
|
|
console.error(`Can not find chunk with path [${chunkPath}]`);
|
|
return;
|
|
}
|
|
chunks += chunk + '\n';
|
|
chunkIncludes += ` #include <shader_graph_${chunkName}>\n`;
|
|
});
|
|
|
|
code = code.replace('{{chunks}}', chunks);
|
|
code = code.replace('{{vs_chunks}}', chunkIncludes);
|
|
code = code.replace('{{fs_chunks}}', chunkIncludes);
|
|
|
|
return code;
|
|
}
|
|
|
|
generateDefines(code: string) {
|
|
const defines: string[] = [];
|
|
const allNodes = shaderContext.allNodes;
|
|
allNodes.forEach(node => {
|
|
node.defines.forEach(def => {
|
|
if (!defines.includes(def)) {
|
|
defines.push(def);
|
|
}
|
|
});
|
|
});
|
|
|
|
let define = '';
|
|
defines.forEach(df => {
|
|
define += `${df}\n`;
|
|
});
|
|
|
|
define = ensureEnumDefines(NormalSpace, define);
|
|
define = ensureEnumDefines(PositionSpace, define);
|
|
define = ensureEnumDefines(ViewDirSpace, define);
|
|
|
|
// add spaces
|
|
let lines = define.split('\n');
|
|
lines = lines.map(l => ' ' + l);
|
|
define = lines.join('\n');
|
|
|
|
return code.replace(/{{defines}}/g, define);
|
|
}
|
|
|
|
generateSlotsCode(slots: ShaderSlot[]) {
|
|
const code: string[] = ['\n'];
|
|
|
|
const nodes = this.getConnectNodes(slots);
|
|
nodes.forEach(node => {
|
|
node.calcConcretePrecision();
|
|
node.generateCode().split('\n').forEach(c => {
|
|
if (c) {
|
|
c += ` // ${node.constructor.name}`;
|
|
code.push(' ' + c);
|
|
}
|
|
});
|
|
});
|
|
|
|
return code.join('\n');
|
|
}
|
|
|
|
generateCodeChunk(code) {
|
|
const codeChunkSlots: any[] = [];
|
|
this.inputs.forEach(input => {
|
|
const data = input.data as MasterSlotDefine;
|
|
if (!codeChunkSlots[data.codeChunk]) {
|
|
codeChunkSlots[data.codeChunk] = [];
|
|
}
|
|
|
|
codeChunkSlots[data.codeChunk].push(input);
|
|
});
|
|
|
|
codeChunkSlots.forEach((slots, chunkIdx) => {
|
|
const codeChunk = this.generateSlotsCode(slots);
|
|
code = code.replace(`{{code_chunk_${chunkIdx}}}`, codeChunk);
|
|
|
|
// console.log(`{{code_chunk_${chunkIdx}}} : \n ` + codeChunk);
|
|
});
|
|
|
|
return code;
|
|
}
|
|
|
|
generateCode() {
|
|
let code = fs.readFileSync(this.templatePath, 'utf-8');
|
|
|
|
code = this.generateCodeChunk(code);
|
|
code = this.generateDefines(code);
|
|
code = this.replaceChunks(code);
|
|
|
|
if (!shaderContext.properties || shaderContext.properties.length === 0) {
|
|
code = code.replace(/properties: &props/g, '');
|
|
code = code.replace(/properties: \*props/g, '');
|
|
}
|
|
|
|
const props = this.generatePropertiesCode();
|
|
code = code.replace('{{properties}}', props.uniform);
|
|
code = code.replace('{{properties_sampler}}', props.uniformSampler);
|
|
code = code.replace('{{properties_mtl}}', props.mtl);
|
|
|
|
// 如果 slot 没有连接,使用 template 中定义的默认值
|
|
const slotsToUseTemplateDefault = ['Vertex Position', 'Vertex Normal', 'Vertex Tangent', 'Position'];
|
|
|
|
this.inputs.forEach(slot => {
|
|
const tempName = `slot_${slot.displayName.replace(/ /g, '_')}`;
|
|
let value;
|
|
if (slotsToUseTemplateDefault.includes(slot.displayName) || slot.displayName === 'Normal') {
|
|
if (slot.connectSlot) {
|
|
value = slot.slotValue;
|
|
}
|
|
}
|
|
else {
|
|
value = slot.slotValue;
|
|
}
|
|
|
|
const reg = new RegExp(`{{${tempName} *= *(.*)}}`, 'g');
|
|
if (value === undefined) {
|
|
const res = reg.exec(code);
|
|
if (res) {
|
|
value = res[1];
|
|
}
|
|
}
|
|
code = code.replace(reg, value);
|
|
});
|
|
|
|
// vertexSlotNames.forEach(name => {
|
|
// const tempName = `slot_${name.replace(/ /g, '_')}`;
|
|
// let value = '';
|
|
// const reg = new RegExp(`{{${tempName} *= *(.*)}}`, 'g');
|
|
// const res = reg.exec(code);
|
|
// if (res) {
|
|
// value = res[1];
|
|
// }
|
|
// code = code.replace(reg, value);
|
|
// });
|
|
|
|
return code;
|
|
}
|
|
|
|
async createMaterial(buildEffect: (name: string, code: string) => Promise<any/*IEffectInfo*/>) {
|
|
const code = this.generateCode();
|
|
|
|
const material = new Material();
|
|
const name = 'shader-graph-preview.effect';
|
|
const effect = await buildEffect(name, code);
|
|
const result = new cc.EffectAsset();
|
|
Object.assign(result, effect);
|
|
result.onLoaded();
|
|
material.initialize({ effectAsset: effect });
|
|
|
|
await Promise.all(shaderContext.properties.map(async p => {
|
|
if (p.type === ShaderPropertyType.Texture2D || p.type === ShaderPropertyType.TextureCube) {
|
|
const uuid = (p.value as Texture2D).uuid;
|
|
return new Promise(resolve => {
|
|
assetManager.loadAny(uuid, (err: any, asset: Texture2D) => {
|
|
if (err) {
|
|
console.error(err);
|
|
return resolve(null);
|
|
}
|
|
material.setProperty(p.name, asset);
|
|
resolve(null);
|
|
});
|
|
});
|
|
}
|
|
}));
|
|
|
|
return material;
|
|
}
|
|
}
|