from typing import Union import bpy import mathutils import lnx.log as log import lnx.material.cycles as c import lnx.material.cycles_functions as c_functions import lnx.material.mat_state as mat_state from lnx.material.parser_state import ParserState, ParserContext from lnx.material.shader import floatstr, vec3str import lnx.utils if lnx.is_reload(__name__): log = lnx.reload_module(log) c = lnx.reload_module(c) c_functions = lnx.reload_module(c_functions) mat_state = lnx.reload_module(mat_state) lnx.material.parser_state = lnx.reload_module(lnx.material.parser_state) from lnx.material.parser_state import ParserState, ParserContext lnx.material.shader = lnx.reload_module(lnx.material.shader) from lnx.material.shader import floatstr, vec3str lnx.utils = lnx.reload_module(lnx.utils) else: lnx.enable_reload(__name__) def parse_attribute(node: bpy.types.ShaderNodeAttribute, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: out_type = 'float' if out_socket.type == 'VALUE' else 'vec3' if node.attribute_name == 'time': state.curshader.add_uniform('float time', link='_time') if out_socket == node.outputs[3]: return '1.0' return c.cast_value('time', from_type='float', to_type=out_type) # UV maps (higher priority) and vertex colors if node.attribute_type == 'GEOMETRY': # Alpha output. Leenkx doesn't support vertex colors with alpha # values yet and UV maps don't have an alpha channel if out_socket == node.outputs[3]: return '1.0' # UV maps mat = c.mat_get_material() mat_users = c.mat_get_material_users() if mat_users is not None and mat in mat_users: mat_user = mat_users[mat][0] # Curves don't have uv layers, so check that first if hasattr(mat_user.data, 'uv_layers'): lays = mat_user.data.uv_layers # First UV map referenced if len(lays) > 0 and node.attribute_name == lays[0].name: state.con.add_elem('tex', 'short2norm') state.dxdy_varying_input_value = True return c.cast_value('vec3(texCoord.x, 1.0 - texCoord.y, 0.0)', from_type='vec3', to_type=out_type) # Second UV map referenced elif len(lays) > 1 and node.attribute_name == lays[1].name: state.con.add_elem('tex1', 'short2norm') state.dxdy_varying_input_value = True return c.cast_value('vec3(texCoord1.x, 1.0 - texCoord1.y, 0.0)', from_type='vec3', to_type=out_type) # Vertex colors # TODO: support multiple vertex color sets state.con.add_elem('col', 'short4norm') state.dxdy_varying_input_value = True return c.cast_value('vcolor', from_type='vec3', to_type=out_type) # Check object properties # see https://developer.blender.org/rB6fdcca8de6 for reference mat = c.mat_get_material() mat_users = c.mat_get_material_users() if mat_users is not None and mat in mat_users: # Use first material user for now... mat_user = mat_users[mat][0] val = None # Custom properties first if node.attribute_name in mat_user: val = mat_user[node.attribute_name] # Blender properties elif hasattr(mat_user, node.attribute_name): val = getattr(mat_user, node.attribute_name) if val is not None: if isinstance(val, float): return c.cast_value(str(val), from_type='float', to_type=out_type) elif isinstance(val, int): return c.cast_value(str(val), from_type='int', to_type=out_type) elif isinstance(val, mathutils.Vector) and len(val) <= 4: out = val.to_4d() if out_socket == node.outputs[3]: return c.to_vec1(out[3]) return c.cast_value(c.to_vec3(out), from_type='vec3', to_type=out_type) # Default values, attribute name did not match if out_socket == node.outputs[3]: return '1.0' return c.cast_value('0.0', from_type='float', to_type=out_type) def parse_rgb(node: bpy.types.ShaderNodeRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: if node.lnx_material_param: nn = 'param_' + c.node_name(node.name) v = out_socket.default_value value = [float(v[0]), float(v[1]), float(v[2])] state.curshader.add_uniform(f'vec3 {nn}', link=f'{node.name}', default_value=value, is_lnx_mat_param=True) return nn else: return c.to_vec3(out_socket.default_value) def parse_vertex_color(node: bpy.types.ShaderNodeVertexColor, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: state.con.add_elem('col', 'short4norm') return 'vcolor' def parse_camera(node: bpy.types.ShaderNodeCameraData, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: # View Vector in camera space if out_socket == node.outputs[0]: state.dxdy_varying_input_value = True return 'vVecCam' # View Z Depth elif out_socket == node.outputs[1]: state.curshader.add_include('std/math.glsl') state.curshader.add_uniform('vec2 cameraProj', link='_cameraPlaneProj') state.dxdy_varying_input_value = True return 'linearize(gl_FragCoord.z, cameraProj)' # View Distance else: state.curshader.add_uniform('vec3 eye', link='_cameraPosition') state.dxdy_varying_input_value = True return 'distance(eye, wposition)' def parse_geometry(node: bpy.types.ShaderNodeNewGeometry, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: # Position if out_socket == node.outputs[0]: state.dxdy_varying_input_value = True return 'wposition' # Normal elif out_socket == node.outputs[1]: state.dxdy_varying_input_value = True return 'n' if state.curshader.shader_type == 'frag' else 'wnormal' # Tangent elif out_socket == node.outputs[2]: state.dxdy_varying_input_value = True return 'wtangent' # True Normal elif out_socket == node.outputs[3]: state.dxdy_varying_input_value = True return 'n' if state.curshader.shader_type == 'frag' else 'wnormal' # Incoming elif out_socket == node.outputs[4]: state.dxdy_varying_input_value = True return 'vVec' # Parametric elif out_socket == node.outputs[5]: state.dxdy_varying_input_value = True return 'mposition' # Backfacing elif out_socket == node.outputs[6]: return '(1.0 - float(gl_FrontFacing))' if state.context == ParserContext.OBJECT else '0.0' # Pointiness elif out_socket == node.outputs[7]: return '0.0' # Random Per Island elif out_socket == node.outputs[8]: return '0.0' def parse_hairinfo(node: bpy.types.ShaderNodeHairInfo, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: # Tangent Normal if out_socket == node.outputs[3]: return 'vec3(0.0)' else: # Is Strand # Intercept # Thickness # Random return '0.5' def parse_objectinfo(node: bpy.types.ShaderNodeObjectInfo, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: # Location if out_socket == node.outputs[0]: if state.context == ParserContext.WORLD: return c.to_vec3((0.0, 0.0, 0.0)) return 'wposition' # Color elif out_socket == node.outputs[1]: if state.context == ParserContext.WORLD: # Use world strength like Blender background_node = c.node_by_type(state.world.node_tree.nodes, 'BACKGROUND') if background_node is None: return c.to_vec3((0.0, 0.0, 0.0)) return c.to_vec3([background_node.inputs[1].default_value] * 3) # TODO: Implement object color in Iron # state.curshader.add_uniform('vec3 objectInfoColor', link='_objectInfoColor') # return 'objectInfoColor' return c.to_vec3((1.0, 1.0, 1.0)) # Alpha elif out_socket == node.outputs[2]: # TODO, see color output above return '0.0' # Object Index elif out_socket == node.outputs[3]: if state.context == ParserContext.WORLD: return '0.0' state.curshader.add_uniform('float objectInfoIndex', link='_objectInfoIndex') return 'objectInfoIndex' # Material Index elif out_socket == node.outputs[4]: if state.context == ParserContext.WORLD: return '0.0' state.curshader.add_uniform('float objectInfoMaterialIndex', link='_objectInfoMaterialIndex') return 'objectInfoMaterialIndex' # Random elif out_socket == node.outputs[5]: if state.context == ParserContext.WORLD: return '0.0' # Use random value per instance if mat_state.uses_instancing: state.vert.add_out(f'flat float irand') state.frag.add_in(f'flat float irand') state.vert.write(f'irand = fract(sin(gl_InstanceID) * 43758.5453);') return 'irand' state.curshader.add_uniform('float objectInfoRandom', link='_objectInfoRandom') return 'objectInfoRandom' def parse_particleinfo(node: bpy.types.ShaderNodeParticleInfo, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: particles_on = lnx.utils.get_rp().lnx_particles == 'On' # Index if out_socket == node.outputs[0]: c.particle_info['index'] = True return 'p_index' if particles_on else '0.0' # TODO: Random if out_socket == node.outputs[1]: return '0.0' # Age elif out_socket == node.outputs[2]: c.particle_info['age'] = True return 'p_age' if particles_on else '0.0' # Lifetime elif out_socket == node.outputs[3]: c.particle_info['lifetime'] = True return 'p_lifetime' if particles_on else '0.0' # Location if out_socket == node.outputs[4]: c.particle_info['location'] = True return 'p_location' if particles_on else 'vec3(0.0)' # Size elif out_socket == node.outputs[5]: c.particle_info['size'] = True return '1.0' # Velocity elif out_socket == node.outputs[6]: c.particle_info['velocity'] = True return 'p_velocity' if particles_on else 'vec3(0.0)' # Angular Velocity elif out_socket == node.outputs[7]: c.particle_info['angular_velocity'] = True return 'vec3(0.0)' def parse_tangent(node: bpy.types.ShaderNodeTangent, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: state.dxdy_varying_input_value = True return 'wtangent' def parse_texcoord(node: bpy.types.ShaderNodeTexCoord, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: #obj = node.object #instance = node.from_instance if out_socket == node.outputs[0]: # Generated - bounds state.dxdy_varying_input_value = True return 'bposition' elif out_socket == node.outputs[1]: # Normal state.dxdy_varying_input_value = True return 'n' elif out_socket == node.outputs[2]: # UV if state.context == ParserContext.WORLD: return 'vec3(0.0)' state.con.add_elem('tex', 'short2norm') state.dxdy_varying_input_value = True return 'vec3(texCoord.x, 1.0 - texCoord.y, 0.0)' elif out_socket == node.outputs[3]: # Object state.dxdy_varying_input_value = True return 'mposition' elif out_socket == node.outputs[4]: # Camera return 'vec3(0.0)' # 'vposition' elif out_socket == node.outputs[5]: # Window # TODO: Don't use gl_FragCoord here, it uses different axes on different graphics APIs state.frag.add_uniform('vec2 screenSize', link='_screenSize') state.dxdy_varying_input_value = True return f'vec3(gl_FragCoord.xy / screenSize, 0.0)' elif out_socket == node.outputs[6]: # Reflection if state.context == ParserContext.WORLD: state.dxdy_varying_input_value = True return 'n' return 'vec3(0.0)' def parse_uvmap(node: bpy.types.ShaderNodeUVMap, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: # instance = node.from_instance state.con.add_elem('tex', 'short2norm') mat = c.mat_get_material() mat_users = c.mat_get_material_users() state.dxdy_varying_input_value = True if mat_users is not None and mat in mat_users: mat_user = mat_users[mat][0] if hasattr(mat_user.data, 'uv_layers'): layers = mat_user.data.uv_layers # Second UV map referenced if len(layers) > 1 and node.uv_map == layers[1].name: state.con.add_elem('tex1', 'short2norm') return 'vec3(texCoord1.x, 1.0 - texCoord1.y, 0.0)' return 'vec3(texCoord.x, 1.0 - texCoord.y, 0.0)' def parse_fresnel(node: bpy.types.ShaderNodeFresnel, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: state.curshader.add_function(c_functions.str_fresnel) ior = c.parse_value_input(node.inputs[0]) if node.inputs[1].is_linked: dotnv = 'dot({0}, vVec)'.format(c.parse_vector_input(node.inputs[1])) else: dotnv = 'dotNV' state.dxdy_varying_input_value = True return 'fresnel({0}, {1})'.format(ior, dotnv) def parse_layerweight(node: bpy.types.ShaderNodeLayerWeight, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: blend = c.parse_value_input(node.inputs[0]) if node.inputs[1].is_linked: dotnv = 'dot({0}, vVec)'.format(c.parse_vector_input(node.inputs[1])) else: dotnv = 'dotNV' state.dxdy_varying_input_value = True # Fresnel if out_socket == node.outputs[0]: state.curshader.add_function(c_functions.str_fresnel) return 'fresnel(1.0 / (1.0 - {0}), {1})'.format(blend, dotnv) # Facing elif out_socket == node.outputs[1]: return '(1.0 - pow({0}, ({1} < 0.5) ? 2.0 * {1} : 0.5 / (1.0 - {1})))'.format(dotnv, blend) def parse_lightpath(node: bpy.types.ShaderNodeLightPath, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: # https://github.com/blender/blender/blob/master/source/blender/gpu/shaders/material/gpu_shader_material_light_path.glsl if out_socket == node.outputs['Is Camera Ray']: return '1.0' elif out_socket == node.outputs['Is Shadow Ray']: return '0.0' elif out_socket == node.outputs['Is Diffuse Ray']: return '1.0' elif out_socket == node.outputs['Is Glossy Ray']: return '1.0' elif out_socket == node.outputs['Is Singular Ray']: return '0.0' elif out_socket == node.outputs['Is Reflection Ray']: return '0.0' elif out_socket == node.outputs['Is Transmission Ray']: return '0.0' elif out_socket == node.outputs['Ray Length']: return '1.0' elif out_socket == node.outputs['Ray Depth']: return '0.0' elif out_socket == node.outputs['Diffuse Depth']: return '0.0' elif out_socket == node.outputs['Glossy Depth']: return '0.0' elif out_socket == node.outputs['Transparent Depth']: return '0.0' elif out_socket == node.outputs['Transmission Depth']: return '0.0' log.warn(f'Light Path node: unsupported output {out_socket.identifier}.') return '0.0' def parse_value(node: bpy.types.ShaderNodeValue, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: if node.lnx_material_param: nn = 'param_' + c.node_name(node.name) value = node.outputs[0].default_value is_lnx_mat_param = True state.curshader.add_uniform('float {0}'.format(nn), link='{0}'.format(node.name), default_value=value, is_lnx_mat_param=is_lnx_mat_param) return nn else: return c.to_vec1(node.outputs[0].default_value) def parse_wireframe(node: bpy.types.ShaderNodeWireframe, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: # node.use_pixel_size # size = c.parse_value_input(node.inputs[0]) return '0.0'