шейдеры билбордов
This commit is contained in:
@@ -735,6 +735,16 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
- WMO window materials such as `MM_ELWYNN_WND_EXT__01.BLP` carry high `MOMT.sidnColor`/emissive values. Emissive is texture-masked and capped for window paths so it brightens the pane pattern instead of flooding the whole quad white.
|
- WMO window materials such as `MM_ELWYNN_WND_EXT__01.BLP` carry high `MOMT.sidnColor`/emissive values. Emissive is texture-masked and capped for window paths so it brightens the pane pattern instead of flooding the whole quad white.
|
||||||
- Runtime WMO cache material refresh is versioned separately from WMO geometry cache. Shader-only WMO material fixes should bump `WMO_MATERIAL_REFRESH_VERSION` instead of forcing a full `wmo_render_v1` rebake.
|
- Runtime WMO cache material refresh is versioned separately from WMO geometry cache. Shader-only WMO material fixes should bump `WMO_MATERIAL_REFRESH_VERSION` instead of forcing a full `wmo_render_v1` rebake.
|
||||||
|
|
||||||
|
## 2026-07-07 M2 Billboard Pass
|
||||||
|
|
||||||
|
- Native `M2Loader` now reads `M2CompBone.flags` for billboard bits `0x8/0x10/0x20/0x40` on the static path, not only on the animated path.
|
||||||
|
- Billboard metadata is resolved per drawn vertex through the skin section bone palette and vertex bone indices. This avoids treating an entire mixed surface as billboard just because the palette contains a billboard bone.
|
||||||
|
- `M2Builder` stores billboard pivot/mode in `Mesh.ARRAY_CUSTOM0` as RGBA float data. `CUSTOM0.xyz` is the Godot-space bone pivot; `CUSTOM0.w` is the billboard mode: spherical, lock X, lock Y, or lock Z.
|
||||||
|
- `WowM2Material` applies camera-facing billboarding in the vertex shader for marked vertices. This works with grouped ADT M2 `MultiMesh` batches because the shader uses each instance transform.
|
||||||
|
- Runtime M2 cache refresh now has `M2_MATERIAL_REFRESH_VERSION`. Old cached `.tscn/.glb` meshes are replaced in memory from raw `.m2` when available, so the billboard/custom-attribute path does not require an immediate cache rebake.
|
||||||
|
- Verified with `ElwynnGrass1.m2`: native data exposes a billboard batch, the built mesh contains `CUSTOM0`, and the M2 shader compiles with the billboard uniform enabled.
|
||||||
|
- Remaining parity work: many Elwynn tree canopy M2s do not carry billboard bone flags, so their flat-looking leaves are a separate material/geometry parity issue, not this bone-billboard path.
|
||||||
|
|
||||||
## Practical Rule For Future Work
|
## Practical Rule For Future Work
|
||||||
|
|
||||||
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
|
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
class_name M2Builder
|
class_name M2Builder
|
||||||
|
|
||||||
const WOW_M2_MATERIAL := preload("res://addons/mpq_extractor/loaders/wow_m2_material.gd")
|
const WOW_M2_MATERIAL := preload("res://addons/mpq_extractor/loaders/wow_m2_material.gd")
|
||||||
|
const MATERIAL_FORMAT_VERSION := 1
|
||||||
|
|
||||||
static var _texture_cache: Dictionary = {}
|
static var _texture_cache: Dictionary = {}
|
||||||
|
|
||||||
@@ -67,7 +68,8 @@ static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
|||||||
arrays[Mesh.ARRAY_TEX_UV2] = uvs2
|
arrays[Mesh.ARRAY_TEX_UV2] = uvs2
|
||||||
arrays[Mesh.ARRAY_INDEX] = batch_indices
|
arrays[Mesh.ARRAY_INDEX] = batch_indices
|
||||||
|
|
||||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
var surface_flags := _apply_billboard_custom_data(arrays, batch, verts.size())
|
||||||
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays, [], {}, surface_flags)
|
||||||
|
|
||||||
var surf_idx := mesh.get_surface_count() - 1
|
var surf_idx := mesh.get_surface_count() - 1
|
||||||
var mat_def: Dictionary = materials[mat_id] if mat_id >= 0 and mat_id < materials.size() else {}
|
var mat_def: Dictionary = materials[mat_id] if mat_id >= 0 and mat_id < materials.size() else {}
|
||||||
@@ -94,6 +96,8 @@ static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
|||||||
|
|
||||||
if mesh.get_surface_count() == 0:
|
if mesh.get_surface_count() == 0:
|
||||||
return root
|
return root
|
||||||
|
mesh.set_meta("wow_m2_material_refresh_version", MATERIAL_FORMAT_VERSION)
|
||||||
|
mesh.set_meta("wow_m2_source_path", model_path)
|
||||||
|
|
||||||
var mi := MeshInstance3D.new()
|
var mi := MeshInstance3D.new()
|
||||||
mi.name = "Mesh"
|
mi.name = "Mesh"
|
||||||
@@ -163,6 +167,7 @@ static func _build_material(
|
|||||||
combiner["vertex_shader_id"] = _m2_vertex_shader_id(texture_count, int(batch.get("shader_id", 0)))
|
combiner["vertex_shader_id"] = _m2_vertex_shader_id(texture_count, int(batch.get("shader_id", 0)))
|
||||||
combiner["pixel_shader_id"] = _m2_pixel_shader_id(texture_count, int(batch.get("shader_id", 0)))
|
combiner["pixel_shader_id"] = _m2_pixel_shader_id(texture_count, int(batch.get("shader_id", 0)))
|
||||||
combiner["priority_plane"] = int(batch.get("priority_plane", 0))
|
combiner["priority_plane"] = int(batch.get("priority_plane", 0))
|
||||||
|
combiner["billboard_enabled"] = bool(batch.get("has_billboard", false)) or int(batch.get("billboard_vertex_count", 0)) > 0
|
||||||
if _is_soft_waterfall_material(model_path, blend_mode, combiner):
|
if _is_soft_waterfall_material(model_path, blend_mode, combiner):
|
||||||
combiner["effect_alpha_scale"] = 1.12
|
combiner["effect_alpha_scale"] = 1.12
|
||||||
combiner["effect_alpha_power"] = 0.92
|
combiner["effect_alpha_power"] = 0.92
|
||||||
@@ -187,9 +192,19 @@ static func _build_material(
|
|||||||
mat.set_meta("wow_m2_vertex_shader_id", int(combiner.get("vertex_shader_id", 0)))
|
mat.set_meta("wow_m2_vertex_shader_id", int(combiner.get("vertex_shader_id", 0)))
|
||||||
mat.set_meta("wow_m2_pixel_shader_id", int(combiner.get("pixel_shader_id", 0)))
|
mat.set_meta("wow_m2_pixel_shader_id", int(combiner.get("pixel_shader_id", 0)))
|
||||||
mat.set_meta("wow_priority_plane", int(batch.get("priority_plane", 0)))
|
mat.set_meta("wow_priority_plane", int(batch.get("priority_plane", 0)))
|
||||||
|
mat.set_meta("wow_m2_billboard_enabled", bool(combiner.get("billboard_enabled", false)))
|
||||||
|
mat.set_meta("wow_m2_billboard_bone_flags", int(batch.get("billboard_bone_flags", 0)))
|
||||||
return mat
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
static func _apply_billboard_custom_data(arrays: Array, batch: Dictionary, vertex_count: int) -> int:
|
||||||
|
var billboard_data: PackedFloat32Array = batch.get("billboard_data", PackedFloat32Array())
|
||||||
|
if billboard_data.size() != vertex_count * 4:
|
||||||
|
return 0
|
||||||
|
arrays[Mesh.ARRAY_CUSTOM0] = billboard_data
|
||||||
|
return int(Mesh.ARRAY_CUSTOM_RGBA_FLOAT) << int(Mesh.ARRAY_FORMAT_CUSTOM0_SHIFT)
|
||||||
|
|
||||||
|
|
||||||
static func _is_soft_waterfall_material(model_path: String, blend_mode: int, combiner: Dictionary) -> bool:
|
static func _is_soft_waterfall_material(model_path: String, blend_mode: int, combiner: Dictionary) -> bool:
|
||||||
if blend_mode != 2:
|
if blend_mode != 2:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ static func _build_mesh(data: Dictionary, surfaces: Array, extracted_dir: String
|
|||||||
arrays[Mesh.ARRAY_TEX_UV2] = uvs2
|
arrays[Mesh.ARRAY_TEX_UV2] = uvs2
|
||||||
arrays[Mesh.ARRAY_INDEX] = indices
|
arrays[Mesh.ARRAY_INDEX] = indices
|
||||||
|
|
||||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
var surface_flags := M2_BUILDER_SCRIPT._apply_billboard_custom_data(arrays, surface, verts.size())
|
||||||
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays, [], {}, surface_flags)
|
||||||
var surface_index := mesh.get_surface_count() - 1
|
var surface_index := mesh.get_surface_count() - 1
|
||||||
var material_id := int(surface.get("material_id", -1))
|
var material_id := int(surface.get("material_id", -1))
|
||||||
var texture_combo_index := int(surface.get("texture_combo_index", -1))
|
var texture_combo_index := int(surface.get("texture_combo_index", -1))
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ static func build(
|
|||||||
mat.set_shader_parameter("effect_alpha_scale", combiner.get("effect_alpha_scale", 1.0))
|
mat.set_shader_parameter("effect_alpha_scale", combiner.get("effect_alpha_scale", 1.0))
|
||||||
mat.set_shader_parameter("effect_alpha_power", combiner.get("effect_alpha_power", 1.0))
|
mat.set_shader_parameter("effect_alpha_power", combiner.get("effect_alpha_power", 1.0))
|
||||||
mat.set_shader_parameter("uv_edge_fade_strength", combiner.get("uv_edge_fade_strength", 0.0))
|
mat.set_shader_parameter("uv_edge_fade_strength", combiner.get("uv_edge_fade_strength", 0.0))
|
||||||
|
mat.set_shader_parameter("billboard_enabled", bool(combiner.get("billboard_enabled", false)))
|
||||||
mat.set_meta("wow_flags", material_flags)
|
mat.set_meta("wow_flags", material_flags)
|
||||||
mat.set_meta("wow_blend_mode", blend_mode)
|
mat.set_meta("wow_blend_mode", blend_mode)
|
||||||
mat.set_meta("wow_priority_plane", int(combiner.get("priority_plane", 0)))
|
mat.set_meta("wow_priority_plane", int(combiner.get("priority_plane", 0)))
|
||||||
@@ -94,6 +95,7 @@ static func build(
|
|||||||
mat.set_meta("wow_texture2_flags", texture2_flags)
|
mat.set_meta("wow_texture2_flags", texture2_flags)
|
||||||
mat.set_meta("wow_texture3_flags", texture3_flags)
|
mat.set_meta("wow_texture3_flags", texture3_flags)
|
||||||
mat.set_meta("wow_texture4_flags", texture4_flags)
|
mat.set_meta("wow_texture4_flags", texture4_flags)
|
||||||
|
mat.set_meta("wow_m2_billboard_enabled", bool(combiner.get("billboard_enabled", false)))
|
||||||
return mat
|
return mat
|
||||||
|
|
||||||
|
|
||||||
@@ -228,6 +230,7 @@ uniform vec2 stage3_uv_scroll = vec2(0.0);
|
|||||||
uniform float effect_alpha_scale = 1.0;
|
uniform float effect_alpha_scale = 1.0;
|
||||||
uniform float effect_alpha_power = 1.0;
|
uniform float effect_alpha_power = 1.0;
|
||||||
uniform float uv_edge_fade_strength = 0.0;
|
uniform float uv_edge_fade_strength = 0.0;
|
||||||
|
uniform bool billboard_enabled = false;
|
||||||
|
|
||||||
global uniform vec4 wow_ambient_color;
|
global uniform vec4 wow_ambient_color;
|
||||||
global uniform vec4 wow_light_color;
|
global uniform vec4 wow_light_color;
|
||||||
@@ -244,8 +247,58 @@ varying vec2 uv_interp;
|
|||||||
varying vec2 uv2_interp;
|
varying vec2 uv2_interp;
|
||||||
varying float edge_fade;
|
varying float edge_fade;
|
||||||
|
|
||||||
|
vec3 safe_normalize(vec3 value, vec3 fallback) {
|
||||||
|
float len = length(value);
|
||||||
|
if (len <= 0.00001) {
|
||||||
|
return normalize(fallback);
|
||||||
|
}
|
||||||
|
return value / len;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 apply_m2_billboard(vec3 vertex, vec3 normal, vec4 custom0, mat4 model_matrix, mat3 model_normal_matrix, mat4 inv_view_matrix, out vec3 billboard_world_normal) {
|
||||||
|
billboard_world_normal = safe_normalize(model_normal_matrix * normal, normal);
|
||||||
|
if (!billboard_enabled) {
|
||||||
|
return vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
int mode = int(custom0.w + 0.5);
|
||||||
|
if (mode <= 0) {
|
||||||
|
return vertex;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 pivot = custom0.xyz;
|
||||||
|
vec3 offset = vertex - pivot;
|
||||||
|
vec3 camera_right = safe_normalize(inv_view_matrix[0].xyz, vec3(1.0, 0.0, 0.0));
|
||||||
|
vec3 camera_up = safe_normalize(inv_view_matrix[1].xyz, vec3(0.0, 1.0, 0.0));
|
||||||
|
vec3 camera_back = safe_normalize(inv_view_matrix[2].xyz, vec3(0.0, 0.0, 1.0));
|
||||||
|
vec3 world_x = camera_right;
|
||||||
|
vec3 world_y = camera_up;
|
||||||
|
vec3 world_z = camera_back;
|
||||||
|
|
||||||
|
if (mode == 2) {
|
||||||
|
world_x = safe_normalize((model_matrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz, camera_right);
|
||||||
|
world_y = safe_normalize(camera_up - world_x * dot(camera_up, world_x), (model_matrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz);
|
||||||
|
world_z = safe_normalize(cross(world_x, world_y), camera_back);
|
||||||
|
} else if (mode == 3) {
|
||||||
|
world_z = safe_normalize((model_matrix * vec4(0.0, 0.0, 1.0, 0.0)).xyz, camera_back);
|
||||||
|
world_x = safe_normalize(camera_right - world_z * dot(camera_right, world_z), (model_matrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz);
|
||||||
|
world_y = safe_normalize(cross(world_z, world_x), camera_up);
|
||||||
|
} else if (mode == 4) {
|
||||||
|
world_y = safe_normalize((model_matrix * vec4(0.0, 1.0, 0.0, 0.0)).xyz, camera_up);
|
||||||
|
world_x = safe_normalize(cross(world_y, camera_back), (model_matrix * vec4(1.0, 0.0, 0.0, 0.0)).xyz);
|
||||||
|
world_z = safe_normalize(cross(world_x, world_y), camera_back);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 world_pivot = (model_matrix * vec4(pivot, 1.0)).xyz;
|
||||||
|
vec3 billboard_world_pos = world_pivot + world_x * offset.x + world_y * offset.y + world_z * offset.z;
|
||||||
|
billboard_world_normal = world_z;
|
||||||
|
return (inverse(model_matrix) * vec4(billboard_world_pos, 1.0)).xyz;
|
||||||
|
}
|
||||||
|
|
||||||
void vertex() {
|
void vertex() {
|
||||||
world_normal = normalize(MODEL_NORMAL_MATRIX * NORMAL);
|
vec3 billboard_normal;
|
||||||
|
VERTEX = apply_m2_billboard(VERTEX, NORMAL, CUSTOM0, MODEL_MATRIX, MODEL_NORMAL_MATRIX, INV_VIEW_MATRIX, billboard_normal);
|
||||||
|
world_normal = billboard_normal;
|
||||||
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
view_pos = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
view_pos = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
uv_interp = UV;
|
uv_interp = UV;
|
||||||
|
|||||||
@@ -236,6 +236,15 @@ struct SkinTextureUnit {
|
|||||||
|
|
||||||
static constexpr uint32_t MAGIC_MD20 = 0x3032444D; // 'MD20' LE
|
static constexpr uint32_t MAGIC_MD20 = 0x3032444D; // 'MD20' LE
|
||||||
static constexpr uint32_t MAGIC_SKIN = 0x4E494B53; // 'SKIN' LE
|
static constexpr uint32_t MAGIC_SKIN = 0x4E494B53; // 'SKIN' LE
|
||||||
|
static constexpr uint32_t M2_BONE_FLAG_SPHERICAL_BILLBOARD = 0x8;
|
||||||
|
static constexpr uint32_t M2_BONE_FLAG_CYLINDRICAL_BILLBOARD_LOCK_X = 0x10;
|
||||||
|
static constexpr uint32_t M2_BONE_FLAG_CYLINDRICAL_BILLBOARD_LOCK_Y = 0x20;
|
||||||
|
static constexpr uint32_t M2_BONE_FLAG_CYLINDRICAL_BILLBOARD_LOCK_Z = 0x40;
|
||||||
|
static constexpr uint32_t M2_BONE_BILLBOARD_MASK =
|
||||||
|
M2_BONE_FLAG_SPHERICAL_BILLBOARD
|
||||||
|
| M2_BONE_FLAG_CYLINDRICAL_BILLBOARD_LOCK_X
|
||||||
|
| M2_BONE_FLAG_CYLINDRICAL_BILLBOARD_LOCK_Y
|
||||||
|
| M2_BONE_FLAG_CYLINDRICAL_BILLBOARD_LOCK_Z;
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Helpers
|
// Helpers
|
||||||
@@ -275,6 +284,89 @@ static Vector3 wow_vec3_to_godot(float x, float y, float z) {
|
|||||||
return Vector3(x, z, -y);
|
return Vector3(x, z, -y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int billboard_mode_from_bone_flags(uint32_t flags) {
|
||||||
|
if (flags & M2_BONE_FLAG_SPHERICAL_BILLBOARD) return 1;
|
||||||
|
if (flags & M2_BONE_FLAG_CYLINDRICAL_BILLBOARD_LOCK_X) return 2;
|
||||||
|
if (flags & M2_BONE_FLAG_CYLINDRICAL_BILLBOARD_LOCK_Y) return 3;
|
||||||
|
if (flags & M2_BONE_FLAG_CYLINDRICAL_BILLBOARD_LOCK_Z) return 4;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool resolve_vertex_billboard(
|
||||||
|
const M2Vertex *verts,
|
||||||
|
uint32_t vertex_count,
|
||||||
|
uint16_t vertex_lookup,
|
||||||
|
uint16_t global_vertex_index,
|
||||||
|
const uint8_t *skin_bones,
|
||||||
|
uint32_t skin_property_count,
|
||||||
|
const uint16_t *bone_combo_arr,
|
||||||
|
uint32_t bone_combo_count,
|
||||||
|
uint16_t bone_start,
|
||||||
|
const M2CompBone *bones,
|
||||||
|
uint32_t bone_count,
|
||||||
|
Vector3 &pivot_out,
|
||||||
|
int &mode_out,
|
||||||
|
uint32_t &flags_out) {
|
||||||
|
if (!verts || !bones || global_vertex_index >= vertex_count) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const M2Vertex &vertex = verts[global_vertex_index];
|
||||||
|
bool all_weights_zero = true;
|
||||||
|
for (uint32_t i = 0; i < 4; ++i) {
|
||||||
|
if (vertex.boneWeights[i] > 0) {
|
||||||
|
all_weights_zero = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int best_bone = -1;
|
||||||
|
uint32_t best_flags = 0;
|
||||||
|
uint8_t best_weight = 0;
|
||||||
|
for (uint32_t i = 0; i < 4; ++i) {
|
||||||
|
uint8_t weight = vertex.boneWeights[i];
|
||||||
|
if (!all_weights_zero && weight == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int local_bone = (int)vertex.boneIndices[i];
|
||||||
|
if (skin_bones && vertex_lookup < skin_property_count) {
|
||||||
|
local_bone = (int)skin_bones[(uint32_t)vertex_lookup * 4 + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
int global_bone = local_bone;
|
||||||
|
uint32_t lookup_index = (uint32_t)bone_start + (uint32_t)std::max(local_bone, 0);
|
||||||
|
if (bone_combo_arr && lookup_index < bone_combo_count) {
|
||||||
|
global_bone = (int)bone_combo_arr[lookup_index];
|
||||||
|
}
|
||||||
|
if (global_bone < 0 || (uint32_t)global_bone >= bone_count) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t flags = bones[global_bone].flags;
|
||||||
|
if ((flags & M2_BONE_BILLBOARD_MASK) == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (best_bone < 0 || weight > best_weight) {
|
||||||
|
best_bone = global_bone;
|
||||||
|
best_flags = flags;
|
||||||
|
best_weight = weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (best_bone < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mode_out = billboard_mode_from_bone_flags(best_flags);
|
||||||
|
if (mode_out <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pivot_out = wow_vec3_to_godot(bones[best_bone].pivot);
|
||||||
|
flags_out = best_flags;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static Vector4 normalize_quat(float x, float y, float z, float w) {
|
static Vector4 normalize_quat(float x, float y, float z, float w) {
|
||||||
float len = std::sqrt(x * x + y * y + z * z + w * w);
|
float len = std::sqrt(x * x + y * y + z * z + w * w);
|
||||||
if (len <= 0.000001f) {
|
if (len <= 0.000001f) {
|
||||||
@@ -643,6 +735,7 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
PackedInt32Array bone_lookup_table;
|
PackedInt32Array bone_lookup_table;
|
||||||
Array batches;
|
Array batches;
|
||||||
Array animated_surfaces;
|
Array animated_surfaces;
|
||||||
|
const M2CompBone *bones = safe_array<M2CompBone>(buf, hdr.ofsBones, hdr.nBones);
|
||||||
const uint16_t *bone_combo_arr = safe_array<uint16_t>(buf, hdr.ofsBoneCombos, hdr.nBoneCombos);
|
const uint16_t *bone_combo_arr = safe_array<uint16_t>(buf, hdr.ofsBoneCombos, hdr.nBoneCombos);
|
||||||
if (bone_combo_arr) {
|
if (bone_combo_arr) {
|
||||||
bone_lookup_table.resize(hdr.nBoneCombos);
|
bone_lookup_table.resize(hdr.nBoneCombos);
|
||||||
@@ -691,6 +784,59 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
int idx_count = (int)indices.size() - idx_start;
|
int idx_count = (int)indices.size() - idx_start;
|
||||||
if (idx_count <= 0) continue;
|
if (idx_count <= 0) continue;
|
||||||
|
|
||||||
|
PackedFloat32Array billboard_data;
|
||||||
|
uint32_t billboard_vertex_count = 0;
|
||||||
|
uint32_t billboard_flags_seen = 0;
|
||||||
|
if (verts && bones) {
|
||||||
|
for (uint32_t t = 0; t + 2 < tri_count; t += 3) {
|
||||||
|
uint16_t tri_local[3] = {
|
||||||
|
skin_tri[tri_start + t],
|
||||||
|
skin_tri[tri_start + t + 2],
|
||||||
|
skin_tri[tri_start + t + 1],
|
||||||
|
};
|
||||||
|
for (uint32_t corner = 0; corner < 3; ++corner) {
|
||||||
|
uint16_t vertex_lookup = tri_local[corner];
|
||||||
|
if (vertex_lookup >= skin.nIndices) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint16_t global_vertex_index = skin_idx[vertex_lookup];
|
||||||
|
Vector3 pivot;
|
||||||
|
int mode = 0;
|
||||||
|
uint32_t bone_flags = 0;
|
||||||
|
if (!resolve_vertex_billboard(
|
||||||
|
verts,
|
||||||
|
hdr.nVertices,
|
||||||
|
vertex_lookup,
|
||||||
|
global_vertex_index,
|
||||||
|
skin_bones,
|
||||||
|
skin.nProperties,
|
||||||
|
bone_combo_arr,
|
||||||
|
hdr.nBoneCombos,
|
||||||
|
sm.boneStart,
|
||||||
|
bones,
|
||||||
|
hdr.nBones,
|
||||||
|
pivot,
|
||||||
|
mode,
|
||||||
|
bone_flags)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (billboard_data.is_empty()) {
|
||||||
|
billboard_data.resize(hdr.nVertices * 4);
|
||||||
|
}
|
||||||
|
uint32_t dst = (uint32_t)global_vertex_index * 4;
|
||||||
|
if (dst + 3 >= (uint32_t)billboard_data.size()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
billboard_data[dst] = pivot.x;
|
||||||
|
billboard_data[dst + 1] = pivot.y;
|
||||||
|
billboard_data[dst + 2] = pivot.z;
|
||||||
|
billboard_data[dst + 3] = (float)mode;
|
||||||
|
billboard_vertex_count++;
|
||||||
|
billboard_flags_seen |= (bone_flags & M2_BONE_BILLBOARD_MASK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Dictionary batch;
|
Dictionary batch;
|
||||||
batch["index_start"] = idx_start;
|
batch["index_start"] = idx_start;
|
||||||
batch["index_count"] = idx_count;
|
batch["index_count"] = idx_count;
|
||||||
@@ -708,12 +854,19 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
batch["skin_flags2"] = (int)tu[u].flags2;
|
batch["skin_flags2"] = (int)tu[u].flags2;
|
||||||
batch["bone_count"] = (int)sm.boneCount;
|
batch["bone_count"] = (int)sm.boneCount;
|
||||||
batch["bone_combo_index"] = (int)sm.boneStart;
|
batch["bone_combo_index"] = (int)sm.boneStart;
|
||||||
|
if (billboard_vertex_count > 0) {
|
||||||
|
batch["has_billboard"] = true;
|
||||||
|
batch["billboard_vertex_count"] = (int)billboard_vertex_count;
|
||||||
|
batch["billboard_bone_flags"] = (int)billboard_flags_seen;
|
||||||
|
batch["billboard_data"] = billboard_data;
|
||||||
|
}
|
||||||
batches.push_back(batch);
|
batches.push_back(batch);
|
||||||
|
|
||||||
if (include_animation && verts) {
|
if (include_animation && verts) {
|
||||||
PackedVector3Array surface_vertices;
|
PackedVector3Array surface_vertices;
|
||||||
PackedVector3Array surface_normals;
|
PackedVector3Array surface_normals;
|
||||||
PackedVector2Array surface_uvs, surface_uvs2;
|
PackedVector2Array surface_uvs, surface_uvs2;
|
||||||
|
PackedFloat32Array surface_billboard_data;
|
||||||
PackedInt32Array surface_bones;
|
PackedInt32Array surface_bones;
|
||||||
PackedFloat32Array surface_weights;
|
PackedFloat32Array surface_weights;
|
||||||
PackedInt32Array surface_indices;
|
PackedInt32Array surface_indices;
|
||||||
@@ -725,6 +878,8 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
surface_bones.resize(tri_count * 4);
|
surface_bones.resize(tri_count * 4);
|
||||||
surface_weights.resize(tri_count * 4);
|
surface_weights.resize(tri_count * 4);
|
||||||
surface_indices.resize(tri_count);
|
surface_indices.resize(tri_count);
|
||||||
|
uint32_t surface_billboard_vertex_count = 0;
|
||||||
|
uint32_t surface_billboard_flags_seen = 0;
|
||||||
|
|
||||||
for (uint32_t t = 0; t + 2 < tri_count; t += 3) {
|
for (uint32_t t = 0; t + 2 < tri_count; t += 3) {
|
||||||
uint16_t tri_local[3] = {
|
uint16_t tri_local[3] = {
|
||||||
@@ -748,6 +903,36 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
surface_uvs2[expanded_count] = Vector2(v.texCoords2[0], v.texCoords2[1]);
|
surface_uvs2[expanded_count] = Vector2(v.texCoords2[0], v.texCoords2[1]);
|
||||||
surface_indices[expanded_count] = (int)expanded_count;
|
surface_indices[expanded_count] = (int)expanded_count;
|
||||||
|
|
||||||
|
Vector3 billboard_pivot;
|
||||||
|
int billboard_mode = 0;
|
||||||
|
uint32_t billboard_bone_flags = 0;
|
||||||
|
if (resolve_vertex_billboard(
|
||||||
|
verts,
|
||||||
|
hdr.nVertices,
|
||||||
|
vertex_lookup,
|
||||||
|
global_vertex_index,
|
||||||
|
skin_bones,
|
||||||
|
skin.nProperties,
|
||||||
|
bone_combo_arr,
|
||||||
|
hdr.nBoneCombos,
|
||||||
|
sm.boneStart,
|
||||||
|
bones,
|
||||||
|
hdr.nBones,
|
||||||
|
billboard_pivot,
|
||||||
|
billboard_mode,
|
||||||
|
billboard_bone_flags)) {
|
||||||
|
if (surface_billboard_data.is_empty()) {
|
||||||
|
surface_billboard_data.resize(tri_count * 4);
|
||||||
|
}
|
||||||
|
uint32_t dst = expanded_count * 4;
|
||||||
|
surface_billboard_data[dst] = billboard_pivot.x;
|
||||||
|
surface_billboard_data[dst + 1] = billboard_pivot.y;
|
||||||
|
surface_billboard_data[dst + 2] = billboard_pivot.z;
|
||||||
|
surface_billboard_data[dst + 3] = (float)billboard_mode;
|
||||||
|
surface_billboard_vertex_count++;
|
||||||
|
surface_billboard_flags_seen |= (billboard_bone_flags & M2_BONE_BILLBOARD_MASK);
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t weight_sum = 0;
|
uint32_t weight_sum = 0;
|
||||||
for (uint32_t j = 0; j < 4; ++j) {
|
for (uint32_t j = 0; j < 4; ++j) {
|
||||||
weight_sum += (uint32_t)v.boneWeights[j];
|
weight_sum += (uint32_t)v.boneWeights[j];
|
||||||
@@ -786,6 +971,13 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
surface["normals"] = surface_normals;
|
surface["normals"] = surface_normals;
|
||||||
surface["uvs"] = surface_uvs;
|
surface["uvs"] = surface_uvs;
|
||||||
surface["uvs2"] = surface_uvs2;
|
surface["uvs2"] = surface_uvs2;
|
||||||
|
if (surface_billboard_vertex_count > 0) {
|
||||||
|
surface_billboard_data.resize(expanded_count * 4);
|
||||||
|
surface["has_billboard"] = true;
|
||||||
|
surface["billboard_vertex_count"] = (int)surface_billboard_vertex_count;
|
||||||
|
surface["billboard_bone_flags"] = (int)surface_billboard_flags_seen;
|
||||||
|
surface["billboard_data"] = surface_billboard_data;
|
||||||
|
}
|
||||||
surface["bones"] = surface_bones;
|
surface["bones"] = surface_bones;
|
||||||
surface["weights"] = surface_weights;
|
surface["weights"] = surface_weights;
|
||||||
surface["indices"] = surface_indices;
|
surface["indices"] = surface_indices;
|
||||||
@@ -822,7 +1014,6 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
uint32_t animation_activity_score = 0;
|
uint32_t animation_activity_score = 0;
|
||||||
if (include_animation) {
|
if (include_animation) {
|
||||||
const M2Sequence *seqs = safe_array<M2Sequence>(buf, hdr.ofsAnimations, hdr.nAnimations);
|
const M2Sequence *seqs = safe_array<M2Sequence>(buf, hdr.ofsAnimations, hdr.nAnimations);
|
||||||
const M2CompBone *bones = safe_array<M2CompBone>(buf, hdr.ofsBones, hdr.nBones);
|
|
||||||
if (seqs && hdr.nAnimations > 0) {
|
if (seqs && hdr.nAnimations > 0) {
|
||||||
std::string lower_path = path;
|
std::string lower_path = path;
|
||||||
std::transform(lower_path.begin(), lower_path.end(), lower_path.begin(), [](unsigned char c) { return (char)std::tolower(c); });
|
std::transform(lower_path.begin(), lower_path.end(), lower_path.begin(), [](unsigned char c) { return (char)std::tolower(c); });
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native
|
|||||||
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 4
|
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 4
|
||||||
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
|
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
|
||||||
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
|
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
|
||||||
|
const M2_MATERIAL_REFRESH_VERSION := 1
|
||||||
const WMO_MATERIAL_REFRESH_VERSION := 3
|
const WMO_MATERIAL_REFRESH_VERSION := 3
|
||||||
|
|
||||||
const TILE_SIZE := 533.33333
|
const TILE_SIZE := 533.33333
|
||||||
@@ -4400,9 +4401,46 @@ func _request_m2_mesh_load(normalized_rel: String) -> void:
|
|||||||
func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
|
func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
|
||||||
if mesh == null:
|
if mesh == null:
|
||||||
return null
|
return null
|
||||||
|
if int(mesh.get_meta("wow_m2_material_refresh_version", 0)) >= M2_MATERIAL_REFRESH_VERSION:
|
||||||
|
return mesh
|
||||||
|
var rebuilt := _rebuild_m2_mesh_from_raw(normalized_rel)
|
||||||
|
if rebuilt != null:
|
||||||
|
return rebuilt
|
||||||
|
mesh.set_meta("wow_m2_material_refresh_version", M2_MATERIAL_REFRESH_VERSION)
|
||||||
return mesh
|
return mesh
|
||||||
|
|
||||||
|
|
||||||
|
func _prepare_m2_node_for_runtime(normalized_rel: String, root: Node3D) -> void:
|
||||||
|
if root == null:
|
||||||
|
return
|
||||||
|
for mesh_instance in _find_mesh_instances_recursive(root):
|
||||||
|
if mesh_instance == null or mesh_instance.mesh == null:
|
||||||
|
continue
|
||||||
|
var prepared := _prepare_m2_mesh_for_runtime(normalized_rel, mesh_instance.mesh)
|
||||||
|
if prepared != null:
|
||||||
|
mesh_instance.mesh = prepared
|
||||||
|
|
||||||
|
|
||||||
|
func _rebuild_m2_mesh_from_raw(normalized_rel: String) -> Mesh:
|
||||||
|
if normalized_rel.is_empty() or not ClassDB.class_exists("M2Loader"):
|
||||||
|
return null
|
||||||
|
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel))
|
||||||
|
if not FileAccess.file_exists(abs_path):
|
||||||
|
return null
|
||||||
|
var loader = ClassDB.instantiate("M2Loader")
|
||||||
|
if loader == null:
|
||||||
|
return null
|
||||||
|
var data: Dictionary = loader.call("load_m2", abs_path)
|
||||||
|
if data.is_empty():
|
||||||
|
return null
|
||||||
|
var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir)
|
||||||
|
if prototype == null:
|
||||||
|
return null
|
||||||
|
var rebuilt := _find_first_mesh_recursive(prototype)
|
||||||
|
prototype.free()
|
||||||
|
return rebuilt
|
||||||
|
|
||||||
|
|
||||||
func _extract_first_mesh_from_m2_resource(resource: Resource) -> Mesh:
|
func _extract_first_mesh_from_m2_resource(resource: Resource) -> Mesh:
|
||||||
if resource is Mesh:
|
if resource is Mesh:
|
||||||
return resource as Mesh
|
return resource as Mesh
|
||||||
@@ -4572,7 +4610,9 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
|
|||||||
if normalized_rel.is_empty():
|
if normalized_rel.is_empty():
|
||||||
return null
|
return null
|
||||||
if _m2_scene_cache.has(normalized_rel):
|
if _m2_scene_cache.has(normalized_rel):
|
||||||
return _m2_scene_cache[normalized_rel]
|
var cached: Node3D = _m2_scene_cache[normalized_rel]
|
||||||
|
_prepare_m2_node_for_runtime(normalized_rel, cached)
|
||||||
|
return cached
|
||||||
if _m2_missing_cache.has(normalized_rel):
|
if _m2_missing_cache.has(normalized_rel):
|
||||||
return null
|
return null
|
||||||
|
|
||||||
@@ -4586,6 +4626,7 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
|
|||||||
if resource is PackedScene:
|
if resource is PackedScene:
|
||||||
var node = (resource as PackedScene).instantiate()
|
var node = (resource as PackedScene).instantiate()
|
||||||
if node is Node3D:
|
if node is Node3D:
|
||||||
|
_prepare_m2_node_for_runtime(normalized_rel, node as Node3D)
|
||||||
_m2_scene_cache[normalized_rel] = node as Node3D
|
_m2_scene_cache[normalized_rel] = node as Node3D
|
||||||
return node as Node3D
|
return node as Node3D
|
||||||
break
|
break
|
||||||
@@ -4768,7 +4809,9 @@ func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
|
|||||||
if normalized_rel.is_empty():
|
if normalized_rel.is_empty():
|
||||||
return null
|
return null
|
||||||
if _m2_scene_cache.has(normalized_rel):
|
if _m2_scene_cache.has(normalized_rel):
|
||||||
return _m2_scene_cache[normalized_rel]
|
var cached: Node3D = _m2_scene_cache[normalized_rel]
|
||||||
|
_prepare_m2_node_for_runtime(normalized_rel, cached)
|
||||||
|
return cached
|
||||||
|
|
||||||
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn"]):
|
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn"]):
|
||||||
if not ResourceLoader.exists(cache_res_path):
|
if not ResourceLoader.exists(cache_res_path):
|
||||||
@@ -4777,6 +4820,7 @@ func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
|
|||||||
if resource is PackedScene:
|
if resource is PackedScene:
|
||||||
var node = (resource as PackedScene).instantiate()
|
var node = (resource as PackedScene).instantiate()
|
||||||
if node is Node3D:
|
if node is Node3D:
|
||||||
|
_prepare_m2_node_for_runtime(normalized_rel, node as Node3D)
|
||||||
_m2_scene_cache[normalized_rel] = node as Node3D
|
_m2_scene_cache[normalized_rel] = node as Node3D
|
||||||
return node as Node3D
|
return node as Node3D
|
||||||
return null
|
return null
|
||||||
|
|||||||
Reference in New Issue
Block a user