def parse_mixshader(node: bpy.types.ShaderNodeMixShader, out_socket: NodeSocket, state: ParserState) -> None: prefix = '' if node.inputs[0].is_linked else 'const ' fac = c.parse_value_input(node.inputs[0]) fac_var = c.node_name(node.name) + '_fac' fac_inv_var = c.node_name(node.name) + '_fac_inv' state.curshader.write('{0}float {1} = {2};'.format(prefix, fac_var, fac)) state.curshader.write('{0}float {1} = 1.0 - {2};'.format( prefix, fac_inv_var, fac_var)) bc1, rough1, met1, occ1, spec1, opac1, emi1 = c.parse_shader_input( node.inputs[1]) bc2, rough2, met2, occ2, spec2, opac2, emi2 = c.parse_shader_input( node.inputs[2]) if state.parse_surface: state.out_basecol = '({0} * {3} + {1} * {2})'.format( bc1, bc2, fac_var, fac_inv_var) state.out_roughness = '({0} * {3} + {1} * {2})'.format( rough1, rough2, fac_var, fac_inv_var) state.out_metallic = '({0} * {3} + {1} * {2})'.format( met1, met2, fac_var, fac_inv_var) state.out_occlusion = '({0} * {3} + {1} * {2})'.format( occ1, occ2, fac_var, fac_inv_var) state.out_specular = '({0} * {3} + {1} * {2})'.format( spec1, spec2, fac_var, fac_inv_var) state.out_emission = '({0} * {3} + {1} * {2})'.format( emi1, emi2, fac_var, fac_inv_var) if state.parse_opacity: state.out_opacity = '({0} * {3} + {1} * {2})'.format( opac1, opac2, fac_var, fac_inv_var)
def parse_rgb(node: bpy.types.ShaderNodeRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: if node.arm_material_param: nn = 'param_' + c.node_name(node.name) state.curshader.add_uniform(f'vec3 {nn}', link=f'{node.name}') return nn else: return c.to_vec3(out_socket.default_value)
def parse_mapping(node: bpy.types.ShaderNodeMapping, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: # Only "Point", "Texture" and "Vector" types supported for now.. # More information about the order of operations for this node: # https://docs.blender.org/manual/en/latest/render/shader_nodes/vector/mapping.html#properties input_vector: bpy.types.NodeSocket = node.inputs[0] input_location: bpy.types.NodeSocket = node.inputs['Location'] input_rotation: bpy.types.NodeSocket = node.inputs['Rotation'] input_scale: bpy.types.NodeSocket = node.inputs['Scale'] out = c.parse_vector_input(input_vector) if input_vector.is_linked else c.to_vec3(input_vector.default_value) location = c.parse_vector_input(input_location) if input_location.is_linked else c.to_vec3(input_location.default_value) rotation = c.parse_vector_input(input_rotation) if input_rotation.is_linked else c.to_vec3(input_rotation.default_value) scale = c.parse_vector_input(input_scale) if input_scale.is_linked else c.to_vec3(input_scale.default_value) # Use inner functions because the order of operations varies between # mapping node vector types. This adds a slight overhead but makes # the code much more readable. # - "Point" and "Vector" use Scale -> Rotate -> Translate # - "Texture" uses Translate -> Rotate -> Scale def calc_location(output: str) -> str: # Vectors and Eulers support the "!=" operator if input_scale.is_linked or input_scale.default_value != Vector((1, 1, 1)): if node.vector_type == 'TEXTURE': output = f'({output} / {scale})' else: output = f'({output} * {scale})' return output def calc_scale(output: str) -> str: if input_location.is_linked or input_location.default_value != Vector((0, 0, 0)): # z location is a little off sometimes?... if node.vector_type == 'TEXTURE': output = f'({output} - {location})' else: output = f'({output} + {location})' return output out = calc_location(out) if node.vector_type == 'TEXTURE' else calc_scale(out) if input_rotation.is_linked or input_rotation.default_value != Euler((0, 0, 0)): var_name = c.node_name(node.name) + "_rotation" if node.vector_type == 'TEXTURE': state.curshader.write(f'mat3 {var_name}X = mat3(1.0, 0.0, 0.0, 0.0, cos({rotation}.x), sin({rotation}.x), 0.0, -sin({rotation}.x), cos({rotation}.x));') state.curshader.write(f'mat3 {var_name}Y = mat3(cos({rotation}.y), 0.0, -sin({rotation}.y), 0.0, 1.0, 0.0, sin({rotation}.y), 0.0, cos({rotation}.y));') state.curshader.write(f'mat3 {var_name}Z = mat3(cos({rotation}.z), sin({rotation}.z), 0.0, -sin({rotation}.z), cos({rotation}.z), 0.0, 0.0, 0.0, 1.0);') else: # A little bit redundant, but faster than 12 more multiplications to make it work dynamically state.curshader.write(f'mat3 {var_name}X = mat3(1.0, 0.0, 0.0, 0.0, cos(-{rotation}.x), sin(-{rotation}.x), 0.0, -sin(-{rotation}.x), cos(-{rotation}.x));') state.curshader.write(f'mat3 {var_name}Y = mat3(cos(-{rotation}.y), 0.0, -sin(-{rotation}.y), 0.0, 1.0, 0.0, sin(-{rotation}.y), 0.0, cos(-{rotation}.y));') state.curshader.write(f'mat3 {var_name}Z = mat3(cos(-{rotation}.z), sin(-{rotation}.z), 0.0, -sin(-{rotation}.z), cos(-{rotation}.z), 0.0, 0.0, 0.0, 1.0);') # XYZ-order euler rotation out = f'{out} * {var_name}X * {var_name}Y * {var_name}Z' out = calc_scale(out) if node.vector_type == 'TEXTURE' else calc_location(out) return out
def parse_value(node: bpy.types.ShaderNodeValue, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: if node.arm_material_param: nn = 'param_' + c.node_name(node.name) state.curshader.add_uniform('float {0}'.format(nn), link='{0}'.format(node.name)) return nn else: return c.to_vec1(node.outputs[0].default_value)
def parse_curvevec(node: bpy.types.ShaderNodeVectorCurve, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: fac = c.parse_value_input(node.inputs[0]) vec = c.parse_vector_input(node.inputs[1]) curves = node.mapping.curves name = c.node_name(node.name) # mapping.curves[0].points[0].handle_type # bezier curve return '(vec3({0}, {1}, {2}) * {3})'.format( c.vector_curve(name + '0', vec + '.x', curves[0].points), c.vector_curve(name + '1', vec + '.y', curves[1].points), c.vector_curve(name + '2', vec + '.z', curves[2].points), fac)
def parse_curvergb(node: bpy.types.ShaderNodeRGBCurve, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: fac = c.parse_value_input(node.inputs[0]) vec = c.parse_vector_input(node.inputs[1]) curves = node.mapping.curves name = c.node_name(node.name) # mapping.curves[0].points[0].handle_type return '(sqrt(vec3({0}, {1}, {2}) * vec3({4}, {5}, {6})) * {3})'.format( c.vector_curve(name + '0', vec + '.x', curves[0].points), c.vector_curve(name + '1', vec + '.y', curves[1].points), c.vector_curve(name + '2', vec + '.z', curves[2].points), fac, c.vector_curve(name + '3a', vec + '.x', curves[3].points), c.vector_curve(name + '3b', vec + '.y', curves[3].points), c.vector_curve(name + '3c', vec + '.z', curves[3].points))
def parse_sephsv(node: bpy.types.ShaderNodeSeparateHSV, out_socket: bpy.types.NodeSocket, state: ParserState) -> floatstr: state.curshader.add_function(c_functions.str_hue_sat) hsv_var = c.node_name(node.name) + '_hsv' state.curshader.write( f'const vec3 {hsv_var} = rgb_to_hsv({c.parse_vector_input(node.inputs["Color"])}.rgb);' ) if out_socket == node.outputs[0]: return f'{hsv_var}.x' elif out_socket == node.outputs[1]: return f'{hsv_var}.y' elif out_socket == node.outputs[2]: return f'{hsv_var}.z'
def parse_rgb(node: bpy.types.ShaderNodeRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> vec3str: if node.arm_material_param: nn = 'param_' + c.node_name(node.name) v = out_socket.default_value value = [] value.append(float(v[0])) value.append(float(v[1])) value.append(float(v[2])) is_arm_mat_param = True state.curshader.add_uniform(f'vec3 {nn}', link=f'{node.name}', default_value=value, is_arm_mat_param=is_arm_mat_param) return nn else: return c.to_vec3(out_socket.default_value)
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_tex_image(node: bpy.types.ShaderNodeTexImage, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: if state.context == ParserContext.OBJECT: # Color or Alpha output use_color_out = out_socket == node.outputs[0] # Already fetched if c.is_parsed(c.store_var_name(node)): if use_color_out: return f'{c.store_var_name(node)}.rgb' else: return f'{c.store_var_name(node)}.a' tex_name = c.node_name(node.name) tex = c.make_texture(node, tex_name) tex_link = None tex_default_file = None is_arm_mat_param = None if node.arm_material_param: tex_link = node.name is_arm_mat_param = True if tex is not None: state.curshader.write_textures += 1 if node.arm_material_param and tex['file'] is not None: tex_default_file = tex['file'] if use_color_out: to_linear = node.image is not None and node.image.colorspace_settings.name == 'sRGB' res = f'{c.texture_store(node, tex, tex_name, to_linear, tex_link=tex_link, default_value=tex_default_file, is_arm_mat_param=is_arm_mat_param)}.rgb' else: res = f'{c.texture_store(node, tex, tex_name, tex_link=tex_link, default_value=tex_default_file, is_arm_mat_param=is_arm_mat_param)}.a' state.curshader.write_textures -= 1 return res # Empty texture elif node.image is None: tex = { 'name': tex_name, 'file': '' } if use_color_out: return '{0}.rgb'.format(c.texture_store(node, tex, tex_name, to_linear=False, tex_link=tex_link, is_arm_mat_param=is_arm_mat_param)) return '{0}.a'.format(c.texture_store(node, tex, tex_name, to_linear=True, tex_link=tex_link, is_arm_mat_param=is_arm_mat_param)) # Pink color for missing texture else: tex_store = c.store_var_name(node) if use_color_out: state.parsed.add(tex_store) state.curshader.write_textures += 1 state.curshader.write(f'vec4 {tex_store} = vec4(1.0, 0.0, 1.0, 1.0);') state.curshader.write_textures -= 1 return f'{tex_store}.rgb' else: state.curshader.write(f'vec4 {tex_store} = vec4(1.0, 0.0, 1.0, 1.0);') return f'{tex_store}.a' # World context # TODO: Merge with above implementation to also allow mappings other than using view coordinates else: world = state.world world.world_defs += '_EnvImg' # Background texture state.curshader.add_uniform('sampler2D envmap', link='_envmap') state.curshader.add_uniform('vec2 screenSize', link='_screenSize') image = node.image filepath = image.filepath if image.packed_file is not 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 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: # Link image path to assets assets.add(arm.utils.asset_path(image.filepath)) # 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' # 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" rpdat = arm.utils.get_rp() 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 # Will have to get rid of gl_FragCoord, pass texture coords from vertex shader state.curshader.write_init('vec2 texco = gl_FragCoord.xy / screenSize;') return 'texture(envmap, vec2(texco.x, 1.0 - texco.y)).rgb * envmapStrength'
def parse_valtorgb(node: bpy.types.ShaderNodeValToRGB, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: # Alpha (TODO: make ColorRamp calculation vec4-based and split afterwards) if out_socket == node.outputs[1]: return '1.0' input_fac: bpy.types.NodeSocket = node.inputs[0] fac: str = c.parse_value_input( input_fac) if input_fac.is_linked else c.to_vec1( input_fac.default_value) interp = node.color_ramp.interpolation elems = node.color_ramp.elements if len(elems) == 1: return c.to_vec3(elems[0].color) # Write color array # The last entry is included twice so that the interpolation # between indices works (no out of bounds error) cols_var = c.node_name(node.name).upper() + '_COLS' cols_entries = ', '.join( f'vec3({elem.color[0]}, {elem.color[1]}, {elem.color[2]})' for elem in elems) cols_entries += f', vec3({elems[len(elems) - 1].color[0]}, {elems[len(elems) - 1].color[1]}, {elems[len(elems) - 1].color[2]})' state.curshader.add_const("vec3", cols_var, cols_entries, array_size=len(elems) + 1) fac_var = c.node_name(node.name) + '_fac' state.curshader.write(f'float {fac_var} = {fac};') # Get index of the nearest left element relative to the factor index = '0 + ' index += ' + '.join([ f'(({fac_var} > {elems[i].position}) ? 1 : 0)' for i in range(1, len(elems)) ]) # Write index index_var = c.node_name(node.name) + '_i' state.curshader.write(f'int {index_var} = {index};') if interp == 'CONSTANT': return f'{cols_var}[{index_var}]' # Linear interpolation else: # Write factor array facs_var = c.node_name(node.name).upper() + '_FACS' facs_entries = ', '.join(str(elem.position) for elem in elems) # Add one more entry at the rightmost position so that the # interpolation between indices works (no out of bounds error) facs_entries += ', 1.0' state.curshader.add_const("float", facs_var, facs_entries, array_size=len(elems) + 1) # Mix color prev_stop_fac = f'{facs_var}[{index_var}]' next_stop_fac = f'{facs_var}[{index_var} + 1]' prev_stop_col = f'{cols_var}[{index_var}]' next_stop_col = f'{cols_var}[{index_var} + 1]' rel_pos = f'({fac_var} - {prev_stop_fac}) * (1.0 / ({next_stop_fac} - {prev_stop_fac}))' return f'mix({prev_stop_col}, {next_stop_col}, max({rel_pos}, 0.0))'