from enum import IntEnum, unique from typing import List, Set, Tuple, Union, Optional import bpy import lnx from lnx.material.shader import Shader, ShaderContext, vec3str, floatstr if lnx.is_reload(__name__): lnx.material.shader = lnx.reload_module(lnx.material.shader) from lnx.material.shader import Shader, ShaderContext, vec3str, floatstr else: lnx.enable_reload(__name__) @unique class ParserContext(IntEnum): """Describes which kind of node tree is parsed.""" OBJECT = 0 # Texture node trees are not supported yet # TEXTURE = 1 WORLD = 2 @unique class ParserPass(IntEnum): """In some situations, a node tree (or a subtree of that) needs to be parsed multiple times in different contexts called _passes_. Nodes can output different code in reaction to the parser state's current pass; for more information on the individual passes please refer to below enum items. """ REGULAR = 0 """The tree is parsed to generate regular shader code.""" DX_SCREEN_SPACE = 1 """The tree is parsed to output shader code to compute the derivative of a value with respect to the screen's x coordinate.""" DY_SCREEN_SPACE = 2 """The tree is parsed to output shader code to compute the derivative of a value with respect to the screen's y coordinate.""" class ParserState: """Dataclass to keep track of the current state while parsing a shader tree.""" def __init__(self, context: ParserContext, tree_name: str, world: Optional[bpy.types.World] = None): self.context = context self.tree_name = tree_name self.current_pass = ParserPass.REGULAR # The current world, if parsing a world node tree self.world = world # Active shader - frag for surface / tese for displacement self.curshader: Shader = None self.con: ShaderContext = None self.vert: Shader = None self.frag: Shader = None self.geom: Shader = None self.tesc: Shader = None self.tese: Shader = None # Group stack (last in the list = innermost group) self.parents: List[bpy.types.Node] = [] # Cache for computing nodes only once self.parsed: Set[str] = set() # What to parse from the node tree self.parse_surface = True self.parse_opacity = True self.parse_displacement = True self.basecol_only = False self.procedurals_written: set[Shader] = set() # Already exported radiance/irradiance (currently we can only convert # an already existing texture as radiance/irradiance) self.radiance_written = False self.normal_parsed = False self.dxdy_varying_input_value = False """Whether the result of the previously parsed node differs between fragments and represents an input value to which to apply dx/dy offsets (if required by the parser pass). """ # Shader output values self.out_basecol: vec3str = 'vec3(0.8)' self.out_roughness: floatstr = '0.0' self.out_metallic: floatstr = '0.0' self.out_occlusion: floatstr = '1.0' self.out_specular: floatstr = '1.0' self.out_opacity: floatstr = '1.0' self.out_ior: floatstr = '1.450' self.out_emission_col: vec3str = 'vec3(0.0)' def reset_outs(self): """Reset the shader output values to their default values.""" self.out_basecol = 'vec3(0.8)' self.out_roughness = '0.0' self.out_metallic = '0.0' self.out_occlusion = '1.0' self.out_specular = '1.0' self.out_opacity = '1.0' self.out_ior = '1.450' self.out_emission_col = 'vec3(0.0)' def get_outs(self) -> Tuple[vec3str, floatstr, floatstr, floatstr, floatstr, floatstr, floatstr, vec3str]: """Return the shader output values as a tuple.""" return (self.out_basecol, self.out_roughness, self.out_metallic, self.out_occlusion, self.out_specular, self.out_opacity, self.out_ior, self.out_emission_col) def get_parser_pass_suffix(self) -> str: """Return a suffix for the current parser pass that can be appended to shader variables to avoid compilation errors due to redefinitions. """ if self.current_pass == ParserPass.DX_SCREEN_SPACE: return '_dx' elif self.current_pass == ParserPass.DY_SCREEN_SPACE: return '_dy' return ''