extends RefCounted class_name WowWMOMaterial const WMOBLEND_OPAQUE := 0 const WMOBLEND_ALPHA_KEY := 1 const WMOBLEND_ALPHA := 2 const SHADER_VERSION := 10 static var _shader_cache: Dictionary = {} static func clear_cache() -> void: _shader_cache.clear() static func _ensure_wow_shader_globals() -> void: pass static func build( texture: Texture2D, texture2: Texture2D, material_flags: int, shader_id: int, blend_mode: int, texture_path: String = "", texture2_path: String = "", diffuse_color: Color = Color.WHITE, emissive_color: Color = Color.BLACK, secondary_color: Color = Color.WHITE, texture3: Texture2D = null, texture3_path: String = "") -> ShaderMaterial: var is_window_material := _is_window_texture(texture_path) var is_glass_blend_material := is_window_material and blend_mode == WMOBLEND_ALPHA var mode := _blend_shader_mode(blend_mode, is_glass_blend_material) var effective_diffuse := diffuse_color if maxf(maxf(effective_diffuse.r, effective_diffuse.g), effective_diffuse.b) <= 0.001: effective_diffuse = Color.WHITE _ensure_wow_shader_globals() var mat := ShaderMaterial.new() mat.resource_name = texture_path.get_file().get_basename() var pixel_shader := _wmo_pixel_shader_id(shader_id) mat.shader = _get_shader(mode, texture2 != null, texture3 != null, is_window_material) mat.render_priority = 0 if blend_mode <= WMOBLEND_ALPHA_KEY else 1 mat.set_shader_parameter("albedo_tex", texture) mat.set_shader_parameter("detail_tex", texture2) mat.set_shader_parameter("extra_tex", texture3) mat.set_shader_parameter("use_texture", texture != null) mat.set_shader_parameter("use_detail_texture", texture2 != null) mat.set_shader_parameter("use_extra_texture", texture3 != null) mat.set_shader_parameter("pixel_shader", pixel_shader) mat.set_shader_parameter("alpha_ref", _alpha_ref(blend_mode, pixel_shader)) mat.set_shader_parameter("alpha_output_strength", _alpha_output_strength(blend_mode, pixel_shader)) mat.set_shader_parameter("vertex_color_strength", 0.72) mat.set_shader_parameter("ambient_floor", 0.22) mat.set_shader_parameter("unlit_material", 1.0 if (material_flags & 0x01) != 0 else 0.0) mat.set_shader_parameter("diffuse_color", effective_diffuse) mat.set_shader_parameter("emissive_color", emissive_color) mat.set_shader_parameter("material_emissive_strength", _material_emissive_strength(material_flags, emissive_color, texture_path)) mat.set_shader_parameter("secondary_color", _effective_secondary_color(secondary_color)) mat.set_shader_parameter("detail_strength", 1.0 if texture2 != null else 0.0) mat.set_shader_parameter("window_material", 1.0 if is_glass_blend_material else 0.0) mat.set_meta("texture0_path", texture_path) mat.set_meta("texture1_path", texture2_path) mat.set_meta("texture2_path", texture3_path) mat.set_meta("wow_flags", material_flags) mat.set_meta("wow_shader", shader_id) mat.set_meta("wow_wmo_pixel_shader", pixel_shader) mat.set_meta("wow_blend_mode", blend_mode) mat.set_meta("wow_wmo_window_material", is_window_material) mat.set_meta("wow_wmo_glass_blend_material", is_glass_blend_material) return mat static func _wmo_pixel_shader_id(shader_id: int) -> int: match shader_id: 0: return 0 # Diffuse 1: return 1 # Specular 2: return 2 # Metal 3: return 3 # Env 4: return 4 # Opaque 5: return 5 # EnvMetal 6: return 6 # TwoLayerDiffuse 7: return 7 # TwoLayerEnvMetal 8: return 8 # TwoLayerTerrain 9: return 9 # DiffuseEmissive 11: return 10 # MaskedEnvMetal 12: return 11 # EnvMetalEmissive 13: return 12 # TwoLayerDiffuseOpaque 15: return 13 # TwoLayerDiffuseEmissive 16: return 0 # DiffuseTerrain 17: return 14 # AdditiveMaskedEnvMetal 18: return 15 # TwoLayerDiffuseMod2x 19: return 16 # TwoLayerDiffuseMod2xNA 20: return 17 # TwoLayerDiffuseAlpha 21: return 18 # Lod 22: return 19 # Parallax 23: return 20 # DF shader _: return 0 static func _shader_alpha_is_opacity(pixel_shader: int) -> bool: match pixel_shader: 0, 1, 2, 4, 18, 19: return true _: return false static func _alpha_ref(blend_mode: int, pixel_shader: int) -> float: if blend_mode == WMOBLEND_ALPHA_KEY: return 0.501960814 return 0.0 static func _alpha_output_strength(blend_mode: int, pixel_shader: int) -> float: if blend_mode == WMOBLEND_ALPHA and _shader_alpha_is_opacity(pixel_shader): return 1.0 return 0.0 static func _effective_secondary_color(color: Color) -> Color: if maxf(maxf(color.r, color.g), color.b) <= 0.001: return Color.WHITE return color static func _material_emissive_strength(material_flags: int, color: Color, texture_path: String) -> float: var color_strength := maxf(maxf(color.r, color.g), color.b) * color.a if color_strength <= 0.001: return 0.0 var strength := 0.12 if (material_flags & 0x10) != 0: strength = 0.26 if _is_window_texture(texture_path): strength = minf(strength, 0.18) return strength static func _is_window_texture(texture_path: String) -> bool: var lower_path := texture_path.to_lower().replace("\\", "/") var file_name := lower_path.get_file() return lower_path.contains("/windows/") \ or lower_path.contains("/glass/") \ or file_name.contains("window") \ or file_name.contains("glass") \ or file_name.contains("_wnd_") static func _blend_shader_mode(blend_mode: int, is_glass_blend_material: bool) -> String: if is_glass_blend_material: return "glass" match blend_mode: WMOBLEND_ALPHA_KEY: return "cutout" WMOBLEND_ALPHA: return "alpha" _: return "opaque" static func _get_shader(mode: String, has_detail: bool, has_extra: bool, stable_albedo_sampler: bool = false) -> Shader: var key := "v=%d|%s|detail=%s|extra=%s|stable_albedo=%s" % [SHADER_VERSION, mode, str(has_detail), str(has_extra), str(stable_albedo_sampler)] if _shader_cache.has(key): return _shader_cache[key] var shader := Shader.new() shader.code = _shader_code(mode, has_detail, has_extra, stable_albedo_sampler) _shader_cache[key] = shader return shader static func _shader_code(mode: String, has_detail: bool, has_extra: bool, stable_albedo_sampler: bool = false) -> String: var render_modes: Array[String] = ["blend_mix", "unshaded"] render_modes.append("cull_back" if mode == "glass" or stable_albedo_sampler else "cull_disabled") if mode == "opaque" or mode == "cutout": render_modes.append("depth_draw_opaque") else: render_modes.append("depth_draw_never") var albedo_sampler_hint := "filter_linear, repeat_disable" if stable_albedo_sampler else "filter_linear_mipmap_anisotropic, repeat_enable" return """ shader_type spatial; render_mode %s; uniform sampler2D albedo_tex : source_color, %s; uniform sampler2D detail_tex : source_color, filter_linear_mipmap_anisotropic, repeat_enable; uniform sampler2D extra_tex : source_color, filter_linear_mipmap_anisotropic, repeat_enable; uniform bool use_texture = false; uniform bool use_detail_texture = false; uniform bool use_extra_texture = false; uniform int pixel_shader = 0; uniform float alpha_ref = 0.0; uniform float alpha_output_strength = 0.0; uniform float vertex_color_strength = 0.88; uniform float ambient_floor = 0.20; uniform float unlit_material = 0.0; uniform vec4 diffuse_color : source_color = vec4(1.0); uniform vec4 emissive_color : source_color = vec4(0.0); uniform float material_emissive_strength = 0.0; uniform vec4 secondary_color : source_color = vec4(1.0); uniform float detail_strength = 0.0; uniform float window_material = 0.0; global uniform vec4 wow_ambient_color; global uniform vec4 wow_light_color; global uniform vec3 wow_light_dir; global uniform vec4 wow_fog_color; global uniform vec2 wow_fog_range; global uniform float wow_fog_density; global uniform float wow_sun_elevation; varying vec3 world_normal; varying vec3 world_pos; varying vec3 view_pos; varying vec3 view_normal; void vertex() { world_normal = normalize(MODEL_NORMAL_MATRIX * NORMAL); world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; view_pos = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz; view_normal = normalize((MODELVIEW_MATRIX * vec4(NORMAL, 0.0)).xyz); } vec3 apply_wow_fog(vec3 color) { float dist = length(view_pos); float range_fog = smoothstep(wow_fog_range.x, wow_fog_range.y, dist); float fog_amount = range_fog * clamp(wow_fog_density, 0.0, 0.55); return mix(color, wow_fog_color.rgb, fog_amount); } float color_luma(vec3 color) { return dot(color, vec3(0.2126, 0.7152, 0.0722)); } vec3 textured_material_tint(vec3 color) { float luma = max(color_luma(color), 0.001); vec3 hue = clamp(color / luma, vec3(0.58), vec3(1.20)); return mix(vec3(1.0), hue, 0.28); } vec3 untextured_material_color(vec3 color) { float luma = color_luma(color); vec3 muted = mix(vec3(luma), color, 0.42); return clamp(muted * 0.82, vec3(0.08), vec3(0.72)); } void resolve_wmo_pixel(vec4 tex1, vec4 tex2, vec4 tex3, bool has_detail, bool has_extra, out vec3 diffuse, out vec3 emissive) { float layer_weight = clamp(secondary_color.a, 0.0, 1.0); diffuse = tex1.rgb; emissive = vec3(0.0); if (!has_detail) { return; } if (pixel_shader == 3) { emissive = tex2.rgb * tex1.a; } else if (pixel_shader == 5) { emissive = (tex1.rgb * tex1.a) * tex2.rgb; } else if (pixel_shader == 6) { vec3 layer2 = mix(tex1.rgb, tex2.rgb, tex2.a); diffuse = mix(layer2, tex1.rgb, layer_weight); } else if (pixel_shader == 8 || pixel_shader == 12) { diffuse = mix(tex2.rgb, tex1.rgb, layer_weight); } else if (pixel_shader == 9) { emissive = tex2.rgb * tex2.a * layer_weight; } else if (pixel_shader == 10 && has_extra) { float mix_factor = clamp(tex3.a * layer_weight, 0.0, 1.0); diffuse = mix(mix(tex1.rgb * tex2.rgb * 2.0, tex3.rgb, mix_factor), tex1.rgb, tex1.a); } else if (pixel_shader == 11 && has_extra) { emissive = ((tex1.rgb * tex1.a) * tex2.rgb) + ((tex3.rgb * tex3.a) * layer_weight); } else if (pixel_shader == 13) { vec3 layer = tex2.rgb * (1.0 - tex2.a); diffuse = mix(layer, tex1.rgb, layer_weight); emissive = (tex2.rgb * tex2.a) * (1.0 - layer_weight); } else if (pixel_shader == 14 && has_extra) { diffuse = mix( (tex1.rgb * tex2.rgb * 2.0) + (tex3.rgb * clamp(tex3.a * layer_weight, 0.0, 1.0)), tex1.rgb, tex1.a ); } else if (pixel_shader == 15) { vec3 layer2 = mix(tex1.rgb, tex2.rgb, tex2.a); vec3 layer3 = mix(layer2, tex1.rgb, layer_weight); diffuse = has_extra ? layer3 * tex3.rgb * 2.0 : layer3; } else if (pixel_shader == 16) { vec3 mod_layer = tex1.rgb * tex2.rgb * 2.0; diffuse = mix(tex1.rgb, mod_layer, layer_weight); } else if (pixel_shader == 17) { vec3 layer2 = mix(tex1.rgb, tex2.rgb, tex2.a); vec3 layer3 = mix(layer2, tex1.rgb, has_extra ? tex3.a : layer_weight); diffuse = has_extra ? layer3 * tex3.rgb * 2.0 : layer3; } } void fragment() { vec4 tex1 = use_texture ? texture(albedo_tex, UV) : vec4(1.0); vec4 tex2 = use_detail_texture ? texture(detail_tex, UV) : vec4(1.0); vec4 tex3 = use_extra_texture ? texture(extra_tex, UV) : vec4(1.0); if (alpha_ref > 0.0 && tex1.a < alpha_ref) { discard; } vec3 mat_diffuse; vec3 shader_emissive; resolve_wmo_pixel(tex1, tex2, tex3, use_detail_texture, use_extra_texture, mat_diffuse, shader_emissive); vec3 baked = max(COLOR.rgb, vec3(ambient_floor)); vec3 baked_lit = mix(vec3(1.0), baked, clamp(vertex_color_strength, 0.0, 1.0)); vec3 n = normalize(world_normal); if (!FRONT_FACING) { n = -n; } float ndl = max(dot(n, normalize(wow_light_dir)), 0.0); vec3 zone_ambient = mix(vec3(0.78), max(wow_ambient_color.rgb, vec3(0.08)), 0.16); vec3 zone_light = mix(vec3(1.0), wow_light_color.rgb, 0.10); float day_strength = mix(0.48, 0.92, clamp(wow_sun_elevation, 0.0, 1.0)); vec3 lit = baked_lit * (zone_ambient * 0.86 + zone_light * ndl * 0.24 * day_strength); if (unlit_material > 0.5) { lit = vec3(1.0); } lit = clamp(lit, vec3(0.46), vec3(1.05)); vec3 material_color = untextured_material_color(diffuse_color.rgb); if (use_texture) { material_color = mat_diffuse * textured_material_tint(diffuse_color.rgb); } float emissive_mask = smoothstep(0.35, 0.92, color_luma(mat_diffuse)); vec3 material_emissive = mat_diffuse * emissive_color.rgb * emissive_color.a * material_emissive_strength * emissive_mask; vec3 emissive = shader_emissive * 0.35 + material_emissive; vec3 color = clamp(material_color * lit + emissive, vec3(0.0), vec3(1.0)); float window_facing = 1.0; if (window_material > 0.5) { vec3 vn = normalize(view_normal); if (!FRONT_FACING) { vn = -vn; } window_facing = abs(dot(vn, normalize(-view_pos))); float edge_reflect = pow(1.0 - window_facing, 3.0); vec3 glass_tint = mix(color, vec3(0.58, 0.70, 0.82), 0.24); color = clamp(mix(color, glass_tint + vec3(edge_reflect) * 0.12, 0.36), vec3(0.0), vec3(1.0)); } ALBEDO = apply_wow_fog(color); %s } """ % [ ", ".join(render_modes), albedo_sampler_hint, _alpha_output_code(mode), ] static func _alpha_output_code(mode: String) -> String: if mode == "opaque" or mode == "cutout": return "" if mode == "glass": return "\tALPHA = clamp(mix(0.40, 0.95, window_facing) * max(diffuse_color.a, 0.45), 0.35, 0.95);" return "\tALPHA = mix(diffuse_color.a, tex1.a * diffuse_color.a, clamp(alpha_output_strength, 0.0, 1.0));"