шейдеры билбордов

This commit is contained in:
2026-07-07 11:33:57 +04:00
parent 44614a79e4
commit f561e52aa9
6 changed files with 320 additions and 6 deletions
+192 -1
View File
@@ -236,6 +236,15 @@ struct SkinTextureUnit {
static constexpr uint32_t MAGIC_MD20 = 0x3032444D; // 'MD20' 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
@@ -275,6 +284,89 @@ static Vector3 wow_vec3_to_godot(float x, float y, float z) {
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) {
float len = std::sqrt(x * x + y * y + z * z + w * w);
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;
Array batches;
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);
if (bone_combo_arr) {
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;
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;
batch["index_start"] = idx_start;
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["bone_count"] = (int)sm.boneCount;
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);
if (include_animation && verts) {
PackedVector3Array surface_vertices;
PackedVector3Array surface_normals;
PackedVector2Array surface_uvs, surface_uvs2;
PackedFloat32Array surface_billboard_data;
PackedInt32Array surface_bones;
PackedFloat32Array surface_weights;
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_weights.resize(tri_count * 4);
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) {
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_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;
for (uint32_t j = 0; j < 4; ++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["uvs"] = surface_uvs;
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["weights"] = surface_weights;
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;
if (include_animation) {
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) {
std::string lower_path = path;
std::transform(lower_path.begin(), lower_path.end(), lower_path.begin(), [](unsigned char c) { return (char)std::tolower(c); });