# # This module builds upon Cycles nodes work licensed as # Copyright 2011-2013 Blender Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import os import shutil from typing import Any, Callable, Dict, Optional, Tuple import bpy import arm.assets import arm.log as log import arm.make_state import arm.material.cycles_functions as c_functions from arm.material.cycles_nodes import * import arm.material.mat_state as mat_state from arm.material.parser_state import ParserState, ParserContext from arm.material.shader import Shader, ShaderContext, floatstr, vec3str import arm.utils if arm.is_reload(__name__): arm.assets = arm.reload_module(arm.assets) log = arm.reload_module(log) arm.make_state = arm.reload_module(arm.make_state) c_functions = arm.reload_module(c_functions) arm.material.cycles_nodes = arm.reload_module(arm.material.cycles_nodes) from arm.material.cycles_nodes import * mat_state = arm.reload_module(mat_state) arm.material.parser_state = arm.reload_module(arm.material.parser_state) from arm.material.parser_state import ParserState, ParserContext arm.material.shader = arm.reload_module(arm.material.shader) from arm.material.shader import Shader, ShaderContext, floatstr, vec3str arm.utils = arm.reload_module(arm.utils) else: arm.enable_reload(__name__) # Particle info export particle_info: Dict[str, bool] = {} state: Optional[ParserState] def parse(nodes, con: ShaderContext, vert: Shader, frag: Shader, geom: Shader, tesc: Shader, tese: Shader, parse_surface=True, parse_opacity=True, parse_displacement=True, basecol_only=False): global state state = ParserState(ParserContext.OBJECT) state.parse_surface = parse_surface state.parse_opacity = parse_opacity state.parse_displacement = parse_displacement state.basecol_only = basecol_only state.con = con state.vert = vert state.frag = frag state.geom = geom state.tesc = tesc state.tese = tese output_node = node_by_type(nodes, 'OUTPUT_MATERIAL') if output_node is not None: custom_particle_node = node_by_name(nodes, 'ArmCustomParticleNode') parse_material_output(output_node, custom_particle_node) # Make sure that individual functions in this module aren't called with an incorrect/old parser state, set it to # None so that it will raise exceptions when not set state = None def parse_material_output(node: bpy.types.Node, custom_particle_node: bpy.types.Node): global particle_info parse_surface = state.parse_surface parse_opacity = state.parse_opacity parse_displacement = state.parse_displacement state.emission_found = False particle_info = { 'index': False, 'age': False, 'lifetime': False, 'location': False, 'size': False, 'velocity': False, 'angular_velocity': False } state.sample_bump = False state.sample_bump_res = '' state.procedurals_written = False wrd = bpy.data.worlds['Arm'] # Surface if parse_surface or parse_opacity: state.parents = [] state.parsed = set() state.normal_parsed = False curshader = state.frag state.curshader = curshader out_basecol, out_roughness, out_metallic, out_occlusion, out_specular, out_opacity, out_emission = parse_shader_input(node.inputs[0]) if parse_surface: curshader.write('basecol = {0};'.format(out_basecol)) curshader.write('roughness = {0};'.format(out_roughness)) curshader.write('metallic = {0};'.format(out_metallic)) curshader.write('occlusion = {0};'.format(out_occlusion)) curshader.write('specular = {0};'.format(out_specular)) if '_Emission' in wrd.world_defs: curshader.write('emission = {0};'.format(out_emission)) if parse_opacity: curshader.write('opacity = {0} - 0.0002;'.format(out_opacity)) # Volume # parse_volume_input(node.inputs[1]) # Displacement if parse_displacement and disp_enabled() and node.inputs[2].is_linked: state.parents = [] state.parsed = set() state.normal_parsed = False rpdat = arm.utils.get_rp() if rpdat.arm_rp_displacement == 'Tessellation' and state.tese is not None: state.curshader = state.tese else: state.curshader = state.vert out_disp = parse_displacement_input(node.inputs[2]) state.curshader.write('vec3 disp = {0};'.format(out_disp)) if custom_particle_node is not None: if not (parse_displacement and disp_enabled() and node.inputs[2].is_linked): state.parents = [] state.parsed = set() state.normal_parsed = False state.curshader = state.vert custom_particle_node.parse(state.curshader, state.con) def parse_group(node, socket): # Entering group index = socket_index(node, socket) output_node = node_by_type(node.node_tree.nodes, 'GROUP_OUTPUT') if output_node is None: return inp = output_node.inputs[index] state.parents.append(node) out_group = parse_input(inp) state.parents.pop() return out_group def parse_group_input(node: bpy.types.Node, socket: bpy.types.NodeSocket): index = socket_index(node, socket) parent = state.parents.pop() # Leaving group inp = parent.inputs[index] res = parse_input(inp) state.parents.append(parent) # Return to group return res def parse_input(inp: bpy.types.NodeSocket): if inp.type == 'SHADER': return parse_shader_input(inp) elif inp.type in ('RGB', 'RGBA', 'VECTOR'): return parse_vector_input(inp) elif inp.type == 'VALUE': return parse_value_input(inp) def parse_shader_input(inp: bpy.types.NodeSocket) -> Tuple[str, ...]: # Follow input if inp.is_linked: link = inp.links[0] if link.from_node.type == 'REROUTE': return parse_shader_input(link.from_node.inputs[0]) return parse_shader(link.from_node, link.from_socket) # Use direct socket value else: out_basecol = 'vec3(0.8)' out_roughness = '0.0' out_metallic = '0.0' out_occlusion = '1.0' out_specular = '1.0' out_opacity = '1.0' out_emission = '0.0' return out_basecol, out_roughness, out_metallic, out_occlusion, out_specular, out_opacity, out_emission def parse_shader(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> Tuple[str, ...]: # Use switch-like lookup via dictionary # (better performance, better code readability) # 'NODE_TYPE': parser_function node_parser_funcs: Dict[str, Callable] = { 'MIX_SHADER': nodes_shader.parse_mixshader, 'ADD_SHADER': nodes_shader.parse_addshader, 'BSDF_PRINCIPLED': nodes_shader.parse_bsdfprincipled, 'BSDF_DIFFUSE': nodes_shader.parse_bsdfdiffuse, 'BSDF_GLOSSY': nodes_shader.parse_bsdfglossy, 'AMBIENT_OCCLUSION': nodes_shader.parse_ambientocclusion, 'BSDF_ANISOTROPIC': nodes_shader.parse_bsdfanisotropic, 'EMISSION': nodes_shader.parse_emission, 'BSDF_GLASS': nodes_shader.parse_bsdfglass, 'HOLDOUT': nodes_shader.parse_holdout, 'SUBSURFACE_SCATTERING': nodes_shader.parse_subsurfacescattering, 'BSDF_TRANSLUCENT': nodes_shader.parse_bsdftranslucent, 'BSDF_TRANSPARENT': nodes_shader.parse_bsdftransparent, 'BSDF_VELVET': nodes_shader.parse_bsdfvelvet, } state.reset_outs() if node.type in node_parser_funcs: node_parser_funcs[node.type](node, socket, state) elif node.type == 'GROUP': if node.node_tree.name.startswith('Armory PBR'): if state.parse_surface: # Normal if node.inputs[5].is_linked and node.inputs[5].links[0].from_node.type == 'NORMAL_MAP': log.warn(mat_name() + ' - Do not use Normal Map node with Armory PBR, connect Image Texture directly') parse_normal_map_color_input(node.inputs[5]) # Base color state.out_basecol = parse_vector_input(node.inputs[0]) # Occlusion state.out_occlusion = parse_value_input(node.inputs[2]) # Roughness state.out_roughness = parse_value_input(node.inputs[3]) # Metallic state.out_metallic = parse_value_input(node.inputs[4]) # Emission if node.inputs[6].is_linked or node.inputs[6].default_value != 0.0: state.out_emission = parse_value_input(node.inputs[6]) state.emission_found = True if state.parse_opacity: state.out_opacity = parse_value_input(node.inputs[1]) else: return parse_group(node, socket) elif node.type == 'GROUP_INPUT': return parse_group_input(node, socket) elif node.type == 'CUSTOM': if node.bl_idname == 'ArmShaderDataNode': return node.parse(state.frag, state.vert) else: # TODO: Print node tree name (save in ParserState) log.warn(f'Material node type {node.type} not supported') return state.get_outs() def parse_displacement_input(inp): if inp.is_linked: l = inp.links[0] if l.from_node.type == 'REROUTE': return parse_displacement_input(l.from_node.inputs[0]) return parse_vector_input(inp) else: return None def parse_vector_input(inp: bpy.types.NodeSocket) -> vec3str: """Return the parsed result of the given input socket.""" # Follow input if inp.is_linked: link = inp.links[0] if link.from_node.type == 'REROUTE': return parse_vector_input(link.from_node.inputs[0]) res_var = write_result(link) st = link.from_socket.type if st in ('RGB', 'RGBA', 'VECTOR'): return res_var else: # VALUE return f'vec3({res_var})' # Unlinked reroute elif inp.type == 'VALUE': return to_vec3([0.0, 0.0, 0.0]) # Use direct socket value else: if mat_batch() and inp.is_uniform: return to_uniform(inp) else: return to_vec3(inp.default_value) def parse_vector(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: """Parses the vector/color output value from the given node and socket.""" node_parser_funcs: Dict[str, Callable] = { 'ATTRIBUTE': nodes_input.parse_attribute, # RGB outputs 'RGB': nodes_input.parse_rgb, 'TEX_BRICK': nodes_texture.parse_tex_brick, 'TEX_CHECKER': nodes_texture.parse_tex_checker, 'TEX_ENVIRONMENT': nodes_texture.parse_tex_environment, 'TEX_GRADIENT': nodes_texture.parse_tex_gradient, 'TEX_IMAGE': nodes_texture.parse_tex_image, 'TEX_MAGIC': nodes_texture.parse_tex_magic, 'TEX_MUSGRAVE': nodes_texture.parse_tex_musgrave, 'TEX_NOISE': nodes_texture.parse_tex_noise, 'TEX_POINTDENSITY': nodes_texture.parse_tex_pointdensity, 'TEX_SKY': nodes_texture.parse_tex_sky, 'TEX_VORONOI': nodes_texture.parse_tex_voronoi, 'TEX_WAVE': nodes_texture.parse_tex_wave, 'VERTEX_COLOR': nodes_input.parse_vertex_color, 'BRIGHTCONTRAST': nodes_color.parse_brightcontrast, 'GAMMA': nodes_color.parse_gamma, 'HUE_SAT': nodes_color.parse_huesat, 'INVERT': nodes_color.parse_invert, 'MIX_RGB': nodes_color.parse_mixrgb, 'BLACKBODY': nodes_converter.parse_blackbody, 'VALTORGB': nodes_converter.parse_valtorgb, # ColorRamp 'CURVE_VEC': nodes_vector.parse_curvevec, # Vector Curves 'CURVE_RGB': nodes_color.parse_curvergb, 'COMBHSV': nodes_converter.parse_combhsv, 'COMBRGB': nodes_converter.parse_combrgb, 'WAVELENGTH': nodes_converter.parse_wavelength, # Vector outputs 'CAMERA': nodes_input.parse_camera, 'NEW_GEOMETRY': nodes_input.parse_geometry, 'HAIR_INFO': nodes_input.parse_hairinfo, 'OBJECT_INFO': nodes_input.parse_objectinfo, 'PARTICLE_INFO': nodes_input.parse_particleinfo, 'TANGENT': nodes_input.parse_tangent, 'TEX_COORD': nodes_input.parse_texcoord, 'UVMAP': nodes_input.parse_uvmap, 'BUMP': nodes_vector.parse_bump, 'MAPPING': nodes_vector.parse_mapping, 'NORMAL': nodes_vector.parse_normal, 'NORMAL_MAP': nodes_vector.parse_normalmap, 'VECT_TRANSFORM': nodes_vector.parse_vectortransform, 'COMBXYZ': nodes_converter.parse_combxyz, 'VECT_MATH': nodes_converter.parse_vectormath, 'DISPLACEMENT': nodes_vector.parse_displacement, 'VECTOR_ROTATE': nodes_vector.parse_vectorrotate, } if node.type in node_parser_funcs: return node_parser_funcs[node.type](node, socket, state) elif node.type == 'GROUP': return parse_group(node, socket) elif node.type == 'GROUP_INPUT': return parse_group_input(node, socket) elif node.type == 'CUSTOM': if node.bl_idname == 'ArmShaderDataNode': return node.parse(state.frag, state.vert) log.warn(f'Material node type {node.type} not supported') return "vec3(0, 0, 0)" def parse_normal_map_color_input(inp, strength_input=None): frag = state.frag if state.basecol_only or not inp.is_linked or state.normal_parsed: return state.normal_parsed = True frag.write_normal += 1 if not get_arm_export_tangents() or mat_get_material().arm_decal: # Compute TBN matrix frag.write('vec3 texn = ({0}) * 2.0 - 1.0;'.format(parse_vector_input(inp))) frag.write('texn.y = -texn.y;') frag.add_include('std/normals.glsl') frag.write('mat3 TBN = cotangentFrame(n, -vVec, texCoord);') frag.write('n = TBN * normalize(texn);') else: frag.write('n = ({0}) * 2.0 - 1.0;'.format(parse_vector_input(inp))) if strength_input is not None: strength = parse_value_input(strength_input) if strength != '1.0': frag.write('n.xy *= {0};'.format(strength)) frag.write('n = normalize(TBN * n);') state.con.add_elem('tang', 'short4norm') frag.write_normal -= 1 def parse_value_input(inp: bpy.types.NodeSocket) -> floatstr: # Follow input if inp.is_linked: link = inp.links[0] if link.from_node.type == 'REROUTE': return parse_value_input(link.from_node.inputs[0]) res_var = write_result(link) socket_type = link.from_socket.type if socket_type in ('RGB', 'RGBA', 'VECTOR'): # RGB to BW return rgb_to_bw(res_var) # VALUE else: return res_var # Use value from socket else: if mat_batch() and inp.is_uniform: return to_uniform(inp) else: return to_vec1(inp.default_value) def parse_value(node, socket): node_parser_funcs: Dict[str, Callable] = { 'ATTRIBUTE': nodes_input.parse_attribute, 'CAMERA': nodes_input.parse_camera, 'FRESNEL': nodes_input.parse_fresnel, 'NEW_GEOMETRY': nodes_input.parse_geometry, 'HAIR_INFO': nodes_input.parse_hairinfo, 'LAYER_WEIGHT': nodes_input.parse_layerweight, 'LIGHT_PATH': nodes_input.parse_lightpath, 'OBJECT_INFO': nodes_input.parse_objectinfo, 'PARTICLE_INFO': nodes_input.parse_particleinfo, 'VALUE': nodes_input.parse_value, 'WIREFRAME': nodes_input.parse_wireframe, 'TEX_BRICK': nodes_texture.parse_tex_brick, 'TEX_CHECKER': nodes_texture.parse_tex_checker, 'TEX_GRADIENT': nodes_texture.parse_tex_gradient, 'TEX_IMAGE': nodes_texture.parse_tex_image, 'TEX_MAGIC': nodes_texture.parse_tex_magic, 'TEX_MUSGRAVE': nodes_texture.parse_tex_musgrave, 'TEX_NOISE': nodes_texture.parse_tex_noise, 'TEX_POINTDENSITY': nodes_texture.parse_tex_pointdensity, 'TEX_VORONOI': nodes_texture.parse_tex_voronoi, 'TEX_WAVE': nodes_texture.parse_tex_wave, 'LIGHT_FALLOFF': nodes_color.parse_lightfalloff, 'NORMAL': nodes_vector.parse_normal, 'CLAMP': nodes_converter.parse_clamp, 'VALTORGB': nodes_converter.parse_valtorgb, 'MATH': nodes_converter.parse_math, 'RGBTOBW': nodes_converter.parse_rgbtobw, 'SEPHSV': nodes_converter.parse_sephsv, 'SEPRGB': nodes_converter.parse_seprgb, 'SEPXYZ': nodes_converter.parse_sepxyz, 'VECT_MATH': nodes_converter.parse_vectormath, 'MAP_RANGE': nodes_converter.parse_maprange, } if node.type in node_parser_funcs: return node_parser_funcs[node.type](node, socket, state) elif node.type == 'GROUP': if node.node_tree.name.startswith('Armory PBR'): # Displacement if socket == node.outputs[1]: return parse_value_input(node.inputs[7]) else: return None else: return parse_group(node, socket) elif node.type == 'GROUP_INPUT': return parse_group_input(node, socket) elif node.type == 'CUSTOM': if node.bl_idname == 'ArmShaderDataNode': return node.parse(state.frag, state.vert) log.warn(f'Material node type {node.type} not supported') return '0.0' def vector_curve(name, fac, points): curshader = state.curshader # Write Ys array ys_var = name + '_ys' curshader.write('float {0}[{1}];'.format(ys_var, len(points))) # TODO: Make const for i in range(0, len(points)): curshader.write('{0}[{1}] = {2};'.format(ys_var, i, points[i].location[1])) # Get index fac_var = name + '_fac' curshader.write('float {0} = {1};'.format(fac_var, fac)) index = '0' for i in range(1, len(points)): index += ' + ({0} > {1} ? 1 : 0)'.format(fac_var, points[i].location[0]) # Write index index_var = name + '_i' curshader.write('int {0} = {1};'.format(index_var, index)) # Linear # Write Xs array facs_var = name + '_xs' curshader.write('float {0}[{1}];'.format(facs_var, len(points))) # TODO: Make const for i in range(0, len(points)): curshader.write('{0}[{1}] = {2};'.format(facs_var, i, points[i].location[0])) # Map vector return 'mix({0}[{1}], {0}[{1} + 1], ({2} - {3}[{1}]) * (1.0 / ({3}[{1} + 1] - {3}[{1}]) ))'.format(ys_var, index_var, fac_var, facs_var) def write_normal(inp): if inp.is_linked and inp.links[0].from_node.type != 'GROUP_INPUT': normal_res = parse_vector_input(inp) if normal_res != None: state.curshader.write('n = {0};'.format(normal_res)) def is_parsed(node_store_name: str): return node_store_name in state.parsed def res_var_name(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> str: """Return the name of the variable that stores the parsed result from the given node and socket.""" return node_name(node.name) + '_' + safesrc(socket.name) + '_res' def write_result(link: bpy.types.NodeLink) -> Optional[str]: """Write the parsed result of the given node link to the shader.""" res_var = res_var_name(link.from_node, link.from_socket) # Unparsed node if not is_parsed(res_var): state.parsed.add(res_var) st = link.from_socket.type if st in ('RGB', 'RGBA', 'VECTOR'): res = parse_vector(link.from_node, link.from_socket) if res is None: log.error(f'{link.from_node.name} returned `None` while parsing!') return None state.curshader.write(f'vec3 {res_var} = {res};') elif st == 'VALUE': res = parse_value(link.from_node, link.from_socket) if res is None: log.error(f'{link.from_node.name} returned `None` while parsing!') return None if link.from_node.type == "VALUE" and not link.from_node.arm_material_param: state.curshader.add_const('float', res_var, res) else: state.curshader.write(f'float {res_var} = {res};') # Normal map already parsed, return elif link.from_node.type == 'NORMAL_MAP': return None return res_var def write_procedurals(): if not state.procedurals_written: state.curshader.add_function(c_functions.str_tex_proc) state.procedurals_written = True return def glsl_type(socket_type: str): """Socket to glsl type.""" if socket_type in ('RGB', 'RGBA', 'VECTOR'): return 'vec3' else: return 'float' def to_uniform(inp: bpy.types.NodeSocket): uname = safesrc(inp.node.name) + safesrc(inp.name) state.curshader.add_uniform(glsl_type(inp.type) + ' ' + uname) return uname def store_var_name(node: bpy.types.Node): return node_name(node.name) + '_store' def texture_store(node, tex, tex_name, to_linear=False, tex_link=None, default_value=None, is_arm_mat_param=None): curshader = state.curshader tex_store = store_var_name(node) if is_parsed(tex_store): return tex_store state.parsed.add(tex_store) mat_bind_texture(tex) state.con.add_elem('tex', 'short2norm') curshader.add_uniform('sampler2D {0}'.format(tex_name), link=tex_link, default_value=default_value, is_arm_mat_param=is_arm_mat_param) triplanar = node.projection == 'BOX' if node.inputs[0].is_linked: uv_name = parse_vector_input(node.inputs[0]) if triplanar: uv_name = 'vec3({0}.x, 1.0 - {0}.y, {0}.z)'.format(uv_name) else: uv_name = 'vec2({0}.x, 1.0 - {0}.y)'.format(uv_name) else: uv_name = 'texCoord' if triplanar: if not curshader.has_include('std/mapping.glsl'): curshader.add_include('std/mapping.glsl') if state.normal_parsed: nor = 'TBN[2]' else: nor = 'n' curshader.write('vec4 {0} = vec4(triplanarMapping({1}, {2}, {3}), 0.0);'.format(tex_store, tex_name, nor, uv_name)) else: if mat_state.texture_grad: curshader.write('vec4 {0} = textureGrad({1}, {2}.xy, g2.xy, g2.zw);'.format(tex_store, tex_name, uv_name)) else: curshader.write('vec4 {0} = texture({1}, {2}.xy);'.format(tex_store, tex_name, uv_name)) if state.sample_bump: state.sample_bump_res = tex_store curshader.write('float {0}_1 = textureOffset({1}, {2}.xy, ivec2(-2, 0)).r;'.format(tex_store, tex_name, uv_name)) curshader.write('float {0}_2 = textureOffset({1}, {2}.xy, ivec2(2, 0)).r;'.format(tex_store, tex_name, uv_name)) curshader.write('float {0}_3 = textureOffset({1}, {2}.xy, ivec2(0, -2)).r;'.format(tex_store, tex_name, uv_name)) curshader.write('float {0}_4 = textureOffset({1}, {2}.xy, ivec2(0, 2)).r;'.format(tex_store, tex_name, uv_name)) state.sample_bump = False if to_linear: curshader.write('{0}.rgb = pow({0}.rgb, vec3(2.2));'.format(tex_store)) return tex_store def write_bump(node: bpy.types.Node, out_socket: bpy.types.NodeSocket, res: str, scl=0.001): """Sample texture values around the current texture coordinate for bump mapping. The result of the sampling is stored in 4 variables named after state.sample_bump_res with _[0-3] appended.""" state.sample_bump_res = store_var_name(node) + '_bump' # Testing.. get function parts.. ar = res.split('(', 1) pre = ar[0] + '(' if ',' in ar[1]: ar2 = ar[1].split(',', 1) co = ar2[0] post = ',' + ar2[1] else: co = ar[1][:-1] post = ')' coordinate_offsets = ( f'vec3(-{scl}, 0.0, 0.0)', f'vec3({scl}, 0.0, {scl})', f'vec3(0.0, -{scl}, 0.0)', f'vec3(0.0, {scl}, -{scl})' ) needs_conversion_bw = glsl_type(out_socket.type) == "vec3" curshader = state.curshader for i in range(1, 5): if needs_conversion_bw: vec_var = f'{state.sample_bump_res}_vec{i}' curshader.write(f'vec3 {vec_var} = {pre}{co} + {coordinate_offsets[i - 1]}{post};') curshader.write(f'float {state.sample_bump_res}_{i} = {rgb_to_bw(vec_var)};') else: curshader.write(f'float {state.sample_bump_res}_{i} = {pre}{co} + {coordinate_offsets[i - 1]}{post};') state.sample_bump = False def to_vec1(v): return str(v) def to_vec2(v): return f'vec2({v[0]}, {v[1]})' def to_vec3(v): return f'vec3({v[0]}, {v[1]}, {v[2]})' def cast_value(val: str, from_type: str, to_type: str) -> str: """Casts a value that is already parsed in a glsl string to another value in a string. vec2 types are not supported (not used in the node editor) and there is no cast towards int types. If casting from vec3 to vec4, the w coordinate/alpha channel is filled with a 1. If this function is called with invalid parameters, a TypeError is raised. """ if from_type == to_type: return val if from_type in ('int', 'float'): if to_type in ('int', 'float'): return val elif to_type in ('vec2', 'vec3', 'vec4'): return f'{to_type}({val})' elif from_type == 'vec3': if to_type == 'float': return rgb_to_bw(val) elif to_type == 'vec4': return f'vec4({val}, 1.0)' elif from_type == 'vec4': if to_type == 'float': return rgb_to_bw(val) elif to_type == 'vec3': return f'{val}.xyz' raise TypeError("Invalid type cast in shader!") def rgb_to_bw(res_var: vec3str) -> floatstr: return f'((({res_var}.r * 0.3 + {res_var}.g * 0.59 + {res_var}.b * 0.11) / 3.0) * 2.5)' def node_by_type(nodes, ntype: str) -> bpy.types.Node: for n in nodes: if n.type == ntype: return n def node_by_name(nodes, name: str) -> bpy.types.Node: for n in nodes: if n.bl_idname == name: return n def socket_index(node: bpy.types.Node, socket: bpy.types.NodeSocket) -> int: for i in range(0, len(node.outputs)): if node.outputs[i] == socket: return i def node_name(s: str) -> str: """Return a unique and safe name for a node for shader code usage.""" for p in state.parents: s = p.name + '_' + s if state.curshader.write_textures > 0: s += '_texread' s = safesrc(s) if '__' in s: # Consecutive _ are reserved s = s.replace('_', '_x') return s ## def make_texture(image_node: bpy.types.ShaderNodeTexImage, tex_name: str, matname: str = None) -> Optional[Dict[str, Any]]: """Creates a texture reference for the export data for a given texture node.""" tex = {'name': tex_name} if matname is None: matname = mat_state.material.name image = image_node.image if image is None: return None # Get filepath filepath = image.filepath if filepath == '': if image.packed_file is not None: filepath = './' + image.name has_ext = filepath.endswith(('.jpg', '.png', '.hdr')) if not has_ext: # Raw bytes, write converted .jpg to /unpacked filepath += '.raw' elif image.source == "GENERATED": unpack_path = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'unpacked') if not os.path.exists(unpack_path): os.makedirs(unpack_path) filepath = os.path.join(unpack_path, image.name + ".jpg") arm.utils.convert_image(image, filepath, "JPEG") else: log.warn(matname + '/' + image.name + ' - invalid file path') return None else: filepath = arm.utils.to_absolute_path(filepath, image.library) # Reference image name texpath = arm.utils.asset_path(filepath) texfile = arm.utils.extract_filename(filepath) tex['file'] = arm.utils.safestr(texfile) s = tex['file'].rsplit('.', 1) if len(s) == 1: log.warn(matname + '/' + image.name + ' - file extension required for image name') return None ext = s[1].lower() do_convert = ext not in ('jpg', 'png', 'hdr', 'mp4') # Convert image if do_convert: new_ext = 'png' if (ext in ('tga', 'dds')) else 'jpg' tex['file'] = tex['file'].rsplit('.', 1)[0] + '.' + new_ext if image.packed_file is not None or not is_ascii(texfile): # Extract packed data / copy non-ascii texture unpack_path = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'unpacked') if not os.path.exists(unpack_path): os.makedirs(unpack_path) unpack_filepath = os.path.join(unpack_path, tex['file']) if do_convert: if not os.path.isfile(unpack_filepath): fmt = 'PNG' if new_ext == 'png' else 'JPEG' arm.utils.convert_image(image, unpack_filepath, file_format=fmt) else: # Write bytes if size is different or file does not exist yet if image.packed_file is not None: if not os.path.isfile(unpack_filepath) or os.path.getsize(unpack_filepath) != image.packed_file.size: with open(unpack_filepath, 'wb') as f: f.write(image.packed_file.data) # Copy non-ascii texture else: if not os.path.isfile(unpack_filepath) or os.path.getsize(unpack_filepath) != os.path.getsize(texpath): shutil.copy(texpath, unpack_filepath) arm.assets.add(unpack_filepath) else: if not os.path.isfile(arm.utils.asset_path(filepath)): log.warn('Material ' + matname + '/' + image.name + ' - file not found(' + filepath + ')') return None if do_convert: unpack_path = os.path.join(arm.utils.get_fp_build(), 'compiled', 'Assets', 'unpacked') if not os.path.exists(unpack_path): os.makedirs(unpack_path) converted_path = os.path.join(unpack_path, tex['file']) # TODO: delete cache when file changes if not os.path.isfile(converted_path): fmt = 'PNG' if new_ext == 'png' else 'JPEG' arm.utils.convert_image(image, converted_path, file_format=fmt) arm.assets.add(converted_path) else: # Link image path to assets # TODO: Khamake converts .PNG to .jpg? Convert ext to lowercase on windows if arm.utils.get_os() == 'win': s = filepath.rsplit('.', 1) arm.assets.add(arm.utils.asset_path(s[0] + '.' + s[1].lower())) else: arm.assets.add(arm.utils.asset_path(filepath)) # if image_format != 'RGBA32': # tex['format'] = image_format interpolation = image_node.interpolation rpdat = arm.utils.get_rp() texfilter = rpdat.arm_texture_filter if texfilter == 'Anisotropic': interpolation = 'Smart' elif texfilter == 'Linear': interpolation = 'Linear' elif texfilter == 'Point': interpolation = 'Closest' # TODO: Blender seems to load full images on size request, cache size instead powimage = is_pow(image.size[0]) and is_pow(image.size[1]) if interpolation == 'Cubic': # Mipmap linear tex['mipmap_filter'] = 'linear' tex['generate_mipmaps'] = True elif interpolation == 'Smart': # Mipmap anisotropic tex['min_filter'] = 'anisotropic' tex['mipmap_filter'] = 'linear' tex['generate_mipmaps'] = True elif interpolation == 'Closest': tex['min_filter'] = 'point' tex['mag_filter'] = 'point' # else defaults to linear if image_node.extension != 'REPEAT': # Extend or clip tex['u_addressing'] = 'clamp' tex['v_addressing'] = 'clamp' if image.source == 'MOVIE': tex['source'] = 'movie' tex['min_filter'] = 'linear' tex['mag_filter'] = 'linear' tex['mipmap_filter'] = 'no' tex['generate_mipmaps'] = False return tex def is_pow(num): return ((num & (num - 1)) == 0) and num != 0 def is_ascii(s): return len(s) == len(s.encode()) ## def get_arm_export_tangents(): return bpy.data.worlds['Arm'].arm_export_tangents def safesrc(name): return arm.utils.safesrc(name) def disp_enabled(): return arm.utils.disp_enabled(arm.make_state.target) def assets_add(path): arm.assets.add(path) def assets_add_embedded_data(path): arm.assets.add_embedded_data(path) def mat_name(): return mat_state.material.name def mat_batch(): return mat_state.batch def mat_bind_texture(tex): mat_state.bind_textures.append(tex) def mat_get_material(): return mat_state.material def mat_get_material_users(): return mat_state.mat_users