From 6a38c183eaa47344b93086b3936f4e96937168c2 Mon Sep 17 00:00:00 2001 From: Andrew Ray Date: Sun, 16 Aug 2026 16:57:38 -0700 Subject: [PATCH] Updating export capability --- README.md | 87 +++++++++- package.json | 2 +- src/plugins/three/FrogMaterial.ts | 22 ++- src/plugins/three/index.ts | 3 + src/plugins/three/threngine.ts | 264 +++++++++++++++++++++++------- 5 files changed, 305 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index dad9f54..ea5afc8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,91 @@ # Shaderfrog Core -🚨 This library is experimental! 🚨 +ShaderFrog Core is the core library that powers [Shaderfrog.com](https://shaderfrog.com/). You're proably here for exported materials from Shaderfrog using `FrogMaterial`. + +# FrogMaterial: Three.js Material Export + +`FrogMaterial` creates an extensible Three.js material with a high degree of control over the source code, making Three.js materials more customizable than the standard path using [onBeforeCompile](https://threejs.org/docs/#Material.onBeforeCompile). + +Usage example: + +```ts +import { MesHPhysicalMaterial } from 'three'; +import { FrogMaterial } from '@shaderfrog/core/plugins/three'; + +const material = new FrogMaterial({ + baseMaterial: MeshPhysicalMaterial, + materialName: 'MeshPhysicalMaterial', + fragmentShader, + fragmentOutput: "(main_Edge_Glow()+ main_MeshPhysicalMaterial())", + vertexShader, + vertexOutput: "main_Parallax();\n\n \n main_Edge_Glow();\n\n \n main_Striped_Mandelbrot();\n\n \n main_Julia();\n\n \n gl_Position = main_MeshPhysicalMaterial();\n", + uniforms, + map: "main_Parallax()", + fragmentInjections: [{ + search: new RegExp("(normal = ).+;"), replace: "$1(vNormal + sampledDiffuseColor.rgb * 0.5);" + }], + vertexInjections: [{ + search: new RegExp("(normal = ).+;"), replace: "$1(vNormal + sampledDiffuseColor.rgb * 0.5);" + }], + metalness: 0, + roughness: 0.065, +}); +``` + +The FrogMaterial API: + +| Field | Type | Required | Description | +| --- | --- | --- | --- | +| `baseMaterial` | Three.js material constructor, e.g. `MeshPhysicalMaterial` | Yes | The Three.js material class to extend. `FrogMaterial` constructs an instance of this class and drives its `onBeforeCompile`. | +| `materialName` | `string` | No | Stable name used as the GLSL engine function prefix (e.g. `'MeshPhysicalMaterial'`). Falls back to `baseMaterial.name`, but that's mangled by minifiers, so set this explicitly in production bundles. | +| `fragmentShader` | `string` | Yes | GLSL source injected above `main()` in the fragment shader. Must declare a `main_()`-style function per `fragmentOutput`/injections below. | +| `fragmentOutput` | `string` | Yes | GLSL expression assigned to `gl_FragColor` in the generated `main()`, e.g. `"(main_Edge_Glow() + main_MeshPhysicalMaterial())"`. | +| `vertexShader` | `string` | Yes | GLSL source injected above `main()` in the vertex shader, mirroring `fragmentShader`. | +| `vertexOutput` | `string` | Yes | Statement(s) run inside the generated vertex `main()`; should end by assigning `gl_Position`. | +| `uniforms` | `Record` | No | Custom uniforms merged into `shader.uniforms` in `onBeforeCompile`. | +| `fragmentInjections` | `ShaderInjection[]` | No | Raw `{ search, replace }` patches applied to the final fragment shader source, after chunk expansion and output wiring. | +| `vertexInjections` | `ShaderInjection[]` | No | Same as `fragmentInjections`, applied to the final vertex shader source. | +| `onBeforeCompile` | `(shader: WebGLProgramParametersWithUniforms, renderer: WebGLRenderer) => void` | No | Called last, after all FrogMaterial transforms are applied to the shader. | +| *texture-injectable keys* (`map`, `normalMap`, `aoMap`, `emissiveMap`, `roughnessMap`, `specularMap`, `displacementMap`, `bumpMap`, `transmissionMap`, `gradientMap`, `thickness`, `transmission`, `position`) | `string \| Texture` | No | Any of `baseMaterial`'s own texture/property fields. Pass a `Texture` for normal Three.js behavior, or a GLSL expression string (e.g. `"main_Parallax()"`) to wire a generated function's output into that slot instead. | +| *(remaining fields)* | Whatever `baseMaterial`'s constructor accepts (e.g. `metalness`, `roughness`, `color`) | No | Any other property `baseMaterial`'s constructor takes is passed straight through, e.g. `metalness: 0, roughness: 0.065`. | + +#### `uniforms` + +Merged directly into the compiled shader's uniforms, using the same shape Three.js uniforms use: + +```ts +uniforms: { + time: { value: 0 }, + resolution: { value: new Vector2(1, 1) }, +} +``` + +#### `fragmentInjections` / `vertexInjections` + +Each entry is a `{ search: string | RegExp, replace: string }` pair applied via `shader.replace(search, replace)` against the fully assembled shader source, after chunk expansion and after `fragmentOutput`/`vertexOutput` wiring — use these for edits that can't be expressed as a plain injectable property: + +```ts +fragmentInjections: [ + { + search: new RegExp('(normal = ).+;'), + replace: '$1(vNormal + sampledDiffuseColor.rgb * 0.5);', + }, +], +``` + +#### `onBeforeCompile` + +Runs after FrogMaterial's own `onBeforeCompile` logic, so `shader.fragmentShader`/`shader.vertexShader` already reflect every injection above: + +```ts +onBeforeCompile: (shader, renderer) => { + shader.uniforms.time.value = performance.now() / 1000; +}, +``` + +# Core Shaderfrog Graph API -🚨 The API can change at any time! 🚨 +🚨 This Core Graph API is experimental and can change at any time! 🚨 The core graph API that powers Shaderfrog. This API, built on top of the [@Shaderfrog/glsl-parser](https://github.com/ShaderFrog/glsl-parser), compiles diff --git a/package.json b/package.json index 8c15318..ad09e42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shaderfrog/core", - "version": "4.0.0", + "version": "4.0.2", "description": "Shaderfrog core", "type": "module", "files": [ diff --git a/src/plugins/three/FrogMaterial.ts b/src/plugins/three/FrogMaterial.ts index f765737..eed2471 100644 --- a/src/plugins/three/FrogMaterial.ts +++ b/src/plugins/three/FrogMaterial.ts @@ -139,6 +139,7 @@ type ConstructorParams = C extends new ( type FrogSpecificKeys = | 'baseMaterial' + | 'materialName' | 'fragmentShader' | 'fragmentOutput' | 'vertexShader' @@ -152,6 +153,9 @@ export type FrogMaterialParams< C extends MaterialConstructor = MaterialConstructor, > = { baseMaterial: C; + /** Stable name used as the GLSL engine function prefix (e.g. 'MeshPhysicalMaterial'). + * Required in minified/production bundles where Function.name is mangled. */ + materialName?: string; fragmentShader: string; fragmentOutput: string; vertexShader: string; @@ -164,10 +168,12 @@ export type FrogMaterialParams< shader: WebGLProgramParametersWithUniforms, renderer: WebGLRenderer, ) => void; -} & Omit>, FrogSpecificKeys>; +} & Omit>, FrogSpecificKeys> & + Partial>; function _create({ baseMaterial: BaseMaterial, + materialName, fragmentShader, fragmentOutput, vertexShader, @@ -179,7 +185,7 @@ function _create({ ...baseProps }: FrogMaterialParams): Material { // Split baseProps: injectable strings become GLSL injections + dummy textures - const glslInjections: Array<{ find: RegExp; replace: string }> = []; + const glslInjections: ShaderInjection[] = []; const materialProps: Record = {}; for (const [key, value] of Object.entries( @@ -190,7 +196,7 @@ function _create({ FRAGMENT_INJECTABLE[key as InjectableKey] || VERTEX_INJECTABLES[key as InjectableKey]; if (inj) { - glslInjections.push({ find: inj.find, replace: inj.replace(value) }); + glslInjections.push({ search: inj.find, replace: inj.replace(value) }); if (inj.forceProperty) { materialProps[inj.forceProperty] = new Texture(); } @@ -203,7 +209,7 @@ function _create({ } const mat = new BaseMaterial(materialProps as ConstructorParams); - const engineFnName = `main_${BaseMaterial.name || 'BaseMaterial'}`; + const engineFnName = `main_${materialName || BaseMaterial.name || 'BaseMaterial'}`; mat.onBeforeCompile = (shader, renderer) => { Object.assign(shader.uniforms, uniforms); @@ -231,8 +237,8 @@ function _create({ `\n\nvec4 ${engineFnName}() {\n vec4 fragColor = vec4(0.0);`, ) + `\n\nvoid main() { gl_FragColor = ${fragmentOutput}; }`; - for (const { find, replace } of glslInjections) { - shader.fragmentShader = shader.fragmentShader.replace(find, replace); + for (const { search, replace } of glslInjections) { + shader.fragmentShader = shader.fragmentShader.replace(search, replace); } for (const { search, replace } of fragmentInjections) { @@ -261,8 +267,8 @@ function _create({ `\n\nvec4 ${engineFnName}() {\n vec4 fragPosition = vec4(0.0);`, ) + `\n\nvoid main() { ${vertexOutput} }`; - for (const { find, replace } of glslInjections) { - shader.vertexShader = shader.vertexShader.replace(find, replace); + for (const { search, replace } of glslInjections) { + shader.vertexShader = shader.vertexShader.replace(search, replace); } for (const { search, replace } of vertexInjections) { diff --git a/src/plugins/three/index.ts b/src/plugins/three/index.ts index d8cdbc9..d53f40b 100644 --- a/src/plugins/three/index.ts +++ b/src/plugins/three/index.ts @@ -4,6 +4,9 @@ export { createMaterial, threngine as engine }; export { createFrogMaterialResult, engineNodeTypeToConstructor, + engineNodeTypeToConstructorName, + prepareFrogMaterialExport, } from './threngine'; +export type { FrogMaterialExport } from './threngine'; export { FrogMaterial, expandChunks } from './FrogMaterial'; export type { FrogMaterialParams } from './FrogMaterial'; diff --git a/src/plugins/three/threngine.ts b/src/plugins/three/threngine.ts index 918ac0d..c582a10 100644 --- a/src/plugins/three/threngine.ts +++ b/src/plugins/three/threngine.ts @@ -30,7 +30,11 @@ import { prepopulatePropertyInputs, mangleMainFn } from '../../graph/graph'; import importers from './importers'; import { Engine, EngineContext, EngineNodeType } from '../../engine'; -import { doesLinkThruShader, CompileResult } from '../../graph/graph'; +import { + doesLinkThruShader, + CompileResult, + EngineNodeProperties, +} from '../../graph/graph'; import { filterSections, filterQualifiedStatements, @@ -73,7 +77,7 @@ export const phongNode = ( name: string, position: NodePosition, uniforms: UniformDataType[], - stage: ShaderStage | undefined + stage: ShaderStage | undefined, ): CodeNode => prepopulatePropertyInputs({ id, @@ -93,13 +97,13 @@ export const phongNode = ( 'Emissive Map', 'emissiveMap', 'texture', - 'filler_emissiveMap' + 'filler_emissiveMap', ), property( 'Emissive Intensity', 'emissiveIntensity', 'number', - 'uniform_emissive' + 'uniform_emissive', ), property('Texture', 'map', 'texture', 'filler_map'), property('Normal Map', 'normalMap', 'texture', 'filler_normalMap'), @@ -109,7 +113,7 @@ export const phongNode = ( 'AO Intensity', 'aoMapIntensity', 'number', - 'filler_aoMapIntensity' + 'filler_aoMapIntensity', ), property('Shininess', 'shininess', 'number'), property('Reflectivity', 'reflectivity', 'number'), @@ -119,13 +123,13 @@ export const phongNode = ( 'Specular Map', 'specularMap', 'texture', - 'filler_specularMap' + 'filler_specularMap', ), property( 'Displacement Map', 'displacementMap', 'texture', - 'filler_displacementMap' + 'filler_displacementMap', ), property('Displacement Scale', 'displacementScale', 'number'), property('Bump Map', 'bumpMap', 'texture', 'filler_bumpMap'), @@ -146,7 +150,7 @@ export const phongNode = ( 'filler', undefined, // Data type for what plugs into this filler ['code', 'data'], - true + true, ), ], outputs: [ @@ -166,7 +170,7 @@ export const physicalNode = ( name: string, position: NodePosition, uniforms: UniformDataType[], - stage: ShaderStage | undefined + stage: ShaderStage | undefined, ): CodeNode => prepopulatePropertyInputs({ id, @@ -190,7 +194,7 @@ export const physicalNode = ( 'AO Intensity', 'aoMapIntensity', 'number', - 'filler_aoMapIntensity' + 'filler_aoMapIntensity', ), property('Metalness', 'metalness', 'number', 'uniform_metalness'), property('Roughness', 'roughness', 'number', 'uniform_roughness'), @@ -198,13 +202,13 @@ export const physicalNode = ( 'Roughness Map', 'roughnessMap', 'texture', - 'filler_roughnessMap' + 'filler_roughnessMap', ), property( 'Displacement Map', 'displacementMap', 'texture', - 'filler_displacementMap' + 'filler_displacementMap', ), property('Displacement Scale', 'displacementScale', 'number'), // MeshPhysicalMaterial gets envMap from the scene. MeshStandardMaterial @@ -214,14 +218,14 @@ export const physicalNode = ( 'Env Map Intensity', 'envMapIntensity', 'number', - 'uniform_envMapIntensity' + 'uniform_envMapIntensity', ), property('Transmission', 'transmission', 'number'), property( 'Transmission Map', 'transmissionMap', 'texture', - 'filler_transmissionMap' + 'filler_transmissionMap', ), property('Thickness', 'thickness', 'number'), property('Index of Refraction', 'ior', 'number'), @@ -238,7 +242,7 @@ export const physicalNode = ( 'iridescenceThicknessRange', 'array', undefined, - ['100', '400'] + ['100', '400'], ), ], hardCodedProperties: { @@ -269,7 +273,7 @@ export const physicalNode = ( 'filler', undefined, // Data type for what plugs into this filler ['code', 'data'], - true + true, ), ] : [], @@ -300,7 +304,7 @@ export const defaultPropertySetting = (property: NodeProperty) => { const threeMaterialProperties = ( graph: Graph, node: SourceNode, - sibling?: SourceNode + sibling?: SourceNode, ): Record => { // Find inputs to this node that are dependent on a property of the material const propertyInputs = indexById(node.inputs.filter((i) => i.property)); @@ -314,7 +318,7 @@ const threeMaterialProperties = ( if (propertyInput) { // Find the property itself const property = (node.config.properties || []).find( - (p) => p.property === propertyInput.property + (p) => p.property === propertyInput.property, ) as NodeProperty; // Initialize the property on the material @@ -342,7 +346,7 @@ const programCacheKey = ( engineContext: EngineContext, graph: Graph, node: SourceNode, - sibling?: SourceNode + sibling?: SourceNode, ) => { const { scene } = engineContext.runtime; const lights: string[] = []; @@ -369,7 +373,7 @@ const programCacheKey = ( const onBeforeCompileMegaShader = ( engineContext: EngineContext, - newMat: any + newMat: any, ) => { log('Compiling three megashader!'); const { renderer, sceneData, scene, camera } = engineContext.runtime; @@ -417,7 +421,7 @@ const cacher = ( graph: Graph, node: SourceNode, sibling: SourceNode | undefined, - newValue: (...args: any[]) => any + newValue: (...args: any[]) => any, ) => { const cacheKey = programCacheKey(engineContext, graph, node, sibling); @@ -486,27 +490,27 @@ const evaluateNode = (node: DataNode) => { return new Vector3( parseFloat(node.value[0]), parseFloat(node.value[1]), - parseFloat(node.value[2]) + parseFloat(node.value[2]), ); } else if (node.type === 'vector4') { return new Vector4( parseFloat(node.value[0]), parseFloat(node.value[1]), parseFloat(node.value[2]), - parseFloat(node.value[3]) + parseFloat(node.value[3]), ); } else if (node.type === 'rgb') { return new Color( parseFloat(node.value[0]), parseFloat(node.value[1]), - parseFloat(node.value[2]) + parseFloat(node.value[2]), ); } else if (node.type === 'rgba') { return new Vector4( parseFloat(node.value[0]), parseFloat(node.value[1]), parseFloat(node.value[2]), - parseFloat(node.value[3]) + parseFloat(node.value[3]), ); } else { return node.value; @@ -518,7 +522,7 @@ export const toonNode = ( name: string, position: NodePosition, uniforms: UniformDataType[], - stage: ShaderStage | undefined + stage: ShaderStage | undefined, ): CodeNode => prepopulatePropertyInputs({ id, @@ -538,7 +542,7 @@ export const toonNode = ( 'Gradient Map', 'gradientMap', 'texture', - 'filler_gradientMap' + 'filler_gradientMap', ), property('Normal Map', 'normalMap', 'texture', 'filler_normalMap'), property('Normal Scale', 'normalScale', 'vector2'), @@ -547,13 +551,13 @@ export const toonNode = ( 'AO Intensity', 'aoMapIntensity', 'number', - 'filler_aoMapIntensity' + 'filler_aoMapIntensity', ), property( 'Displacement Map', 'displacementMap', 'texture', - 'filler_displacementMap' + 'filler_displacementMap', ), property('Displacement Scale', 'displacementScale', 'number'), property('Env Map', 'envMap', 'samplerCube'), @@ -680,7 +684,7 @@ export const threngine: Engine = { ast, inputEdges, node, - sibling + sibling, ) => { const programAst = ast as Program; const mainName = 'main'; // || nodeName(node); @@ -705,8 +709,8 @@ export const threngine: Engine = { // @ts-ignore isMeshPhongMaterial: true, ...threeMaterialProperties(graph, node, sibling), - }) - ) + }), + ), ), produceFiller: (_node, _ast) => @@ -721,8 +725,8 @@ export const threngine: Engine = { new MeshPhysicalMaterial({ ...node.config.hardCodedProperties, ...threeMaterialProperties(graph, node, sibling), - }) - ) + }), + ), ), produceFiller: (_node, _ast) => @@ -739,8 +743,8 @@ export const threngine: Engine = { // @ts-ignore isMeshToonMaterial: true, ...threeMaterialProperties(graph, node, sibling), - }) - ) + }), + ), ), produceFiller: (_node, _ast) => @@ -752,7 +756,7 @@ export const threngine: Engine = { export const createMaterial = ( compileResult: CompileResult, - ctx: EngineContext + ctx: EngineContext, ) => { const { engineMaterial } = ctx.runtime as ThreeRuntime; @@ -778,7 +782,7 @@ export const createMaterial = ( vertexShader: compileResult?.vertexResult.replace('#version 300 es', ''), fragmentShader: compileResult?.fragmentResult.replace( '#version 300 es', - '' + '', ), }; @@ -805,14 +809,14 @@ export const createMaterial = ( // WebGLProgram // https://github.com/mrdoob/three.js/blob/e7042de7c1a2c70e38654a04b6fd97d9c978e781/src/renderers/webgl/WebGLProgram.js#L392 // which occurs if we set isMeshPhysicalMaterial/isMeshStandardMaterial - property !== 'defines' + property !== 'defines', ) .reduce( (acc, [key, value]) => ({ ...acc, [key]: value, }), - {} + {}, ); const material = new RawShaderMaterial(initialProperties); @@ -837,7 +841,7 @@ export const engineNodeTypeToConstructor = (type: string) => { const frogMergeOptions = { includePrecisions: false, includeVersion: false }; const getStructTypeName = ( - stmt: DeclarationStatementNode + stmt: DeclarationStatementNode, ): string | undefined => { try { const specifier = (stmt.declaration as any).specified_type?.specifier @@ -849,7 +853,7 @@ const getStructTypeName = ( }; const collectThreeNames = ( - sections: ShaderSections + sections: ShaderSections, ): { uniforms: Set; qualified: Set; structs: Set } => { const uniforms = new Set(); const qualified = new Set(); @@ -862,7 +866,7 @@ const collectThreeNames = ( if (id) uniforms.add(id); } else if (decl.type === 'declarator_list') { (decl as DeclaratorListNode).declarations?.forEach((d) => - uniforms.add(d.identifier.identifier) + uniforms.add(d.identifier.identifier), ); } } @@ -885,7 +889,7 @@ const collectThreeNames = ( // sections from the engine node context rather than a hardcoded list. const stripThreeDeclarations = ( sections: ShaderSections, - threeShaderSections: ShaderSections + threeShaderSections: ShaderSections, ): ShaderSections => { const { uniforms: threeUniforms, @@ -897,18 +901,18 @@ const stripThreeDeclarations = ( ...sections, inStatements: filterQualifiedStatements( sections.inStatements, - (name) => !threeQualified.has(name) + (name) => !threeQualified.has(name), ), outStatements: filterQualifiedStatements( sections.outStatements, - (name) => !threeQualified.has(name) + (name) => !threeQualified.has(name), ), uniforms: filterUniformNames( sections.uniforms, - (name) => !threeUniforms.has(name) + (name) => !threeUniforms.has(name), ), structs: sections.structs.filter( - (s) => !threeStructs.has(getStructTypeName(s.source) ?? '') + (s) => !threeStructs.has(getStructTypeName(s.source) ?? ''), ), }; }; @@ -921,7 +925,7 @@ const extractOutputExpr = ( sections: ShaderSections, outputNodeId: string, assignTarget: string, - fallback: string + fallback: string, ): string => { const entry = sections.program.find((s) => s.nodeId === outputNodeId); if (!entry) return fallback; @@ -946,7 +950,7 @@ const extractOutputExpr = ( const extractVertexMainStmts = ( sections: ShaderSections, outputNodeId: string, - fallback: string + fallback: string, ): string => { const entry = sections.program.find((s) => s.nodeId === outputNodeId); if (!entry) return `gl_Position = ${fallback};`; @@ -958,12 +962,12 @@ const extractVertexMainStmts = ( export const createFrogMaterialResult = ( compileResult: CompileResult, ctx: EngineContext, - graph: Graph + graph: Graph, ) => { const { compileResult: graphResult } = compileResult; const engineNodeIds = new Set( - graph.nodes.filter((node) => (node as CodeNode).engine).map(({ id }) => id) + graph.nodes.filter((node) => (node as CodeNode).engine).map(({ id }) => id), ); const engineNode = graph.nodes.find((n) => engineNodeIds.has(n.id)); @@ -992,7 +996,7 @@ export const createFrogMaterialResult = ( // node's megashader AST is computed during this compilation pass. const { updatedNodeContext } = compileResult; const engineNodes = Array.from(engineNodeIds).map( - (id) => graph.nodes.find((n) => n.id === id) as CodeNode + (id) => graph.nodes.find((n) => n.id === id) as CodeNode, ); const threeFragSections = (() => { const fragNode = engineNodes.find((n) => n?.stage === 'fragment'); @@ -1018,20 +1022,20 @@ export const createFrogMaterialResult = ( stripThreeDeclarations( filterSections(noSkip, graphResult.fragment), // graphResult.fragment, - threeFragSections + threeFragSections, ), - frogMergeOptions - ).program + frogMergeOptions, + ).program, ); const vertexShader = generate( shaderSectionsToProgram( stripThreeDeclarations( filterSections(noSkip, graphResult.vertex), // graphResult.vertex, - threeVertSections + threeVertSections, ), - frogMergeOptions - ).program + frogMergeOptions, + ).program, ); // Extract the final output expressions from the output nodes' compiled AST. @@ -1041,12 +1045,12 @@ export const createFrogMaterialResult = ( graphResult.fragment, graphResult.outputFrag.id, 'frogFragOut', - 'vec4(1.0)' + 'vec4(1.0)', ); const vertexOutput = extractVertexMainStmts( graphResult.vertex, graphResult.outputVert.id, - 'vec4(1.0)' + 'vec4(1.0)', ); const uniforms: Record = { @@ -1059,7 +1063,7 @@ export const createFrogMaterialResult = ( let vertexInjections: ShaderInjection[] = []; const additionalProperties = Object.entries( - compileResult.compileResult.engineNodeProperties + compileResult.compileResult.engineNodeProperties, ).reduce>((acc, [name, property]) => { if ( property.fillerGroup.filler @@ -1082,6 +1086,7 @@ export const createFrogMaterialResult = ( const mat = new FrogMaterial({ baseMaterial: BaseMaterial as any, + materialName: engineNodeTypeToConstructorName(engineNode.type) ?? undefined, fragmentShader, fragmentOutput, vertexShader, @@ -1102,3 +1107,138 @@ export const createFrogMaterialResult = ( return mat; }; + +export const engineNodeTypeToConstructorName = ( + type: string, +): 'MeshPhongMaterial' | 'MeshPhysicalMaterial' | 'MeshToonMaterial' | null => { + if (type === EngineNodeType.physical) return 'MeshPhysicalMaterial'; + if (type === EngineNodeType.phong) return 'MeshPhongMaterial'; + if (type === EngineNodeType.toon) return 'MeshToonMaterial'; + return null; +}; + +export type FrogMaterialExport = { + fragmentShader: string; + vertexShader: string; + fragmentOutput: string; + vertexOutput: string; + baseMaterialType: + | 'MeshPhongMaterial' + | 'MeshPhysicalMaterial' + | 'MeshToonMaterial'; + // Properties passed as-is to FrogMaterial (map, normalMap, etc. as GLSL strings) + injectableProps: Record; + // Injections that replace assignment patterns in Three's compiled shader + fragmentInjections: ShaderInjection[]; + vertexInjections: ShaderInjection[]; +}; + +export const prepareFrogMaterialExport = ( + compileResult: CompileResult, + graph: Graph, +): FrogMaterialExport | null => { + const { compileResult: graphResult } = compileResult; + + const engineNodeIds = new Set( + graph.nodes.filter((node) => (node as CodeNode).engine).map(({ id }) => id), + ); + const engineNode = graph.nodes.find((n) => engineNodeIds.has(n.id)); + + if (!engineNode) return null; + + const baseMaterialType = engineNodeTypeToConstructorName(engineNode.type); + if (!baseMaterialType) return null; + + const skipIds = new Set([ + graphResult.outputFrag.id, + graphResult.outputVert.id, + ]); + const noSkip = (s: LineAndSource) => !skipIds.has(s.nodeId); + + const { updatedNodeContext } = compileResult; + const engineNodes = Array.from(engineNodeIds).map( + (id) => graph.nodes.find((n) => n.id === id) as CodeNode, + ); + + const threeFragSections = (() => { + const fragNode = engineNodes.find((n) => n?.stage === 'fragment'); + const ast = fragNode + ? (updatedNodeContext[fragNode.id]?.ast as Program) + : null; + return ast?.program?.length + ? findShaderSections('three', ast) + : shaderSectionsCons(); + })(); + + const threeVertSections = (() => { + const vertNode = engineNodes.find((n) => n?.stage === 'vertex'); + const ast = vertNode + ? (updatedNodeContext[vertNode.id]?.ast as Program) + : null; + return ast?.program?.length + ? findShaderSections('three', ast) + : shaderSectionsCons(); + })(); + + const fragmentShader = generate( + shaderSectionsToProgram( + stripThreeDeclarations( + filterSections(noSkip, graphResult.fragment), + threeFragSections, + ), + frogMergeOptions, + ).program, + ); + + const vertexShader = generate( + shaderSectionsToProgram( + stripThreeDeclarations( + filterSections(noSkip, graphResult.vertex), + threeVertSections, + ), + frogMergeOptions, + ).program, + ); + + const fragmentOutput = extractOutputExpr( + graphResult.fragment, + graphResult.outputFrag.id, + 'frogFragOut', + 'vec4(1.0)', + ); + + const vertexOutput = extractVertexMainStmts( + graphResult.vertex, + graphResult.outputVert.id, + 'vec4(1.0)', + ); + + // Mirror the same split createFrogMaterialResult does + const fragmentInjections: ShaderInjection[] = []; + const vertexInjections: ShaderInjection[] = []; + const injectableProps: Record = {}; + + Object.entries(graphResult.engineNodeProperties).forEach(([name, prop]) => { + if (!prop.result) return; + if ( + prop.fillerGroup.filler.toString().includes('strategy_type_assignmentTo') + ) { + const replace = `$1${prop.result};`; + fragmentInjections.push({ search: `(${name} = ).+;`, replace }); + vertexInjections.push({ search: `(${name} = ).+;`, replace }); + } else { + injectableProps[name] = prop.result; + } + }); + + return { + fragmentShader, + vertexShader, + fragmentOutput, + vertexOutput, + baseMaterialType, + injectableProps, + fragmentInjections, + vertexInjections, + }; +};