├── __init__.py ├── ShadersOut └── ShaderSourceFilesHere.txt ├── .gitignore ├── test2.py ├── graph ├── basic.gen ├── tex.gen └── lit.gen ├── License.txt ├── library └── basic.txt ├── param.py ├── test.py ├── manager.py ├── renderState.py ├── README.rst ├── nodes.py └── shaderBuilder.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ShadersOut/ShaderSourceFilesHere.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | ShadersOut/*.sha 3 | ShadersOut/*.svg 4 | graph/*.svg -------------------------------------------------------------------------------- /test2.py: -------------------------------------------------------------------------------- 1 | from panda3d.core import loadPrcFileData,NodePath 2 | loadPrcFileData("","notify-level-gobj debug") 3 | 4 | import manager 5 | 6 | shaderManager=manager.getManager(["library"],"graph/basic.gen") 7 | shader=shaderManager.makeShader(NodePath("")) 8 | -------------------------------------------------------------------------------- /graph/basic.gen: -------------------------------------------------------------------------------- 1 | # perform the standard vertex projection 2 | vertexPos=Input("float4 vtx_position : POSITION") 3 | vProj=vertProject(vertexPos) 4 | Output("vshader",vProj,"float4 l_position : POSITION") 5 | 6 | # fshader 7 | red=Constant("float4","float4(1,0,0,1)") 8 | Output("fshader",red,"float4 o_color: COLOR") 9 | -------------------------------------------------------------------------------- /graph/tex.gen: -------------------------------------------------------------------------------- 1 | # the nodes instanced here are defined either by the library (in library/basic.txt) 2 | # or by nodes.py 3 | # see those locations for details on the nodes 4 | # all names preloaded into global scope are node classes 5 | 6 | # vshader 7 | # perform the standard vertex projection using the node from the library 8 | vertexPos=Input("float4 vtx_position : POSITION") 9 | vProj=vertProject(vertexPos) 10 | Output("vshader",vProj,"float4 l_position : POSITION") 11 | 12 | # uv 13 | vert_uv=Input("float2 vtx_texcoord0 : TEXCOORD0") 14 | uv=Output("vshader",vert_uv,"float2 l_texcoord0 : TEXCOORD0") 15 | 16 | 17 | 18 | # fshader 19 | 20 | # sample texture if there is one 21 | # get the texture 22 | _tex=Input("uniform sampler2D tex_0 : TEXUNIT0") 23 | # is there a texture? 24 | hasTex=HasTextureAttrib() 25 | # make tex avaliable only if there is a texture 26 | tex=ConditionalPassThrough(hasTex,_tex) 27 | # sample the texture. These are simple code nodes, which 28 | # means their output is available if all their inputs are 29 | # so there is a diffuse color only when tex is avalaible 30 | diffuseColor4=sampleTexure(tex,uv) 31 | diffuseColor=float4To3(diffuseColor4) 32 | 33 | # Output color 34 | Output("fshader",diffuseColor,"float3 o_color: COLOR") -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Craig Macomber 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /library/basic.txt: -------------------------------------------------------------------------------- 1 | :: lib 2 | : code 3 | // Some Example Lib 0 // 4 | 5 | 6 | :: lib 7 | : code 8 | // Some Example Lib 1 // 9 | 10 | :: node 11 | : info 12 | name float4To3 13 | : inlinks 14 | float4 v 15 | : outlinks 16 | float3 outv 17 | : code 18 | outv = v.xyz; 19 | 20 | :: node 21 | : info 22 | name float4To1 23 | : inlinks 24 | float4 v 25 | : outlinks 26 | float outv 27 | : code 28 | outv = v.x; 29 | 30 | # sample a texture 31 | :: node 32 | : info 33 | name sampleTexure 34 | : inlinks 35 | sampler2D tex 36 | float2 l_texcoord0 37 | : outlinks 38 | float4 color 39 | : code 40 | color = tex2D(tex, l_texcoord0); 41 | 42 | # produce specularLight and diffuseLight values from directional light named "dlight" 43 | :: node 44 | : info 45 | name directionalLight 46 | : shaderinputs 47 | uniform float4x4 dlight_dlight_to_model 48 | :inlinks 49 | float3 l_normal 50 | : outlinks 51 | float3 specularLight 52 | float3 diffuseLight 53 | : code 54 | float4x4 mat=(dlight_dlight_to_model); 55 | float NdotL=dot(l_normal,normalize(mat[2].xyz)); 56 | // here H is approximate for the whole model, not per pixel or even per vertex! 57 | float NdotH=dot(l_normal,normalize(mat[3].xyz)); 58 | float4 l=lit(NdotL , NdotH , 100); 59 | specularLight=mat[1].xyz*l.z*1; 60 | diffuseLight=mat[0].xyz*l.y; 61 | 62 | # transform from model to projected space, and ouput as vertex pos 63 | :: node 64 | : info 65 | name vertProject 66 | : shaderInputs 67 | uniform float4x4 mat_modelproj 68 | : inlinks 69 | float4 vtx_position 70 | : outlinks 71 | float4 l_position 72 | : code 73 | l_position = mul(mat_modelproj, vtx_position); 74 | 75 | # transform from model to view space 76 | :: node 77 | : info 78 | name viewSpace 79 | : shaderInputs 80 | uniform float4x4 trans_model_to_view 81 | : inlinks 82 | float4 vtx_position 83 | : outlinks 84 | float4 l_vpos 85 | : code 86 | l_vpos=mul(trans_model_to_view, vtx_position); 87 | 88 | # Basic exponential fog computation 89 | :: node 90 | : info 91 | name fog 92 | : inlinks 93 | float4 viewPos 94 | float3 color 95 | float3 fogColor 96 | float density 97 | : outlinks 98 | float3 outColor 99 | : code 100 | float f=exp(-density*length(viewPos.xyz)); 101 | outColor=lerp(fogColor,color,f); 102 | -------------------------------------------------------------------------------- /graph/lit.gen: -------------------------------------------------------------------------------- 1 | # the nodes instanced here are defined either by the library (in library/basic.txt) 2 | # or by nodes.py 3 | # see those locations for details on the nodes 4 | # all names preloaded into global scope are node classes 5 | 6 | # vshader 7 | # perform the standard vertex projection using the node from the library 8 | vertexPos=Input("float4 vtx_position : POSITION") 9 | vProj=vertProject(vertexPos) 10 | Output("vshader",vProj,"float4 l_position : POSITION") 11 | 12 | # pass the normals through to the fshader 13 | vNorm=Input("varying float3 vtx_normal: NORMAL") 14 | norm=Output("vshader",vNorm,"float3 l_normal : TEXCOORD2") 15 | 16 | # uv 17 | vert_uv=Input("float2 vtx_texcoord0 : TEXCOORD0") 18 | uv=Output("vshader",vert_uv,"float2 l_texcoord0 : TEXCOORD0") 19 | 20 | # view space position 21 | vert_viewPos=viewSpace(vertexPos) 22 | viewPos=Output("vshader",vert_viewPos,"float4 l_vpos : TEXCOORD1") 23 | 24 | 25 | # fshader 26 | 27 | # sample texture if there is one 28 | # get the texture 29 | _tex=Input("uniform sampler2D tex_0 : TEXUNIT0") 30 | # is there a texture? 31 | hasTex=HasTextureAttrib() 32 | # make tex available only if there is a texture 33 | tex=ConditionalPassThrough(hasTex,_tex) 34 | # sample the texture. These are simple code nodes, which 35 | # means their output is available if all their inputs are 36 | # so there is a diffuse color only when tex is available 37 | diffuseColor4=sampleTexure(tex,uv) 38 | diffuseColor=float4To3(diffuseColor4) 39 | 40 | # apply lighting 41 | dlight=directionalLight(norm) 42 | alight4=Input("uniform float4 alight_alight") 43 | alight=float4To3(alight4) 44 | # the boolean here is is the requireAll value. 45 | # False means to apply the operator to all available inputs 46 | diffuseLighting=Operator(False,"+",dlight.diffuseLight,alight) 47 | 48 | diffuse=Operator(False,"*",diffuseColor,diffuseLighting) 49 | lighting=Operator(True,"+",diffuse,dlight.specularLight) 50 | 51 | # apply come crummy fog 52 | #fogColor=Constant("float3","float3(.7,.7,.8)") 53 | #densityConstant=Constant("float","0.03") 54 | #densityInput=ConditionalInput("uniform float4 k_fogDensity") 55 | #densityInputFloat=float4To1(densityInput) 56 | #density=FirstAvailable(densityInputFloat,densityConstant) 57 | #fogged=fog(viewPos,lighting,fogColor,density) 58 | 59 | # Output color 60 | Output("fshader",lighting,"float3 o_color: COLOR") -------------------------------------------------------------------------------- /param.py: -------------------------------------------------------------------------------- 1 | def linkEndFromDefCode(defCode): 2 | """ 3 | 4 | returns a Param representing the entry on a link into a NodeType. This allows the Nodes to have their own internal names 5 | for their inputs and outputs that are seperate from the names of Links. The types should match though! 6 | 7 | """ 8 | 9 | t=defCode.split() 10 | 11 | name=t[-1] 12 | type=" ".join(t[:-1]) 13 | return Param(name,type) 14 | 15 | 16 | def shaderParamFromDefCode(defCode): 17 | """ 18 | 19 | example usage: 20 | shaderParam=shaderParamFromDefCode("uniform sampler2D k_grassData: TEXUNIT0") 21 | shaderParam=shaderParamFromDefCode("float4 o_color") 22 | 23 | """ 24 | i=defCode.find(':') 25 | if i==-1: 26 | semantic=None 27 | t=defCode.split() 28 | else: 29 | semantic=defCode[i+1:].strip() 30 | t=defCode[:i].split() 31 | name=t[-1] 32 | type=" ".join(t[:-1]) 33 | return ShaderParam(name,type,semantic) 34 | 35 | class Param(object): 36 | def __init__(self,name,type): 37 | self.name=name 38 | self.type=type 39 | def getName(self): return self.name 40 | def getType(self): return self.type 41 | def __repr__(self): return self.__class__.__name__+"("+self.name+", "+self.type+")" 42 | def __str__(self): return self.type+" "+self.name 43 | def __hash__(self): 44 | return hash(self.name)^hash(self.type) 45 | def __eq__(self,other): 46 | return self.__class__==other.__class__ and self.name==other.name and self.type==other.type 47 | 48 | class ShaderParam(Param): 49 | def __init__(self,name,type,semantic=None): 50 | Param.__init__(self,name,type) 51 | self.semantic=semantic 52 | def getSemantic(self): return self.semantic 53 | def getDefCode(self): return self.type+" "+self.name+((" : "+self.semantic) if self.semantic else "") 54 | def __eq__(self,other): 55 | return Param.__eq__(self,other) and self.semantic==other.semantic 56 | def getShortType(self): return self.type.split()[-1] 57 | def __str__(self): 58 | s=Param.__str__(self) 59 | if self.semantic: 60 | return s+" : "+self.semantic 61 | else: 62 | return s 63 | class ShaderInput(ShaderParam): pass 64 | class ShaderOutput(ShaderParam): pass 65 | 66 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from panda3d.core import loadPrcFileData 2 | loadPrcFileData("","show-frame-rate-meter #t") 3 | loadPrcFileData("","sync-video #f") 4 | 5 | from panda3d.core import * 6 | from direct.task import Task 7 | from direct.actor import Actor 8 | from direct.interval.IntervalGlobal import * 9 | import math 10 | import direct.directbase.DirectStart 11 | print PandaSystem.getVersionString() 12 | 13 | 14 | import manager 15 | 16 | 17 | """ 18 | Shader Generator Demo, see bottom for application of shader system to scene 19 | 20 | """ 21 | 22 | 23 | 24 | 25 | 26 | # Setup an interesting scene graph to run effects on: 27 | base.disableMouse() 28 | 29 | #Load the first environment model 30 | environ = loader.loadModel("models/environment") 31 | environ.reparentTo(render) 32 | environ.setScale(0.25,0.25,0.25) 33 | environ.setPos(-8,42,0) 34 | 35 | #Task to move the camera 36 | def SpinCameraTask(task): 37 | angledegrees = task.time * 6.0 38 | angleradians = angledegrees * (math.pi / 180.0) 39 | base.camera.setPos(20*math.sin(angleradians),-20.0*math.cos(angleradians),3) 40 | base.camera.setHpr(angledegrees, 0, 0) 41 | return Task.cont 42 | 43 | taskMgr.add(SpinCameraTask, "SpinCameraTask") 44 | 45 | #Load the panda actor, and loop its animation 46 | pandaActor = Actor.Actor("models/panda-model",{"walk":"models/panda-walk4"}) 47 | pandaActor.setScale(0.005,0.005,0.005) 48 | pandaActor.reparentTo(render) 49 | pandaActor.loop("walk") 50 | 51 | #Create the four lerp intervals needed to walk back and forth 52 | pandaPosInterval1= pandaActor.posInterval(13,Point3(0,-10,0), startPos=Point3(0,10,0)) 53 | pandaPosInterval2= pandaActor.posInterval(13,Point3(0,10,0), startPos=Point3(0,-10,0)) 54 | pandaHprInterval1= pandaActor.hprInterval(3,Point3(180,0,0), startHpr=Point3(0,0,0)) 55 | pandaHprInterval2= pandaActor.hprInterval(3,Point3(0,0,0), startHpr=Point3(180,0,0)) 56 | 57 | #Create and play the sequence that coordinates the intervals 58 | pandaPace = Sequence(pandaPosInterval1, pandaHprInterval1, 59 | pandaPosInterval2, pandaHprInterval2, name = "pandaPace") 60 | pandaPace.loop() 61 | 62 | 63 | 64 | #Set up some lights 65 | 66 | # A crazy bright spinning red light seems pretty cool 67 | dlight = DirectionalLight('dlight') 68 | dlight.setColor(Vec4(4.9, 0.9, 0.8, 1)) 69 | dlight.setSpecularColor(Vec4(0.9, 0.9, 0.8, 10)) 70 | dlnp = render.attachNewNode(dlight) 71 | dlnp.setHpr(0, 0, 0) 72 | #render.setLight(dlnp) 73 | render.setShaderInput('dlight',dlnp) 74 | 75 | dayCycle=dlnp.hprInterval(10.0,Point3(0,360,0)) 76 | dayCycle.loop() 77 | 78 | # and an ambient light 79 | alight = AmbientLight('alight') 80 | alight.setColor(Vec4(0.2, 0.2, 0.2, 1)) 81 | alnp = render.attachNewNode(alight) 82 | render.setLight(alnp) 83 | #render.setShaderInput('alight',alnp) 84 | 85 | 86 | #render.setTransparency(TransparencyAttrib.MNone,100) 87 | 88 | 89 | ##-----------Generate Shaders with Shader Generator-----------## 90 | shaderManager=manager.getManager(["library"],"graph/lit.gen") 91 | shaderManager.genShaders(render,"ShadersOut/debug") 92 | 93 | 94 | run() 95 | -------------------------------------------------------------------------------- /manager.py: -------------------------------------------------------------------------------- 1 | """ 2 | some usage assistance stuff resides here. 3 | 4 | the majority of users of this shader generator system 5 | should only call into this file. It provides all needed interfaces 6 | for basic use. 7 | 8 | For more advanced usage, the code here serves as an example. 9 | 10 | """ 11 | from panda3d.core import ShaderAttrib 12 | import shaderBuilder 13 | 14 | def getManager(libPaths,scriptPath,nodeTypeClassMap={},renderStateFactory=None,viewDebugScriptGraph=False,debugPath=None): 15 | """ a utility function to avoid having to make the library and builders for simple cases """ 16 | lib=shaderBuilder.Library(libPaths,nodeTypeClassMap) 17 | builder=lib.loadScript(scriptPath,viewGraph=viewDebugScriptGraph) 18 | return Manager(builder,renderStateFactory,debugPath=debugPath) 19 | 20 | # a helper 21 | def _getShaderAtrib(renderState): 22 | shaderAtrib=renderState.getAttrib(ShaderAttrib.getClassSlot()) 23 | if not shaderAtrib: 24 | shaderAtrib = ShaderAttrib.make() 25 | return shaderAtrib 26 | 27 | 28 | 29 | class Manager(object): 30 | def __init__(self,builder,renderStateFactory=None,debugPath=None,flags=()): 31 | self.builder=builder 32 | self.renderStateFactory=builder.setupRenderStateFactory(renderStateFactory) 33 | self.debugPath=debugPath 34 | self.flags=set(flags) 35 | def makeShader(self,pandaNode,pandaRenderState=None,geomVertexFormat=None,debugCodePrefix=None,debugGraphPrefix=None,extraFlags=()): 36 | """ generate and return (but not apply) a shader """ 37 | genRenderState=self.renderStateFactory.getRenderState(pandaNode,pandaRenderState,geomVertexFormat,self.flags|set(extraFlags)) 38 | debugPath=None 39 | debugGraphPath=None 40 | if self.debugPath is not None: 41 | if debugCodePrefix is not None: debugPath=self.debugPath+debugCodePrefix 42 | if debugGraphPrefix is not None: debugGraphPath=self.debugPath+debugGraphPrefix 43 | return self.builder.getShader(genRenderState,debugPath,debugGraphPath=debugGraphPath) 44 | 45 | def genShaders(self,node,debugCodePrefix=None,debugGraphPrefix=None): 46 | """ walk all geoms and apply generateed shaders for them """ 47 | nn=node.node() 48 | if nn.isGeomNode(): 49 | for i,renderState in enumerate(nn.getGeomStates()): 50 | geomVertexFormat=nn.getGeom(i).getVertexData().getFormat() 51 | 52 | # TODO : the order of composing might be wrong! 53 | netRs=renderState.compose(node.getNetState()) 54 | 55 | shader=self.makeShader(node,netRs,geomVertexFormat,debugCodePrefix=debugCodePrefix,debugGraphPrefix=debugGraphPrefix) 56 | shaderAtrib=_getShaderAtrib(renderState) 57 | shaderAtrib=shaderAtrib.setShader(shader) 58 | renderState=renderState.setAttrib(shaderAtrib) 59 | nn.setGeomState(i,renderState) 60 | 61 | for n in node.getChildren(): 62 | self.genShaders(n,debugCodePrefix,debugGraphPrefix) -------------------------------------------------------------------------------- /renderState.py: -------------------------------------------------------------------------------- 1 | from panda3d.core import ShaderAttrib 2 | 3 | 4 | # Since the NodeType subclasses need some convention on what will be used for the renderState 5 | # objects, we will go ahead and define our renderState class here 6 | # if using addational custom NodeType sublcasses, you many want to subclass RenderState. 7 | # to optimize caching, this RenderState class only stores the minimum state data that is needed. 8 | # The set of what to store is collected by the RenderStateFactory 9 | 10 | class RenderStateFactory(object): 11 | def __init__(self): 12 | self.tags=set() # add tags we care about here, by name 13 | self.shaderInputs=set() # add shaderInput names we care about here, by name 14 | self.hasRenderAttribs=set() # add RenderAttrib.getClassSlot() values here if the presense matters (not the specific value) 15 | #self.renderAttribs=set() # add RenderAttrib.getClassSlot() values here 16 | self.geomVertexDataColumns=set() # by name 17 | self.flags=set() 18 | def getRenderState(self,pandaNode,pandaRenderState=None,geomVertexFormat=None,flags=()): 19 | """ 20 | returns a RenderState instance for a given pandaNode, and optionally a specified panda3d.RenderState 21 | 22 | since this is usally used on geoms, but tags and such are set on pandaNodes or NodePaths 23 | (pandaNode may be either), the pandaRenderState is will default to pandaNode.getNetState(), 24 | but can be passed seperatly as in the cause when using geoms. In that case, pandaNode should 25 | be the panda3d.GeomNode 26 | 27 | """ 28 | if pandaRenderState is None: pandaRenderState=pandaNode.getNetState() 29 | 30 | return self._getRenderState(self._getTagDict(pandaNode),pandaRenderState,geomVertexFormat,flags) 31 | 32 | def _getTagDict(self,pandaNode): 33 | tags={} 34 | for t in self.tags: 35 | if pandaNode.hasNetTag(t): tags[t]=pandaNode.getNetTag(t) 36 | return tags 37 | 38 | def _getRenderState(self,tagDict,pandaRenderState,geomVertexFormat,flags): 39 | shaderAtrib=pandaRenderState.getAttrib(ShaderAttrib.getClassSlot()) 40 | if shaderAtrib: 41 | # basically we want the intersection of self.shaderInputs, and the shader inputs in shaderAtrib 42 | # but to do this, we need this rather contrived approach, 43 | # since there is no way to get the set of shader inputs in a shaderAtrib, 44 | # or ask if it contains one 45 | shaderInputs=frozenset(s for s in self.shaderInputs if shaderAtrib.getShaderInput(s).getName() is not None) 46 | else: 47 | shaderInputs=frozenset() 48 | 49 | hasRenderAttribs=frozenset(slot for slot in self.hasRenderAttribs if pandaRenderState.hasAttrib(slot)) 50 | if geomVertexFormat is not None: 51 | columns=frozenset(column for column in self.geomVertexDataColumns if geomVertexFormat.hasColumn(column)) 52 | else: 53 | columns=frozenset() 54 | 55 | return RenderState(pandaRenderState,tagDict,shaderInputs,hasRenderAttribs,columns,frozenset(flags)) 56 | 57 | class RenderState(object): 58 | """ 59 | 60 | Panda3D has a RenderState class, but we don't use it directly for a few reasons: 61 | It stores stuff we want to ignore (which could be removed, but its simpler to just store what we want) 62 | It does not store everything we may want (allowing tags to trigger shader generator stuff is cool) 63 | 64 | Flyweight is not used here because it can be confusing when the class gets subclassed, 65 | and isn't needed anyway. Regardless, a fast comparison, and good hash are needed for the cache. 66 | When subclassing to add more fields, be sure to __eq__ the comparison and __hash__ methods. 67 | 68 | """ 69 | def __init__(self,pandaRenderState,tagDict,shaderInputSet,hasRenderAttribs,columns,flags): 70 | self.shaderInputs=shaderInputSet 71 | self.tags=tagDict 72 | self.hasRenderAttribs=hasRenderAttribs 73 | self.columns=columns 74 | self.flags=flags 75 | # TODO : better hash 76 | self._hash = hash(shaderInputSet) ^ len(tagDict) ^ hash(hasRenderAttribs) ^ hash(columns) ^ hash(flags) 77 | 78 | def hasGeomVertexDataColumns(self,name): 79 | return name in self.columns 80 | 81 | def hasRenderAttrib(self,slot): 82 | return slot in self.hasRenderAttribs 83 | 84 | def hasShaderInput(self,name): 85 | return name in self.shaderInputs 86 | 87 | def hasTag(self,name): 88 | return name in self.tags 89 | 90 | def getTag(self,name,default=None): 91 | return self.tag.get(name,default) 92 | 93 | def hasFlag(self,name): 94 | return name in self.flags 95 | 96 | def __hash__(self): 97 | return self._hash 98 | 99 | def __eq__(self,other): 100 | return self.shaderInputs==other.shaderInputs and self.tags==other.tags and self.hasRenderAttribs==other.hasRenderAttribs and self.columns==other.columns and self.flags==other.flags 101 | 102 | def __repr__(self): return "RenderState"+str((self.shaderInputs,self.tags,self.shaderInputs,self.hasRenderAttribs,self.columns,self.flags)) 103 | 104 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | A shader generator for Panda3D 2 | Specifically, a Shader Meta-Language for implementing, customizing and extending shader generators. 3 | 4 | Target Users 5 | ============ 6 | This is a common area of confusion, so to be clear: 7 | This system is intended for advanced shader programmers. 8 | If you find yourself struggling with maintaining many different but related shader files, 9 | this may be a tool for you. If you want to have a special shader for each combination or several atributes 10 | generated from a single set of shader code, this is the tool for you. 11 | 12 | Example: Some of my models (or even parts of models) have textures. 13 | Some have model colors. Some have material colors. Some have normal maps. I can use this tool 14 | to define a shader generator to handle all possible combinations of these correctly. 15 | 16 | It can even handle adjusting shaders based on tags placed on models, available vertex data columns, textures, and global flags. 17 | More custom controls can also be added, though I think tags are general enough to cover most special needs. 18 | 19 | Example: I have several adjustable settings, such as enabling deferred shading and cartoon inking. 20 | Using the flags feature, my generator can be designed to reorder and change the shaders to act accordingly. 21 | In the deferred shading case, the lighting computation code can be shared between the deferred lighting pass shader, 22 | and the model shader for forward shading. For the cartoon inking, 23 | the inking code in the post process is simply omitted of its flag is off. 24 | 25 | This is very powerful, and very flexible, but it does not save writing the shader code! 26 | It simply provides a way to avoid having to maintain and select between many related shaders. 27 | 28 | High level conceptual overview 29 | ============================== 30 | This is how it works, not a ussage guide. 31 | Some example usage is in the shader tutorial here: https://github.com/Craig-Macomber/Shader-Tut 32 | 33 | - A Script produces a Generator Graph. 34 | 35 | - RenderStates are used as input to the Generator Graph to produce the Active Graph for to the 36 | provided RenderState. 37 | 38 | - The Active Graphs are compiled into shaders. 39 | 40 | 41 | Scripts --> Generator Graphs 42 | ++++++++++++++++++++++++++++ 43 | Each script file describes (generates) a graph structure (The generator graph) 44 | This is done by constructing nodes and passing in incomming edges (and possibly some constants). 45 | 46 | nodeInstance=NodeType(input1,input2,"SomeConstant") 47 | 48 | The types of nodes in the graph are defined in nodes.py, and via libraries (which are loaded as CodeNodes from nodes.py) 49 | 50 | These node types have 0 or more outputs, accessable as nodeInstance.name 51 | They may (and might not) have a default output which can be accessed by just passing the node itself: 52 | 53 | nodeInstance2=SomeNode(nodeInstance.someOutput,nodeInstance) 54 | 55 | Thus, the scripts describe directed acyclic graphs. The graphs do not need to be connected. 56 | 57 | RenderStates 58 | ++++++++++++ 59 | See also renderState.py 60 | 61 | When generating a shader from the Generator Graph, a RenderState is provided as input (and its the only input). 62 | 63 | The shader generator uses a very similar RenderState class to panda3d.core.RenderState. 64 | 65 | The reason the panda RenderState class is not used is tweo fold: 66 | 67 | - It contains lots of unneeded data (hurts caching) 68 | 69 | - It is missing some needed data (tags, geomVertexFormat etc) 70 | 71 | To get the exact minimum needed data stored in the RenderStates for the ShaderBuilder, 72 | the Generator Graph is used to setup a RenderStateFactory the produces ideal minimal 73 | RenderStates for a specific Generator Graph 74 | 75 | Generating the Shader 76 | +++++++++++++++++++++ 77 | A shader builder uses it's Generator Graph, and a passed in RenderState and produces an Shader. 78 | shaderBuilder.ShaderBuilder.getShader does this. It includes caching, since the process is a 79 | deterministic process that depends only on the Generator Graph (constant for the builder), 80 | and the input RenderState. 81 | 82 | 83 | Generating the Active Graph 84 | --------------------------- 85 | The first step is to generate the Active Graph. See shaderBuilder.makeStages. 86 | 87 | Compiling the Active Graph into Stages 88 | -------------------------------------- 89 | See shaderBuilder.makeStage. 90 | 91 | 92 | 93 | 94 | 95 | 96 | Misc Notes 97 | ========== 98 | 99 | Outputting visual graphs requires pydot and graphviz to be installed. 100 | 101 | Goals: 102 | 103 | - Allow coders to provide all possible shader effects (no restrictions on shader code, stages used, inputs, outputs etc) (Done) 104 | 105 | - Allow easy realtime configuration and tuning of effects (accessible to artists and coders) (Not started, not longer emphasized) 106 | 107 | - Generate custom shaders based on render state and other settings on a per NodePath basis from a single configuration (Done via use of meta-language design) 108 | 109 | - Allow easy use of multiple separate configurations applied to different NodePaths (ex: one for deferred shaded lights, one for models, one for particles. Done.) 110 | 111 | - Produce an extensive collection of useful NodeTypes and example Graphs, sample applications etc. (Most of the remaining work is here) 112 | 113 | It is important that adding, sharing and using libraries of effects is easy. To facilitate this, they are packed into libraries which can simply be placed in a libraries folder (even recursively) 114 | There is however currently no name spacing. For now, manually prefix things if you with to avoid any potential conflicts. 115 | 116 | The focus on allowing full control of the shaders is important. A shader generator that can't use custom shader inputs, render to multiple render targets, or use multiple stages (vshader, fshader, gshader etc) is not complete. This design inherently supports all of these, and more. 117 | 118 | Many more details in shaderBuilder.py, see comments near top. 119 | 120 | Example Meta-Language scripts are in the graph directory. These scripts create a graph structure that is used to generate the final shader graphs for different render states which are compiled into shaders. 121 | 122 | The set of nodes that can be used in these graphs are the regestered classes from nodes.py, and those loaded from the libraries (see the library folder). 123 | 124 | Its possibe to add custom node types implemented in python, simply provide them when instancting the shaderBuilder.Library 125 | 126 | This system currently does not modify render states, add filters or any other changes to the scene graph. 127 | It just generates shaders. It assumes the scene graph will be setup separately. 128 | It would be possible to add in some scene graph modifying active nodes that would be collected while generating the shader, and then applied along with the shader (by passing the node to the apply method of each). 129 | This can be made to work with the current cache system. 130 | -------------------------------------------------------------------------------- /nodes.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import param 3 | 4 | from panda3d.core import MaterialAttrib,ColorAttrib,TextureAttrib 5 | 6 | """ 7 | 8 | This module contains the basic NodeType implementation, including the classes it instances 9 | 10 | """ 11 | 12 | boolLinkType="MetaBool" 13 | 14 | class Link(object): 15 | """ 16 | 17 | An output from a shader Node, and possibly multiple inputs to multiple shader nodes. 18 | 19 | As it can be multiple inputs, links are sets of edges in the graph from one node to multiple others. 20 | 21 | """ 22 | def __init__(self,dataType,name="Unnamed"): 23 | self.dataType=dataType 24 | self.name=name 25 | def getType(self): return self.dataType 26 | def getName(self): return self.name 27 | def __repr__(self): 28 | return "Link"+str(tuple([self.dataType,self.name])) 29 | def __str__(self): 30 | name=self.getName() 31 | text=self.getType() 32 | if name != "Unnamed":text=text+" "+name 33 | return text 34 | 35 | 36 | class ActiveNode(object): 37 | """ 38 | 39 | ActiveNodes should never be modified, and should not be subclassed. 40 | 41 | They use the Flyweight pattern, and thus can be compared by pointer with "is" 42 | 43 | This is important as they are hashed by pointer, 44 | and need to compare properly and quicky for the caching to work. 45 | 46 | """ 47 | cache = {} 48 | 49 | def __new__(cls, *v): 50 | o = cls.cache.get(v, None) 51 | if o: 52 | return o 53 | else: 54 | o = cls.cache[v] = object.__new__(cls) 55 | return o 56 | def __init__(self,shaderInputs,inLinks,outLinks,code,isOutPut,comment="",stage=None): 57 | self.shaderInputs=shaderInputs 58 | self.inLinks=inLinks 59 | self.outLinks=outLinks 60 | self.code=code 61 | self.outPut=isOutPut 62 | if isOutPut: assert stage is not None 63 | self.stage=stage 64 | self.comment=comment 65 | def getShaderInputs(self): return self.shaderInputs 66 | def getInLinks(self): return self.inLinks 67 | def getOutLinks(self): return self.outLinks 68 | def isOutPut(self): return self.outPut 69 | def getCode(self): return self.code 70 | def getComment(self): return self.comment 71 | def __repr__(self): 72 | return "ActiveNode"+str(tuple([self.shaderInputs,self.inLinks,self.outLinks,"",self.outPut])) 73 | 74 | class ActiveOutput(object): 75 | """ 76 | 77 | ActiveOutputs should never be modified, and should not be subclassed. 78 | 79 | They use the Flyweight pattern, and thus can be compared by pointer with "is" 80 | 81 | This is important as they are hashed by pointer, 82 | and need to compare properly and quicky for the caching to work. 83 | 84 | """ 85 | cache = {} 86 | 87 | def __new__(cls, *v): 88 | o = cls.cache.get(v, None) 89 | if o: 90 | return o 91 | else: 92 | o = cls.cache[v] = object.__new__(cls) 93 | return o 94 | def __init__(self,stage,inLink,shaderOutput): 95 | self.shaderOutput=shaderOutput 96 | self.inLink=inLink 97 | self.stage=stage 98 | def isOutPut(self): return True 99 | def getOutLinks(self): return () 100 | def getInLinks(self): return (self.inLink,) 101 | def __repr__(self): 102 | return "ActiveOutput"+str(tuple([self.stage,self.inLink,self.shaderOutput])) 103 | 104 | 105 | 106 | 107 | 108 | def makeFullCode(code,shaderInputs,inLinks,outLinks): 109 | """ 110 | 111 | the code needed to construct Nodes includes the (paramList){code} wrapping stuff, so this addes it 112 | and saves it to self.code 113 | 114 | """ 115 | 116 | fparamChain=itertools.chain( 117 | ("in "+s.getType()+" "+s.getName() for s in shaderInputs), 118 | ("in "+s.getType()+" "+s.getName() for s in inLinks), 119 | ("out "+s.getType()+" "+s.getName() for s in outLinks), 120 | ) 121 | 122 | return "("+",".join(fparamChain)+"){\n"+code+"\n}" 123 | 124 | class Node(object): 125 | """ 126 | base class for all nodes, if used directly, takes no inputs, has no 127 | outputs basically a noop 128 | 129 | this is all thats needed to work with ShaderBuilder 130 | """ 131 | def __init__(self): 132 | pass 133 | def getActiveNodes(self,renderState,linkStatus): 134 | return () 135 | def setupRenderStateFactory(self,renderStateFactory): 136 | pass 137 | 138 | 139 | def assertString(s): assert isinstance(s,str) 140 | def assertLink(s): assert isinstance(s,Link), s.__class__ 141 | def assertParam(s): assert isinstance(s,param.Param), s.__class__ 142 | def assertEqual(a,b): assert a==b, (a,b) 143 | 144 | class LinkError(Exception): 145 | """Base class for exceptions in this module.""" 146 | pass 147 | 148 | defaultNodeClasses={} 149 | def reg(_class,name=None): 150 | """ 151 | intended for use as decorator to regester classes in the defaultNodeClasses dict 152 | """ 153 | if name is None: name=_class.__name__ 154 | defaultNodeClasses[name]=_class 155 | return _class 156 | 157 | @reg 158 | class ScriptNode(Node): 159 | """ 160 | base class for nodes that can auctually be used by script files 161 | """ 162 | # required by script system 163 | def getDefaultLink(self): 164 | raise LinkError("This node has no default link. Type: "+str(self.__class__)) 165 | # required by script system 166 | def getLink(self,name): 167 | raise LinkError("This node has no link named "+name) 168 | 169 | 170 | def allActive(linkStatus,links): 171 | return all(linkStatus[link] for link in links) 172 | 173 | class LinksNode(ScriptNode): 174 | def __init__(self,*inLinks): 175 | for link in inLinks: assertLink(link) 176 | self.links=inLinks 177 | 178 | @reg 179 | class AssertActiveNode(LinksNode): 180 | def getActiveNodes(self,renderState,linkStatus): 181 | assert allActive(linkStatus,self.links), "{0}: links:{1}".format(self,[link for link in self.links if not linkStatus[link]]) 182 | return () 183 | 184 | class AllActiveNode(LinksNode): 185 | def __init__(self,activeNode,*inLinks): 186 | LinksNode.__init__(self,*inLinks) 187 | self.activeNode=activeNode 188 | def getActiveNodes(self,renderState,linkStatus): 189 | if allActive(linkStatus,self.links): 190 | for link in self.activeNode.outLinks: 191 | linkStatus[link]=True 192 | return (self.activeNode,) 193 | else: 194 | return () 195 | 196 | class CodeNode(AllActiveNode): 197 | """ 198 | base class for nodes that fixed contain arbitrary code 199 | """ 200 | def __init__(self,source,shaderInputs,inLinks,outLinks,isOutPut,stage=None,comment="CodeNode"): 201 | self.source=source 202 | activeNode=ActiveNode(tuple(shaderInputs),tuple(inLinks),tuple(outLinks),self.source,isOutPut,comment,stage) 203 | AllActiveNode.__init__(self,activeNode,*inLinks) 204 | def getLink(self,name): 205 | for link in self.activeNode.getOutLinks(): 206 | if link.name==name: 207 | return link 208 | raise LinkError("This node has no link named "+name) 209 | def getDefaultLink(self): 210 | links=self.activeNode.getOutLinks() 211 | if len(links)>0: 212 | return links[0] 213 | else: 214 | raise LinkError("This node ("+self.activeNode.comment+") has no default link because it has no outputs") 215 | 216 | def metaCodeNode(name,source,shaderInputs,inLinks,outLinks,isOutPut=False,stage=None): 217 | """ 218 | makes a usable CodeNode for the specified source and I/O 219 | """ 220 | fullSource=makeFullCode(source,shaderInputs,inLinks,outLinks) 221 | for l in inLinks: assertParam(l) 222 | for l in outLinks: assertParam(l) 223 | class CustomCodeNode(CodeNode): 224 | def __init__(self,*inLinks_): 225 | if len(inLinks)!=len(inLinks_): 226 | raise LinkError("Error: number of inputs does not match node type. Inputs: "+str(inLinks_)+" expected: "+str(inLinks)) 227 | for x in xrange(len(inLinks)): 228 | t1=inLinks_[x].getType() 229 | t0=inLinks[x].getType() 230 | if t0!=t1: 231 | raise LinkError("Error: mismatched type on inLinks. Got: "+t1+" expected: "+t0) 232 | newOutLinks=(Link(link.getType(),link.name) for link in outLinks) 233 | CodeNode.__init__(self,fullSource,shaderInputs,inLinks_,newOutLinks,isOutPut,stage,"CodeNode: "+name) 234 | 235 | return CustomCodeNode 236 | 237 | 238 | def makePassThroughCode(type,backwards=False): 239 | if backwards: 240 | s="(out {0} ouput,in {0} input)" 241 | else: 242 | s="(in {0} input,out {0} ouput)" 243 | return s.format(type)+"{ouput=input;}" 244 | 245 | class SingleOutputMixin(object): 246 | def __init__(self,outLink): 247 | self.outLink=outLink 248 | def getDefaultLink(self): 249 | return self.outLink 250 | 251 | 252 | @reg 253 | class Input(SingleOutputMixin,ScriptNode): 254 | """ 255 | makes an active node that outputs the ConditionalInput shader input from the node's data dict 256 | or no active note if input is not available. 257 | """ 258 | def __init__(self,inputDef): 259 | 260 | ScriptNode.__init__(self) 261 | assertString(inputDef) 262 | 263 | input=param.shaderParamFromDefCode(inputDef) 264 | 265 | name=input.getName() 266 | if name.startswith("k_"): 267 | name=input.name[2:] 268 | self.inputName=name 269 | 270 | 271 | source=makePassThroughCode(input.getType()) 272 | 273 | outLink=Link(input.getShortType()) 274 | SingleOutputMixin.__init__(self,outLink) 275 | self.activeNode=ActiveNode((input,),(),(outLink,),source,False,"Input: "+inputDef) 276 | 277 | 278 | def getActiveNodes(self,renderState,linkStatus): 279 | linkStatus[self.outLink] = True 280 | return (self.activeNode,) 281 | 282 | 283 | @reg 284 | class ConditionalInput(Input): 285 | """ 286 | makes an active node that outputs the ConditionalInput shader input from the node's data dict 287 | or no active note if input is not available. 288 | """ 289 | def getActiveNodes(self,renderState,linkStatus): 290 | if renderState.hasShaderInput(self.inputName): 291 | return Input.getActiveNodes(self,renderState,linkStatus) 292 | else: 293 | return () 294 | 295 | def setupRenderStateFactory(self,renderStateFactory): 296 | renderStateFactory.shaderInputs.add(self.inputName) 297 | 298 | @reg 299 | class FirstAvailable(SingleOutputMixin,LinksNode): 300 | """ 301 | takes a list of inlinks, and chooses the first active one to hook up to the output 302 | 303 | if none are active, output is inactive. 304 | """ 305 | def __init__(self,*inlinks): 306 | LinksNode.__init__(self,*inlinks) 307 | assert len(inlinks)>0 308 | firstType=inlinks[0].getType() 309 | for link in inlinks: 310 | assertEqual(firstType,link.getType()) 311 | 312 | outLink=Link(firstType) 313 | SingleOutputMixin.__init__(self,outLink) 314 | self.source=makePassThroughCode(firstType) 315 | 316 | 317 | def getActiveNodes(self,renderState,linkStatus): 318 | for i,input in enumerate(self.links): 319 | if linkStatus[input]: 320 | linkStatus[self.outLink] = True 321 | return (ActiveNode((),(input,),(self.outLink,),self.source,False, 322 | "FirstAvailable: choose #"+str(i)+" (0-"+str(len(self.links)-1)+")"),) 323 | return () 324 | 325 | @reg 326 | class AllAvailable(SingleOutputMixin,LinksNode): 327 | """ 328 | takes a list of inlinks, and outputs if they are all available 329 | """ 330 | def __init__(self,*inlinks): 331 | LinksNode.__init__(self,*inlinks) 332 | outLink=Link(boolLinkType) 333 | SingleOutputMixin.__init__(self,outLink) 334 | 335 | def getActiveNodes(self,renderState,linkStatus): 336 | for input in self.links: 337 | if not linkStatus[input]: return () 338 | linkStatus[self.outLink] = True 339 | return () 340 | 341 | @reg 342 | class AnyAvailable(SingleOutputMixin,LinksNode): 343 | """ 344 | takes a list of inlinks, and outputs if they any are available 345 | """ 346 | def __init__(self,*inlinks): 347 | LinksNode.__init__(self,*inlinks) 348 | outLink=Link(boolLinkType) 349 | SingleOutputMixin.__init__(self,outLink) 350 | 351 | def getActiveNodes(self,renderState,linkStatus): 352 | for input in self.links: 353 | if linkStatus[input]: 354 | linkStatus[self.outLink] = True 355 | return () 356 | 357 | @reg 358 | class NoneAvailable(SingleOutputMixin,LinksNode): 359 | """ 360 | takes a list of inlinks, and outputs if they any are available 361 | """ 362 | def __init__(self,*inlinks): 363 | LinksNode.__init__(self,*inlinks) 364 | outLink=Link(boolLinkType) 365 | SingleOutputMixin.__init__(self,outLink) 366 | 367 | def getActiveNodes(self,renderState,linkStatus): 368 | for input in self.links: 369 | if linkStatus[input]: return () 370 | linkStatus[self.outLink] = True 371 | return () 372 | 373 | @reg 374 | class HasTag(SingleOutputMixin,ScriptNode): 375 | """ 376 | this node produces no activeNode, but marks it's outlink as active if the tag is present 377 | """ 378 | def __init__(self,tagName): 379 | ScriptNode.__init__(self) 380 | assertString(tagName) 381 | self.tagName=tagName 382 | outLink=Link(boolLinkType) 383 | SingleOutputMixin.__init__(self,outLink) 384 | 385 | def getActiveNodes(self,renderState,linkStatus): 386 | if renderState.hasTag(self.tagName): 387 | linkStatus[self.outLink] = True 388 | return () 389 | 390 | def setupRenderStateFactory(self,renderStateFactory): 391 | renderStateFactory.tags.add(self.tagName) 392 | 393 | 394 | 395 | @reg 396 | class HasFlag(SingleOutputMixin,ScriptNode): 397 | """ 398 | this node produces no activeNode, but marks it's outlink as active if the tag is present 399 | """ 400 | def __init__(self,flagName): 401 | ScriptNode.__init__(self) 402 | assertString(flagName) 403 | self.flagName=flagName 404 | outLink=Link(boolLinkType) 405 | SingleOutputMixin.__init__(self,outLink) 406 | 407 | def getActiveNodes(self,renderState,linkStatus): 408 | if renderState.hasFlag(self.flagName): 409 | linkStatus[self.outLink] = True 410 | return () 411 | 412 | def setupRenderStateFactory(self,renderStateFactory): 413 | renderStateFactory.flags.add(self.flagName) 414 | 415 | 416 | @reg 417 | class ConditionalPassThrough(SingleOutputMixin,ScriptNode): 418 | def __init__(self,conditionLink,dataLink): 419 | ScriptNode.__init__(self) 420 | assertLink(conditionLink) 421 | self.conditionLink=conditionLink 422 | self.dataLink=dataLink 423 | type=dataLink.getType() 424 | outLink=Link(type) 425 | source=makePassThroughCode(type) 426 | self.activeNode=ActiveNode((),(dataLink,),(outLink,),source,False,"ConditionalPassThrough") 427 | SingleOutputMixin.__init__(self,outLink) 428 | 429 | def getActiveNodes(self,renderState,linkStatus): 430 | if linkStatus[self.conditionLink]: 431 | linkStatus[self.outLink] = True 432 | return (self.activeNode,) 433 | else: 434 | return () 435 | 436 | 437 | @reg 438 | class Operator(SingleOutputMixin,LinksNode): 439 | def __init__(self,requireAll,op,*inlinks): 440 | LinksNode.__init__(self,*inlinks) 441 | self.requireAll=bool(requireAll) 442 | assertString(op) 443 | self.op=op 444 | 445 | assert len(inlinks)>0 446 | firstType=inlinks[0].getType() 447 | # for link in inlinks: 448 | # assert firstType==link.getType() 449 | 450 | outLink=Link(firstType,"output") 451 | 452 | SingleOutputMixin.__init__(self,outLink) 453 | if requireAll: 454 | self.activeNode=self.makeActiveNode(inlinks) 455 | 456 | def makeActiveNode(self,inlinks): 457 | params=[param.Param("input"+str(i),link.getType()) for i,link in enumerate(inlinks)] 458 | type=self.outLink.getType() 459 | code="output="+self.op.join(p.getName() for p in params)+";" 460 | source=makeFullCode(code,(),params,(self.outLink,)) 461 | return ActiveNode((),inlinks,(self.outLink,),source,False,"Operator: "+self.op) 462 | 463 | def getActiveNodes(self,renderState,linkStatus): 464 | if self.requireAll: 465 | if allActive(linkStatus,self.links): 466 | linkStatus[self.outLink]=True 467 | return (self.activeNode,) 468 | else: 469 | return () 470 | else: 471 | activeInputs=[link for link in self.links if linkStatus[link]] 472 | if len(activeInputs)>0: 473 | linkStatus[self.outLink]=True 474 | return (self.makeActiveNode(tuple(activeInputs)),) 475 | else: 476 | return () 477 | 478 | @reg 479 | class Output(ScriptNode): 480 | def __init__(self,stage,inlink,outputDef): 481 | ScriptNode.__init__(self) 482 | assertString(stage) 483 | assertString(outputDef) 484 | output=param.shaderParamFromDefCode(outputDef) 485 | 486 | assertEqual(inlink.getType(),output.getShortType()) 487 | 488 | source=makePassThroughCode(output.getType(),True) 489 | self.activeNode=ActiveOutput(stage,inlink,output) 490 | 491 | self.inlink=inlink 492 | 493 | self.shaderInput=Input(outputDef) 494 | 495 | def getDefaultLink(self): 496 | return self.shaderInput.getDefaultLink() 497 | 498 | def getActiveNodes(self,renderState,linkStatus): 499 | assert linkStatus[self.inlink], "Output node '{out}'' must have active input '{input}'".format(out=self.shaderInput.activeNode,input=self.inlink) 500 | return (self.activeNode,self.shaderInput.getActiveNodes(renderState,linkStatus)[0]) 501 | 502 | @reg 503 | class ConditionalOutput(Output): 504 | def getActiveNodes(self,renderState,linkStatus): 505 | if linkStatus[self.inlink]: 506 | return Output.getActiveNodes(self,renderState,linkStatus) 507 | else: 508 | return () 509 | 510 | @reg 511 | class Constant(SingleOutputMixin,ScriptNode): 512 | def __init__(self,type,value): 513 | ScriptNode.__init__(self) 514 | assertString(type) 515 | assertString(value) 516 | 517 | outLink=Link(type,"output") 518 | 519 | SingleOutputMixin.__init__(self,outLink) 520 | code="output="+value+";" 521 | source=makeFullCode(code,(),(),(self.outLink,)) 522 | self.activeNode=ActiveNode((),(),(self.outLink,),source,False,"Constant: "+type+"="+value) 523 | 524 | def getActiveNodes(self,renderState,linkStatus): 525 | linkStatus[self.outLink]=True 526 | return (self.activeNode,) 527 | 528 | 529 | def metaHasRenderAttrib(slot): 530 | class HasRenderAttrib(SingleOutputMixin,ScriptNode): 531 | def __init__(self): 532 | ScriptNode.__init__(self) 533 | outLink=Link(boolLinkType) 534 | SingleOutputMixin.__init__(self,outLink) 535 | 536 | def getActiveNodes(self,renderState,linkStatus): 537 | if renderState.hasRenderAttrib(slot): 538 | linkStatus[self.outLink] = True 539 | return () 540 | 541 | def setupRenderStateFactory(self,renderStateFactory): 542 | renderStateFactory.hasRenderAttribs.add(slot) 543 | return HasRenderAttrib 544 | 545 | reg(metaHasRenderAttrib(MaterialAttrib.getClassSlot()),"HasMaterial") 546 | reg(metaHasRenderAttrib(ColorAttrib.getClassSlot()),"HasColorAttrib") 547 | reg(metaHasRenderAttrib(TextureAttrib.getClassSlot()),"HasTextureAttrib") 548 | 549 | @reg 550 | class HasColumn(SingleOutputMixin,ScriptNode): 551 | def __init__(self,name): 552 | assertString(name) 553 | self.name=name 554 | ScriptNode.__init__(self) 555 | outLink=Link(boolLinkType) 556 | SingleOutputMixin.__init__(self,outLink) 557 | 558 | def getActiveNodes(self,renderState,linkStatus): 559 | if renderState.hasGeomVertexDataColumns(self.name): 560 | linkStatus[self.outLink] = True 561 | return () 562 | 563 | def setupRenderStateFactory(self,renderStateFactory): 564 | renderStateFactory.geomVertexDataColumns.add(self.name) 565 | 566 | 567 | @reg 568 | class Select(SingleOutputMixin,ScriptNode): 569 | """ 570 | Passes through the dataLinkA input if conditionLink is true, else dataLinkB 571 | """ 572 | def __init__(self,conditionLink,dataLinkA,dataLinkB): 573 | ScriptNode.__init__(self) 574 | assertLink(conditionLink) 575 | assertEqual(dataLinkA.getType(),dataLinkB.getType()) 576 | self.conditionLink=conditionLink 577 | self.dataLinks=(dataLinkA,dataLinkB) 578 | type=dataLinkA.getType() 579 | outLink=Link(type) 580 | source=makePassThroughCode(type) 581 | self.activeNodes=( 582 | ActiveNode((),(dataLinkA,),(outLink,),source,False,"SelectA"), 583 | ActiveNode((),(dataLinkB,),(outLink,),source,False,"SelectB") 584 | ) 585 | SingleOutputMixin.__init__(self,outLink) 586 | 587 | def getActiveNodes(self,renderState,linkStatus): 588 | source=0 if linkStatus[self.conditionLink] else 1 589 | if linkStatus[self.dataLinks[source]]: 590 | linkStatus[self.outLink] = True 591 | return (self.activeNodes[source],) 592 | return () 593 | -------------------------------------------------------------------------------- /shaderBuilder.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import collections 3 | import os 4 | import renderState 5 | 6 | import param 7 | import nodes 8 | 9 | import inspect 10 | 11 | from panda3d.core import Shader 12 | from direct.showbase.AppRunnerGlobal import appRunner 13 | isP3d=bool(appRunner) 14 | 15 | """ 16 | 17 | A system for generating shader generators based on the generator specifications (the graph files). 18 | 19 | IMPORTANT: 20 | The script files do NOT discribe specific shaders. 21 | They describe a shader generator (ShaderBuilder), which takes input (render states) and outputs shaders. 22 | To enable this, the nodes is the shader graph files (defined by the libraries) are not all simply shader functions. 23 | They are all code generators, which may or may not produce the same code in all cases. 24 | The graph files specifify how to instance and connect the code generator nodes together. 25 | Thus, this shaderBuilder system is an implementation of a Shader Meta-Language. It is NOT a Shader Language. 26 | 27 | Specifically, a script file and library files, together with any NodeType subclasses, 28 | are used as souce code (in the Shader Meta-Language) to 29 | essencially compile a function (ShaderBuilder instance) that accepts renderStates and retuns CG Shader code. 30 | 31 | 32 | Usage: 33 | - Load up a Library instance from a list of folders on disc 34 | - Use Library.loadScript to load up a shader generator specification graph file (perhaps made with the editor) 35 | that uses the NodeTypes from the Library. Returns a ShaderBuilder 36 | - Use one or more ShaderBuilders to generate (from one or more Libraryies and scripts) to generate shaders for 37 | your scene 38 | 39 | 40 | TODO : 41 | 42 | Loaders really need to assoiate file name and line numbers with all the loaded items 43 | so error reporting can be far more useful! 44 | 45 | TODO : 46 | 47 | Deployment system could concatenate libraries down to 1 file if desired, 48 | or one could pregenerate shaders and store them with their models in a cache 49 | if they don't need dynamic generation 50 | 51 | TODO : 52 | auto generate semantics? 53 | 54 | TODO : 55 | fix generator graph node text for first line (line 1) 56 | """ 57 | 58 | 59 | from direct.stdpy import file 60 | def join(*paths): 61 | """ an join function for file paths that works in p3d, and regular file systems""" 62 | if len(paths)>1 and paths[0]=='': return join(*paths[1:]) 63 | if len(paths)==1: return paths[0] 64 | return reduce(file.join,paths) 65 | 66 | 67 | # if true, adds comments to generated source to aid debugging 68 | debugText=True 69 | 70 | 71 | def _parseFile(path): 72 | majorSections=collections.defaultdict(list) 73 | f = open(path, 'r') 74 | 75 | majorSection=None 76 | section=None 77 | 78 | lineNum=0 79 | 80 | def warnText(): return "Warning: "+path+" line "+str(lineNum)+": " 81 | 82 | for t in f.readlines(): 83 | lineNum+=1 84 | 85 | # Remove comments 86 | i=t.find('#') 87 | if i!=-1: t=t[:i] 88 | 89 | # Strip excess whitespace 90 | t=t.strip() 91 | 92 | 93 | if len(t)>0: 94 | # Process line 95 | if len(t)>1 and t[0:2]=='::': 96 | section=None 97 | majorSection=t[2:].lower().strip() 98 | majorSectionList=majorSections[majorSection] 99 | d={} 100 | majorSectionList.append(d) 101 | 102 | elif t[0]==':': 103 | # if section header, prefixed with : 104 | if majorSection is None: 105 | print warnText()+"throwing away invalid section occuring before first majorSection in: "+path 106 | else: 107 | currentList=[] 108 | section=t[1:].lower().strip() 109 | d[section]=currentList 110 | 111 | else: 112 | if section is None: 113 | print warnText()+"throwing away invalid line occuring before first section in: "+path+" section: "+str(section) 114 | elif currentList!=None: 115 | currentList.append(t) 116 | f.close() 117 | return majorSections 118 | 119 | def _parseInfoLines(lines,currentFile): 120 | info={} 121 | for line in lines: 122 | s=line.split(None, 1) 123 | if len(s)!=2: 124 | print "invalid info entry '"+line+"' in: "+currentFile 125 | else: 126 | info[s[0]]=s[1] 127 | return info 128 | 129 | 130 | 131 | 132 | 133 | class NodeWrapper(object): 134 | """ 135 | A wrapper around a node, 136 | intended to be returned as a node in script files 137 | 138 | sourceTracker is an optional debug dict for link to source scriptNode tracking 139 | """ 140 | def __init__(self,scriptNode,sourceTracker=None): 141 | self._scriptNode=scriptNode 142 | self.sourceTracker=sourceTracker 143 | def __getattr__(self,name): 144 | link=self._scriptNode.getLink(name) 145 | if self.sourceTracker is not None: self.sourceTracker[link]=self._scriptNode 146 | return link 147 | 148 | 149 | def _preprocessParam(param,sourceTracker=None): 150 | if isinstance(param,NodeWrapper): 151 | link=param._scriptNode.getDefaultLink() 152 | if sourceTracker is not None: sourceTracker[link]=param._scriptNode 153 | return link 154 | else: 155 | return param 156 | 157 | class Library(object): 158 | def __init__(self,paths,nodeTypeClassMap={}): 159 | """ 160 | 161 | path should be a path to a library folder 162 | 163 | builds an instance made from the contents of the passed folder path. 164 | 165 | 166 | nodeTypeClassMap should be a dict mapping strings to NodeType subclasses. 167 | The strings should correspond to the "class" info field used in the nodes in the library. 168 | no "class" info (a None in the dictionary) maps to NodeType, not a subclass. 169 | 170 | 171 | """ 172 | 173 | 174 | self.nodeTypeClassMap=dict(nodes.defaultNodeClasses) 175 | self.nodeTypeClassMap.update(nodeTypeClassMap) 176 | self.loadPath(paths) 177 | 178 | def loadPath(self,paths): 179 | """ 180 | 181 | called by init, but can be called again if you wish to reload the same paths, or a different one 182 | 183 | """ 184 | 185 | 186 | libs=[] 187 | 188 | for root, dirs, files in itertools.chain.from_iterable(os.walk(path) for path in paths): 189 | for name in files: 190 | ext=os.path.splitext(name)[1] 191 | if ext==".txt": 192 | currentFile=join(root, name) 193 | for key,xitems in _parseFile(currentFile).iteritems(): 194 | if key=="node": 195 | for items in xitems: 196 | if "info" not in items: 197 | print "node missing info section in: "+currentFile 198 | else: 199 | 200 | 201 | info=_parseInfoLines(items["info"],currentFile) 202 | 203 | if "name" not in info: 204 | print "invalid info entry missing name in: "+currentFile 205 | else: 206 | name=info["name"] 207 | 208 | shaderInputs=[] 209 | if "shaderinputs" in items: 210 | for s in items["shaderinputs"]: 211 | shaderInputs.append(param.shaderParamFromDefCode(s)) 212 | 213 | if "output" in info: 214 | o=info["output"] 215 | assert o in ["True","False"] 216 | isOutPut=o=="True" 217 | assert "stage" in info 218 | stage=info["stage"] 219 | else: 220 | isOutPut=False 221 | stage=None 222 | 223 | inLinks=[] 224 | if "inlinks" in items: 225 | for s in items["inlinks"]: 226 | inLinks.append(param.linkEndFromDefCode(s)) 227 | outLinks=[] 228 | if "outlinks" in items: 229 | for s in items["outlinks"]: 230 | outLinks.append(param.linkEndFromDefCode(s)) 231 | 232 | 233 | code="" 234 | if "code" in items: 235 | code="\n".join(items["code"]) 236 | 237 | node=nodes.metaCodeNode(name,code,shaderInputs,inLinks,outLinks,isOutPut=isOutPut,stage=stage) 238 | if name in self.nodeTypeClassMap: 239 | print "Warning: overwriting node "+repr(self.nodeTypeClassMap[name])+" with "+repr(node)+" from "+currentFile 240 | self.nodeTypeClassMap[name]=node 241 | 242 | elif key=="lib": 243 | libs.append(xitems) 244 | else: 245 | print "Warning: throwing away invalid majorSection with unrecognized name: "+key+" in file: "+currentFile 246 | 247 | libSource="\n".join(itertools.chain.from_iterable(lib["code"] for lib in itertools.chain.from_iterable(libs) if "code" in lib)) 248 | 249 | self.libSource=libSource 250 | 251 | 252 | 253 | def loadScript(self,path,viewGraph=False): 254 | """ 255 | loads a generater script at path, returns a ShaderBuilder 256 | """ 257 | return ShaderBuilder(self._parseScript(path,viewGraph),self.libSource) 258 | 259 | def _parseScript(self,path,viewGraph=False): 260 | # setup some globals with the names of the Node classes in self.nodeTypeClassMap 261 | scriptGlobals={} 262 | if viewGraph: 263 | nodeInfoDict={} 264 | sourceTracker={} 265 | else: 266 | sourceTracker=None 267 | 268 | for name,nodeType in self.nodeTypeClassMap.iteritems(): 269 | 270 | # this closure is the auctual item put into the scriptGlobals for the script 271 | # it poses as a Node class, but produces NodeWrappers instead of Nodes, 272 | # and also runs _preprocessParam on all passed arguments 273 | def wrapperMaker(name,nodeType): 274 | def scriptNodeWrapper(*args,**kargs): 275 | pargs=[_preprocessParam(param,sourceTracker) for param in args] 276 | for name,param in kargs.iteritems(): 277 | kargs[name]=_preprocessParam(param) 278 | node=nodeType(*pargs,**kargs) 279 | nodeList.append(node) 280 | if viewGraph: 281 | stack=inspect.stack() 282 | frame, filename, lineNum, functionName, contextLines, contextIndex=stack[1] 283 | debugInfo=(filename, lineNum, contextLines[contextIndex].rstrip(), pargs, kargs) 284 | nodeInfoDict[node]=debugInfo 285 | return NodeWrapper(node,sourceTracker) 286 | return scriptNodeWrapper 287 | scriptGlobals[name]=wrapperMaker(name,nodeType) 288 | 289 | 290 | # run the script with the newly made scriptGlobals 291 | nodeList=[] 292 | 293 | if isP3d: 294 | # don't use execfile since this works easier within p3d files 295 | exec open(path).read() in scriptGlobals 296 | else: 297 | execfile(path,scriptGlobals,{}) 298 | 299 | if viewGraph: 300 | import pydot 301 | 302 | graph = pydot.Dot(graph_type='digraph') 303 | 304 | for node,info in nodeInfoDict.iteritems(): 305 | filename, lineNum, line, pargs, kargs=info 306 | graph.add_node(pydot.Node(strId(node), label=str(lineNum)+": "+line, shape="rectangle")) 307 | for a in pargs: 308 | if isinstance(a,nodes.Link): 309 | e = pydot.Edge( strId(sourceTracker[a]),strId(node), label=str(a) ) 310 | graph.add_edge(e) 311 | for name,a in kargs.iteritems(): 312 | if isinstance(a,nodes.Link): 313 | e = pydot.Edge(strId(sourceTracker[a]),strId(node), label=name+"="+str(a)) 314 | graph.add_edge(e) 315 | writeGraph(graph,path) 316 | 317 | return nodeList 318 | 319 | 320 | # a little helper for naming stuff in viewGraph's graphs 321 | def strId(obj): return str(id(obj)) 322 | 323 | # writes out a graph in the chosen format (svg for now) 324 | def writeGraph(graph,path): 325 | format="svg" 326 | finalPath=path+"."+format 327 | print 'Making Graph: '+finalPath 328 | graph.write(finalPath,format=format) 329 | 330 | class ShaderBuilder(object): 331 | """ 332 | 333 | A factory for shaders based off a set of Nodes. Make one instance for each distinct set of stages. 334 | 335 | """ 336 | def __init__(self,nodes,libSource=""): 337 | """ 338 | 339 | Takes an dict of lists of Nodes, and sets this instance up to produce shaders based on them. 340 | 341 | """ 342 | self.nodes=nodes 343 | 344 | # a cache of finished shaders. Maps RenderState to Shader 345 | self.cache={} 346 | 347 | # a cache of finished shaders. Maps set of stage source strings to Shader 348 | self.casheByStages={} 349 | 350 | self.header="//Cg\n//AUTO-GENERATED-SHADER//\n\n"+libSource+"\n\n" 351 | self.footer="\n\n//END-AUTO-GENERATED-SHADER//\n" 352 | 353 | 354 | 355 | def setupRenderStateFactory(self,factory=None): 356 | """ 357 | configures and returns a RenderStateFactory (see renderState.RenderStateFactory) 358 | for this ShaderBuilder. Use it to get renderstates for getShader 359 | """ 360 | if factory is None: factory=renderState.RenderStateFactory() 361 | for n in self.nodes: 362 | n.setupRenderStateFactory(factory) 363 | return factory 364 | 365 | 366 | def getShader(self,renderState,debugFile=None,noChache=False,debugGraphPath=None): 367 | """ 368 | 369 | returns a shader appropriate for the passed RenderState 370 | 371 | will generate or fetch from cache as needed 372 | 373 | noChache forces the generation of the shader (but it will still get cached). 374 | Useful for use with debugFile if you need to see the source, but it may be cached 375 | 376 | caching system isn't verg good in the case where the render state is different, but the resulting shader is the same. 377 | It will find the shader in the cache, but it will take a while. 378 | 379 | """ 380 | 381 | 382 | 383 | shader=self.cache.get(renderState) 384 | if shader and not noChache: 385 | #if debugFile: print "Shader is cached (renderState cache). Skipping generating shader to: "+debugFile 386 | return shader 387 | 388 | 389 | if debugGraphPath: 390 | debugGraphPath+=str(len(self.casheByStages)) 391 | 392 | stages=makeStages(self.nodes,renderState,debugGraphPath) 393 | 394 | stages=frozenset(stages) 395 | shader=self.casheByStages.get(stages) 396 | if shader and not noChache: 397 | self.cache[renderState]=shader 398 | #if debugFile: print "Shader is cached (renderState cache). Skipping generating shader to: "+debugFile 399 | return shader 400 | 401 | 402 | # TODO : Auto generate/match unspecified semantics here 403 | 404 | stageCode="\n\n".join(stages) 405 | 406 | 407 | source = self.header+"\n\n"+stageCode+self.footer 408 | 409 | 410 | 411 | 412 | if debugFile: 413 | debugFile+=str(len(self.casheByStages))+".sha" 414 | print 'Making Shader: '+debugFile 415 | 416 | 417 | if debugFile: 418 | fOut=open(debugFile, 'w') 419 | fOut.write(source) 420 | fOut.close() 421 | 422 | shader=Shader.make(source, Shader.SLCg) 423 | self.cache[renderState]=shader 424 | self.casheByStages[stages]=shader 425 | return shader 426 | 427 | 428 | 429 | def makeStages(nodes,renderState,debugGraphPath=None): 430 | # process from top down (topological sorted order) to see what part of graph is active, and produce active graph 431 | # nodes are only processed when all nodes above them have been processed. 432 | 433 | # set of activeNodes that are needed because they produce output values 434 | # maps stage to its set of outputs 435 | activeOutputs=collections.defaultdict(set) 436 | 437 | # linksStatus defaults to false for all links. 438 | # a linkStatus for links (edges) in the active graph may be associated with the link 439 | # by the node that outputs it when generated. 440 | # generally false means inactive/not available, and true means available/active 441 | # though some nodes may use the status differently 442 | linkStatus=collections.defaultdict(lambda:False) 443 | 444 | # dict mapping links to the activeNode that outputs them 445 | linkToSource={} 446 | 447 | # list of active nodes, in the same order as source nodes, which should be topologically sorted 448 | sortedActive=[] 449 | 450 | # traverse nodes, filling in data-structures inited above. 451 | for n in nodes: 452 | aa=n.getActiveNodes(renderState,linkStatus) 453 | for a in aa: 454 | sortedActive.append(a) 455 | for link in a.getOutLinks(): 456 | linkToSource[link]=a 457 | 458 | if a.isOutPut(): 459 | activeOutputs[a.stage].add(a) 460 | 461 | # yield the resulting stages. 462 | path=None 463 | for name,outputs in activeOutputs.iteritems(): 464 | if debugGraphPath: path=debugGraphPath+name 465 | yield makeStage(name,sortedActive,outputs,linkToSource,path) 466 | 467 | def makeStage(name,sortedActive,activeOutputs,linkToSource,debugGraphPath=None): 468 | # walk upward from outputs to find nodes the current stage requires recusrivly (aka needed nodes) 469 | 470 | neededSet=set(activeOutputs) 471 | neededNodes=[] 472 | 473 | for n in reversed(sortedActive): 474 | if n in neededSet: 475 | neededNodes.append(n) 476 | for link in n.getInLinks(): 477 | neededSet.add(linkToSource[link]) 478 | 479 | if debugGraphPath: 480 | import pydot 481 | 482 | graph = pydot.Dot(graph_type='digraph') 483 | 484 | 485 | for node in neededSet: 486 | if isinstance(node,nodes.ActiveOutput): 487 | n=pydot.Node(strId(node), label=node.stage+" Output: "+str(node.shaderOutput), shape="rectangle") 488 | else: 489 | n=pydot.Node(strId(node), label=node.getComment(), shape="rectangle") 490 | graph.add_node(n) 491 | for link in node.getInLinks(): 492 | e = pydot.Edge(strId(linkToSource[link]),strId(node), label=str(link) ) 493 | graph.add_edge(e) 494 | 495 | writeGraph(graph,debugGraphPath) 496 | 497 | return makeStageFromActiveNodes(name,tuple(neededNodes)) 498 | 499 | 500 | 501 | stageCache={} 502 | def makeStageFromActiveNodes(name,activeNodes): 503 | key=(name,activeNodes) 504 | s=stageCache.get(key) 505 | if s is None: 506 | b=StageBuilder() 507 | namer=AutoNamer("__"+name+"_") 508 | for node in activeNodes: b.addNode(node,namer) 509 | s=b.generateSource(name) 510 | 511 | s="\n\n".join(namer.getItems())+"\n\n"+s 512 | 513 | stageCache[key]=s 514 | return s 515 | 516 | class AutoNamer(object): 517 | """ 518 | 519 | A simple class for associating unique names with hashables 520 | 521 | """ 522 | def __init__(self,prefix): 523 | self.items={} 524 | self.prefix=prefix 525 | def addItem(self,item): 526 | if item not in self.items: 527 | self.items[item]=self.nextName() 528 | def getItems(self): return self.items 529 | def nextName(self): return self.prefix+str(len(self.items)) 530 | 531 | 532 | class StageBuilder(object): 533 | """ 534 | 535 | Used by ShaderBuilder to build the different stages in the shaders 536 | 537 | All nodes used in here are ActiveNodes 538 | 539 | built bottom up 540 | 541 | """ 542 | def __init__(self): 543 | self.links=AutoNamer("__x") 544 | self.inputs=set() 545 | self.outputs=set() 546 | self.sourceLines=[] 547 | def _addLink(self,link): 548 | self.links.addItem(link) 549 | 550 | def addNode(self,node,functionNamer): 551 | """ 552 | links=list of links passed to Node's function. Contains in and out ones. 553 | """ 554 | if isinstance(node,nodes.ActiveOutput): 555 | self._addLink(node.inLink) 556 | o=node.shaderOutput 557 | self.outputs.add(o) 558 | code=o.getName()+"="+self.links.getItems()[node.inLink]+";" 559 | self.sourceLines.append(code) 560 | else: 561 | inputs=node.getShaderInputs() 562 | self.inputs.update(inputs) 563 | 564 | inLinks=node.getInLinks() 565 | outLinks=node.getOutLinks() 566 | 567 | for link in itertools.chain(inLinks,outLinks): 568 | self._addLink(link) 569 | 570 | ld=self.links.getItems() 571 | 572 | paramChain=itertools.chain( 573 | (s.getName() for s in inputs), 574 | (ld[s] for s in itertools.chain(inLinks,outLinks)), 575 | ) 576 | 577 | fname=functionNamer.nextName() 578 | callSource=fname+"("+",".join(paramChain)+");" 579 | self.sourceLines.append(callSource) 580 | 581 | # make the function 582 | f="void "+fname+node.getCode() 583 | 584 | if debugText: 585 | comment="//"+node.getComment() 586 | f=comment+'\n'+f 587 | self.sourceLines.append('\n'+comment) 588 | functionNamer.addItem(f) 589 | 590 | 591 | def generateSource(self,name): 592 | paramChain=itertools.chain( 593 | ("in "+s.getDefCode() for s in self.inputs), 594 | ("out "+s.getDefCode() for s in self.outputs) 595 | ) 596 | 597 | header="void "+name+"(\n "+",\n ".join(paramChain)+")\n{\n\n" 598 | footer="}" 599 | linkDeclarations='\n'.join(link.getType()+" "+name+";//"+link.name for link,name in self.links.getItems().iteritems()) 600 | source='\n'.join(reversed(self.sourceLines)) 601 | return header+linkDeclarations+'\n\n'+source+'\n'+footer 602 | --------------------------------------------------------------------------------