def parse_surface(world: bpy.types.World, node_surface: bpy.types.Node, frag: Shader): wrd = bpy.data.worlds['Arm'] rpdat = arm.utils.get_rp() solid_mat = rpdat.arm_material_model == 'Solid' if node_surface.type in ('BACKGROUND', 'EMISSION'): # Append irradiance define if rpdat.arm_irradiance and not solid_mat: wrd.world_defs += '_Irr' # Extract environment strength # Todo: follow/parse strength input world.arm_envtex_strength = node_surface.inputs[1].default_value # Color out = cycles.parse_vector_input(node_surface.inputs[0]) frag.write(f'fragColor.rgb = {out};') if not node_surface.inputs[0].is_linked: solid_mat = rpdat.arm_material_model == 'Solid' if rpdat.arm_irradiance and not solid_mat: world.world_defs += '_Irr' world.arm_envtex_color = node_surface.inputs[0].default_value world.arm_envtex_strength = 1.0 else: log.warn( f'World node type {node_surface.type} must not be connected to the world output node!' ) # Invalidate the parser state for subsequent executions cycles.state = None
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_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, } 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_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) elif socket_type in ('VALUE', 'INT'): return res_var else: log.warn(f'Node tree "{tree_name()}": socket "{link.from_socket.name}" of node "{link.from_node.name}" cannot be connected to a scalar value socket') return '0.0' # 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_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 elif st in ('VALUE', 'INT'): return f'vec3({res_var})' else: log.warn(f'Node tree "{tree_name()}": socket "{link.from_socket.name}" of node "{link.from_node.name}" cannot be connected to a vector-like socket') return to_vec3([0.0, 0.0, 0.0]) # 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_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 disp_linked(output_node): # Armory PBR with unlinked height socket linked = output_node.inputs[2].is_linked tess_enabled = arm.utils.tess_enabled(make_state.target) if linked: l = output_node.inputs[2].links[0] if l.from_node.type == 'GROUP' and l.from_node.node_tree.name.startswith( 'Armory PBR') and l.from_node.inputs[10].is_linked == False: return False if linked and not tess_enabled: log.warn('Tessellation not available on ' + make_state.target) return tess_enabled and linked
def make_instancing_and_skinning( mat: Material, mat_users: Dict[Material, List[Object]]) -> None: """Build material with instancing or skinning if enabled. If the material is a custom material, only validation checks for instancing are performed.""" global_elems = [] if mat_users is not None and mat in mat_users: # Whether there are both an instanced object and a not instanced object with this material instancing_usage = [False, False] mat_state.uses_instancing = False for bo in mat_users[mat]: if mat.arm_custom_material == '': # Morph Targets if arm.utils.export_morph_targets(bo): global_elems.append({ 'name': 'morph', 'data': 'short2norm' }) # GPU Skinning if arm.utils.export_bone_data(bo): global_elems.append({'name': 'bone', 'data': 'short4norm'}) global_elems.append({ 'name': 'weight', 'data': 'short4norm' }) # Instancing inst = bo.arm_instanced if inst != 'Off' or mat.arm_particle_flag: instancing_usage[0] = True mat_state.uses_instancing = True if mat.arm_custom_material == '': global_elems.append({'name': 'ipos', 'data': 'float3'}) if 'Rot' in inst: global_elems.append({'name': 'irot', 'data': 'float3'}) if 'Scale' in inst: global_elems.append({'name': 'iscl', 'data': 'float3'}) elif inst == 'Off': # Ignore children of instanced objects, they are instanced even when set to 'Off' instancing_usage[ 1] = bo.parent is None or bo.parent.arm_instanced == 'Off' if instancing_usage[0] and instancing_usage[1]: # Display a warning for invalid instancing configurations # See https://github.com/armory3d/armory/issues/2072 log.warn( f'Material "{mat.name}" has both instanced and not instanced objects, objects might flicker!' ) if mat.arm_custom_material == '': mat_state.data.global_elems = global_elems
def disp_linked(output_node): # Armory PBR with unlinked height socket linked = output_node.inputs[2].is_linked disp_enabled = arm.utils.disp_enabled(make_state.target) if linked: l = output_node.inputs[2].links[0] if l.from_node.type == 'GROUP' and l.from_node.node_tree.name.startswith('Armory PBR') and \ ((len(l.from_node.inputs) == 14 and l.from_node.inputs[10].is_linked == False) or (len(l.from_node.inputs) != 14 and l.from_node.inputs[7].is_linked == False)): return False rpdat = arm.utils.get_rp() if linked and not disp_enabled and rpdat.arm_displacement: log.warn('Tessellation not available on ' + make_state.target) return disp_enabled and linked
def disp_linked(output_node): linked = output_node.inputs[2].is_linked if not linked: return False # Armory PBR with unlinked height socket l = output_node.inputs[2].links[0] if l.from_node.type == 'GROUP' and l.from_node.node_tree.name.startswith('Armory PBR') and \ l.from_node.inputs[7].is_linked == False: return False disp_enabled = arm.utils.disp_enabled(make_state.target) rpdat = arm.utils.get_rp() if not disp_enabled and rpdat.arm_rp_displacement == 'Tessellation': log.warn('Tessellation not available on ' + make_state.target) return disp_enabled
def cpu_count(*, physical_only=False) -> Optional[int]: """Returns the number of logical (default) or physical CPUs. The result can be `None` if `os.cpu_count()` was not able to get the correct count of logical CPUs. """ if not physical_only: return os.cpu_count() err_reason = '' command = [] _os = get_os() try: if _os == 'win': sysroot = os.environ.get("SYSTEMROOT", default="C:\\WINDOWS") command = [ f'{sysroot}\\System32\\wbem\\wmic.exe', 'cpu', 'get', 'NumberOfCores' ] result = subprocess.check_output(command) result = result.decode('utf-8').splitlines() result = int(result[2]) if result > 0: return result elif _os == 'linux': command = ["grep -P '^core id' /proc/cpuinfo | sort -u | wc -l"] result = subprocess.check_output(command[0], shell=True) result = result.decode('utf-8').splitlines() result = int(result[0]) if result > 0: return result # macOS else: command = ['sysctl', '-n', 'hw.physicalcpu'] return int(subprocess.check_output(command)) except subprocess.CalledProcessError as e: err_reason = f'Reason: command {command} exited with code {e.returncode}.' except FileNotFoundError as e: err_reason = f'Reason: couldn\'t open file from command {command} ({e.errno=}).' # Last resort even though it can be wrong log.warn( "Could not retrieve count of physical CPUs, using logical CPU count instead.\n\t" + err_reason) return os.cpu_count()
def check_sdkpath(self): s = get_sdk_path() if not check_path(s): msg = f"SDK path '{s}' contains special characters. Please move SDK to different path for now." self.report({"ERROR"}, msg) if self is not None else log.warn(msg) return False else: return True
def check_projectpath(self): s = get_fp() if not check_path(s): msg = f"Project path '{s}' contains special characters, build process may fail." self.report({"ERROR"}, msg) if self is not None else log.warn(msg) return False else: return True
def parse_clamp(node: bpy.types.ShaderNodeClamp, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: value = c.parse_value_input(node.inputs['Value']) minVal = c.parse_value_input(node.inputs['Min']) maxVal = c.parse_value_input(node.inputs['Max']) if node.clamp_type == 'MINMAX': # Condition is minVal < maxVal, otherwise use 'RANGE' type return f'clamp({value}, {minVal}, {maxVal})' elif node.clamp_type == 'RANGE': return f'{minVal} < {maxVal} ? clamp({value}, {minVal}, {maxVal}) : clamp({value}, {maxVal}, {minVal})' else: log.warn(f'Clamp node: unsupported clamp type {node.clamp_type}.') return value
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]) if link.from_socket.type != 'SHADER': log.warn(f'Node tree "{tree_name()}": socket "{link.from_socket.name}" of node "{link.from_node.name}" cannot be connected to a shader socket') state.reset_outs() return state.get_outs() return parse_shader(link.from_node, link.from_socket) else: # Return default shader values state.reset_outs() return state.get_outs()
def register(): wm = bpy.context.window_manager addon_keyconfig = wm.keyconfigs.addon # Keyconfigs are not available in background mode. If the keyconfig # was not found despite running _not_ in background mode, a warning # is printed if addon_keyconfig is None: if not bpy.app.background: log.warn("No keyconfig path found") return km = addon_keyconfig.keymaps.new(name='Window', space_type='EMPTY', region_type="WINDOW") km.keymap_items.new(props_ui.ArmoryPlayButton.bl_idname, type='F5', value='PRESS') km.keymap_items.new("tlm.build_lightmaps", type='F6', value='PRESS') km.keymap_items.new("tlm.clean_lightmaps", type='F7', value='PRESS') arm_keymaps.append(km)
def add(asset_file): global assets # Asset already exists, do nothing if asset_file in assets: return asset_file_base = os.path.basename(asset_file) for f in assets: f_file_base = os.path.basename(f) if f_file_base == asset_file_base: log.warn( f'Asset name "{asset_file_base}" already exists, skipping') return assets.append(asset_file) # Reserved file name for f in reserved_names: if f in asset_file: log.warn( f'File "{asset_file}" contains reserved keyword, this will break C++ builds!' )
def add_world_defs(): wrd = bpy.data.worlds['Arm'] rpdat = arm.utils.get_rp() # Screen-space ray-traced shadows if rpdat.arm_ssrs: wrd.world_defs += '_SSRS' if rpdat.arm_two_sided_area_light: wrd.world_defs += '_TwoSidedAreaLight' # Store contexts if rpdat.rp_hdr == False: wrd.world_defs += '_LDR' # Alternative models if rpdat.arm_diffuse_model == 'OrenNayar': wrd.world_defs += '_OrenNayar' # TODO: Light texture test.. if wrd.arm_light_texture != '': wrd.world_defs += '_LightColTex' if wrd.arm_light_ies_texture != '': wrd.world_defs += '_LightIES' assets.add_embedded_data('iestexture.png') if wrd.arm_light_clouds_texture != '': wrd.world_defs += '_LightClouds' assets.add_embedded_data('cloudstexture.png') if rpdat.rp_renderer == 'Deferred': assets.add_khafile_def('arm_deferred') wrd.world_defs += '_Deferred' # GI voxelgi = False voxelao = False has_voxels = arm.utils.voxel_support() if has_voxels: if rpdat.rp_gi == 'Voxel GI': voxelgi = True elif rpdat.rp_gi == 'Voxel AO': voxelao = True # Shadows if rpdat.rp_shadowmap == 'Off': wrd.world_defs += '_NoShadows' assets.add_khafile_def('arm_no_shadows') else: if rpdat.rp_shadowmap_cascades != '1': if voxelgi: log.warn( 'Disabling shadow cascades - Voxel GI does not support cascades yet' ) else: wrd.world_defs += '_CSM' assets.add_khafile_def('arm_csm') # SS # if rpdat.rp_dfrs: # wrd.world_defs += '_DFRS' # assets.add_khafile_def('arm_sdf') # if rpdat.rp_dfao: # wrd.world_defs += '_DFAO' # assets.add_khafile_def('arm_sdf') # if rpdat.rp_dfgi: # wrd.world_defs += '_DFGI' # assets.add_khafile_def('arm_sdf') if rpdat.rp_ssgi == 'RTGI' or rpdat.rp_ssgi == 'RTAO': if rpdat.rp_ssgi == 'RTGI': wrd.world_defs += '_RTGI' if rpdat.arm_ssgi_rays == '9': wrd.world_defs += '_SSGICone9' if rpdat.rp_autoexposure: wrd.world_defs += '_AutoExposure' if voxelgi or voxelao: assets.add_khafile_def('arm_voxelgi') wrd.world_defs += '_VoxelCones' + rpdat.arm_voxelgi_cones if rpdat.arm_voxelgi_revoxelize: assets.add_khafile_def('arm_voxelgi_revox') if rpdat.arm_voxelgi_camera: wrd.world_defs += '_VoxelGICam' if rpdat.arm_voxelgi_temporal: assets.add_khafile_def('arm_voxelgi_temporal') wrd.world_defs += '_VoxelGITemporal' if voxelgi: wrd.world_defs += '_VoxelGI' assets.add_shader_external( arm.utils.get_sdk_path() + '/armory/Shaders/voxel_light/voxel_light.comp.glsl') if rpdat.arm_voxelgi_bounces != "1": assets.add_khafile_def('rp_gi_bounces={0}'.format( rpdat.arm_voxelgi_bounces)) assets.add_shader_external( arm.utils.get_sdk_path() + '/armory/Shaders/voxel_bounce/voxel_bounce.comp.glsl') if rpdat.arm_voxelgi_shadows: wrd.world_defs += '_VoxelGIDirect' wrd.world_defs += '_VoxelGIShadow' if rpdat.arm_voxelgi_refraction: wrd.world_defs += '_VoxelGIDirect' wrd.world_defs += '_VoxelGIRefract' if rpdat.rp_voxelgi_relight: assets.add_khafile_def('rp_voxelgi_relight') elif voxelao: wrd.world_defs += '_VoxelAO' if arm.utils.get_gapi().startswith( 'direct3d'): # Flip Y axis in drawQuad command wrd.world_defs += '_InvY' if arm.utils.get_legacy_shaders() and not state.is_viewport: wrd.world_defs += '_Legacy' # Area lights lights = bpy.data.lights if bpy.app.version >= (2, 80, 1) else bpy.data.lamps for light in lights: if light.type == 'AREA': wrd.world_defs += '_LTC' assets.add_khafile_def('arm_ltc') break if '_Rad' in wrd.world_defs or '_VoxelGI' in wrd.world_defs: wrd.world_defs += '_Brdf' if '_Brdf' in wrd.world_defs or '_VoxelAO' in wrd.world_defs: wrd.world_defs += '_IndPos'
def parse_shader(node, socket): global parsing_basecol out_basecol = 'vec3(0.8)' out_roughness = '0.0' out_metallic = '0.0' out_occlusion = '1.0' out_opacity = '1.0' if node.type == 'GROUP': if node.node_tree.name.startswith('Armory PBR'): if parse_surface: # Base color parsing_basecol = True out_basecol = parse_vector_input(node.inputs[0]) parsing_basecol = False # Occlusion TODO: deprecated, occlussion is value instead of vector now if node.inputs[1].type == 'RGBA': out_occlusion = '{0}.r'.format( parse_vector_input(node.inputs[1])) else: out_occlusion = parse_value_input(node.inputs[1]) if node.inputs[ 2].is_linked or node.inputs[2].default_value != 1.0: out_occlusion = '({0} * {1})'.format( out_occlusion, parse_value_input(node.inputs[2])) # Roughness out_roughness = parse_value_input(node.inputs[3]) if node.inputs[ 4].is_linked or node.inputs[4].default_value != 1.0: out_roughness = '({0} * {1})'.format( out_roughness, parse_value_input(node.inputs[4])) # Metallic out_metallic = parse_value_input(node.inputs[5]) # Normal if node.inputs[6].is_linked and node.inputs[6].links[ 0].from_node.type == 'NORMAL_MAP': log.warn( mat_state.material.name + ' - Do not use Normal Map node with Armory PBR, connect Image Texture directly' ) parse_normal_map_color_input(node.inputs[6], node.inputs[7]) # Emission if node.inputs[8].is_linked: parsing_basecol = True out_emission = parse_vector_input(node.inputs[8]) parsing_basecol = False if node.inputs[9].is_linked or node.inputs[ 9].default_value != 1.0: out_emission = '({0} * {1})'.format( out_emission, parse_value_input(node.inputs[9])) out_basecol = '({0} + {1})'.format(out_basecol, out_emission) if parse_opacity: out_opacity = parse_value_input(node.inputs[12]) if node.inputs[ 13].is_linked or node.inputs[13].default_value != 1.0: out_opacity = '({0} * {1})'.format( out_opacity, parse_value_input(node.inputs[13])) else: return parse_group(node, socket) elif node.type == 'GROUP_INPUT': return parse_group_input(node, socket) elif node.type == 'MIX_SHADER': prefix = '' if node.inputs[0].is_linked else 'const ' fac = parse_value_input(node.inputs[0]) fac_var = node_name(node.name) + '_fac' fac_inv_var = node_name(node.name) + '_fac_inv' curshader.write('{0}float {1} = {2};'.format(prefix, fac_var, fac)) curshader.write('{0}float {1} = 1.0 - {2};'.format( prefix, fac_inv_var, fac_var)) bc1, rough1, met1, occ1, opac1 = parse_shader_input(node.inputs[1]) bc2, rough2, met2, occ2, opac2 = parse_shader_input(node.inputs[2]) if parse_surface: parsing_basecol = True out_basecol = '({0} * {3} + {1} * {2})'.format( bc1, bc2, fac_var, fac_inv_var) parsing_basecol = False out_roughness = '({0} * {3} + {1} * {2})'.format( rough1, rough2, fac_var, fac_inv_var) out_metallic = '({0} * {3} + {1} * {2})'.format( met1, met2, fac_var, fac_inv_var) out_occlusion = '({0} * {3} + {1} * {2})'.format( occ1, occ2, fac_var, fac_inv_var) if parse_opacity: out_opacity = '({0} * {3} + {1} * {2})'.format( opac1, opac2, fac_var, fac_inv_var) elif node.type == 'ADD_SHADER': bc1, rough1, met1, occ1, opac1 = parse_shader_input(node.inputs[0]) bc2, rough2, met2, occ2, opac2 = parse_shader_input(node.inputs[1]) if parse_surface: parsing_basecol = True out_basecol = '({0} + {1})'.format(bc1, bc2) parsing_basecol = False out_roughness = '({0} * 0.5 + {1} * 0.5)'.format(rough1, rough2) out_metallic = '({0} * 0.5 + {1} * 0.5)'.format(met1, met2) out_occlusion = '({0} * 0.5 + {1} * 0.5)'.format(occ1, occ2) if parse_opacity: out_opacity = '({0} * 0.5 + {1} * 0.5)'.format(opac1, opac2) elif node.type == 'BSDF_DIFFUSE': if parse_surface: write_normal(node.inputs[2]) parsing_basecol = True out_basecol = parse_vector_input(node.inputs[0]) parsing_basecol = False out_roughness = parse_value_input(node.inputs[1]) elif node.type == 'BSDF_GLOSSY': if parse_surface: write_normal(node.inputs[2]) parsing_basecol = True out_basecol = parse_vector_input(node.inputs[0]) parsing_basecol = False out_roughness = parse_value_input(node.inputs[1]) out_metallic = '1.0' elif node.type == 'AMBIENT_OCCLUSION': if parse_surface: # Single channel out_occlusion = parse_vector_input(node.inputs[0]) + '.r' elif node.type == 'BSDF_ANISOTROPIC': if parse_surface: write_normal(node.inputs[4]) # Revert to glossy parsing_basecol = True out_basecol = parse_vector_input(node.inputs[0]) parsing_basecol = False out_roughness = parse_value_input(node.inputs[1]) out_metallic = '1.0' elif node.type == 'EMISSION': if parse_surface: # Multiply basecol parsing_basecol = True out_basecol = parse_vector_input(node.inputs[0]) parsing_basecol = False strength = parse_value_input(node.inputs[1]) out_basecol = '({0} * {1} * 50.0)'.format(out_basecol, strength) elif node.type == 'BSDF_GLASS': if parse_surface: write_normal(node.inputs[3]) out_roughness = parse_value_input(node.inputs[1]) if parse_opacity: out_opacity = '(1.0 - {0}.r)'.format( parse_vector_input(node.inputs[0])) elif node.type == 'BSDF_HAIR': pass elif node.type == 'HOLDOUT': if parse_surface: # Occlude out_occlusion = '0.0' elif node.type == 'BSDF_REFRACTION': # write_normal(node.inputs[3]) pass elif node.type == 'SUBSURFACE_SCATTERING': # write_normal(node.inputs[4]) pass elif node.type == 'BSDF_TOON': # write_normal(node.inputs[3]) pass elif node.type == 'BSDF_TRANSLUCENT': if parse_surface: write_normal(node.inputs[1]) if parse_opacity: out_opacity = '(1.0 - {0}.r)'.format( parse_vector_input(node.inputs[0])) elif node.type == 'BSDF_TRANSPARENT': if parse_opacity: out_opacity = '(1.0 - {0}.r)'.format( parse_vector_input(node.inputs[0])) elif node.type == 'BSDF_VELVET': if parse_surface: write_normal(node.inputs[2]) parsing_basecol = True out_basecol = parse_vector_input(node.inputs[0]) parsing_basecol = False out_roughness = '1.0' out_metallic = '1.0' elif node.type == 'VOLUME_ABSORPTION': pass elif node.type == 'VOLUME_SCATTER': pass return out_basecol, out_roughness, out_metallic, out_occlusion, out_opacity
def make_texture(image_node: bpy.types.ShaderNodeTexImage, tex_name: str, matname: str = None) -> Optional[Dict[str, Any]]: 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 # 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 check_saved(self): if bpy.data.filepath == "": msg = "Save blend file first" self.report({"ERROR"}, msg) if self is not None else log.warn(msg) return False return True
def play_project(in_viewport, is_render=False, is_render_anim=False): global scripts_mtime global code_parsed wrd = bpy.data.worlds['Arm'] log.clear() # Store area if arm.utils.with_krom() and in_viewport and bpy.context.area != None and bpy.context.area.type == 'VIEW_3D': state.play_area = bpy.context.area state.target = runtime_to_target(in_viewport) # Build data build_project(is_play=True, is_render=is_render, is_render_anim=is_render_anim, in_viewport=in_viewport) khajs_path = get_khajs_path(in_viewport, state.target) if not wrd.arm_cache_compiler or \ not os.path.isfile(khajs_path) or \ assets.khafile_defs_last != assets.khafile_defs or \ state.last_target != state.target or \ state.last_in_viewport != state.in_viewport or \ state.target == 'native': wrd.arm_recompile = True state.last_target = state.target state.last_in_viewport = state.in_viewport if state.in_viewport: if arm.utils.get_rp().rp_gi != 'Off' and bpy.app.version < (2, 80, 1): log.warn('Use Blender 2.8 to run Voxel GI in viewport') # Trait sources modified state.mod_scripts = [] script_path = arm.utils.get_fp() + '/Sources/' + arm.utils.safestr(wrd.arm_project_package) if os.path.isdir(script_path): new_mtime = scripts_mtime for fn in glob.iglob(os.path.join(script_path, '**', '*.hx'), recursive=True): mtime = os.path.getmtime(fn) if scripts_mtime < mtime: arm.utils.fetch_script_props(fn) # Trait props fn = fn.split('Sources/')[1] fn = fn[:-3] #.hx fn = fn.replace('/', '.') state.mod_scripts.append(fn) wrd.arm_recompile = True if new_mtime < mtime: new_mtime = mtime scripts_mtime = new_mtime if len(state.mod_scripts) > 0: # Trait props arm.utils.fetch_trait_props() # New compile requred - traits changed if wrd.arm_recompile: state.recompiled = True if state.krom_running: # TODO: Unable to live-patch, stop player bpy.ops.arm.space_stop('EXEC_DEFAULT') return if not code_parsed: code_parsed = True barmory.parse_code() else: code_parsed = False mode = 'play' if state.target == 'native': state.compileproc = compile_project(target_name='--run') elif state.target == 'krom': if in_viewport: mode = 'play_viewport' state.compileproc = compile_project(target_name='krom') else: # Browser state.compileproc = compile_project(target_name='html5') threading.Timer(0.1, watch_compile, [mode]).start() else: # kha.js up to date state.recompiled = False compile_project(patch=True)
def parse_mixrgb(node: bpy.types.ShaderNodeMixRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: col1 = c.parse_vector_input(node.inputs[1]) col2 = c.parse_vector_input(node.inputs[2]) # Store factor in variable for linked factor input if node.inputs[0].is_linked: fac = c.node_name(node.name) + '_fac' state.curshader.write('float {0} = {1};'.format( fac, c.parse_value_input(node.inputs[0]))) else: fac = c.parse_value_input(node.inputs[0]) # TODO: Do not mix if factor is constant 0.0 or 1.0? blend = node.blend_type if blend == 'MIX': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) elif blend == 'ADD': out_col = 'mix({0}, {0} + {1}, {2})'.format(col1, col2, fac) elif blend == 'MULTIPLY': out_col = 'mix({0}, {0} * {1}, {2})'.format(col1, col2, fac) elif blend == 'SUBTRACT': out_col = 'mix({0}, {0} - {1}, {2})'.format(col1, col2, fac) elif blend == 'SCREEN': out_col = '(vec3(1.0) - (vec3(1.0 - {2}) + {2} * (vec3(1.0) - {1})) * (vec3(1.0) - {0}))'.format( col1, col2, fac) elif blend == 'DIVIDE': out_col = '(vec3((1.0 - {2}) * {0} + {2} * {0} / {1}))'.format( col1, col2, fac) elif blend == 'DIFFERENCE': out_col = 'mix({0}, abs({0} - {1}), {2})'.format(col1, col2, fac) elif blend == 'DARKEN': out_col = 'min({0}, {1} * {2})'.format(col1, col2, fac) elif blend == 'LIGHTEN': out_col = 'max({0}, {1} * {2})'.format(col1, col2, fac) elif blend == 'OVERLAY': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix elif blend == 'DODGE': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix elif blend == 'BURN': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix elif blend == 'HUE': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix elif blend == 'SATURATION': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix elif blend == 'VALUE': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix elif blend == 'COLOR': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix elif blend == 'SOFT_LIGHT': out_col = '((1.0 - {2}) * {0} + {2} * ((vec3(1.0) - {0}) * {1} * {0} + {0} * (vec3(1.0) - (vec3(1.0) - {1}) * (vec3(1.0) - {0}))));'.format( col1, col2, fac) elif blend == 'LINEAR_LIGHT': out_col = 'mix({0}, {1}, {2})'.format(col1, col2, fac) # Revert to mix # out_col = '({0} + {2} * (2.0 * ({1} - vec3(0.5))))'.format(col1, col2, fac_var) else: log.warn(f'MixRGB node: unsupported blend type {node.blend_type}.') return col1 if node.use_clamp: return 'clamp({0}, vec3(0.0), vec3(1.0))'.format(out_col) return out_col
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 build(): rpdat = arm.utils.get_rp() if rpdat.rp_driver != 'Armory' and arm.api.drivers[ rpdat.rp_driver]['make_rpath'] != None: arm.api.drivers[rpdat.rp_driver]['make_rpath']() return assets_path = arm.utils.get_sdk_path() + '/armory/Assets/' wrd = bpy.data.worlds['Arm'] add_world_defs() mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid' if not mobile_mat: # Always include assets.add(assets_path + 'brdf.png') assets.add_embedded_data('brdf.png') if rpdat.rp_hdr: assets.add_khafile_def('rp_hdr') assets.add_khafile_def('rp_renderer={0}'.format(rpdat.rp_renderer)) if rpdat.rp_depthprepass: assets.add_khafile_def('rp_depthprepass') if rpdat.rp_shadowmap != 'Off': assets.add_khafile_def('rp_shadowmap') assets.add_khafile_def('rp_shadowmap_size={0}'.format( rpdat.rp_shadowmap)) assets.add_khafile_def('rp_background={0}'.format(rpdat.rp_background)) if rpdat.rp_background == 'World': assets.add_shader_pass('world_pass') if '_EnvClouds' in wrd.world_defs: assets.add(assets_path + 'noise256.png') assets.add_embedded_data('noise256.png') if rpdat.rp_renderer == 'Deferred' and not rpdat.rp_compositornodes: assets.add_shader_pass('copy_pass') if rpdat.rp_renderer == 'Forward' and not rpdat.rp_compositornodes and rpdat.rp_render_to_texture: assets.add_shader_pass('copy_pass') if rpdat.rp_render_to_texture: assets.add_khafile_def('rp_render_to_texture') if rpdat.rp_compositornodes: assets.add_khafile_def('rp_compositornodes') compo_depth = False if rpdat.arm_tonemap != 'Off': wrd.compo_defs = '_CTone' + rpdat.arm_tonemap if rpdat.rp_antialiasing == 'FXAA': wrd.compo_defs += '_CFXAA' if rpdat.arm_letterbox: wrd.compo_defs += '_CLetterbox' if rpdat.arm_grain: wrd.compo_defs += '_CGrain' if rpdat.arm_sharpen: wrd.compo_defs += '_CSharpen' if bpy.data.scenes[0].cycles.film_exposure != 1.0: wrd.compo_defs += '_CExposure' if rpdat.arm_fog: wrd.compo_defs += '_CFog' compo_depth = True if len(bpy.data.cameras ) > 0 and bpy.data.cameras[0].dof_distance > 0.0: wrd.compo_defs += '_CDOF' compo_depth = True if compo_depth: wrd.compo_defs += '_CDepth' assets.add_khafile_def('rp_compositordepth') if rpdat.arm_lens_texture != '': wrd.compo_defs += '_CLensTex' assets.add_embedded_data('lenstexture.jpg') if rpdat.arm_fisheye: wrd.compo_defs += '_CFishEye' if rpdat.arm_vignette: wrd.compo_defs += '_CVignette' if rpdat.arm_lensflare: wrd.compo_defs += '_CGlare' if rpdat.arm_lut_texture != '': wrd.compo_defs += '_CLUT' assets.add_embedded_data('luttexture.jpg') if '_CDOF' in wrd.compo_defs or '_CFXAA' in wrd.compo_defs or '_CSharpen' in wrd.compo_defs: wrd.compo_defs += '_CTexStep' if '_CDOF' in wrd.compo_defs or '_CFog' in wrd.compo_defs or '_CGlare' in wrd.compo_defs: wrd.compo_defs += '_CCameraProj' assets.add_shader_pass('compositor_pass') assets.add_khafile_def('rp_antialiasing={0}'.format( rpdat.rp_antialiasing)) if rpdat.rp_antialiasing == 'SMAA' or rpdat.rp_antialiasing == 'TAA': assets.add_shader_pass('smaa_edge_detect') assets.add_shader_pass('smaa_blend_weight') assets.add_shader_pass('smaa_neighborhood_blend') assets.add(assets_path + 'smaa_area.png') assets.add(assets_path + 'smaa_search.png') assets.add_embedded_data('smaa_area.png') assets.add_embedded_data('smaa_search.png') wrd.world_defs += '_SMAA' if rpdat.rp_antialiasing == 'TAA': assets.add_shader_pass('taa_pass') assets.add_shader_pass('copy_pass') if rpdat.rp_antialiasing == 'TAA' or rpdat.rp_motionblur == 'Object': assets.add_khafile_def('arm_veloc') wrd.world_defs += '_Veloc' if rpdat.rp_antialiasing == 'TAA': assets.add_khafile_def('arm_taa') assets.add_khafile_def('rp_supersampling={0}'.format( rpdat.rp_supersampling)) if rpdat.rp_supersampling == '4': assets.add_shader_pass('supersample_resolve') if rpdat.rp_overlays: assets.add_khafile_def('rp_overlays') if rpdat.rp_translucency: assets.add_khafile_def('rp_translucency') assets.add_shader_pass('translucent_resolve') if rpdat.rp_stereo: assets.add_khafile_def('rp_stereo') assets.add_khafile_def('arm_vr') wrd.world_defs += '_VR' assets.add(assets_path + 'vr.png') assets.add_embedded_data('vr.png') rp_gi = rpdat.rp_gi has_voxels = arm.utils.voxel_support() if not has_voxels: rp_gi = 'Off' assets.add_khafile_def('rp_gi={0}'.format(rp_gi)) if rpdat.rp_gi != 'Off': if has_voxels: assets.add_khafile_def('rp_gi={0}'.format(rpdat.rp_gi)) assets.add_khafile_def('rp_voxelgi_resolution={0}'.format( rpdat.rp_voxelgi_resolution)) assets.add_khafile_def('rp_voxelgi_resolution_z={0}'.format( rpdat.rp_voxelgi_resolution_z)) if rpdat.rp_voxelgi_hdr: assets.add_khafile_def('rp_voxelgi_hdr') if rpdat.arm_voxelgi_shadows: assets.add_khafile_def('rp_voxelgi_shadows') if rpdat.arm_voxelgi_refraction: assets.add_khafile_def('rp_voxelgi_refraction') else: log.warn( 'Disabling Voxel GI - unsupported target - use Krom instead') if rpdat.arm_rp_resolution == 'Custom': assets.add_khafile_def('rp_resolution_filter={0}'.format( rpdat.arm_rp_resolution_filter)) assets.add_khafile_def('rp_ssgi={0}'.format(rpdat.rp_ssgi)) if rpdat.rp_ssgi != 'Off': wrd.world_defs += '_SSAO' if rpdat.rp_ssgi == 'SSAO': assets.add_shader_pass('ssao_pass') assets.add_shader_pass('blur_edge_pass') assets.add(assets_path + 'noise8.png') assets.add_embedded_data('noise8.png') else: assets.add_shader_pass('ssgi_pass') assets.add_shader_pass('ssgi_blur_pass') if rpdat.rp_renderer == 'Deferred': assets.add_shader_pass('deferred_indirect') assets.add_shader_pass('deferred_light') assets.add_shader_pass('deferred_light_quad') if rpdat.rp_volumetriclight: assets.add_khafile_def('rp_volumetriclight') assets.add_shader_pass('volumetric_light_quad') assets.add_shader_pass('volumetric_light') assets.add_shader_pass('blur_bilat_pass') assets.add_shader_pass('blur_bilat_blend_pass') assets.add(assets_path + 'blue_noise64.png') assets.add_embedded_data('blue_noise64.png') if rpdat.rp_decals: assets.add_khafile_def('rp_decals') if rpdat.rp_ocean: assets.add_khafile_def('rp_ocean') assets.add_shader_pass('water_pass') if rpdat.rp_blending: assets.add_khafile_def('rp_blending') if rpdat.rp_bloom: assets.add_khafile_def('rp_bloom') assets.add_shader_pass('bloom_pass') assets.add_shader_pass('blur_gaus_pass') if rpdat.rp_sss: assets.add_khafile_def('rp_sss') wrd.world_defs += '_SSS' assets.add_shader_pass('sss_pass') if rpdat.rp_ssr: assets.add_khafile_def('rp_ssr') assets.add_shader_pass('ssr_pass') assets.add_shader_pass('blur_adaptive_pass') if rpdat.arm_ssr_half_res: assets.add_khafile_def('rp_ssr_half') if rpdat.rp_ssr_z_only: wrd.world_defs += '_SSRZOnly' if rpdat.rp_motionblur != 'Off': assets.add_khafile_def('rp_motionblur={0}'.format(rpdat.rp_motionblur)) assets.add_shader_pass('copy_pass') if rpdat.rp_motionblur == 'Camera': assets.add_shader_pass('motion_blur_pass') else: assets.add_shader_pass('motion_blur_veloc_pass') if rpdat.rp_compositornodes and rpdat.rp_autoexposure: assets.add_khafile_def('rp_autoexposure') if rpdat.rp_dynres: assets.add_khafile_def('rp_dynres') if rpdat.arm_soft_shadows == 'On': if rpdat.rp_shadowmap_cascades == '1': assets.add_shader_pass('dilate_pass') assets.add_shader_pass('visibility_pass') assets.add_shader_pass('blur_shadow_pass') assets.add_khafile_def('rp_soft_shadows') wrd.world_defs += '_SoftShadows' if rpdat.arm_soft_shadows_penumbra != 1: wrd.world_defs += '_PenumbraScale' else: log.warn( 'Disabling soft shadows - "Armory Render Path - Cascades" requires to be set to 1 for now' ) gbuffer2_direct = '_SSS' in wrd.world_defs or '_Hair' in wrd.world_defs or rpdat.arm_voxelgi_refraction gbuffer2 = '_Veloc' in wrd.world_defs or gbuffer2_direct if gbuffer2: assets.add_khafile_def('rp_gbuffer2') wrd.world_defs += '_gbuffer2' if gbuffer2_direct: assets.add_khafile_def('rp_gbuffer2_direct') wrd.world_defs += '_gbuffer2direct' if callback != None: callback()
def make(image_node, tex_name, matname=None): wrd = bpy.data.worlds['Arm'] tex = {} tex['name'] = tex_name image = image_node.image if matname == None: matname = mat_state.material.name if image == None: return None if image.filepath == '': log.warn(matname + '/' + image.name + ' - file path not found') return None # Reference image name texpath = arm.utils.asset_path(image.filepath) texfile = arm.utils.extract_filename(image.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 != 'jpg' and ext != 'png' and ext != 'hdr' and ext != 'mp4' # Convert image if do_convert: tex['file'] = tex['file'].rsplit('.', 1)[0] + '.jpg' # log.warn(matname + '/' + image.name + ' - image format is not (jpg/png/hdr), converting to jpg.') if image.packed_file != None or not is_ascii(texfile): # Extract packed data / copy non-ascii texture unpack_path = arm.utils.get_fp_build() + '/compiled/Assets/unpacked' if not os.path.exists(unpack_path): os.makedirs(unpack_path) unpack_filepath = unpack_path + '/' + tex['file'] if do_convert: if not os.path.isfile(unpack_filepath): arm.utils.write_image(image, unpack_filepath) else: # Write bytes if size is different or file does not exist yet if image.packed_file != 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) assets.add(unpack_filepath) else: if not os.path.isfile(arm.utils.asset_path(image.filepath)): log.warn('Material ' + matname + '/' + image.name + ' - file not found(' + image.filepath + ')') return None if do_convert: converted_path = arm.utils.get_fp_build() + '/compiled/Assets/unpacked/' + tex['file'] # TODO: delete cache when file changes if not os.path.isfile(converted_path): arm.utils.write_image(image, converted_path) 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 = image.filepath.rsplit('.', 1) assets.add(arm.utils.asset_path(s[0] + '.' + s[1].lower())) else: assets.add(arm.utils.asset_path(image.filepath)) # if image_format != 'RGBA32': # tex['format'] = image_format interpolation = image_node.interpolation texfilter = wrd.texture_filtering_state 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 state.target == 'html5' and powimage == False and (image_node.interpolation == 'Cubic' or image_node.interpolation == 'Smart'): log.warn(matname + '/' + image.name + ' - non power of 2 texture using ' + image_node.interpolation + ' interpolation requires WebGL2') 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' else: if state.target == 'html5' and powimage == False: log.warn(matname + '/' + image.name + ' - non power of 2 texture using repeat mode requires WebGL2') # tex['u_addressing'] = 'clamp' # tex['v_addressing'] = 'clamp' if image.source == 'MOVIE': # Just append movie texture trait for now movie_trait = {} movie_trait['type'] = 'Script' movie_trait['class_name'] = 'armory.trait.internal.MovieTexture' movie_trait['parameters'] = [tex['file']] for o in mat_state.mat_armusers[mat_state.material]: o['traits'].append(movie_trait) tex['source'] = 'movie' tex['file'] = '' # MovieTexture will load the video return tex
def parse_tex_environment(node: bpy.types.ShaderNodeTexEnvironment, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: if state.context == ParserContext.OBJECT: log.warn('Environment Texture node is not supported for object node trees, using default value') return c.to_vec3([0.0, 0.0, 0.0]) if node.image is None: return c.to_vec3([1.0, 0.0, 1.0]) world = state.world world.world_defs += '_EnvTex' curshader = state.curshader curshader.add_include('std/math.glsl') curshader.add_uniform('sampler2D envmap', link='_envmap') image = node.image filepath = image.filepath if image.packed_file is None and not os.path.isfile(arm.utils.asset_path(filepath)): log.warn(world.name + ' - unable to open ' + image.filepath) return c.to_vec3([1.0, 0.0, 1.0]) # Reference image name tex_file = arm.utils.extract_filename(image.filepath) base = tex_file.rsplit('.', 1) ext = base[1].lower() if ext == 'hdr': target_format = 'HDR' else: target_format = 'JPEG' do_convert = ext != 'hdr' and ext != 'jpg' if do_convert: if ext == 'exr': tex_file = base[0] + '.hdr' target_format = 'HDR' else: tex_file = base[0] + '.jpg' target_format = 'JPEG' if image.packed_file is not None: # Extract packed data unpack_path = arm.utils.get_fp_build() + '/compiled/Assets/unpacked' if not os.path.exists(unpack_path): os.makedirs(unpack_path) unpack_filepath = unpack_path + '/' + tex_file filepath = unpack_filepath if do_convert: if not os.path.isfile(unpack_filepath): arm.utils.unpack_image(image, unpack_filepath, file_format=target_format) elif 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) assets.add(unpack_filepath) else: if do_convert: unpack_path = arm.utils.get_fp_build() + '/compiled/Assets/unpacked' if not os.path.exists(unpack_path): os.makedirs(unpack_path) converted_path = unpack_path + '/' + tex_file filepath = converted_path # TODO: delete cache when file changes if not os.path.isfile(converted_path): arm.utils.convert_image(image, converted_path, file_format=target_format) assets.add(converted_path) else: # Link image path to assets assets.add(arm.utils.asset_path(image.filepath)) rpdat = arm.utils.get_rp() if not state.radiance_written: # Generate prefiltered envmaps world.arm_envtex_name = tex_file world.arm_envtex_irr_name = tex_file.rsplit('.', 1)[0] disable_hdr = target_format == 'JPEG' from_srgb = image.colorspace_settings.name == "sRGB" mip_count = world.arm_envtex_num_mips mip_count = write_probes.write_probes(filepath, disable_hdr, from_srgb, mip_count, arm_radiance=rpdat.arm_radiance) world.arm_envtex_num_mips = mip_count state.radiance_written = True # Append LDR define if disable_hdr: world.world_defs += '_EnvLDR' wrd = bpy.data.worlds['Arm'] mobile_mat = rpdat.arm_material_model == 'Mobile' or rpdat.arm_material_model == 'Solid' # Append radiance define if rpdat.arm_irradiance and rpdat.arm_radiance and not mobile_mat: wrd.world_defs += '_Rad' return 'texture(envmap, envMapEquirect(pos)).rgb * envmapStrength'
def parse(material: Material, mat_data, mat_users: Dict[Material, List[Object]], mat_armusers) -> tuple: wrd = bpy.data.worlds['Arm'] rpdat = arm.utils.get_rp() needs_sss = material_needs_sss(material) if needs_sss and rpdat.rp_sss_state != 'Off' and '_SSS' not in wrd.world_defs: # Must be set before calling make_shader.build() wrd.world_defs += '_SSS' # No batch - shader data per material if material.arm_custom_material != '': rpasses = ['mesh'] con = {'vertex_elements': []} con['vertex_elements'].append({'name': 'pos', 'data': 'short4norm'}) con['vertex_elements'].append({'name': 'nor', 'data': 'short2norm'}) con['vertex_elements'].append({'name': 'tex', 'data': 'short2norm'}) con['vertex_elements'].append({'name': 'tex1', 'data': 'short2norm'}) sd = {'contexts': [con]} shader_data_name = material.arm_custom_material bind_constants = {'mesh': []} bind_textures = {'mesh': []} make_shader.make_instancing_and_skinning(material, mat_users) for idx, item in enumerate(material.arm_bind_textures_list): if item.uniform_name == '': log.warn( f'Material "{material.name}": skipping export of bind texture at slot {idx + 1} with empty uniform name' ) continue if item.image is not None: tex = cycles.make_texture(item.image, item.uniform_name, material.name, 'Linear', 'REPEAT') if tex is None: continue bind_textures['mesh'].append(tex) else: log.warn( f'Material "{material.name}": skipping export of bind texture at slot {idx + 1} ("{item.uniform_name}") with no image selected' ) elif not wrd.arm_batch_materials or material.name.startswith('armdefault'): rpasses, shader_data, shader_data_name, bind_constants, bind_textures = make_shader.build( material, mat_users, mat_armusers) sd = shader_data.sd else: rpasses, shader_data, shader_data_name, bind_constants, bind_textures = mat_batch.get( material) sd = shader_data.sd sss_used = False # Material for rp in rpasses: c = { 'name': rp, 'bind_constants': [] + bind_constants[rp], 'bind_textures': [] + bind_textures[rp], 'depth_read': material.arm_depth_read, } mat_data['contexts'].append(c) if rp == 'mesh': c['bind_constants'].append({ 'name': 'receiveShadow', 'bool': material.arm_receive_shadow }) if material.arm_material_id != 0: c['bind_constants'].append({ 'name': 'materialID', 'int': material.arm_material_id }) if material.arm_material_id == 2: wrd.world_defs += '_Hair' elif rpdat.rp_sss_state != 'Off': const = {'name': 'materialID'} if needs_sss: const['int'] = 2 sss_used = True else: const['int'] = 0 c['bind_constants'].append(const) # TODO: Mesh only material batching if wrd.arm_batch_materials: # Set textures uniforms if len(c['bind_textures']) > 0: c['bind_textures'] = [] for node in material.node_tree.nodes: if node.type == 'TEX_IMAGE': tex_name = arm.utils.safesrc(node.name) tex = cycles.make_texture_from_image_node( node, tex_name) # Empty texture if tex is None: tex = {'name': tex_name, 'file': ''} c['bind_textures'].append(tex) # Set marked inputs as uniforms for node in material.node_tree.nodes: for inp in node.inputs: if inp.is_uniform: uname = arm.utils.safesrc( inp.node.name) + arm.utils.safesrc( inp.name) # Merge with cycles module c['bind_constants'].append({ 'name': uname, cycles.glsl_type(inp.type): glsl_value(inp.default_value) }) elif rp == 'translucent': c['bind_constants'].append({ 'name': 'receiveShadow', 'bool': material.arm_receive_shadow }) if wrd.arm_single_data_file: mat_data['shader'] = shader_data_name else: # Make sure that custom materials are not expected to be in .arm format ext = '' if wrd.arm_minimize and material.arm_custom_material == "" else '.json' mat_data['shader'] = shader_data_name + ext + '/' + shader_data_name return sd, rpasses, sss_used
def parse_color(world, node, context, envmap_strength_const): wrd = bpy.data.worlds['Arm'] # Env map included if node.type == 'TEX_ENVIRONMENT' and node.image != None: image = node.image filepath = image.filepath if image.packed_file == None and not os.path.isfile( arm.utils.asset_path(filepath)): log.warn(world.name + ' - unable to open ' + image.filepath) return tex = {} context['bind_textures'].append(tex) tex['name'] = 'envmap' tex['u_addressing'] = 'clamp' tex['v_addressing'] = 'clamp' # Reference image name tex['file'] = arm.utils.extract_filename(image.filepath) base = tex['file'].rsplit('.', 1) ext = base[1].lower() if ext == 'hdr': target_format = 'HDR' else: target_format = 'JPEG' do_convert = ext != 'hdr' and ext != 'jpg' if do_convert: if ext == 'exr': tex['file'] = base[0] + '.hdr' target_format = 'HDR' else: tex['file'] = base[0] + '.jpg' target_format = 'JPEG' if image.packed_file != None: # Extract packed data unpack_path = arm.utils.get_fp_build( ) + '/compiled/Assets/unpacked' if not os.path.exists(unpack_path): os.makedirs(unpack_path) unpack_filepath = unpack_path + '/' + tex['file'] filepath = unpack_filepath if do_convert: if not os.path.isfile(unpack_filepath): arm.utils.write_image(image, unpack_filepath, file_format=target_format) elif os.path.isfile(unpack_filepath) == False or os.path.getsize( unpack_filepath) != image.packed_file.size: with open(unpack_filepath, 'wb') as f: f.write(image.packed_file.data) assets.add(unpack_filepath) else: if do_convert: converted_path = arm.utils.get_fp_build( ) + '/compiled/Assets/unpacked/' + tex['file'] filepath = converted_path # TODO: delete cache when file changes if not os.path.isfile(converted_path): arm.utils.write_image(image, converted_path, file_format=target_format) assets.add(converted_path) else: # Link image path to assets assets.add(arm.utils.asset_path(image.filepath)) # Generate prefiltered envmaps world.world_envtex_name = tex['file'] world.world_envtex_irr_name = tex['file'].rsplit('.', 1)[0] disable_hdr = target_format == 'JPEG' mip_count = world.world_envtex_num_mips mip_count = write_probes.write_probes( filepath, disable_hdr, mip_count, generate_radiance=wrd.generate_radiance) world.world_envtex_num_mips = mip_count # Append envtex define bpy.data.worlds['Arm'].world_defs += '_EnvTex' # Append LDR define if disable_hdr: bpy.data.worlds['Arm'].world_defs += '_EnvLDR' # Append radiance define if wrd.generate_irradiance and wrd.generate_radiance: bpy.data.worlds['Arm'].world_defs += '_Rad' # Static image background elif node.type == 'TEX_IMAGE': bpy.data.worlds['Arm'].world_defs += '_EnvImg' tex = {} context['bind_textures'].append(tex) tex['name'] = 'envmap' # No repeat for now tex['u_addressing'] = 'clamp' tex['v_addressing'] = 'clamp' image = node.image filepath = image.filepath if image.packed_file != None: # Extract packed data filepath = arm.utils.build_dir() + '/compiled/Assets/unpacked' unpack_path = arm.utils.get_fp() + filepath if not os.path.exists(unpack_path): os.makedirs(unpack_path) unpack_filepath = unpack_path + '/' + image.name if os.path.isfile(unpack_filepath) == False or os.path.getsize( unpack_filepath) != image.packed_file.size: with open(unpack_filepath, 'wb') as f: f.write(image.packed_file.data) assets.add(unpack_filepath) else: # Link image path to assets assets.add(arm.utils.asset_path(image.filepath)) # Reference image name tex['file'] = arm.utils.extract_filename(image.filepath) # Append sky define elif node.type == 'TEX_SKY': # Match to cycles envmap_strength_const['float'] *= 0.1 bpy.data.worlds['Arm'].world_defs += '_EnvSky' # Append sky properties to material const = {} const['name'] = 'sunDirection' sun_direction = [ node.sun_direction[0], node.sun_direction[1], node.sun_direction[2] ] sun_direction[1] *= -1 # Fix Y orientation const['vec3'] = list(sun_direction) context['bind_constants'].append(const) world.world_envtex_sun_direction = sun_direction world.world_envtex_turbidity = node.turbidity world.world_envtex_ground_albedo = node.ground_albedo # Irradiance json file name wname = arm.utils.safestr(world.name) world.world_envtex_irr_name = wname write_probes.write_sky_irradiance(wname) # Radiance if wrd.generate_radiance_sky and wrd.generate_radiance and wrd.generate_irradiance: bpy.data.worlds['Arm'].world_defs += '_Rad' if wrd.generate_radiance_sky_type == 'Hosek': hosek_path = 'armory/Assets/hosek/' else: hosek_path = 'armory/Assets/hosek_fake/' sdk_path = arm.utils.get_sdk_path() # Use fake maps for now assets.add(sdk_path + hosek_path + 'hosek_radiance.hdr') for i in range(0, 8): assets.add(sdk_path + hosek_path + 'hosek_radiance_' + str(i) + '.hdr') world.world_envtex_name = 'hosek' world.world_envtex_num_mips = 8
def add_world_defs(): wrd = bpy.data.worlds['Arm'] rpdat = arm.utils.get_rp() # Screen-space ray-traced shadows if rpdat.arm_ssrs: wrd.world_defs += '_SSRS' if rpdat.arm_two_sided_area_light: wrd.world_defs += '_TwoSidedAreaLight' # Store contexts if rpdat.rp_hdr == False: wrd.world_defs += '_LDR' if wrd.arm_light_ies_texture != '': wrd.world_defs += '_LightIES' assets.add_embedded_data('iestexture.png') if wrd.arm_light_clouds_texture != '': wrd.world_defs += '_LightClouds' assets.add_embedded_data('cloudstexture.png') if rpdat.rp_renderer == 'Deferred': assets.add_khafile_def('arm_deferred') wrd.world_defs += '_Deferred' # GI voxelgi = False voxelao = False has_voxels = arm.utils.voxel_support() if has_voxels and rpdat.arm_material_model == 'Full': if rpdat.rp_gi == 'Voxel GI': voxelgi = True elif rpdat.rp_gi == 'Voxel AO': voxelao = True # Shadows if rpdat.rp_shadows: wrd.world_defs += '_ShadowMap' if rpdat.rp_shadowmap_cascades != '1': if voxelgi: log.warn('Disabling shadow cascades - Voxel GI does not support cascades yet') else: wrd.world_defs += '_CSM' assets.add_khafile_def('arm_csm') # SS if rpdat.rp_ssgi == 'RTGI' or rpdat.rp_ssgi == 'RTAO': if rpdat.rp_ssgi == 'RTGI': wrd.world_defs += '_RTGI' if rpdat.arm_ssgi_rays == '9': wrd.world_defs += '_SSGICone9' if rpdat.rp_autoexposure: wrd.world_defs += '_AutoExposure' if voxelgi or voxelao: assets.add_khafile_def('arm_voxelgi') wrd.world_defs += '_VoxelCones' + rpdat.arm_voxelgi_cones if rpdat.arm_voxelgi_revoxelize: assets.add_khafile_def('arm_voxelgi_revox') if rpdat.arm_voxelgi_camera: wrd.world_defs += '_VoxelGICam' if rpdat.arm_voxelgi_temporal: assets.add_khafile_def('arm_voxelgi_temporal') wrd.world_defs += '_VoxelGITemporal' if voxelgi: wrd.world_defs += '_VoxelGI' # assets.add_shader_external(arm.utils.get_sdk_path() + '/armory/Shaders/voxel_light/voxel_light.comp.glsl') # if rpdat.arm_voxelgi_bounces != "1": # assets.add_khafile_def('rp_gi_bounces={0}'.format(rpdat.arm_voxelgi_bounces)) # assets.add_shader_external(arm.utils.get_sdk_path() + '/armory/Shaders/voxel_bounce/voxel_bounce.comp.glsl') # if rpdat.arm_voxelgi_shadows: # wrd.world_defs += '_VoxelGIShadow' if rpdat.rp_voxelgi_relight: assets.add_khafile_def('rp_voxelgi_relight') elif voxelao: wrd.world_defs += '_VoxelAOvar' # Write a shader variant if arm.utils.get_legacy_shaders() and not state.is_viewport: wrd.world_defs += '_Legacy' # Light defines point_lights = 0 for bo in bpy.data.objects: # TODO: temp if bo.type == 'LIGHT': light = bo.data if light.type == 'AREA' and '_LTC' not in wrd.world_defs: wrd.world_defs += '_LTC' assets.add_khafile_def('arm_ltc') if light.type == 'SUN' and '_Sun' not in wrd.world_defs: wrd.world_defs += '_Sun' if light.type == 'POINT' or light.type == 'SPOT': point_lights += 1 if light.type == 'SPOT' and '_Spot' not in wrd.world_defs: wrd.world_defs += '_Spot' assets.add_khafile_def('arm_spot') if point_lights == 1: wrd.world_defs += '_SinglePoint' elif point_lights > 1: wrd.world_defs += '_Clusters' assets.add_khafile_def('arm_clusters') if '_Rad' in wrd.world_defs or '_VoxelGI' in wrd.world_defs: wrd.world_defs += '_Brdf'