победа над водопадами, шейдеры m2
This commit is contained in:
@@ -243,6 +243,140 @@ src/tools/m2_to_gltf.py
|
|||||||
- WoW material flags не полностью совпадают;
|
- WoW material flags не полностью совпадают;
|
||||||
- billboards/leaf bones пока не как в клиенте.
|
- billboards/leaf bones пока не как в клиенте.
|
||||||
|
|
||||||
|
### M2 Animation Research
|
||||||
|
|
||||||
|
Проверка по `reference/whoa`, `reference/open-realm` и `reference/wow.export` показала, что анимации M2 нельзя надежно тащить как обычный Godot/glTF skeletal mesh.
|
||||||
|
|
||||||
|
Оригинальная схема M2:
|
||||||
|
|
||||||
|
- для каждой кости считается матрица через pivot transform: translation/rotation/scale вокруг `bone.pivot`;
|
||||||
|
- parent chain считается вручную;
|
||||||
|
- каждый skin section использует свой bone palette через `bone_combo_index`;
|
||||||
|
- vertex bone indices интерпретируются относительно palette текущего skin section.
|
||||||
|
|
||||||
|
Именно так делают:
|
||||||
|
|
||||||
|
- `reference/whoa/src/model/CM2Model.cpp` - расчет `m_boneMatrices`;
|
||||||
|
- `reference/whoa/src/model/CM2SceneRender.cpp` - upload bone palette из `boneCombos`;
|
||||||
|
- `reference/open-realm/games/world-of-warcraft/renderer/m2/r_m2.c` - `M2_CalculateBoneMatrices` и `M2_UploadBatchBones`;
|
||||||
|
- `reference/wow.export/src/js/3D/renderers/M2RendererGL.js` - ручной pivot-based runtime skinning.
|
||||||
|
|
||||||
|
Вывод: старый экспериментальный путь через cached GLB ломал композитные doodad M2, потому что анимировал node с bind-pivot translation напрямую. Конвертер переведен на схему `pivot node -> animated bone node`, аналогичную wow.export `modelsExportWithBonePrefix`.
|
||||||
|
|
||||||
|
Runtime принимает для M2-анимаций только GLB с меткой:
|
||||||
|
|
||||||
|
```text
|
||||||
|
asset.extras.openwc_m2_anim_schema = pivot_prefix_v1
|
||||||
|
```
|
||||||
|
|
||||||
|
Это защищает мир от старых битых GLB cache. Точечно пересобраны тестовые animated M2:
|
||||||
|
|
||||||
|
```text
|
||||||
|
data/cache/m2_glb/GryphonRoost01.glb
|
||||||
|
data/cache/m2_glb/Fish.glb
|
||||||
|
data/cache/m2_glb/eagle.glb
|
||||||
|
```
|
||||||
|
|
||||||
|
Ограничение runtime:
|
||||||
|
|
||||||
|
```text
|
||||||
|
enable_m2_animated_instances = true
|
||||||
|
m2_animated_max_primitives = 3
|
||||||
|
m2_animated_denylist_patterns = ["gryphonroost"]
|
||||||
|
m2_animated_allowlist_patterns = ["creature/fish/", "creature/eagle/", "world/critter/"]
|
||||||
|
```
|
||||||
|
|
||||||
|
`GryphonRoost` stays out of the animated GLB path. The cached GLB has `Stand`
|
||||||
|
tracks, but the current converter/runtime breaks its composite skin/material layout.
|
||||||
|
Use the native M2 runtime path for this class of doodad; the old GLB animation
|
||||||
|
path remains disabled for `gryphonroost`.
|
||||||
|
|
||||||
|
Native M2 animation first pass for composite doodads:
|
||||||
|
|
||||||
|
- `M2Loader.load_m2_animated()` parses M2 bones, Stand track keys, `.skin` bone lookup/palette data, and expanded skinned surfaces.
|
||||||
|
- `M2NativeAnimatedBuilder` builds a regular `ArrayMesh` and keeps the original per-surface skin data.
|
||||||
|
- `M2NativeAnimator` evaluates the selected Stand sequence and applies WoW-style bone matrices:
|
||||||
|
`T(pivot + translation) * R * S * T(-pivot) * parent`.
|
||||||
|
- Vertex influences are resolved through `.skin` local bone indices and the M2 `boneCombos` palette, matching the original section/batch renderer model used by WoW/whoa.
|
||||||
|
- `StreamingWorldLoader` routes only `gryphonroost` through this native path; simple critters still use the existing GLB allowlist and most M2 world props stay on static MultiMesh.
|
||||||
|
- Current implementation rebuilds the animated `ArrayMesh` on CPU. This is correct enough for the problematic rare doodads and gives us the same data layout that can later move to shader/GPU skinning.
|
||||||
|
|
||||||
|
Причина: композитные doodad вроде `GryphonRoost` снова ломают визуал при GLB-анимации. До M2-native renderer все world placement M2 должны оставаться статическими. GLB-анимация оставлена только как вручную включаемый debug experiment через allowlist.
|
||||||
|
|
||||||
|
Дополнительно static M2 cache lookup ищет both original-case and lowercase paths. Это важно для ADT paths вроде `world/GENERIC/...`, потому что запеченный nested cache лежит lowercase. Static path также не использует GLB с `pivot_prefix_v1`, чтобы animated test cache не мог подменить корректный `.tscn`.
|
||||||
|
|
||||||
|
Полноценный M2-native animation renderer/cache все еще остается правильным долгосрочным решением для 1:1 поведения, particles/ribbons и производительности на большом количестве animated props.
|
||||||
|
|
||||||
|
### Blizzlike Shader Research
|
||||||
|
|
||||||
|
Идея для следующего visual fidelity слоя: уходить от Godot `StandardMaterial3D` для WoW assets к собственным WoW-style `ShaderMaterial`.
|
||||||
|
|
||||||
|
Что видно по whoa:
|
||||||
|
|
||||||
|
- M2 использует маленькие shader permutations вроде `Diffuse_T1`, `Diffuse_T2`, `Diffuse_Env`, `Combiners_Mod`, `Combiners_Add`, `Combiners_Mod2x`;
|
||||||
|
- material state задается отдельно: blend mode, alpha ref, culling, depth test/write;
|
||||||
|
- light model старый и ручной: ambient + diffuse + emissive + fog constants, а не PBR;
|
||||||
|
- texture stages учитывают wrap/clamp, texture transform matrices и env/sphere map.
|
||||||
|
|
||||||
|
Практический вывод для проекта:
|
||||||
|
|
||||||
|
- terrain уже ближе к WoW, потому что использует `ShaderMaterial` и ручной light/fog;
|
||||||
|
- M2/WMO пока используют `StandardMaterial3D`, поэтому выглядят не совсем blizzlike;
|
||||||
|
- следующий шаг - `wow_m2_material` / `wow_wmo_material` с WoW blend modes, alpha ref, combiners и ручным fog/light.
|
||||||
|
|
||||||
|
### M2 ShaderMaterial First Pass
|
||||||
|
|
||||||
|
Implemented file: `addons/mpq_extractor/loaders/wow_m2_material.gd`.
|
||||||
|
|
||||||
|
`M2Builder._build_material()` now returns a WoW-style `ShaderMaterial` instead of `StandardMaterial3D`. The same path is reused by `M2NativeAnimatedBuilder`, so static M2 batches and the native animated `gryphonroost` path share material behavior.
|
||||||
|
|
||||||
|
Current M2 shader layer:
|
||||||
|
|
||||||
|
- maps M2 blend modes 0..6 to Godot shader variants: opaque, alpha-key, alpha blend, additive, mod and mod2x approximation;
|
||||||
|
- uses alpha reference values observed in `reference/whoa/src/model/CM2SceneRender.cpp`: 0 for opaque, about 0.878 for alpha-key, 1/255 for blended passes;
|
||||||
|
- applies M2 render flags for unlit (`0x01`), two-sided/culling (`0x04`), depth test (`0x08`) and depth write (`0x10`);
|
||||||
|
- keeps BLP mipmap generation and anisotropic sampler hints;
|
||||||
|
- respects M2 texture wrap flags by compiling repeat/clamp shader variants;
|
||||||
|
- `M2Loader` exposes UV2, `texture_coord_combos`, `texture_combiner_combos`, `m2_flags`, batch `shader_id`, `texture_count` and texture-coordinate combo indices;
|
||||||
|
- `M2Loader` also exposes `m2_colors`, `texture_weights`, `texture_transforms`, `texture_weight_combos` and `texture_transform_combos`;
|
||||||
|
- `M2Builder` now resolves the first two texture stages from texture combos and passes combiner state into `WowM2Material`;
|
||||||
|
- `WowM2Material` has a first-pass implementation for combiner ops: opaque, mod, decal/fade, add, mod2x, mod2xNA/addNA and a cheap env-map UV approximation;
|
||||||
|
- static M2 color/alpha tracks and texture weight tracks are applied to `mesh_color`, matching the `whoa` render path where batch alpha is multiplied by color alpha and texture weight;
|
||||||
|
- static texture transform translation/scale is applied per texture stage through shader UV transform uniforms;
|
||||||
|
- animated texture transform translation tracks are approximated as GPU UV scroll (`translation_speed * TIME`) per texture stage, without per-frame CPU material updates;
|
||||||
|
- native animated M2 rebuild path preserves UV2 and the same material combiner state.
|
||||||
|
- uses a simple old-client-style ambient + diffuse light model and disables PBR/specular behavior.
|
||||||
|
|
||||||
|
Known limits:
|
||||||
|
|
||||||
|
- only the first two texture stages are represented;
|
||||||
|
- texture transform translation animation is supported as linear UV scroll; rotation/scale texture animation and time-varying color/weight tracks are still future work;
|
||||||
|
- exact env/sphere map math, model emissive tracks and full WoW render-state parity are still future work;
|
||||||
|
- WMO has a matching first-pass shader layer, but still needs full material flag parity.
|
||||||
|
|
||||||
|
### WMO ShaderMaterial First Pass
|
||||||
|
|
||||||
|
Implemented file: `addons/mpq_extractor/loaders/wow_wmo_material.gd`.
|
||||||
|
|
||||||
|
`WMOBuilder._build_material()` now returns a WoW-style `ShaderMaterial` instead of `StandardMaterial3D`. WMO group meshes already carry `Mesh.ARRAY_COLOR` from group vertex colors, so the shader treats WMO as mostly baked-lit geometry:
|
||||||
|
|
||||||
|
- blend modes 0..2 map to opaque, alpha-key and alpha blend variants;
|
||||||
|
- alpha-key uses discard instead of transparent sorting;
|
||||||
|
- texture repeat stays enabled for WMO wall/floor tiling;
|
||||||
|
- BLP mipmap generation and anisotropic sampler hints are preserved;
|
||||||
|
- shader is `unshaded` and multiplies texture color by WMO vertex color with a small ambient floor;
|
||||||
|
- DBC zone light is applied as a soft tint only, so baked WMO vertex colors and texture albedo stay readable;
|
||||||
|
- `color2` is passed as a secondary/detail tint, and `texture2` is used as a fallback detail texture when `texture1` is absent;
|
||||||
|
- `WMOLoader` exposes MOMT `diffuse_color`, `emissive_color`, `texture1`, `texture2`, `flags2` and `color2`;
|
||||||
|
- `WMOBuilder` loads `texture1` as a conservative secondary detail/multiply layer and passes MOMT diffuse/emissive colors into the shader;
|
||||||
|
- culling is disabled like the previous WMO material path, which avoids missing interior/backface geometry.
|
||||||
|
|
||||||
|
Known limits:
|
||||||
|
|
||||||
|
- deeper MOMT shader semantics, exact secondary texture mode and `texture2` usage are still approximate;
|
||||||
|
- WMO fog/light constants are not wired into this material yet;
|
||||||
|
- this first pass prioritizes matching old-client baked lighting behavior over physical lighting.
|
||||||
|
|
||||||
## WMO Rendering
|
## WMO Rendering
|
||||||
|
|
||||||
Цель: WMO не должны фризить сцену при догрузке, особенно большие здания/города.
|
Цель: WMO не должны фризить сцену при догрузке, особенно большие здания/города.
|
||||||
@@ -324,6 +458,31 @@ addons/mpq_extractor/loaders/wow_liquid_material.gd
|
|||||||
- LiquidType.dbc texture selection пока не используется;
|
- LiquidType.dbc texture selection пока не используется;
|
||||||
- waterfalls/flowing water требуют отдельного M2/material path.
|
- waterfalls/flowing water требуют отдельного M2/material path.
|
||||||
|
|
||||||
|
## Shared WoW Shader Globals
|
||||||
|
|
||||||
|
Current terrain/M2/WMO/liquid shaders are intentionally `unshaded`: Godot lighting is not a good match for old WoW material rules, M2 blend modes, WMO baked vertex colors and control-splat terrain. To keep them visually tied to the active zone without updating every material instance, `WowSkyController` owns a shared global shader state:
|
||||||
|
|
||||||
|
```text
|
||||||
|
wow_ambient_color
|
||||||
|
wow_light_color
|
||||||
|
wow_light_dir
|
||||||
|
wow_fog_color
|
||||||
|
wow_fog_range
|
||||||
|
wow_fog_density
|
||||||
|
wow_sun_elevation
|
||||||
|
```
|
||||||
|
|
||||||
|
These parameters are registered through `RenderingServer.global_shader_parameter_add` and updated from `Light.dbc` / `LightParams.dbc` sampling in `src/scenes/sky/wow_sky_controller.gd`.
|
||||||
|
|
||||||
|
Consumers:
|
||||||
|
|
||||||
|
- `addons/mpq_extractor/loaders/adt_builder.gd`: control-splat, fallback layered terrain and single-texture terrain.
|
||||||
|
- `addons/mpq_extractor/loaders/wow_m2_material.gd`: M2 combiner material uses zone ambient/diffuse light and distance fog.
|
||||||
|
- `addons/mpq_extractor/loaders/wow_wmo_material.gd`: WMO material keeps baked vertex lighting, but tints it by zone light and applies distance fog.
|
||||||
|
- `addons/mpq_extractor/loaders/wow_liquid_material.gd`: liquids use the shared fog color/range/density.
|
||||||
|
|
||||||
|
The fog distance is computed from view-space position passed from `vertex()` to `fragment()`. This avoids per-camera material updates and keeps shader code compatible with Godot 4.6's spatial shader parser.
|
||||||
|
|
||||||
## Sky And Lighting
|
## Sky And Lighting
|
||||||
|
|
||||||
Добавлен `WowSkyController`:
|
Добавлен `WowSkyController`:
|
||||||
@@ -451,7 +610,7 @@ res://data/cache/wmo_render_v1
|
|||||||
Baked terrain required: 4
|
Baked terrain required: 4
|
||||||
Splat terrain required: 1
|
Splat terrain required: 1
|
||||||
Control splat required: 3
|
Control splat required: 3
|
||||||
WMO streaming resource: 1
|
WMO streaming resource: 2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Debug Logs
|
## Debug Logs
|
||||||
@@ -532,7 +691,7 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
|
|
||||||
- M2 animation/skinning.
|
- M2 animation/skinning.
|
||||||
- M2 particles/ribbons.
|
- M2 particles/ribbons.
|
||||||
- Full WoW material flag parity.
|
- Full WoW material flag parity. Current first pass covers common M2 combiners and WMO shader ids 0, 3, 5, 6, 8, 9, 12, 13, 15, 16, 17 approximately.
|
||||||
- True WMO portal/room culling.
|
- True WMO portal/room culling.
|
||||||
- Selective cheap dynamic shadows.
|
- Selective cheap dynamic shadows.
|
||||||
- Liquid depth fade/shore blending.
|
- Liquid depth fade/shore blending.
|
||||||
@@ -540,6 +699,42 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
- Better skybox transition validation per zone.
|
- Better skybox transition validation per zone.
|
||||||
- Automated renderer regression scenes/screenshots.
|
- Automated renderer regression scenes/screenshots.
|
||||||
|
|
||||||
|
## 2026-07-04 WMO Shader Pass
|
||||||
|
|
||||||
|
- WMO materials now consume shared `wow_light_dir` and `wow_sun_elevation` shader globals.
|
||||||
|
- Cached WMO meshes are refreshed at attach time, so already baked render-cache meshes can receive the latest WMO material shader without immediately rebuilding the whole WMO cache.
|
||||||
|
- The WMO shader now uses normals for a cheap WoW-style ambient + directional lighting term instead of relying only on texture * vertex color.
|
||||||
|
- Added approximate pixel shader branches for common WMO modes: diffuse, env, env metal, two-layer diffuse, terrain-style two-layer, diffuse emissive and mod2x variants.
|
||||||
|
- Still missing for true parity: second/third WMO vertex color streams, extra WMO texture coordinate sets, third+ texture samplers, exact env-map coordinate generation and portal/room visibility.
|
||||||
|
|
||||||
|
## 2026-07-04 M2 Shader Mapping Pass
|
||||||
|
|
||||||
|
- M2 material build now maps raw skin `shaderID` through the same vertex/pixel shader selection rules used by `wow.export`.
|
||||||
|
- `WowM2Material` receives `vertex_shader_id` and `pixel_shader_id` uniforms instead of relying only on generic two-stage ops.
|
||||||
|
- Vertex shader ids 0..18 now drive UV source selection, UV2 use, environment coordinates and edge-fade modes.
|
||||||
|
- Pixel shader ids 0..29 have first-pass two-texture combiner behavior for the common opaque/mod/add/mod2x/alpha cases.
|
||||||
|
- M2 materials now load and bind up to four texture combo entries. This enables first-pass support for glow/specular/crossfade/guild/const combiner cases that need `tex3` or `tex4`.
|
||||||
|
- Texture weight combos are exposed to the shader as `tex_sample_alpha`, matching the common WoW combiner weight slots.
|
||||||
|
- Native animated M2 surfaces use the same `_build_material()` path, so this also applies to runtime-animated M2 objects.
|
||||||
|
- Debug metadata is stored on materials: raw `wow_m2_shader_id`, mapped `wow_m2_vertex_shader_id`, and mapped `wow_m2_pixel_shader_id`.
|
||||||
|
- Transparent M2 passes such as waterfalls now avoid depth writes/prepass (`depth_draw_never`). This prevents layered alpha planes from self-occluding into white/solid webbing while keeping cutout foliage on the opaque-depth path.
|
||||||
|
- Scrolling alpha-blend M2 models with `waterfall` in the path get a conservative soft-alpha profile: lower opacity, stronger alpha falloff and UV-edge fade. This hides the rectangular card edges without changing normal foliage/cutout props.
|
||||||
|
- `.skin` texture unit `priority` is exposed as `priority_plane` for future transparent-pass ordering. This needs the native GDExtension DLL to be rebuilt when the editor is not holding it open.
|
||||||
|
- Waterfalls seen near terrain cliffs are usually WMO embedded doodads rather than ADT `MDDF` placements. WMO doodad transforms now use the converted MODD quaternion directly, without the old extra `+90 deg` M2 yaw offset.
|
||||||
|
- WMO render/scene caches now carry version guards for this transform path. Rebuild `wmo_render_v1`/`wmo_tscn` after changing WMO doodad placement code; old caches are ignored by runtime.
|
||||||
|
- Elwynn/Thunder Falls style outdoor waterfalls are ADT `MDDF` M2 placements. Regular ADT M2 doodads use the native cached Godot-space Euler directly via `Basis.from_euler(rot)`; do not apply a global yaw/order remap, because it flips ordinary props such as wagons, stalls, barrels, fences and trees. `ElwynnCliffRock01/02` are a model-specific exception: these are open-backed shell meshes, so they use a narrow `YZX` placement correction with `rot.y + 180 deg` to push the hollow side into the terrain. The waterfall transform correction is also filename-allowlisted to the actual sheet M2s (`NewWaterfall`, `ElwynnTallWaterfall01`). Sheet M2s keep the calibrated world-space `+90 deg` Godot-Y yaw to anchor the top to the upper pool, plus model-specific twist where needed. Tall waterfall sheets twist around their local fall line with an origin offset so top/bottom anchors stay fixed instead of rotating around the model pivot; `ElwynnTallWaterfall01` uses the opposite twist sign from `NewWaterfall` to face the correct side. Do not apply the raw-M2/WoWee render-space basis here; that double-converts M2 vertices. Do not cull boundary waterfall placements by `floor(world_pos / TILE_SIZE)`: tiles can carry a small cross-border placement that is still the active streamed instance at low M2 radius. Future cross-tile dedupe should use `MDDF.uniqueId` plus an ADT cache format bump/rebake.
|
||||||
|
- Still missing for true parity: exact shader array entries beyond the common WotLK set, particles/ribbons, full edge-scan tuning, exact guild/color constants and specialized blend-state edge cases.
|
||||||
|
|
||||||
|
## 2026-07-07 World Shader Light Stabilization
|
||||||
|
|
||||||
|
- DBC sky colors remain the source for `Environment` and sky rendering, but material shader globals now receive desaturated/neutralized ambient, diffuse and fog colors. This prevents Elwynn daylight profiles from tinting all ADT/M2/WMO albedo cyan or green.
|
||||||
|
- ADT terrain shaders now transform normals with `MODEL_NORMAL_MATRIX` before comparing them against the world-space `wow_light_dir`. Using fragment `NORMAL` directly made terrain lighting depend on the local surface basis and produced blue/green patches on slopes and roads.
|
||||||
|
- ADT, M2 and WMO material fog clamps `wow_fog_density` before mixing into `ALBEDO`; fog should add distance haze, not repaint nearby assets.
|
||||||
|
- M2/WMO shaders now reduce zone-color strength and clamp final albedo/specular/emissive contribution. Additive/parity edge cases still need exact WoW pass ordering later, but ordinary Elwynn props should no longer produce overexposed or acid-green surfaces.
|
||||||
|
- Human building WMO materials such as `HumanTwoStory` and `GoldshireInn` carry strong painted `MOMT.diffColor` values, including green trim colors. Textured WMO surfaces now use this as a soft hue tint instead of multiplying texture albedo by the raw RGB value; untextured WMO surfaces still use a muted material paint color.
|
||||||
|
- 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.
|
||||||
|
|
||||||
## 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.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -21,6 +21,10 @@ static var _baked_texture_material_cache: Dictionary = {}
|
|||||||
static var _alpha_texture_cache: Dictionary = {}
|
static var _alpha_texture_cache: Dictionary = {}
|
||||||
static var _layered_material_cache: Dictionary = {}
|
static var _layered_material_cache: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
static func _ensure_wow_shader_globals() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
## Build a Node3D with 256 terrain chunk meshes.
|
## Build a Node3D with 256 terrain chunk meshes.
|
||||||
## `extracted_dir` must be an absolute path to the extracted/ folder so BLPs can be loaded.
|
## `extracted_dir` must be an absolute path to the extracted/ folder so BLPs can be loaded.
|
||||||
static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
||||||
@@ -1253,6 +1257,7 @@ static func _get_control_splat_shader() -> Shader:
|
|||||||
if _control_splat_shader:
|
if _control_splat_shader:
|
||||||
return _control_splat_shader
|
return _control_splat_shader
|
||||||
|
|
||||||
|
_ensure_wow_shader_globals()
|
||||||
_control_splat_shader = Shader.new()
|
_control_splat_shader = Shader.new()
|
||||||
_control_splat_shader.code = """
|
_control_splat_shader.code = """
|
||||||
shader_type spatial;
|
shader_type spatial;
|
||||||
@@ -1269,10 +1274,34 @@ uniform float ambient = 0.62;
|
|||||||
uniform float diffuse = 0.38;
|
uniform float diffuse = 0.38;
|
||||||
uniform float hemi = 0.12;
|
uniform float hemi = 0.12;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
varying vec3 world_pos;
|
||||||
|
varying vec3 world_normal;
|
||||||
|
varying vec3 view_pos;
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
|
world_normal = normalize(MODEL_NORMAL_MATRIX * NORMAL);
|
||||||
|
view_pos = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
|
}
|
||||||
|
|
||||||
vec3 sample_layer(float layer, vec2 continuous_chunk_uv) {
|
vec3 sample_layer(float layer, vec2 continuous_chunk_uv) {
|
||||||
return texture(terrain_tex, vec3(continuous_chunk_uv * uv_scale, layer), mip_bias).rgb;
|
return texture(terrain_tex, vec3(continuous_chunk_uv * uv_scale, layer), mip_bias).rgb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec2 tile_uv = clamp(UV, vec2(0.0), vec2(0.999999));
|
vec2 tile_uv = clamp(UV, vec2(0.0), vec2(0.999999));
|
||||||
vec2 chunk_uv = tile_uv * 16.0;
|
vec2 chunk_uv = tile_uv * 16.0;
|
||||||
@@ -1300,15 +1329,18 @@ void fragment() {
|
|||||||
sample_layer(layers.z, chunk_uv) * w2 +
|
sample_layer(layers.z, chunk_uv) * w2 +
|
||||||
sample_layer(layers.w, chunk_uv) * w3;
|
sample_layer(layers.w, chunk_uv) * w3;
|
||||||
|
|
||||||
vec3 n = normalize(NORMAL);
|
vec3 n = normalize(world_normal);
|
||||||
if (!FRONT_FACING) {
|
if (!FRONT_FACING) {
|
||||||
n = -n;
|
n = -n;
|
||||||
}
|
}
|
||||||
vec3 l = normalize(light_dir);
|
vec3 l = normalize(wow_light_dir);
|
||||||
float ndl = max(dot(n, l), 0.0);
|
float ndl = max(dot(n, l), 0.0);
|
||||||
float hemi_term = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
|
float hemi_term = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
|
||||||
float light = ambient + diffuse * ndl + hemi * hemi_term;
|
vec3 ambient_tint = mix(vec3(0.74), max(wow_ambient_color.rgb, vec3(0.08)), 0.22);
|
||||||
ALBEDO = albedo * light;
|
vec3 light_tint = mix(vec3(1.0), wow_light_color.rgb, 0.12);
|
||||||
|
vec3 light = ambient_tint * ambient + light_tint * diffuse * ndl + vec3(hemi * hemi_term);
|
||||||
|
light = clamp(light, vec3(0.48), vec3(1.08));
|
||||||
|
ALBEDO = apply_wow_fog(albedo * light);
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
return _control_splat_shader
|
return _control_splat_shader
|
||||||
@@ -1465,6 +1497,7 @@ static func _get_terrain_shader() -> Shader:
|
|||||||
if _terrain_shader:
|
if _terrain_shader:
|
||||||
return _terrain_shader
|
return _terrain_shader
|
||||||
|
|
||||||
|
_ensure_wow_shader_globals()
|
||||||
_terrain_shader = Shader.new()
|
_terrain_shader = Shader.new()
|
||||||
_terrain_shader.code = """
|
_terrain_shader.code = """
|
||||||
shader_type spatial;
|
shader_type spatial;
|
||||||
@@ -1487,6 +1520,30 @@ uniform float ambient = 0.62;
|
|||||||
uniform float diffuse = 0.38;
|
uniform float diffuse = 0.38;
|
||||||
uniform float hemi = 0.12;
|
uniform float hemi = 0.12;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
varying vec3 world_pos;
|
||||||
|
varying vec3 world_normal;
|
||||||
|
varying vec3 view_pos;
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
|
world_normal = normalize(MODEL_NORMAL_MATRIX * NORMAL);
|
||||||
|
view_pos = (MODELVIEW_MATRIX * vec4(VERTEX, 1.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);
|
||||||
|
}
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec2 tiled_uv = UV * uv_scale;
|
vec2 tiled_uv = UV * uv_scale;
|
||||||
|
|
||||||
@@ -1516,15 +1573,18 @@ void fragment() {
|
|||||||
if (layer_count > 2) { albedo += c2 * w2; }
|
if (layer_count > 2) { albedo += c2 * w2; }
|
||||||
if (layer_count > 3) { albedo += c3 * w3; }
|
if (layer_count > 3) { albedo += c3 * w3; }
|
||||||
|
|
||||||
vec3 n = normalize(NORMAL);
|
vec3 n = normalize(world_normal);
|
||||||
if (!FRONT_FACING) {
|
if (!FRONT_FACING) {
|
||||||
n = -n;
|
n = -n;
|
||||||
}
|
}
|
||||||
vec3 l = normalize(light_dir);
|
vec3 l = normalize(wow_light_dir);
|
||||||
float ndl = max(dot(n, l), 0.0);
|
float ndl = max(dot(n, l), 0.0);
|
||||||
float hemi_term = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
|
float hemi_term = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
|
||||||
float light = ambient + diffuse * ndl + hemi * hemi_term;
|
vec3 ambient_tint = mix(vec3(0.74), max(wow_ambient_color.rgb, vec3(0.08)), 0.22);
|
||||||
ALBEDO = albedo.rgb * light;
|
vec3 light_tint = mix(vec3(1.0), wow_light_color.rgb, 0.12);
|
||||||
|
vec3 light = ambient_tint * ambient + light_tint * diffuse * ndl + vec3(hemi * hemi_term);
|
||||||
|
light = clamp(light, vec3(0.48), vec3(1.08));
|
||||||
|
ALBEDO = apply_wow_fog(albedo.rgb * light);
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
return _terrain_shader
|
return _terrain_shader
|
||||||
@@ -1534,6 +1594,7 @@ static func _get_single_texture_shader() -> Shader:
|
|||||||
if _single_texture_shader:
|
if _single_texture_shader:
|
||||||
return _single_texture_shader
|
return _single_texture_shader
|
||||||
|
|
||||||
|
_ensure_wow_shader_globals()
|
||||||
_single_texture_shader = Shader.new()
|
_single_texture_shader = Shader.new()
|
||||||
_single_texture_shader.code = """
|
_single_texture_shader.code = """
|
||||||
shader_type spatial;
|
shader_type spatial;
|
||||||
@@ -1548,6 +1609,23 @@ uniform float ambient = 0.62;
|
|||||||
uniform float diffuse = 0.38;
|
uniform float diffuse = 0.38;
|
||||||
uniform float hemi = 0.12;
|
uniform float hemi = 0.12;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
varying vec3 world_pos;
|
||||||
|
varying vec3 world_normal;
|
||||||
|
varying vec3 view_pos;
|
||||||
|
|
||||||
|
void vertex() {
|
||||||
|
world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
|
world_normal = normalize(MODEL_NORMAL_MATRIX * NORMAL);
|
||||||
|
view_pos = (MODELVIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
|
||||||
|
}
|
||||||
|
|
||||||
vec3 sample_sharp(sampler2D tex, vec2 uv) {
|
vec3 sample_sharp(sampler2D tex, vec2 uv) {
|
||||||
vec3 center = texture(tex, uv, mip_bias).rgb;
|
vec3 center = texture(tex, uv, mip_bias).rgb;
|
||||||
if (sharpen_strength <= 0.001) {
|
if (sharpen_strength <= 0.001) {
|
||||||
@@ -1564,16 +1642,26 @@ vec3 sample_sharp(sampler2D tex, vec2 uv) {
|
|||||||
return clamp(center * (1.0 + 4.0 * sharpen_strength) - side * sharpen_strength, vec3(0.0), vec3(1.0));
|
return clamp(center * (1.0 + 4.0 * sharpen_strength) - side * sharpen_strength, vec3(0.0), vec3(1.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
void fragment() {
|
void fragment() {
|
||||||
vec3 n = normalize(NORMAL);
|
vec3 n = normalize(world_normal);
|
||||||
if (!FRONT_FACING) {
|
if (!FRONT_FACING) {
|
||||||
n = -n;
|
n = -n;
|
||||||
}
|
}
|
||||||
vec3 l = normalize(light_dir);
|
vec3 l = normalize(wow_light_dir);
|
||||||
float ndl = max(dot(n, l), 0.0);
|
float ndl = max(dot(n, l), 0.0);
|
||||||
float hemi_term = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
|
float hemi_term = clamp(n.y * 0.5 + 0.5, 0.0, 1.0);
|
||||||
float light = ambient + diffuse * ndl + hemi * hemi_term;
|
vec3 ambient_tint = mix(vec3(0.74), max(wow_ambient_color.rgb, vec3(0.08)), 0.22);
|
||||||
ALBEDO = sample_sharp(tex0, UV * uv_scale) * light;
|
vec3 light_tint = mix(vec3(1.0), wow_light_color.rgb, 0.12);
|
||||||
|
vec3 light = ambient_tint * ambient + light_tint * diffuse * ndl + vec3(hemi * hemi_term);
|
||||||
|
light = clamp(light, vec3(0.48), vec3(1.08));
|
||||||
|
ALBEDO = apply_wow_fog(sample_sharp(tex0, UV * uv_scale) * light);
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
return _single_texture_shader
|
return _single_texture_shader
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
## add_child(node)
|
## add_child(node)
|
||||||
class_name M2Builder
|
class_name M2Builder
|
||||||
|
|
||||||
|
const WOW_M2_MATERIAL := preload("res://addons/mpq_extractor/loaders/wow_m2_material.gd")
|
||||||
|
|
||||||
static var _texture_cache: Dictionary = {}
|
static var _texture_cache: Dictionary = {}
|
||||||
|
|
||||||
static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
||||||
@@ -14,6 +16,7 @@ static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
|||||||
var verts: PackedVector3Array = data.get("vertices", PackedVector3Array())
|
var verts: PackedVector3Array = data.get("vertices", PackedVector3Array())
|
||||||
var normals: PackedVector3Array = data.get("normals", PackedVector3Array())
|
var normals: PackedVector3Array = data.get("normals", PackedVector3Array())
|
||||||
var uvs: PackedVector2Array = data.get("uvs", PackedVector2Array())
|
var uvs: PackedVector2Array = data.get("uvs", PackedVector2Array())
|
||||||
|
var uvs2: PackedVector2Array = data.get("uvs2", PackedVector2Array())
|
||||||
var indices: PackedInt32Array = data.get("indices", PackedInt32Array())
|
var indices: PackedInt32Array = data.get("indices", PackedInt32Array())
|
||||||
var batches: Array = data.get("batches", [])
|
var batches: Array = data.get("batches", [])
|
||||||
|
|
||||||
@@ -25,6 +28,14 @@ static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
|||||||
var texture_flags: PackedInt32Array = data.get("texture_flags", PackedInt32Array())
|
var texture_flags: PackedInt32Array = data.get("texture_flags", PackedInt32Array())
|
||||||
var materials: Array = data.get("materials", [])
|
var materials: Array = data.get("materials", [])
|
||||||
var tex_combos: PackedInt32Array = data.get("texture_combos", PackedInt32Array())
|
var tex_combos: PackedInt32Array = data.get("texture_combos", PackedInt32Array())
|
||||||
|
var tex_coord_combos: PackedInt32Array = data.get("texture_coord_combos", PackedInt32Array())
|
||||||
|
var tex_weight_combos: PackedInt32Array = data.get("texture_weight_combos", PackedInt32Array())
|
||||||
|
var tex_transform_combos: PackedInt32Array = data.get("texture_transform_combos", PackedInt32Array())
|
||||||
|
var tex_combiner_combos: PackedInt32Array = data.get("texture_combiner_combos", PackedInt32Array())
|
||||||
|
var m2_colors: Array = data.get("m2_colors", [])
|
||||||
|
var tex_weights: PackedFloat32Array = data.get("texture_weights", PackedFloat32Array())
|
||||||
|
var tex_transforms: Array = data.get("texture_transforms", [])
|
||||||
|
var m2_flags: int = int(data.get("m2_flags", 0))
|
||||||
var model_path: String = str(data.get("model_path", ""))
|
var model_path: String = str(data.get("model_path", ""))
|
||||||
|
|
||||||
var mesh := ArrayMesh.new()
|
var mesh := ArrayMesh.new()
|
||||||
@@ -52,6 +63,8 @@ static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
|||||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||||
if uvs.size() == verts.size():
|
if uvs.size() == verts.size():
|
||||||
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
||||||
|
if uvs2.size() == verts.size():
|
||||||
|
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)
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||||
@@ -67,8 +80,17 @@ static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
|||||||
texture_types,
|
texture_types,
|
||||||
texture_flags,
|
texture_flags,
|
||||||
tex_combos,
|
tex_combos,
|
||||||
|
tex_coord_combos,
|
||||||
|
tex_weight_combos,
|
||||||
|
tex_transform_combos,
|
||||||
|
tex_combiner_combos,
|
||||||
|
m2_colors,
|
||||||
|
tex_weights,
|
||||||
|
tex_transforms,
|
||||||
|
m2_flags,
|
||||||
extracted_dir,
|
extracted_dir,
|
||||||
model_path))
|
model_path,
|
||||||
|
batch))
|
||||||
|
|
||||||
if mesh.get_surface_count() == 0:
|
if mesh.get_surface_count() == 0:
|
||||||
return root
|
return root
|
||||||
@@ -87,50 +109,340 @@ static func _build_material(
|
|||||||
texture_types: PackedInt32Array,
|
texture_types: PackedInt32Array,
|
||||||
texture_flags: PackedInt32Array,
|
texture_flags: PackedInt32Array,
|
||||||
tex_combos: PackedInt32Array,
|
tex_combos: PackedInt32Array,
|
||||||
|
tex_coord_combos: PackedInt32Array,
|
||||||
|
tex_weight_combos: PackedInt32Array,
|
||||||
|
tex_transform_combos: PackedInt32Array,
|
||||||
|
tex_combiner_combos: PackedInt32Array,
|
||||||
|
m2_colors: Array,
|
||||||
|
tex_weights: PackedFloat32Array,
|
||||||
|
tex_transforms: Array,
|
||||||
|
m2_flags: int,
|
||||||
extracted_dir: String,
|
extracted_dir: String,
|
||||||
model_path: String) -> StandardMaterial3D:
|
model_path: String,
|
||||||
|
batch: Dictionary = {}) -> Material:
|
||||||
|
|
||||||
var mat := StandardMaterial3D.new()
|
|
||||||
var flags: int = int(mat_def.get("flags", 0))
|
var flags: int = int(mat_def.get("flags", 0))
|
||||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED if (flags & 0x04) != 0 else BaseMaterial3D.CULL_BACK
|
|
||||||
mat.roughness = 0.85
|
|
||||||
mat.metallic = 0.0
|
|
||||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED if (flags & 0x01) != 0 else BaseMaterial3D.SHADING_MODE_PER_PIXEL
|
|
||||||
mat.specular_mode = BaseMaterial3D.SPECULAR_DISABLED
|
|
||||||
mat.vertex_color_use_as_albedo = false
|
|
||||||
mat.set_flag(BaseMaterial3D.FLAG_USE_TEXTURE_REPEAT, false)
|
|
||||||
|
|
||||||
var blend_mode: int = mat_def.get("blend_mode", 0)
|
var blend_mode: int = mat_def.get("blend_mode", 0)
|
||||||
match blend_mode:
|
var tex_flags := 0
|
||||||
0:
|
var tex: Texture2D = null
|
||||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_DISABLED
|
var tex2: Texture2D = null
|
||||||
1:
|
var tex3: Texture2D = null
|
||||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
|
var tex4: Texture2D = null
|
||||||
mat.alpha_scissor_threshold = 0.5
|
var tex2_flags := 0
|
||||||
3, 4, 6:
|
var tex3_flags := 0
|
||||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
var tex4_flags := 0
|
||||||
mat.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
|
|
||||||
mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_OPAQUE_ONLY
|
|
||||||
_:
|
|
||||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
|
||||||
mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_OPAQUE_ONLY
|
|
||||||
|
|
||||||
if texture_combo_index >= 0 and texture_combo_index < tex_combos.size():
|
var resolved0 := _resolve_combo_texture(texture_combo_index, textures, texture_types, texture_flags, tex_combos, extracted_dir, model_path)
|
||||||
var tex_idx: int = tex_combos[texture_combo_index]
|
tex = resolved0.get("texture", null)
|
||||||
if tex_idx >= 0 and tex_idx < textures.size():
|
tex_flags = int(resolved0.get("flags", 0))
|
||||||
|
var texture_count := maxi(1, int(batch.get("texture_count", 1)))
|
||||||
|
if texture_count > 1:
|
||||||
|
var resolved1 := _resolve_combo_texture(texture_combo_index + 1, textures, texture_types, texture_flags, tex_combos, extracted_dir, model_path)
|
||||||
|
tex2 = resolved1.get("texture", null)
|
||||||
|
tex2_flags = int(resolved1.get("flags", 0))
|
||||||
|
if texture_count > 2:
|
||||||
|
var resolved2 := _resolve_combo_texture(texture_combo_index + 2, textures, texture_types, texture_flags, tex_combos, extracted_dir, model_path)
|
||||||
|
tex3 = resolved2.get("texture", null)
|
||||||
|
tex3_flags = int(resolved2.get("flags", 0))
|
||||||
|
if texture_count > 3:
|
||||||
|
var resolved3 := _resolve_combo_texture(texture_combo_index + 3, textures, texture_types, texture_flags, tex_combos, extracted_dir, model_path)
|
||||||
|
tex4 = resolved3.get("texture", null)
|
||||||
|
tex4_flags = int(resolved3.get("flags", 0))
|
||||||
|
|
||||||
|
var combiner := _resolve_combiner_state(blend_mode, texture_count, batch, tex_coord_combos, tex_combiner_combos, m2_flags)
|
||||||
|
combiner["mesh_color"] = _resolve_mesh_color(batch, m2_colors, tex_weights, tex_weight_combos)
|
||||||
|
combiner["tex_sample_alpha"] = _resolve_texture_sample_alpha(batch, tex_weights, tex_weight_combos)
|
||||||
|
combiner["stage0_uv_transform"] = _resolve_texture_transform(batch, 0, tex_transform_combos, tex_transforms)
|
||||||
|
combiner["stage1_uv_transform"] = _resolve_texture_transform(batch, 1, tex_transform_combos, tex_transforms)
|
||||||
|
combiner["stage2_uv_transform"] = _resolve_texture_transform(batch, 2, tex_transform_combos, tex_transforms)
|
||||||
|
combiner["stage3_uv_transform"] = _resolve_texture_transform(batch, 3, tex_transform_combos, tex_transforms)
|
||||||
|
combiner["stage0_uv_scroll"] = _resolve_texture_scroll(batch, 0, tex_transform_combos, tex_transforms)
|
||||||
|
combiner["stage1_uv_scroll"] = _resolve_texture_scroll(batch, 1, tex_transform_combos, tex_transforms)
|
||||||
|
combiner["stage2_uv_scroll"] = _resolve_texture_scroll(batch, 2, tex_transform_combos, tex_transforms)
|
||||||
|
combiner["stage3_uv_scroll"] = _resolve_texture_scroll(batch, 3, tex_transform_combos, tex_transforms)
|
||||||
|
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["priority_plane"] = int(batch.get("priority_plane", 0))
|
||||||
|
if _is_soft_waterfall_material(model_path, blend_mode, combiner):
|
||||||
|
combiner["effect_alpha_scale"] = 1.12
|
||||||
|
combiner["effect_alpha_power"] = 0.92
|
||||||
|
combiner["uv_edge_fade_strength"] = 0.22
|
||||||
|
|
||||||
|
var mat := WOW_M2_MATERIAL.build(
|
||||||
|
tex,
|
||||||
|
tex2,
|
||||||
|
tex3,
|
||||||
|
tex4,
|
||||||
|
flags,
|
||||||
|
blend_mode,
|
||||||
|
tex_flags,
|
||||||
|
tex2_flags,
|
||||||
|
tex3_flags,
|
||||||
|
tex4_flags,
|
||||||
|
model_path.get_file().get_basename(),
|
||||||
|
combiner)
|
||||||
|
mat.set_meta("wow_flags", flags)
|
||||||
|
mat.set_meta("wow_blend_mode", blend_mode)
|
||||||
|
mat.set_meta("wow_m2_shader_id", int(batch.get("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_priority_plane", int(batch.get("priority_plane", 0)))
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
static func _is_soft_waterfall_material(model_path: String, blend_mode: int, combiner: Dictionary) -> bool:
|
||||||
|
if blend_mode != 2:
|
||||||
|
return false
|
||||||
|
var lower_path := model_path.to_lower()
|
||||||
|
if not lower_path.contains("waterfall"):
|
||||||
|
return false
|
||||||
|
var scroll0: Vector2 = combiner.get("stage0_uv_scroll", Vector2.ZERO)
|
||||||
|
var scroll1: Vector2 = combiner.get("stage1_uv_scroll", Vector2.ZERO)
|
||||||
|
return scroll0.length_squared() > 0.000001 or scroll1.length_squared() > 0.000001
|
||||||
|
|
||||||
|
|
||||||
|
static func _m2_vertex_shader_id(texture_count: int, shader_id: int) -> int:
|
||||||
|
if (shader_id & 0x8000) != 0:
|
||||||
|
return _m2_shader_array_vertex_id(shader_id & 0x7fff)
|
||||||
|
if texture_count == 1:
|
||||||
|
if (shader_id & 0x80) != 0:
|
||||||
|
return 1 # Diffuse_Env
|
||||||
|
if (shader_id & 0x4000) != 0:
|
||||||
|
return 10 # Diffuse_T2
|
||||||
|
return 0 # Diffuse_T1
|
||||||
|
|
||||||
|
if (shader_id & 0x80) != 0:
|
||||||
|
if (shader_id & 0x8) != 0:
|
||||||
|
return 5 # Diffuse_Env_Env
|
||||||
|
return 4 # Diffuse_Env_T1
|
||||||
|
if (shader_id & 0x8) != 0:
|
||||||
|
return 3 # Diffuse_T1_Env
|
||||||
|
if (shader_id & 0x4000) != 0:
|
||||||
|
return 2 # Diffuse_T1_T2
|
||||||
|
return 7 # Diffuse_T1_T1
|
||||||
|
|
||||||
|
|
||||||
|
static func _m2_pixel_shader_id(texture_count: int, shader_id: int) -> int:
|
||||||
|
if (shader_id & 0x8000) != 0:
|
||||||
|
return _m2_shader_array_pixel_id(shader_id & 0x7fff)
|
||||||
|
if texture_count == 1:
|
||||||
|
return 1 if (shader_id & 0x70) != 0 else 0
|
||||||
|
|
||||||
|
if (shader_id & 0x70) != 0:
|
||||||
|
match shader_id & 7:
|
||||||
|
3:
|
||||||
|
return 8 # Combiners_Mod_Add
|
||||||
|
4:
|
||||||
|
return 7 # Combiners_Mod_Mod2x
|
||||||
|
6:
|
||||||
|
return 9 # Combiners_Mod_Mod2xNA
|
||||||
|
7:
|
||||||
|
return 10 # Combiners_Mod_AddNA
|
||||||
|
_:
|
||||||
|
return 6 # Combiners_Mod_Mod
|
||||||
|
|
||||||
|
match shader_id & 7:
|
||||||
|
0:
|
||||||
|
return 5 # Combiners_Opaque_Opaque
|
||||||
|
3, 7:
|
||||||
|
return 13 # Combiners_Opaque_AddAlpha
|
||||||
|
4:
|
||||||
|
return 3 # Combiners_Opaque_Mod2x
|
||||||
|
6:
|
||||||
|
return 4 # Combiners_Opaque_Mod2xNA
|
||||||
|
_:
|
||||||
|
return 2 # Combiners_Opaque_Mod
|
||||||
|
|
||||||
|
|
||||||
|
static func _m2_shader_array_vertex_id(shader_array_index: int) -> int:
|
||||||
|
match shader_array_index:
|
||||||
|
0:
|
||||||
|
return 8
|
||||||
|
1:
|
||||||
|
return 0
|
||||||
|
2:
|
||||||
|
return 1
|
||||||
|
3:
|
||||||
|
return 1
|
||||||
|
4:
|
||||||
|
return 1
|
||||||
|
5:
|
||||||
|
return 1
|
||||||
|
6:
|
||||||
|
return 0
|
||||||
|
7:
|
||||||
|
return 12
|
||||||
|
8:
|
||||||
|
return 9
|
||||||
|
9:
|
||||||
|
return 12
|
||||||
|
_:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
static func _m2_shader_array_pixel_id(shader_array_index: int) -> int:
|
||||||
|
match shader_array_index:
|
||||||
|
0:
|
||||||
|
return 35
|
||||||
|
1:
|
||||||
|
return 0
|
||||||
|
2:
|
||||||
|
return 7
|
||||||
|
3:
|
||||||
|
return 1
|
||||||
|
4:
|
||||||
|
return 6
|
||||||
|
5:
|
||||||
|
return 4
|
||||||
|
6:
|
||||||
|
return 0
|
||||||
|
7:
|
||||||
|
return 7
|
||||||
|
8:
|
||||||
|
return 1
|
||||||
|
9:
|
||||||
|
return 6
|
||||||
|
_:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
static func _resolve_combo_texture(
|
||||||
|
combo_index: int,
|
||||||
|
textures: PackedStringArray,
|
||||||
|
texture_types: PackedInt32Array,
|
||||||
|
texture_flags: PackedInt32Array,
|
||||||
|
tex_combos: PackedInt32Array,
|
||||||
|
extracted_dir: String,
|
||||||
|
model_path: String) -> Dictionary:
|
||||||
|
|
||||||
|
if combo_index < 0 or combo_index >= tex_combos.size():
|
||||||
|
return {}
|
||||||
|
var tex_idx: int = tex_combos[combo_index]
|
||||||
|
if tex_idx < 0 or tex_idx >= textures.size():
|
||||||
|
return {}
|
||||||
var tex_path: String = str(textures[tex_idx]).replace("\\", "/")
|
var tex_path: String = str(textures[tex_idx]).replace("\\", "/")
|
||||||
var tex_type := int(texture_types[tex_idx]) if tex_idx < texture_types.size() else 0
|
var tex_type := int(texture_types[tex_idx]) if tex_idx < texture_types.size() else 0
|
||||||
var tex_flags := int(texture_flags[tex_idx]) if tex_idx < texture_flags.size() else 0
|
var tex_flags := int(texture_flags[tex_idx]) if tex_idx < texture_flags.size() else 0
|
||||||
if tex_path.is_empty():
|
if tex_path.is_empty():
|
||||||
tex_path = _default_texture_path(model_path, tex_type)
|
tex_path = _default_texture_path(model_path, tex_type)
|
||||||
|
var tex: Texture2D = null
|
||||||
if not tex_path.is_empty():
|
if not tex_path.is_empty():
|
||||||
var tex := _load_texture(tex_path, extracted_dir)
|
tex = _load_texture(tex_path, extracted_dir)
|
||||||
if tex:
|
return {
|
||||||
mat.albedo_texture = tex
|
"texture": tex,
|
||||||
mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC
|
"flags": tex_flags,
|
||||||
mat.set_flag(BaseMaterial3D.FLAG_USE_TEXTURE_REPEAT, (tex_flags & 0x03) != 0)
|
"path": tex_path,
|
||||||
|
}
|
||||||
|
|
||||||
return mat
|
|
||||||
|
static func _resolve_combiner_state(
|
||||||
|
blend_mode: int,
|
||||||
|
texture_count: int,
|
||||||
|
batch: Dictionary,
|
||||||
|
tex_coord_combos: PackedInt32Array,
|
||||||
|
tex_combiner_combos: PackedInt32Array,
|
||||||
|
m2_flags: int) -> Dictionary:
|
||||||
|
|
||||||
|
var state := {
|
||||||
|
"op0": 0 if blend_mode == 0 else 1,
|
||||||
|
"op1": 1,
|
||||||
|
"env0": false,
|
||||||
|
"env1": false,
|
||||||
|
"use_uv2_0": false,
|
||||||
|
"use_uv2_1": false,
|
||||||
|
}
|
||||||
|
var shader_id := int(batch.get("shader_id", 0))
|
||||||
|
var coord_index := int(batch.get("texture_coord_combo_index", 0))
|
||||||
|
if (m2_flags & 0x8) != 0:
|
||||||
|
for texture_index in mini(texture_count, 2):
|
||||||
|
var op := 0 if texture_index == 0 and blend_mode == 0 else 1
|
||||||
|
var combiner_index := shader_id + texture_index
|
||||||
|
if combiner_index >= 0 and combiner_index < tex_combiner_combos.size():
|
||||||
|
op = int(tex_combiner_combos[combiner_index])
|
||||||
|
state["op%d" % texture_index] = op
|
||||||
|
_apply_texture_coord_state(state, texture_index, coord_index + texture_index, tex_coord_combos)
|
||||||
|
else:
|
||||||
|
_apply_texture_coord_state(state, 0, coord_index, tex_coord_combos)
|
||||||
|
return state
|
||||||
|
|
||||||
|
|
||||||
|
static func _apply_texture_coord_state(state: Dictionary, texture_index: int, combo_index: int, tex_coord_combos: PackedInt32Array) -> void:
|
||||||
|
if combo_index < 0 or combo_index >= tex_coord_combos.size():
|
||||||
|
return
|
||||||
|
var coord := int(tex_coord_combos[combo_index])
|
||||||
|
state["use_uv2_%d" % texture_index] = coord == 1
|
||||||
|
state["env%d" % texture_index] = coord > 2
|
||||||
|
|
||||||
|
|
||||||
|
static func _resolve_mesh_color(
|
||||||
|
batch: Dictionary,
|
||||||
|
m2_colors: Array,
|
||||||
|
tex_weights: PackedFloat32Array,
|
||||||
|
tex_weight_combos: PackedInt32Array) -> Color:
|
||||||
|
|
||||||
|
var color := Color.WHITE
|
||||||
|
var color_index := int(batch.get("color_index", -1))
|
||||||
|
if color_index >= 0 and color_index < m2_colors.size():
|
||||||
|
var color_entry: Dictionary = m2_colors[color_index] if m2_colors[color_index] is Dictionary else {}
|
||||||
|
color = color_entry.get("color", Color.WHITE)
|
||||||
|
|
||||||
|
var weight_combo_index := int(batch.get("texture_weight_combo_index", -1))
|
||||||
|
if weight_combo_index >= 0 and weight_combo_index < tex_weight_combos.size():
|
||||||
|
var weight_index := int(tex_weight_combos[weight_combo_index])
|
||||||
|
if weight_index >= 0 and weight_index < tex_weights.size():
|
||||||
|
color.a *= clampf(tex_weights[weight_index], 0.0, 1.0)
|
||||||
|
return color
|
||||||
|
|
||||||
|
|
||||||
|
static func _resolve_texture_sample_alpha(
|
||||||
|
batch: Dictionary,
|
||||||
|
tex_weights: PackedFloat32Array,
|
||||||
|
tex_weight_combos: PackedInt32Array) -> Vector3:
|
||||||
|
|
||||||
|
var result := Vector3.ONE
|
||||||
|
var weight_combo_index := int(batch.get("texture_weight_combo_index", -1))
|
||||||
|
for stage in 3:
|
||||||
|
var combo_index := weight_combo_index + stage
|
||||||
|
if combo_index < 0 or combo_index >= tex_weight_combos.size():
|
||||||
|
continue
|
||||||
|
var weight_index := int(tex_weight_combos[combo_index])
|
||||||
|
if weight_index >= 0 and weight_index < tex_weights.size():
|
||||||
|
result[stage] = clampf(tex_weights[weight_index], 0.0, 1.0)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
static func _resolve_texture_transform(
|
||||||
|
batch: Dictionary,
|
||||||
|
stage: int,
|
||||||
|
tex_transform_combos: PackedInt32Array,
|
||||||
|
tex_transforms: Array) -> Vector4:
|
||||||
|
|
||||||
|
var combo_index := int(batch.get("texture_transform_combo_index", -1)) + stage
|
||||||
|
if combo_index < 0 or combo_index >= tex_transform_combos.size():
|
||||||
|
return Vector4(1.0, 1.0, 0.0, 0.0)
|
||||||
|
var transform_index := int(tex_transform_combos[combo_index])
|
||||||
|
if transform_index < 0 or transform_index >= tex_transforms.size():
|
||||||
|
return Vector4(1.0, 1.0, 0.0, 0.0)
|
||||||
|
var transform: Dictionary = tex_transforms[transform_index] if tex_transforms[transform_index] is Dictionary else {}
|
||||||
|
var scale: Vector2 = transform.get("scale", Vector2.ONE)
|
||||||
|
var translation: Vector2 = transform.get("translation", Vector2.ZERO)
|
||||||
|
return Vector4(scale.x, scale.y, translation.x, translation.y)
|
||||||
|
|
||||||
|
|
||||||
|
static func _resolve_texture_scroll(
|
||||||
|
batch: Dictionary,
|
||||||
|
stage: int,
|
||||||
|
tex_transform_combos: PackedInt32Array,
|
||||||
|
tex_transforms: Array) -> Vector2:
|
||||||
|
|
||||||
|
var combo_index := int(batch.get("texture_transform_combo_index", -1)) + stage
|
||||||
|
if combo_index < 0 or combo_index >= tex_transform_combos.size():
|
||||||
|
return Vector2.ZERO
|
||||||
|
var transform_index := int(tex_transform_combos[combo_index])
|
||||||
|
if transform_index < 0 or transform_index >= tex_transforms.size():
|
||||||
|
return Vector2.ZERO
|
||||||
|
var transform: Dictionary = tex_transforms[transform_index] if tex_transforms[transform_index] is Dictionary else {}
|
||||||
|
return transform.get("translation_speed", Vector2.ZERO)
|
||||||
|
|
||||||
|
|
||||||
static func _default_texture_path(model_path: String, texture_type: int) -> String:
|
static func _default_texture_path(model_path: String, texture_type: int) -> String:
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
## Builds a runtime-animated M2 node from native M2Loader data.
|
||||||
|
## M2 skinning is evaluated explicitly because WoW's pivot/palette model does
|
||||||
|
## not map cleanly to Godot Skeleton3D/Skin for composite doodads.
|
||||||
|
class_name M2NativeAnimatedBuilder
|
||||||
|
|
||||||
|
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||||
|
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
|
||||||
|
|
||||||
|
|
||||||
|
static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
||||||
|
var root := Node3D.new()
|
||||||
|
root.name = "M2NativeAnimated"
|
||||||
|
|
||||||
|
var bones: Array = data.get("bones", [])
|
||||||
|
var surfaces: Array = data.get("animated_surfaces", [])
|
||||||
|
if bones.is_empty() or surfaces.is_empty():
|
||||||
|
return root
|
||||||
|
|
||||||
|
var mesh := _build_mesh(data, surfaces, extracted_dir)
|
||||||
|
if mesh == null or mesh.get_surface_count() <= 0:
|
||||||
|
return root
|
||||||
|
|
||||||
|
var mesh_instance := MeshInstance3D.new()
|
||||||
|
mesh_instance.name = "Mesh"
|
||||||
|
mesh_instance.mesh = mesh
|
||||||
|
root.add_child(mesh_instance)
|
||||||
|
|
||||||
|
var animator = M2_NATIVE_ANIMATOR_SCRIPT.new()
|
||||||
|
animator.name = "M2NativeAnimator"
|
||||||
|
root.add_child(animator)
|
||||||
|
animator.setup(mesh_instance, bones, surfaces, float(data.get("animation_length", 0.0)))
|
||||||
|
return root
|
||||||
|
|
||||||
|
|
||||||
|
static func _build_mesh(data: Dictionary, surfaces: Array, extracted_dir: String) -> ArrayMesh:
|
||||||
|
var mesh := ArrayMesh.new()
|
||||||
|
var textures: PackedStringArray = data.get("textures", PackedStringArray())
|
||||||
|
var texture_types: PackedInt32Array = data.get("texture_types", PackedInt32Array())
|
||||||
|
var texture_flags: PackedInt32Array = data.get("texture_flags", PackedInt32Array())
|
||||||
|
var materials: Array = data.get("materials", [])
|
||||||
|
var tex_combos: PackedInt32Array = data.get("texture_combos", PackedInt32Array())
|
||||||
|
var tex_coord_combos: PackedInt32Array = data.get("texture_coord_combos", PackedInt32Array())
|
||||||
|
var tex_weight_combos: PackedInt32Array = data.get("texture_weight_combos", PackedInt32Array())
|
||||||
|
var tex_transform_combos: PackedInt32Array = data.get("texture_transform_combos", PackedInt32Array())
|
||||||
|
var tex_combiner_combos: PackedInt32Array = data.get("texture_combiner_combos", PackedInt32Array())
|
||||||
|
var m2_colors: Array = data.get("m2_colors", [])
|
||||||
|
var tex_weights: PackedFloat32Array = data.get("texture_weights", PackedFloat32Array())
|
||||||
|
var tex_transforms: Array = data.get("texture_transforms", [])
|
||||||
|
var m2_flags: int = int(data.get("m2_flags", 0))
|
||||||
|
var model_path := String(data.get("model_path", ""))
|
||||||
|
|
||||||
|
for surface_variant in surfaces:
|
||||||
|
if not (surface_variant is Dictionary):
|
||||||
|
continue
|
||||||
|
var surface: Dictionary = surface_variant
|
||||||
|
var verts: PackedVector3Array = surface.get("vertices", PackedVector3Array())
|
||||||
|
var indices: PackedInt32Array = surface.get("indices", PackedInt32Array())
|
||||||
|
if verts.is_empty() or indices.is_empty():
|
||||||
|
continue
|
||||||
|
|
||||||
|
var arrays := []
|
||||||
|
arrays.resize(Mesh.ARRAY_MAX)
|
||||||
|
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||||
|
var normals: PackedVector3Array = surface.get("normals", PackedVector3Array())
|
||||||
|
if normals.size() == verts.size():
|
||||||
|
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||||
|
var uvs: PackedVector2Array = surface.get("uvs", PackedVector2Array())
|
||||||
|
if uvs.size() == verts.size():
|
||||||
|
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
||||||
|
var uvs2: PackedVector2Array = surface.get("uvs2", PackedVector2Array())
|
||||||
|
if uvs2.size() == verts.size():
|
||||||
|
arrays[Mesh.ARRAY_TEX_UV2] = uvs2
|
||||||
|
arrays[Mesh.ARRAY_INDEX] = indices
|
||||||
|
|
||||||
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||||
|
var surface_index := mesh.get_surface_count() - 1
|
||||||
|
var material_id := int(surface.get("material_id", -1))
|
||||||
|
var texture_combo_index := int(surface.get("texture_combo_index", -1))
|
||||||
|
var mat_def: Dictionary = materials[material_id] if material_id >= 0 and material_id < materials.size() else {}
|
||||||
|
mesh.surface_set_material(
|
||||||
|
surface_index,
|
||||||
|
M2_BUILDER_SCRIPT._build_material(
|
||||||
|
mat_def,
|
||||||
|
texture_combo_index,
|
||||||
|
textures,
|
||||||
|
texture_types,
|
||||||
|
texture_flags,
|
||||||
|
tex_combos,
|
||||||
|
tex_coord_combos,
|
||||||
|
tex_weight_combos,
|
||||||
|
tex_transform_combos,
|
||||||
|
tex_combiner_combos,
|
||||||
|
m2_colors,
|
||||||
|
tex_weights,
|
||||||
|
tex_transforms,
|
||||||
|
m2_flags,
|
||||||
|
extracted_dir,
|
||||||
|
model_path,
|
||||||
|
surface))
|
||||||
|
|
||||||
|
return mesh
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dngobeqkb20pc
|
||||||
@@ -7,7 +7,9 @@ class_name WMOBuilder
|
|||||||
|
|
||||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||||
const WOW_LIQUID_MATERIAL := preload("res://addons/mpq_extractor/loaders/wow_liquid_material.gd")
|
const WOW_LIQUID_MATERIAL := preload("res://addons/mpq_extractor/loaders/wow_liquid_material.gd")
|
||||||
const M2_RIGHT_YAW_OFFSET := PI * 0.5
|
const WOW_WMO_MATERIAL := preload("res://addons/mpq_extractor/loaders/wow_wmo_material.gd")
|
||||||
|
const WMO_BUILDER_FORMAT_VERSION := 2
|
||||||
|
const WMO_BUILDER_FORMAT_META := "openwc_wmo_builder_format_version"
|
||||||
const BUILD_OCCLUDERS := false
|
const BUILD_OCCLUDERS := false
|
||||||
const OCCLUDER_MIN_TRIANGLES := 16
|
const OCCLUDER_MIN_TRIANGLES := 16
|
||||||
|
|
||||||
@@ -29,11 +31,20 @@ static func clear_caches() -> void:
|
|||||||
_m2_missing_cache.clear()
|
_m2_missing_cache.clear()
|
||||||
_texture_cache.clear()
|
_texture_cache.clear()
|
||||||
WOW_LIQUID_MATERIAL.clear_cache()
|
WOW_LIQUID_MATERIAL.clear_cache()
|
||||||
|
WOW_WMO_MATERIAL.clear_cache()
|
||||||
|
|
||||||
|
|
||||||
|
static func is_scene_cache_current(root: Node) -> bool:
|
||||||
|
if root == null:
|
||||||
|
return false
|
||||||
|
return int(root.get_meta(WMO_BUILDER_FORMAT_META, 0)) >= WMO_BUILDER_FORMAT_VERSION
|
||||||
|
|
||||||
|
|
||||||
# Returns a Node3D containing one MeshInstance3D per WMO group.
|
# Returns a Node3D containing one MeshInstance3D per WMO group.
|
||||||
static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
||||||
var root := Node3D.new()
|
var root := Node3D.new()
|
||||||
root.name = "WMO"
|
root.name = "WMO"
|
||||||
|
root.set_meta(WMO_BUILDER_FORMAT_META, WMO_BUILDER_FORMAT_VERSION)
|
||||||
|
|
||||||
if not data.has("groups"):
|
if not data.has("groups"):
|
||||||
return root
|
return root
|
||||||
@@ -42,7 +53,7 @@ static func build(data: Dictionary, extracted_dir: String = "") -> Node3D:
|
|||||||
var materials: Array = data.get("materials", [])
|
var materials: Array = data.get("materials", [])
|
||||||
|
|
||||||
# Build Godot materials from WMO material definitions
|
# Build Godot materials from WMO material definitions
|
||||||
var godot_materials: Array[StandardMaterial3D] = []
|
var godot_materials: Array[Material] = []
|
||||||
for mat_def in materials:
|
for mat_def in materials:
|
||||||
godot_materials.append(_build_material(mat_def, textures, extracted_dir))
|
godot_materials.append(_build_material(mat_def, textures, extracted_dir))
|
||||||
|
|
||||||
@@ -110,7 +121,10 @@ static func _build_default_doodads(root: Node3D, data: Dictionary,
|
|||||||
var pos: Vector3 = p.get("pos", Vector3.ZERO)
|
var pos: Vector3 = p.get("pos", Vector3.ZERO)
|
||||||
var rot_q: Quaternion = p.get("rot", Quaternion.IDENTITY)
|
var rot_q: Quaternion = p.get("rot", Quaternion.IDENTITY)
|
||||||
var scl: float = float(p.get("scale", 1.0))
|
var scl: float = float(p.get("scale", 1.0))
|
||||||
var basis := (Basis(rot_q) * Basis(Vector3.UP, M2_RIGHT_YAW_OFFSET)).scaled(Vector3.ONE * maxf(scl, 0.001))
|
# MODD is already a local WMO doodad transform. Do not apply the ADT M2
|
||||||
|
# yaw correction here; it rotates anchored effects like waterfalls away
|
||||||
|
# from their authored cliff attachment points.
|
||||||
|
var basis := Basis(rot_q).scaled(Vector3.ONE * maxf(scl, 0.001))
|
||||||
var xform := Transform3D(basis, pos)
|
var xform := Transform3D(basis, pos)
|
||||||
if not groups.has(rel_path):
|
if not groups.has(rel_path):
|
||||||
groups[rel_path] = []
|
groups[rel_path] = []
|
||||||
@@ -207,7 +221,7 @@ static func _build_raw_m2_prototype(rel_path: String, extracted_dir: String) ->
|
|||||||
|
|
||||||
static func _build_group_mesh(
|
static func _build_group_mesh(
|
||||||
g: Dictionary,
|
g: Dictionary,
|
||||||
godot_mats: Array[StandardMaterial3D]) -> MeshInstance3D:
|
godot_mats: Array[Material]) -> MeshInstance3D:
|
||||||
|
|
||||||
var verts: PackedVector3Array = g.get("vertices", PackedVector3Array())
|
var verts: PackedVector3Array = g.get("vertices", PackedVector3Array())
|
||||||
var normals: PackedVector3Array = g.get("normals", PackedVector3Array())
|
var normals: PackedVector3Array = g.get("normals", PackedVector3Array())
|
||||||
@@ -331,37 +345,54 @@ static func _material_can_occlude(mat_id: int, material_defs: Array) -> bool:
|
|||||||
static func _build_material(
|
static func _build_material(
|
||||||
mat_def: Dictionary,
|
mat_def: Dictionary,
|
||||||
textures: PackedStringArray,
|
textures: PackedStringArray,
|
||||||
extracted_dir: String = "") -> StandardMaterial3D:
|
extracted_dir: String = "") -> Material:
|
||||||
|
|
||||||
var mat := StandardMaterial3D.new()
|
|
||||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
|
||||||
mat.roughness = 0.85
|
|
||||||
mat.metallic = 0.0
|
|
||||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
|
||||||
mat.specular_mode = BaseMaterial3D.SPECULAR_DISABLED
|
|
||||||
mat.vertex_color_use_as_albedo = false
|
|
||||||
mat.set_flag(BaseMaterial3D.FLAG_USE_TEXTURE_REPEAT, true)
|
|
||||||
|
|
||||||
var blend_mode: int = mat_def.get("blend_mode", 0)
|
var blend_mode: int = mat_def.get("blend_mode", 0)
|
||||||
match blend_mode:
|
var flags: int = int(mat_def.get("flags", 0))
|
||||||
0: mat.transparency = BaseMaterial3D.TRANSPARENCY_DISABLED
|
var shader_id: int = int(mat_def.get("shader", 0))
|
||||||
1: mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA_SCISSOR
|
var tex: Texture2D = null
|
||||||
_: mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
var tex2: Texture2D = null
|
||||||
|
var tex3: Texture2D = null
|
||||||
|
var tex_path := ""
|
||||||
|
var tex2_path := ""
|
||||||
|
var tex3_path := ""
|
||||||
|
|
||||||
# Texture is loaded later by the scene assembler (needs BLP→Image conversion)
|
# Texture is loaded later by the scene assembler (needs BLP→Image conversion)
|
||||||
# Store the path as metadata for the assembler to pick up
|
# Store the path as metadata for the assembler to pick up
|
||||||
var tex0_id: int = mat_def.get("texture0", -1)
|
var tex0_id: int = mat_def.get("texture0", -1)
|
||||||
if tex0_id >= 0 and tex0_id < textures.size():
|
if tex0_id >= 0 and tex0_id < textures.size():
|
||||||
var tex_path: String = str(textures[tex0_id]).replace("\\", "/")
|
tex_path = str(textures[tex0_id]).replace("\\", "/")
|
||||||
mat.set_meta("texture0_path", tex_path)
|
tex = _load_texture(tex_path, extracted_dir)
|
||||||
var tex := _load_texture(tex_path, extracted_dir)
|
var tex1_id: int = mat_def.get("texture1", -1)
|
||||||
if tex:
|
if tex1_id >= 0 and tex1_id < textures.size():
|
||||||
mat.albedo_texture = tex
|
tex2_path = str(textures[tex1_id]).replace("\\", "/")
|
||||||
mat.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC
|
tex2 = _load_texture(tex2_path, extracted_dir)
|
||||||
mat.set_flag(BaseMaterial3D.FLAG_USE_TEXTURE_REPEAT, true)
|
var tex2_id: int = mat_def.get("texture2", -1)
|
||||||
|
if tex2_id >= 0 and tex2_id < textures.size():
|
||||||
|
tex3_path = str(textures[tex2_id]).replace("\\", "/")
|
||||||
|
tex3 = _load_texture(tex3_path, extracted_dir)
|
||||||
|
|
||||||
mat.set_meta("wow_flags", mat_def.get("flags", 0))
|
var diffuse_color: Color = mat_def.get("diffuse_color", Color.WHITE)
|
||||||
mat.set_meta("wow_shader", mat_def.get("shader", 0))
|
var emissive_color: Color = mat_def.get("emissive_color", Color.BLACK)
|
||||||
|
var secondary_color: Color = mat_def.get("color2", Color.WHITE)
|
||||||
|
var mat := WOW_WMO_MATERIAL.build(
|
||||||
|
tex,
|
||||||
|
tex2,
|
||||||
|
flags,
|
||||||
|
shader_id,
|
||||||
|
blend_mode,
|
||||||
|
tex_path,
|
||||||
|
tex2_path,
|
||||||
|
diffuse_color,
|
||||||
|
emissive_color,
|
||||||
|
secondary_color,
|
||||||
|
tex3,
|
||||||
|
tex3_path)
|
||||||
|
mat.set_meta("texture0_path", tex_path)
|
||||||
|
mat.set_meta("texture1_path", tex2_path)
|
||||||
|
mat.set_meta("texture2_path", tex3_path)
|
||||||
|
mat.set_meta("wow_flags", flags)
|
||||||
|
mat.set_meta("wow_shader", shader_id)
|
||||||
|
|
||||||
return mat
|
return mat
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ static func clear_cache() -> void:
|
|||||||
_cache.clear()
|
_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
static func _ensure_wow_shader_globals() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
static func build(liquid_id: int) -> ShaderMaterial:
|
static func build(liquid_id: int) -> ShaderMaterial:
|
||||||
var key := "adt:%d" % liquid_id
|
var key := "adt:%d" % liquid_id
|
||||||
if _cache.has(key):
|
if _cache.has(key):
|
||||||
@@ -108,6 +112,7 @@ static func _get_shader() -> Shader:
|
|||||||
if _shader:
|
if _shader:
|
||||||
return _shader
|
return _shader
|
||||||
|
|
||||||
|
_ensure_wow_shader_globals()
|
||||||
_shader = Shader.new()
|
_shader = Shader.new()
|
||||||
_shader.code = """
|
_shader.code = """
|
||||||
shader_type spatial;
|
shader_type spatial;
|
||||||
@@ -123,7 +128,12 @@ uniform float fresnel_power = 2.3;
|
|||||||
uniform float emission_strength = 0.025;
|
uniform float emission_strength = 0.025;
|
||||||
uniform float magma_mode = 0.0;
|
uniform float magma_mode = 0.0;
|
||||||
|
|
||||||
|
global uniform vec4 wow_fog_color;
|
||||||
|
global uniform vec2 wow_fog_range;
|
||||||
|
global uniform float wow_fog_density;
|
||||||
|
|
||||||
varying vec3 world_pos;
|
varying vec3 world_pos;
|
||||||
|
varying vec3 view_pos;
|
||||||
|
|
||||||
float hash21(vec2 p) {
|
float hash21(vec2 p) {
|
||||||
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);
|
||||||
@@ -140,8 +150,16 @@ float noise21(vec2 p) {
|
|||||||
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, 1.0);
|
||||||
|
return mix(color, wow_fog_color.rgb, fog_amount);
|
||||||
|
}
|
||||||
|
|
||||||
void vertex() {
|
void vertex() {
|
||||||
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;
|
||||||
float t = TIME * wave_speed;
|
float t = TIME * wave_speed;
|
||||||
float wave_a = sin(world_pos.x * wave_scale + t);
|
float wave_a = sin(world_pos.x * wave_scale + t);
|
||||||
float wave_b = cos(world_pos.z * wave_scale * 1.37 - t * 1.21);
|
float wave_b = cos(world_pos.z * wave_scale * 1.37 - t * 1.21);
|
||||||
@@ -170,7 +188,7 @@ void fragment() {
|
|||||||
EMISSION = color * emission_strength;
|
EMISSION = color * emission_strength;
|
||||||
}
|
}
|
||||||
|
|
||||||
ALBEDO = color;
|
ALBEDO = apply_wow_fog(color);
|
||||||
ALPHA = clamp(alpha_base + fresnel * 0.18 + ripple * 0.06, 0.0, 1.0);
|
ALPHA = clamp(alpha_base + fresnel * 0.18 + ripple * 0.06, 0.0, 1.0);
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -0,0 +1,517 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name WowM2Material
|
||||||
|
|
||||||
|
const M2BLEND_OPAQUE := 0
|
||||||
|
const M2BLEND_ALPHA_KEY := 1
|
||||||
|
const M2BLEND_ALPHA := 2
|
||||||
|
const M2BLEND_NO_ALPHA_ADD := 3
|
||||||
|
const M2BLEND_ADD := 4
|
||||||
|
const M2BLEND_MOD := 5
|
||||||
|
const M2BLEND_MOD_2X := 6
|
||||||
|
|
||||||
|
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,
|
||||||
|
texture3: Texture2D,
|
||||||
|
texture4: Texture2D,
|
||||||
|
material_flags: int,
|
||||||
|
blend_mode: int,
|
||||||
|
texture_flags: int,
|
||||||
|
texture2_flags: int,
|
||||||
|
texture3_flags: int,
|
||||||
|
texture4_flags: int,
|
||||||
|
resource_name: String = "",
|
||||||
|
combiner: Dictionary = {}) -> ShaderMaterial:
|
||||||
|
|
||||||
|
var two_sided := (material_flags & 0x04) != 0
|
||||||
|
var unlit := (material_flags & 0x01) != 0 or blend_mode >= M2BLEND_MOD
|
||||||
|
var depth_test := (material_flags & 0x08) == 0
|
||||||
|
var depth_write := (material_flags & 0x10) == 0
|
||||||
|
var repeat_enabled := (
|
||||||
|
(texture_flags & 0x03) != 0
|
||||||
|
or (texture2_flags & 0x03) != 0
|
||||||
|
or (texture3_flags & 0x03) != 0
|
||||||
|
or (texture4_flags & 0x03) != 0
|
||||||
|
)
|
||||||
|
var mode := _blend_shader_mode(blend_mode)
|
||||||
|
|
||||||
|
_ensure_wow_shader_globals()
|
||||||
|
var mat := ShaderMaterial.new()
|
||||||
|
mat.resource_name = resource_name
|
||||||
|
mat.shader = _get_shader(mode, two_sided, depth_test, depth_write, repeat_enabled, texture2 != null, texture3 != null, texture4 != null)
|
||||||
|
mat.render_priority = clampi(_render_priority(blend_mode) + int(combiner.get("priority_plane", 0)), -128, 127)
|
||||||
|
mat.set_shader_parameter("albedo_tex", texture)
|
||||||
|
mat.set_shader_parameter("stage2_tex", texture2)
|
||||||
|
mat.set_shader_parameter("stage3_tex", texture3)
|
||||||
|
mat.set_shader_parameter("stage4_tex", texture4)
|
||||||
|
mat.set_shader_parameter("use_texture", texture != null)
|
||||||
|
mat.set_shader_parameter("use_stage2", texture2 != null)
|
||||||
|
mat.set_shader_parameter("use_stage3", texture3 != null)
|
||||||
|
mat.set_shader_parameter("use_stage4", texture4 != null)
|
||||||
|
mat.set_shader_parameter("alpha_ref", _alpha_ref(blend_mode))
|
||||||
|
mat.set_shader_parameter("unlit_strength", 1.0 if unlit else 0.0)
|
||||||
|
mat.set_shader_parameter("mesh_color", combiner.get("mesh_color", Color.WHITE))
|
||||||
|
mat.set_shader_parameter("tex_sample_alpha", combiner.get("tex_sample_alpha", Vector3.ONE))
|
||||||
|
mat.set_shader_parameter("ambient_color", Color(0.45, 0.45, 0.42, 1.0))
|
||||||
|
mat.set_shader_parameter("light_color", Color(0.82, 0.78, 0.68, 1.0))
|
||||||
|
mat.set_shader_parameter("light_dir", Vector3(-0.45, -0.75, -0.35).normalized())
|
||||||
|
mat.set_shader_parameter("diffuse_strength", 0.72)
|
||||||
|
mat.set_shader_parameter("modulate_2x", 1.0 if blend_mode == M2BLEND_MOD_2X else 0.0)
|
||||||
|
mat.set_shader_parameter("vertex_shader_id", int(combiner.get("vertex_shader_id", 0)))
|
||||||
|
mat.set_shader_parameter("pixel_shader_id", int(combiner.get("pixel_shader_id", 0)))
|
||||||
|
mat.set_shader_parameter("stage0_op", int(combiner.get("op0", 0 if blend_mode == 0 else 1)))
|
||||||
|
mat.set_shader_parameter("stage1_op", int(combiner.get("op1", 1)))
|
||||||
|
mat.set_shader_parameter("stage0_env", bool(combiner.get("env0", false)))
|
||||||
|
mat.set_shader_parameter("stage1_env", bool(combiner.get("env1", false)))
|
||||||
|
mat.set_shader_parameter("stage0_uv2", bool(combiner.get("use_uv2_0", false)))
|
||||||
|
mat.set_shader_parameter("stage1_uv2", bool(combiner.get("use_uv2_1", false)))
|
||||||
|
mat.set_shader_parameter("stage0_uv_transform", combiner.get("stage0_uv_transform", Vector4(1.0, 1.0, 0.0, 0.0)))
|
||||||
|
mat.set_shader_parameter("stage1_uv_transform", combiner.get("stage1_uv_transform", Vector4(1.0, 1.0, 0.0, 0.0)))
|
||||||
|
mat.set_shader_parameter("stage2_uv_transform", combiner.get("stage2_uv_transform", Vector4(1.0, 1.0, 0.0, 0.0)))
|
||||||
|
mat.set_shader_parameter("stage3_uv_transform", combiner.get("stage3_uv_transform", Vector4(1.0, 1.0, 0.0, 0.0)))
|
||||||
|
mat.set_shader_parameter("stage0_uv_scroll", combiner.get("stage0_uv_scroll", Vector2.ZERO))
|
||||||
|
mat.set_shader_parameter("stage1_uv_scroll", combiner.get("stage1_uv_scroll", Vector2.ZERO))
|
||||||
|
mat.set_shader_parameter("stage2_uv_scroll", combiner.get("stage2_uv_scroll", Vector2.ZERO))
|
||||||
|
mat.set_shader_parameter("stage3_uv_scroll", combiner.get("stage3_uv_scroll", Vector2.ZERO))
|
||||||
|
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("uv_edge_fade_strength", combiner.get("uv_edge_fade_strength", 0.0))
|
||||||
|
mat.set_meta("wow_flags", material_flags)
|
||||||
|
mat.set_meta("wow_blend_mode", blend_mode)
|
||||||
|
mat.set_meta("wow_priority_plane", int(combiner.get("priority_plane", 0)))
|
||||||
|
mat.set_meta("wow_texture_flags", texture_flags)
|
||||||
|
mat.set_meta("wow_texture2_flags", texture2_flags)
|
||||||
|
mat.set_meta("wow_texture3_flags", texture3_flags)
|
||||||
|
mat.set_meta("wow_texture4_flags", texture4_flags)
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
static func _blend_shader_mode(blend_mode: int) -> String:
|
||||||
|
match blend_mode:
|
||||||
|
M2BLEND_ALPHA_KEY:
|
||||||
|
return "cutout"
|
||||||
|
M2BLEND_ALPHA:
|
||||||
|
return "alpha"
|
||||||
|
M2BLEND_NO_ALPHA_ADD, M2BLEND_ADD:
|
||||||
|
return "add"
|
||||||
|
M2BLEND_MOD, M2BLEND_MOD_2X:
|
||||||
|
return "mul"
|
||||||
|
_:
|
||||||
|
return "opaque"
|
||||||
|
|
||||||
|
|
||||||
|
static func _render_priority(blend_mode: int) -> int:
|
||||||
|
if blend_mode <= M2BLEND_ALPHA_KEY:
|
||||||
|
return 0
|
||||||
|
if blend_mode >= M2BLEND_MOD:
|
||||||
|
return 2
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
static func _alpha_ref(blend_mode: int) -> float:
|
||||||
|
if blend_mode == M2BLEND_ALPHA_KEY:
|
||||||
|
return 0.87843138
|
||||||
|
if blend_mode == M2BLEND_OPAQUE:
|
||||||
|
return 0.0
|
||||||
|
return 0.0039215689
|
||||||
|
|
||||||
|
|
||||||
|
static func _get_shader(
|
||||||
|
mode: String,
|
||||||
|
two_sided: bool,
|
||||||
|
depth_test: bool,
|
||||||
|
depth_write: bool,
|
||||||
|
repeat_enabled: bool,
|
||||||
|
has_stage2: bool,
|
||||||
|
has_stage3: bool,
|
||||||
|
has_stage4: bool) -> Shader:
|
||||||
|
|
||||||
|
var key := "%s|sided=%s|dt=%s|dw=%s|repeat=%s" % [
|
||||||
|
mode,
|
||||||
|
str(two_sided),
|
||||||
|
str(depth_test),
|
||||||
|
str(depth_write),
|
||||||
|
str(repeat_enabled),
|
||||||
|
]
|
||||||
|
key += "|stage2=%s" % str(has_stage2)
|
||||||
|
key += "|stage3=%s|stage4=%s" % [str(has_stage3), str(has_stage4)]
|
||||||
|
if _shader_cache.has(key):
|
||||||
|
return _shader_cache[key]
|
||||||
|
|
||||||
|
var shader := Shader.new()
|
||||||
|
shader.code = _shader_code(mode, two_sided, depth_test, depth_write, repeat_enabled, has_stage2, has_stage3, has_stage4)
|
||||||
|
_shader_cache[key] = shader
|
||||||
|
return shader
|
||||||
|
|
||||||
|
|
||||||
|
static func _shader_code(
|
||||||
|
mode: String,
|
||||||
|
two_sided: bool,
|
||||||
|
depth_test: bool,
|
||||||
|
depth_write: bool,
|
||||||
|
repeat_enabled: bool,
|
||||||
|
has_stage2: bool,
|
||||||
|
has_stage3: bool,
|
||||||
|
has_stage4: bool) -> String:
|
||||||
|
|
||||||
|
var render_modes: Array[String] = []
|
||||||
|
match mode:
|
||||||
|
"alpha":
|
||||||
|
render_modes.append("blend_mix")
|
||||||
|
"add":
|
||||||
|
render_modes.append("blend_add")
|
||||||
|
"mul":
|
||||||
|
render_modes.append("blend_mul")
|
||||||
|
_:
|
||||||
|
render_modes.append("blend_mix")
|
||||||
|
|
||||||
|
render_modes.append("cull_disabled" if two_sided else "cull_back")
|
||||||
|
if not depth_test:
|
||||||
|
render_modes.append("depth_test_disabled")
|
||||||
|
if not depth_write:
|
||||||
|
render_modes.append("depth_draw_never")
|
||||||
|
elif mode == "opaque" or mode == "cutout":
|
||||||
|
render_modes.append("depth_draw_opaque")
|
||||||
|
else:
|
||||||
|
render_modes.append("depth_draw_never")
|
||||||
|
render_modes.append("unshaded")
|
||||||
|
|
||||||
|
var repeat_hint := "repeat_enable" if repeat_enabled else "repeat_disable"
|
||||||
|
return """
|
||||||
|
shader_type spatial;
|
||||||
|
render_mode %s;
|
||||||
|
|
||||||
|
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap_anisotropic, %s;
|
||||||
|
uniform sampler2D stage2_tex : source_color, filter_linear_mipmap_anisotropic, %s;
|
||||||
|
uniform sampler2D stage3_tex : source_color, filter_linear_mipmap_anisotropic, %s;
|
||||||
|
uniform sampler2D stage4_tex : source_color, filter_linear_mipmap_anisotropic, %s;
|
||||||
|
uniform bool use_texture = false;
|
||||||
|
uniform bool use_stage2 = false;
|
||||||
|
uniform bool use_stage3 = false;
|
||||||
|
uniform bool use_stage4 = false;
|
||||||
|
uniform float alpha_ref = 0.0;
|
||||||
|
uniform float unlit_strength = 0.0;
|
||||||
|
uniform vec4 mesh_color : source_color = vec4(1.0);
|
||||||
|
uniform vec3 tex_sample_alpha = vec3(1.0);
|
||||||
|
uniform vec4 ambient_color : source_color = vec4(0.45, 0.45, 0.42, 1.0);
|
||||||
|
uniform vec4 light_color : source_color = vec4(0.82, 0.78, 0.68, 1.0);
|
||||||
|
uniform vec3 light_dir = vec3(-0.45, -0.75, -0.35);
|
||||||
|
uniform float diffuse_strength = 0.72;
|
||||||
|
uniform float modulate_2x = 0.0;
|
||||||
|
uniform int vertex_shader_id = 0;
|
||||||
|
uniform int pixel_shader_id = 0;
|
||||||
|
uniform int stage0_op = 0;
|
||||||
|
uniform int stage1_op = 1;
|
||||||
|
uniform bool stage0_env = false;
|
||||||
|
uniform bool stage1_env = false;
|
||||||
|
uniform bool stage0_uv2 = false;
|
||||||
|
uniform bool stage1_uv2 = false;
|
||||||
|
uniform vec4 stage0_uv_transform = vec4(1.0, 1.0, 0.0, 0.0);
|
||||||
|
uniform vec4 stage1_uv_transform = vec4(1.0, 1.0, 0.0, 0.0);
|
||||||
|
uniform vec4 stage2_uv_transform = vec4(1.0, 1.0, 0.0, 0.0);
|
||||||
|
uniform vec4 stage3_uv_transform = vec4(1.0, 1.0, 0.0, 0.0);
|
||||||
|
uniform vec2 stage0_uv_scroll = vec2(0.0);
|
||||||
|
uniform vec2 stage1_uv_scroll = vec2(0.0);
|
||||||
|
uniform vec2 stage2_uv_scroll = vec2(0.0);
|
||||||
|
uniform vec2 stage3_uv_scroll = vec2(0.0);
|
||||||
|
uniform float effect_alpha_scale = 1.0;
|
||||||
|
uniform float effect_alpha_power = 1.0;
|
||||||
|
uniform float uv_edge_fade_strength = 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 vec2 uv_interp;
|
||||||
|
varying vec2 uv2_interp;
|
||||||
|
varying float edge_fade;
|
||||||
|
|
||||||
|
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;
|
||||||
|
uv_interp = UV;
|
||||||
|
uv2_interp = UV2;
|
||||||
|
vec3 view_normal = normalize((MODELVIEW_MATRIX * vec4(NORMAL, 0.0)).xyz);
|
||||||
|
float edge_scan = 1.0 - abs(dot(normalize(-view_pos), view_normal));
|
||||||
|
edge_fade = (vertex_shader_id == 9 || vertex_shader_id == 12 || vertex_shader_id == 13) ? clamp(edge_scan * 2.5, 0.0, 1.0) : 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 transform_uv(vec2 uv, vec4 uv_transform, vec2 uv_scroll) {
|
||||||
|
return uv * uv_transform.xy + uv_transform.zw + uv_scroll * TIME;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 env_uv(vec3 normal) {
|
||||||
|
vec3 n = normalize(normal);
|
||||||
|
return n.xy * 0.5 + vec2(0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec2 stage_uv(int stage, vec3 normal) {
|
||||||
|
vec2 t1 = transform_uv(uv_interp, stage0_uv_transform, stage0_uv_scroll);
|
||||||
|
vec2 t2 = transform_uv(uv2_interp, stage1_uv_transform, stage1_uv_scroll);
|
||||||
|
vec2 t3 = transform_uv(uv2_interp, stage2_uv_transform, stage2_uv_scroll);
|
||||||
|
vec2 t4 = transform_uv(uv2_interp, stage3_uv_transform, stage3_uv_scroll);
|
||||||
|
vec2 env = env_uv(normal);
|
||||||
|
|
||||||
|
if (vertex_shader_id == 1) {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 2) {
|
||||||
|
return stage == 0 ? t1 : t2;
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 3) {
|
||||||
|
return stage == 0 ? t1 : env;
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 4) {
|
||||||
|
return stage == 0 ? env : t1;
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 5) {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 6) {
|
||||||
|
return stage == 1 ? env : t1;
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 7 || vertex_shader_id == 8) {
|
||||||
|
return t1;
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 10) {
|
||||||
|
return t2;
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 11) {
|
||||||
|
return stage == 1 ? env : (stage == 2 ? t2 : t1);
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 12) {
|
||||||
|
return stage == 0 ? t1 : t2;
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 13) {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 14) {
|
||||||
|
return stage == 1 ? t2 : t1;
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 15) {
|
||||||
|
return stage == 0 ? t1 : (stage == 1 ? t2 : uv2_interp);
|
||||||
|
}
|
||||||
|
if (vertex_shader_id == 16) {
|
||||||
|
return stage == 0 ? t2 : uv2_interp;
|
||||||
|
}
|
||||||
|
if (stage == 2) {
|
||||||
|
return t3;
|
||||||
|
}
|
||||||
|
if (stage == 3) {
|
||||||
|
return t4;
|
||||||
|
}
|
||||||
|
if (stage == 0) {
|
||||||
|
if (stage0_env) {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
return transform_uv(stage0_uv2 ? uv2_interp : uv_interp, stage0_uv_transform, stage0_uv_scroll);
|
||||||
|
}
|
||||||
|
if (stage1_env) {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
return transform_uv(stage1_uv2 ? uv2_interp : uv_interp, stage1_uv_transform, stage1_uv_scroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
void resolve_m2_pixel(vec4 tex1, vec4 tex2, vec4 tex3, vec4 tex4, out vec3 diffuse, out vec3 specular, out float discard_alpha, out float can_discard) {
|
||||||
|
vec3 mesh_rgb = mesh_color.rgb;
|
||||||
|
diffuse = mesh_rgb * tex1.rgb;
|
||||||
|
specular = vec3(0.0);
|
||||||
|
discard_alpha = tex1.a * mesh_color.a;
|
||||||
|
can_discard = 0.0;
|
||||||
|
|
||||||
|
if (pixel_shader_id == 0) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb;
|
||||||
|
} else if (pixel_shader_id == 1) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb;
|
||||||
|
discard_alpha = tex1.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 2) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb * tex2.rgb;
|
||||||
|
discard_alpha = tex2.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 3) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb * tex2.rgb * 2.0;
|
||||||
|
discard_alpha = tex2.a * 2.0 * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 4) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb * tex2.rgb * 2.0;
|
||||||
|
} else if (pixel_shader_id == 5) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb * tex2.rgb;
|
||||||
|
} else if (pixel_shader_id == 6) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb * tex2.rgb;
|
||||||
|
discard_alpha = tex1.a * tex2.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 7) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb * tex2.rgb * 2.0;
|
||||||
|
discard_alpha = tex1.a * tex2.a * 2.0 * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 8) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb;
|
||||||
|
specular = tex2.rgb;
|
||||||
|
discard_alpha = (tex1.a + tex2.a) * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 9) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb * tex2.rgb * 2.0;
|
||||||
|
discard_alpha = tex1.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 10) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb;
|
||||||
|
specular = tex2.rgb;
|
||||||
|
discard_alpha = tex1.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 11) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb * tex2.rgb;
|
||||||
|
discard_alpha = tex1.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 12) {
|
||||||
|
diffuse = mesh_rgb * mix(tex1.rgb * tex2.rgb * 2.0, tex1.rgb, vec3(tex1.a));
|
||||||
|
} else if (pixel_shader_id == 13) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb;
|
||||||
|
specular = tex2.rgb * tex2.a;
|
||||||
|
} else if (pixel_shader_id == 14) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb;
|
||||||
|
specular = tex2.rgb * tex2.a * (1.0 - tex1.a);
|
||||||
|
} else if (pixel_shader_id == 15) {
|
||||||
|
diffuse = mesh_rgb * mix(tex1.rgb * tex2.rgb * 2.0, tex1.rgb, vec3(tex1.a));
|
||||||
|
specular = tex3.rgb * tex3.a * tex_sample_alpha.b;
|
||||||
|
} else if (pixel_shader_id == 16) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb;
|
||||||
|
specular = tex2.rgb * tex2.a;
|
||||||
|
discard_alpha = tex1.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 17) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb;
|
||||||
|
specular = tex2.rgb * tex2.a * (1.0 - tex1.a);
|
||||||
|
discard_alpha = (tex1.a + tex2.a * dot(tex2.rgb, vec3(0.3, 0.59, 0.11))) * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 18) {
|
||||||
|
diffuse = mesh_rgb * mix(mix(tex1.rgb, tex2.rgb, vec3(tex2.a)), tex1.rgb, vec3(tex1.a));
|
||||||
|
} else if (pixel_shader_id == 19) {
|
||||||
|
diffuse = mesh_rgb * mix(tex1.rgb * tex2.rgb * 2.0, tex3.rgb, vec3(tex3.a));
|
||||||
|
} else if (pixel_shader_id == 21) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb;
|
||||||
|
specular = tex2.rgb * (1.0 - tex1.a);
|
||||||
|
discard_alpha = (tex1.a + tex2.a) * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 22) {
|
||||||
|
diffuse = mesh_rgb * mix(tex1.rgb * tex2.rgb, tex1.rgb, vec3(tex1.a));
|
||||||
|
} else if (pixel_shader_id == 24) {
|
||||||
|
diffuse = mesh_rgb * mix(tex1.rgb, tex2.rgb, vec3(tex2.a));
|
||||||
|
specular = tex1.rgb * tex1.a * tex_sample_alpha.r;
|
||||||
|
} else if (pixel_shader_id == 25) {
|
||||||
|
float glow_opacity = clamp(tex3.a * tex_sample_alpha.b, 0.0, 1.0);
|
||||||
|
diffuse = mesh_rgb * mix(tex1.rgb * tex2.rgb * 2.0, tex1.rgb, vec3(tex1.a)) * (1.0 - glow_opacity);
|
||||||
|
specular = tex3.rgb * glow_opacity;
|
||||||
|
} else if (pixel_shader_id == 26) {
|
||||||
|
vec4 mixed = mix(mix(tex1, tex2, vec4(clamp(tex_sample_alpha.g, 0.0, 1.0))), tex3, vec4(clamp(tex_sample_alpha.b, 0.0, 1.0)));
|
||||||
|
diffuse = mesh_rgb * mixed.rgb;
|
||||||
|
discard_alpha = mixed.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 27) {
|
||||||
|
diffuse = mesh_rgb * mix(mix(tex1.rgb * tex2.rgb * 2.0, tex3.rgb, vec3(tex3.a)), tex1.rgb, vec3(tex1.a));
|
||||||
|
} else if (pixel_shader_id == 28) {
|
||||||
|
vec4 mixed = mix(mix(tex1, tex2, vec4(clamp(tex_sample_alpha.g, 0.0, 1.0))), tex3, vec4(clamp(tex_sample_alpha.b, 0.0, 1.0)));
|
||||||
|
diffuse = mesh_rgb * mixed.rgb;
|
||||||
|
discard_alpha = mixed.a * tex4.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 29) {
|
||||||
|
diffuse = mesh_rgb * mix(tex1.rgb, tex2.rgb, vec3(tex2.a));
|
||||||
|
} else if (pixel_shader_id == 30) {
|
||||||
|
diffuse = mesh_rgb * mix(tex1.rgb * tex2.rgb, tex3.rgb, vec3(tex3.a));
|
||||||
|
discard_alpha = tex1.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 31) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb * tex2.rgb;
|
||||||
|
discard_alpha = tex1.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 32) {
|
||||||
|
diffuse = mesh_rgb * mix(tex1.rgb * tex2.rgb, tex3.rgb, vec3(tex3.a));
|
||||||
|
} else if (pixel_shader_id == 35) {
|
||||||
|
vec4 combined = tex1 * tex2 * tex3;
|
||||||
|
diffuse = mesh_rgb * combined.rgb;
|
||||||
|
discard_alpha = combined.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
} else if (pixel_shader_id == 36) {
|
||||||
|
diffuse = mesh_rgb * tex1.rgb * tex2.rgb;
|
||||||
|
discard_alpha = tex1.a * tex2.a * mesh_color.a;
|
||||||
|
can_discard = 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec4 tex = use_texture ? texture(albedo_tex, stage_uv(0, world_normal)) : vec4(1.0);
|
||||||
|
%s
|
||||||
|
vec4 stage2 = use_stage2 ? texture(stage2_tex, stage_uv(1, world_normal)) : vec4(1.0);
|
||||||
|
vec4 stage3 = use_stage3 ? texture(stage3_tex, stage_uv(2, world_normal)) : vec4(1.0);
|
||||||
|
vec4 stage4 = use_stage4 ? texture(stage4_tex, stage_uv(3, world_normal)) : vec4(1.0);
|
||||||
|
vec3 mat_diffuse;
|
||||||
|
vec3 specular;
|
||||||
|
float discard_alpha;
|
||||||
|
float can_discard;
|
||||||
|
resolve_m2_pixel(tex, stage2, stage3, stage4, mat_diffuse, specular, discard_alpha, can_discard);
|
||||||
|
discard_alpha *= edge_fade;
|
||||||
|
discard_alpha = pow(clamp(discard_alpha, 0.0, 1.0), max(effect_alpha_power, 0.01)) * effect_alpha_scale;
|
||||||
|
if (uv_edge_fade_strength > 0.0) {
|
||||||
|
float edge_width = 0.12;
|
||||||
|
float edge_mask = smoothstep(0.0, edge_width, uv_interp.x) * smoothstep(0.0, edge_width, 1.0 - uv_interp.x);
|
||||||
|
edge_mask *= smoothstep(0.0, edge_width, uv_interp.y) * smoothstep(0.0, edge_width, 1.0 - uv_interp.y);
|
||||||
|
discard_alpha *= mix(1.0, edge_mask, clamp(uv_edge_fade_strength, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
if (alpha_ref > 0.0 && discard_alpha < alpha_ref) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.76), max(wow_ambient_color.rgb, vec3(0.08)), 0.18);
|
||||||
|
vec3 zone_light = mix(vec3(1.0), wow_light_color.rgb, 0.14);
|
||||||
|
float day_strength = mix(0.48, 0.92, clamp(wow_sun_elevation, 0.0, 1.0));
|
||||||
|
vec3 lit = zone_ambient * 0.82 + zone_light * ndl * diffuse_strength * 0.72 * day_strength;
|
||||||
|
lit = clamp(lit, vec3(0.56), vec3(1.08));
|
||||||
|
lit = mix(lit, vec3(1.0), clamp(unlit_strength, 0.0, 1.0));
|
||||||
|
vec3 color = mat_diffuse * lit + specular * 0.45;
|
||||||
|
color = mix(color, min(color * 2.0, vec3(1.0)), clamp(modulate_2x, 0.0, 1.0));
|
||||||
|
color = clamp(color, vec3(0.0), vec3(1.0));
|
||||||
|
color = apply_wow_fog(color);
|
||||||
|
|
||||||
|
ALBEDO = color;
|
||||||
|
%s
|
||||||
|
}
|
||||||
|
""" % [
|
||||||
|
", ".join(render_modes),
|
||||||
|
repeat_hint,
|
||||||
|
repeat_hint,
|
||||||
|
repeat_hint,
|
||||||
|
repeat_hint,
|
||||||
|
"",
|
||||||
|
"" if mode == "opaque" or mode == "cutout" else "\tALPHA = clamp(discard_alpha, 0.0, 1.0);",
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dbyyg7wwr0y3w
|
||||||
@@ -0,0 +1,336 @@
|
|||||||
|
extends RefCounted
|
||||||
|
class_name WowWMOMaterial
|
||||||
|
|
||||||
|
const WMOBLEND_OPAQUE := 0
|
||||||
|
const WMOBLEND_ALPHA_KEY := 1
|
||||||
|
const WMOBLEND_ALPHA := 2
|
||||||
|
const SHADER_VERSION := 3
|
||||||
|
|
||||||
|
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 mode := _blend_shader_mode(blend_mode)
|
||||||
|
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)
|
||||||
|
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("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_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)
|
||||||
|
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_OPAQUE:
|
||||||
|
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
|
||||||
|
var lower_path := texture_path.to_lower()
|
||||||
|
if lower_path.contains("window") or lower_path.contains("_wnd_") or lower_path.contains("wnd_"):
|
||||||
|
strength = minf(strength, 0.18)
|
||||||
|
return strength
|
||||||
|
|
||||||
|
|
||||||
|
static func _blend_shader_mode(blend_mode: int) -> String:
|
||||||
|
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) -> Shader:
|
||||||
|
var key := "v=%d|%s|detail=%s|extra=%s" % [SHADER_VERSION, mode, str(has_detail), str(has_extra)]
|
||||||
|
if _shader_cache.has(key):
|
||||||
|
return _shader_cache[key]
|
||||||
|
|
||||||
|
var shader := Shader.new()
|
||||||
|
shader.code = _shader_code(mode, has_detail, has_extra)
|
||||||
|
_shader_cache[key] = shader
|
||||||
|
return shader
|
||||||
|
|
||||||
|
|
||||||
|
static func _shader_code(mode: String, has_detail: bool, has_extra: bool) -> String:
|
||||||
|
var render_modes: Array[String] = ["blend_mix", "cull_disabled", "unshaded"]
|
||||||
|
if mode == "opaque" or mode == "cutout":
|
||||||
|
render_modes.append("depth_draw_opaque")
|
||||||
|
else:
|
||||||
|
render_modes.append("depth_prepass_alpha")
|
||||||
|
|
||||||
|
return """
|
||||||
|
shader_type spatial;
|
||||||
|
render_mode %s;
|
||||||
|
|
||||||
|
uniform sampler2D albedo_tex : source_color, filter_linear_mipmap_anisotropic, repeat_enable;
|
||||||
|
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 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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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));
|
||||||
|
ALBEDO = apply_wow_fog(color);
|
||||||
|
%s
|
||||||
|
}
|
||||||
|
""" % [
|
||||||
|
", ".join(render_modes),
|
||||||
|
"" if mode == "opaque" or mode == "cutout" else "\tALPHA = mix(diffuse_color.a, tex1.a * diffuse_color.a, clamp(alpha_output_strength, 0.0, 1.0));",
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c1ju24mvhoi5e
|
||||||
@@ -27,3 +27,34 @@ enabled=PackedStringArray("res://addons/mpq_extractor/plugin.cfg")
|
|||||||
|
|
||||||
rendering_device/driver.windows="d3d12"
|
rendering_device/driver.windows="d3d12"
|
||||||
rendering/rendering_device/d3d12/max_resource_descriptors=1048576
|
rendering/rendering_device/d3d12/max_resource_descriptors=1048576
|
||||||
|
|
||||||
|
[shader_globals]
|
||||||
|
|
||||||
|
wow_ambient_color={
|
||||||
|
"type": "color",
|
||||||
|
"value": Color(0.72, 0.8, 0.88, 1)
|
||||||
|
}
|
||||||
|
wow_light_color={
|
||||||
|
"type": "color",
|
||||||
|
"value": Color(1, 0.91, 0.78, 1)
|
||||||
|
}
|
||||||
|
wow_light_dir={
|
||||||
|
"type": "vec3",
|
||||||
|
"value": Vector3(-0.35, 0.82, -0.45)
|
||||||
|
}
|
||||||
|
wow_fog_color={
|
||||||
|
"type": "color",
|
||||||
|
"value": Color(0.55, 0.66, 0.72, 1)
|
||||||
|
}
|
||||||
|
wow_fog_range={
|
||||||
|
"type": "vec2",
|
||||||
|
"value": Vector2(1200, 5200)
|
||||||
|
}
|
||||||
|
wow_fog_density={
|
||||||
|
"type": "float",
|
||||||
|
"value": 0.0
|
||||||
|
}
|
||||||
|
wow_sun_elevation={
|
||||||
|
"type": "float",
|
||||||
|
"value": 1.0
|
||||||
|
}
|
||||||
|
|||||||
Submodule
+1
Submodule reference/WoWee added at 626243e937
Submodule
+1
Submodule reference/WowUnreal added at c2a4b9827b
Submodule
+1
Submodule reference/whoa added at 74bc963a1c
@@ -6,10 +6,17 @@
|
|||||||
#include <godot_cpp/variant/packed_vector3_array.hpp>
|
#include <godot_cpp/variant/packed_vector3_array.hpp>
|
||||||
#include <godot_cpp/variant/packed_vector2_array.hpp>
|
#include <godot_cpp/variant/packed_vector2_array.hpp>
|
||||||
#include <godot_cpp/variant/packed_int32_array.hpp>
|
#include <godot_cpp/variant/packed_int32_array.hpp>
|
||||||
|
#include <godot_cpp/variant/packed_float32_array.hpp>
|
||||||
|
#include <godot_cpp/variant/packed_vector4_array.hpp>
|
||||||
|
#include <godot_cpp/variant/color.hpp>
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
using namespace godot;
|
using namespace godot;
|
||||||
|
|
||||||
@@ -54,6 +61,38 @@ struct M2Header {
|
|||||||
uint32_t ofsBoneCombos;
|
uint32_t ofsBoneCombos;
|
||||||
uint32_t nTextureCombos; // offset 128
|
uint32_t nTextureCombos; // offset 128
|
||||||
uint32_t ofsTextureCombos; // offset 132
|
uint32_t ofsTextureCombos; // offset 132
|
||||||
|
uint32_t nTextureCoordCombos;
|
||||||
|
uint32_t ofsTextureCoordCombos;
|
||||||
|
uint32_t nTextureWeightCombos;
|
||||||
|
uint32_t ofsTextureWeightCombos;
|
||||||
|
uint32_t nTextureTransformCombos;
|
||||||
|
uint32_t ofsTextureTransformCombos;
|
||||||
|
float bounds[7];
|
||||||
|
float collisionBounds[7];
|
||||||
|
uint32_t nCollisionIndices;
|
||||||
|
uint32_t ofsCollisionIndices;
|
||||||
|
uint32_t nCollisionPositions;
|
||||||
|
uint32_t ofsCollisionPositions;
|
||||||
|
uint32_t nCollisionFaceNormals;
|
||||||
|
uint32_t ofsCollisionFaceNormals;
|
||||||
|
uint32_t nAttachments;
|
||||||
|
uint32_t ofsAttachments;
|
||||||
|
uint32_t nAttachmentLookup;
|
||||||
|
uint32_t ofsAttachmentLookup;
|
||||||
|
uint32_t nEvents;
|
||||||
|
uint32_t ofsEvents;
|
||||||
|
uint32_t nLights;
|
||||||
|
uint32_t ofsLights;
|
||||||
|
uint32_t nCameras;
|
||||||
|
uint32_t ofsCameras;
|
||||||
|
uint32_t nCameraLookup;
|
||||||
|
uint32_t ofsCameraLookup;
|
||||||
|
uint32_t nRibbonEmitters;
|
||||||
|
uint32_t ofsRibbonEmitters;
|
||||||
|
uint32_t nParticleEmitters;
|
||||||
|
uint32_t ofsParticleEmitters;
|
||||||
|
uint32_t nTextureCombinerCombos;
|
||||||
|
uint32_t ofsTextureCombinerCombos;
|
||||||
};
|
};
|
||||||
// Total: 34 × 4 = 136 bytes
|
// Total: 34 × 4 = 136 bytes
|
||||||
|
|
||||||
@@ -79,6 +118,70 @@ struct M2Material {
|
|||||||
uint16_t blendingMode; // 0=opaque 1=alpha_key 2=alpha_blend
|
uint16_t blendingMode; // 0=opaque 1=alpha_key 2=alpha_blend
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct M2ArrayRef {
|
||||||
|
uint32_t count;
|
||||||
|
uint32_t offset;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct M2Track {
|
||||||
|
uint16_t interpolationType;
|
||||||
|
uint16_t globalSequence;
|
||||||
|
M2ArrayRef timestamps;
|
||||||
|
M2ArrayRef values;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct M2Color {
|
||||||
|
M2Track colorTrack;
|
||||||
|
M2Track alphaTrack;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct M2TextureTransform {
|
||||||
|
M2Track translationTrack;
|
||||||
|
M2Track rotationTrack;
|
||||||
|
M2Track scaleTrack;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct M2TextureWeight {
|
||||||
|
M2Track weightTrack;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct M2CompBone {
|
||||||
|
uint32_t boneId;
|
||||||
|
uint32_t flags;
|
||||||
|
uint16_t parentIndex;
|
||||||
|
uint16_t submeshId;
|
||||||
|
uint32_t unknown;
|
||||||
|
M2Track translation;
|
||||||
|
M2Track rotation;
|
||||||
|
M2Track scale;
|
||||||
|
float pivot[3];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct M2Sequence {
|
||||||
|
uint16_t id;
|
||||||
|
uint16_t variationIndex;
|
||||||
|
uint32_t duration;
|
||||||
|
float moveSpeed;
|
||||||
|
uint32_t flags;
|
||||||
|
uint32_t frequency;
|
||||||
|
uint32_t replayMin;
|
||||||
|
uint32_t replayMax;
|
||||||
|
uint32_t blendTime;
|
||||||
|
float minBounds[3];
|
||||||
|
float maxBounds[3];
|
||||||
|
float boundsRadius;
|
||||||
|
uint16_t nextAnimation;
|
||||||
|
uint16_t aliasNext;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct M2Float3 {
|
||||||
|
float v[3];
|
||||||
|
};
|
||||||
|
|
||||||
|
struct M2CompQuat {
|
||||||
|
uint16_t v[4];
|
||||||
|
};
|
||||||
|
|
||||||
struct SkinHeader {
|
struct SkinHeader {
|
||||||
uint32_t magic; // 'SKIN' LE = 0x4E494B53
|
uint32_t magic; // 'SKIN' LE = 0x4E494B53
|
||||||
uint32_t nIndices;
|
uint32_t nIndices;
|
||||||
@@ -164,10 +267,203 @@ static const T *safe_array(const std::vector<uint8_t> &buf, uint32_t ofs, uint32
|
|||||||
return reinterpret_cast<const T *>(buf.data() + ofs);
|
return reinterpret_cast<const T *>(buf.data() + ofs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Vector3 wow_vec3_to_godot(const float v[3]) {
|
||||||
|
return Vector3(v[0], v[2], -v[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vector3 wow_vec3_to_godot(float x, float y, float z) {
|
||||||
|
return Vector3(x, z, -y);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return Vector4(0.0f, 0.0f, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
float inv = 1.0f / len;
|
||||||
|
return Vector4(x * inv, y * inv, z * inv, w * inv);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vector4 wow_quat_to_godot(float x, float y, float z, float w) {
|
||||||
|
// WoW model space is converted to Godot with Z/Y axis swap and Y handedness flip.
|
||||||
|
return normalize_quat(x, z, -y, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vector4 decode_comp_quat(const uint16_t q[4]) {
|
||||||
|
float x = ((float)q[0] - 32768.0f) / 32767.0f;
|
||||||
|
float y = ((float)q[1] - 32768.0f) / 32767.0f;
|
||||||
|
float z = ((float)q[2] - 32768.0f) / 32767.0f;
|
||||||
|
float w = ((float)q[3] - 32768.0f) / 32767.0f;
|
||||||
|
return wow_quat_to_godot(x, y, z, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static const T *track_sequence_array(const std::vector<uint8_t> &buf, const M2ArrayRef &outer, uint32_t sequence_index, uint32_t &count_out) {
|
||||||
|
count_out = 0;
|
||||||
|
if (sequence_index >= outer.count) return nullptr;
|
||||||
|
const M2ArrayRef *inner_arrays = safe_array<M2ArrayRef>(buf, outer.offset, outer.count);
|
||||||
|
if (!inner_arrays) return nullptr;
|
||||||
|
const M2ArrayRef &inner = inner_arrays[sequence_index];
|
||||||
|
const T *items = safe_array<T>(buf, inner.offset, inner.count);
|
||||||
|
if (!items) return nullptr;
|
||||||
|
count_out = inner.count;
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t track_sequence_key_count(const std::vector<uint8_t> &buf, const M2Track &track, uint32_t sequence_index) {
|
||||||
|
if (sequence_index >= track.values.count) return 0;
|
||||||
|
const M2ArrayRef *inner_arrays = safe_array<M2ArrayRef>(buf, track.values.offset, track.values.count);
|
||||||
|
if (!inner_arrays) return 0;
|
||||||
|
return inner_arrays[sequence_index].count;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static const T *first_track_value(const std::vector<uint8_t> &buf, const M2Track &track) {
|
||||||
|
for (uint32_t i = 0; i < track.values.count; ++i) {
|
||||||
|
uint32_t value_count = 0;
|
||||||
|
const T *values = track_sequence_array<T>(buf, track.values, i, value_count);
|
||||||
|
if (values && value_count > 0) {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float fixed16_to_float(int16_t value) {
|
||||||
|
return std::clamp((float)value / 32767.0f, 0.0f, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Vector2 first_vec3_track_xy_speed(const std::vector<uint8_t> &buf, const M2Track &track) {
|
||||||
|
for (uint32_t i = 0; i < track.values.count && i < track.timestamps.count; ++i) {
|
||||||
|
uint32_t time_count = 0;
|
||||||
|
uint32_t value_count = 0;
|
||||||
|
const uint32_t *times = track_sequence_array<uint32_t>(buf, track.timestamps, i, time_count);
|
||||||
|
const M2Float3 *values = track_sequence_array<M2Float3>(buf, track.values, i, value_count);
|
||||||
|
uint32_t key_count = std::min(time_count, value_count);
|
||||||
|
if (!times || !values || key_count < 2) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
uint32_t first = 0;
|
||||||
|
uint32_t last = key_count - 1;
|
||||||
|
float duration = (float)(times[last] - times[first]) / 1000.0f;
|
||||||
|
if (duration <= 0.0001f) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return Vector2(
|
||||||
|
(values[last].v[0] - values[first].v[0]) / duration,
|
||||||
|
(values[last].v[1] - values[first].v[1]) / duration);
|
||||||
|
}
|
||||||
|
return Vector2(0.0f, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t sequence_activity_score(const std::vector<uint8_t> &buf, const M2CompBone *bones, uint32_t bone_count, uint32_t sequence_index) {
|
||||||
|
if (!bones) return 0;
|
||||||
|
uint32_t score = 0;
|
||||||
|
for (uint32_t i = 0; i < bone_count; ++i) {
|
||||||
|
uint32_t t = track_sequence_key_count(buf, bones[i].translation, sequence_index);
|
||||||
|
uint32_t r = track_sequence_key_count(buf, bones[i].rotation, sequence_index);
|
||||||
|
uint32_t s = track_sequence_key_count(buf, bones[i].scale, sequence_index);
|
||||||
|
if (t > 1) score += t;
|
||||||
|
if (r > 1) score += r;
|
||||||
|
if (s > 1) score += s;
|
||||||
|
}
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t choose_animated_sequence(const std::vector<uint8_t> &buf, const M2Sequence *seqs, uint32_t seq_count, const M2CompBone *bones, uint32_t bone_count, bool prefer_calm_stand) {
|
||||||
|
if (!seqs || seq_count == 0) return 0;
|
||||||
|
|
||||||
|
uint32_t best_stand = UINT32_MAX;
|
||||||
|
uint32_t best_stand_score = 0;
|
||||||
|
uint32_t calm_stand = UINT32_MAX;
|
||||||
|
uint32_t calm_stand_score = UINT32_MAX;
|
||||||
|
uint32_t best_any = UINT32_MAX;
|
||||||
|
uint32_t best_any_score = 0;
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < seq_count; ++i) {
|
||||||
|
if (seqs[i].duration == 0) continue;
|
||||||
|
uint32_t score = sequence_activity_score(buf, bones, bone_count, i);
|
||||||
|
if (seqs[i].id == 0 && score > best_stand_score) {
|
||||||
|
best_stand = i;
|
||||||
|
best_stand_score = score;
|
||||||
|
}
|
||||||
|
if (seqs[i].id == 0 && score > 0 && score < calm_stand_score) {
|
||||||
|
calm_stand = i;
|
||||||
|
calm_stand_score = score;
|
||||||
|
}
|
||||||
|
if (score > best_any_score) {
|
||||||
|
best_any = i;
|
||||||
|
best_any_score = score;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefer_calm_stand && calm_stand != UINT32_MAX) return calm_stand;
|
||||||
|
if (best_stand != UINT32_MAX) return best_stand;
|
||||||
|
if (best_any != UINT32_MAX) return best_any;
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < seq_count; ++i) {
|
||||||
|
if (seqs[i].id == 0 && seqs[i].duration > 0) return i;
|
||||||
|
}
|
||||||
|
for (uint32_t i = 0; i < seq_count; ++i) {
|
||||||
|
if (seqs[i].duration > 0) return i;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Dictionary make_vec3_track(const std::vector<uint8_t> &buf, const M2Track &track, uint32_t sequence_index, bool scale_track) {
|
||||||
|
Dictionary result;
|
||||||
|
uint32_t time_count = 0;
|
||||||
|
uint32_t value_count = 0;
|
||||||
|
const uint32_t *times = track_sequence_array<uint32_t>(buf, track.timestamps, sequence_index, time_count);
|
||||||
|
const M2Float3 *values = track_sequence_array<M2Float3>(buf, track.values, sequence_index, value_count);
|
||||||
|
if (!times || !values || time_count == 0 || value_count == 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
uint32_t key_count = std::min(time_count, value_count);
|
||||||
|
PackedFloat32Array out_times;
|
||||||
|
PackedVector3Array out_values;
|
||||||
|
out_times.resize(key_count);
|
||||||
|
out_values.resize(key_count);
|
||||||
|
for (uint32_t i = 0; i < key_count; ++i) {
|
||||||
|
out_times[i] = (float)times[i] / 1000.0f;
|
||||||
|
if (scale_track) {
|
||||||
|
out_values[i] = Vector3(values[i].v[0], values[i].v[1], values[i].v[2]);
|
||||||
|
} else {
|
||||||
|
out_values[i] = wow_vec3_to_godot(values[i].v[0], values[i].v[1], values[i].v[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result["times"] = out_times;
|
||||||
|
result["values"] = out_values;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Dictionary make_quat_track(const std::vector<uint8_t> &buf, const M2Track &track, uint32_t sequence_index) {
|
||||||
|
Dictionary result;
|
||||||
|
uint32_t time_count = 0;
|
||||||
|
uint32_t value_count = 0;
|
||||||
|
const uint32_t *times = track_sequence_array<uint32_t>(buf, track.timestamps, sequence_index, time_count);
|
||||||
|
const M2CompQuat *values = track_sequence_array<M2CompQuat>(buf, track.values, sequence_index, value_count);
|
||||||
|
if (!times || !values || time_count == 0 || value_count == 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
uint32_t key_count = std::min(time_count, value_count);
|
||||||
|
PackedFloat32Array out_times;
|
||||||
|
PackedVector4Array out_values;
|
||||||
|
out_times.resize(key_count);
|
||||||
|
out_values.resize(key_count);
|
||||||
|
for (uint32_t i = 0; i < key_count; ++i) {
|
||||||
|
out_times[i] = (float)times[i] / 1000.0f;
|
||||||
|
out_values[i] = decode_comp_quat(values[i].v);
|
||||||
|
}
|
||||||
|
result["times"] = out_times;
|
||||||
|
result["values"] = out_values;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Core parser
|
// Core parser
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string &path) {
|
Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string &path, bool include_animation) {
|
||||||
if (buf.size() < sizeof(M2Header)) return Dictionary();
|
if (buf.size() < sizeof(M2Header)) return Dictionary();
|
||||||
|
|
||||||
const auto &hdr = *reinterpret_cast<const M2Header *>(buf.data());
|
const auto &hdr = *reinterpret_cast<const M2Header *>(buf.data());
|
||||||
@@ -178,13 +474,14 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
|
|
||||||
// ── Vertices ─────────────────────────────────────────────────────────────
|
// ── Vertices ─────────────────────────────────────────────────────────────
|
||||||
PackedVector3Array vertices, normals;
|
PackedVector3Array vertices, normals;
|
||||||
PackedVector2Array uvs;
|
PackedVector2Array uvs, uvs2;
|
||||||
|
|
||||||
const auto *verts = safe_array<M2Vertex>(buf, hdr.ofsVertices, hdr.nVertices);
|
const auto *verts = safe_array<M2Vertex>(buf, hdr.ofsVertices, hdr.nVertices);
|
||||||
if (verts && hdr.nVertices > 0) {
|
if (verts && hdr.nVertices > 0) {
|
||||||
vertices.resize(hdr.nVertices);
|
vertices.resize(hdr.nVertices);
|
||||||
normals.resize(hdr.nVertices);
|
normals.resize(hdr.nVertices);
|
||||||
uvs.resize(hdr.nVertices);
|
uvs.resize(hdr.nVertices);
|
||||||
|
uvs2.resize(hdr.nVertices);
|
||||||
for (uint32_t i = 0; i < hdr.nVertices; ++i) {
|
for (uint32_t i = 0; i < hdr.nVertices; ++i) {
|
||||||
const auto &v = verts[i];
|
const auto &v = verts[i];
|
||||||
// WoW model space (X right, Y forward, Z up) → Godot (X right, Y up, Z back)
|
// WoW model space (X right, Y forward, Z up) → Godot (X right, Y up, Z back)
|
||||||
@@ -194,6 +491,7 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
// (D3D/Vulkan convention) — pass UVs through unchanged. Same as
|
// (D3D/Vulkan convention) — pass UVs through unchanged. Same as
|
||||||
// wmo_loader's MOTV handling.
|
// wmo_loader's MOTV handling.
|
||||||
uvs[i] = Vector2(v.texCoords[0], v.texCoords[1]);
|
uvs[i] = Vector2(v.texCoords[0], v.texCoords[1]);
|
||||||
|
uvs2[i] = Vector2(v.texCoords2[0], v.texCoords2[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,7 +542,92 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
texture_combos.push_back((int)tc_arr[i]);
|
texture_combos.push_back((int)tc_arr[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PackedInt32Array texture_coord_combos;
|
||||||
|
const auto *tcc_arr = safe_array<uint16_t>(buf, hdr.ofsTextureCoordCombos, hdr.nTextureCoordCombos);
|
||||||
|
if (tcc_arr) {
|
||||||
|
for (uint32_t i = 0; i < hdr.nTextureCoordCombos; ++i)
|
||||||
|
texture_coord_combos.push_back((int)tcc_arr[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
PackedInt32Array texture_weight_combos;
|
||||||
|
const auto *twc_arr = safe_array<uint16_t>(buf, hdr.ofsTextureWeightCombos, hdr.nTextureWeightCombos);
|
||||||
|
if (twc_arr) {
|
||||||
|
for (uint32_t i = 0; i < hdr.nTextureWeightCombos; ++i)
|
||||||
|
texture_weight_combos.push_back((int)twc_arr[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
PackedInt32Array texture_transform_combos;
|
||||||
|
const auto *ttc_arr = safe_array<uint16_t>(buf, hdr.ofsTextureTransformCombos, hdr.nTextureTransformCombos);
|
||||||
|
if (ttc_arr) {
|
||||||
|
for (uint32_t i = 0; i < hdr.nTextureTransformCombos; ++i)
|
||||||
|
texture_transform_combos.push_back((int)ttc_arr[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
PackedInt32Array texture_combiner_combos;
|
||||||
|
const auto *tcomb_arr = safe_array<uint16_t>(buf, hdr.ofsTextureCombinerCombos, hdr.nTextureCombinerCombos);
|
||||||
|
if (tcomb_arr) {
|
||||||
|
for (uint32_t i = 0; i < hdr.nTextureCombinerCombos; ++i)
|
||||||
|
texture_combiner_combos.push_back((int)tcomb_arr[i]);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Skin file ────────────────────────────────────────────────────────────
|
// ── Skin file ────────────────────────────────────────────────────────────
|
||||||
|
Array m2_colors;
|
||||||
|
const auto *color_arr = safe_array<M2Color>(buf, hdr.ofsColors, hdr.nColors);
|
||||||
|
if (color_arr) {
|
||||||
|
for (uint32_t i = 0; i < hdr.nColors; ++i) {
|
||||||
|
Color color(1.0f, 1.0f, 1.0f, 1.0f);
|
||||||
|
const M2Float3 *rgb = first_track_value<M2Float3>(buf, color_arr[i].colorTrack);
|
||||||
|
if (rgb) {
|
||||||
|
color.r = rgb->v[0];
|
||||||
|
color.g = rgb->v[1];
|
||||||
|
color.b = rgb->v[2];
|
||||||
|
}
|
||||||
|
const int16_t *alpha = first_track_value<int16_t>(buf, color_arr[i].alphaTrack);
|
||||||
|
if (alpha) {
|
||||||
|
color.a = fixed16_to_float(*alpha);
|
||||||
|
}
|
||||||
|
Dictionary c;
|
||||||
|
c["color"] = color;
|
||||||
|
m2_colors.push_back(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PackedFloat32Array texture_weights;
|
||||||
|
const auto *weight_arr = safe_array<M2TextureWeight>(buf, hdr.ofsTexWeights, hdr.nTexWeights);
|
||||||
|
if (weight_arr) {
|
||||||
|
texture_weights.resize(hdr.nTexWeights);
|
||||||
|
for (uint32_t i = 0; i < hdr.nTexWeights; ++i) {
|
||||||
|
float weight = 1.0f;
|
||||||
|
const int16_t *value = first_track_value<int16_t>(buf, weight_arr[i].weightTrack);
|
||||||
|
if (value) {
|
||||||
|
weight = fixed16_to_float(*value);
|
||||||
|
}
|
||||||
|
texture_weights[i] = weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Array texture_transforms;
|
||||||
|
const auto *transform_arr = safe_array<M2TextureTransform>(buf, hdr.ofsTexTransforms, hdr.nTexTransforms);
|
||||||
|
if (transform_arr) {
|
||||||
|
for (uint32_t i = 0; i < hdr.nTexTransforms; ++i) {
|
||||||
|
Vector2 translation(0.0f, 0.0f);
|
||||||
|
Vector2 scale(1.0f, 1.0f);
|
||||||
|
const M2Float3 *trans = first_track_value<M2Float3>(buf, transform_arr[i].translationTrack);
|
||||||
|
if (trans) {
|
||||||
|
translation = Vector2(trans->v[0], trans->v[1]);
|
||||||
|
}
|
||||||
|
const M2Float3 *scl = first_track_value<M2Float3>(buf, transform_arr[i].scaleTrack);
|
||||||
|
if (scl) {
|
||||||
|
scale = Vector2(scl->v[0], scl->v[1]);
|
||||||
|
}
|
||||||
|
Dictionary t;
|
||||||
|
t["translation"] = translation;
|
||||||
|
t["scale"] = scale;
|
||||||
|
t["translation_speed"] = first_vec3_track_xy_speed(buf, transform_arr[i].translationTrack);
|
||||||
|
texture_transforms.push_back(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Find <basename>00.skin in the same directory as the .m2
|
// Find <basename>00.skin in the same directory as the .m2
|
||||||
std::string skin_path = path;
|
std::string skin_path = path;
|
||||||
{
|
{
|
||||||
@@ -257,7 +640,16 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
auto skin_buf = read_file(skin_path);
|
auto skin_buf = read_file(skin_path);
|
||||||
|
|
||||||
PackedInt32Array indices;
|
PackedInt32Array indices;
|
||||||
|
PackedInt32Array bone_lookup_table;
|
||||||
Array batches;
|
Array batches;
|
||||||
|
Array animated_surfaces;
|
||||||
|
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);
|
||||||
|
for (uint32_t i = 0; i < hdr.nBoneCombos; ++i) {
|
||||||
|
bone_lookup_table[i] = (int)bone_combo_arr[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (skin_buf.size() >= sizeof(SkinHeader)) {
|
if (skin_buf.size() >= sizeof(SkinHeader)) {
|
||||||
const auto &skin = *reinterpret_cast<const SkinHeader *>(skin_buf.data());
|
const auto &skin = *reinterpret_cast<const SkinHeader *>(skin_buf.data());
|
||||||
@@ -265,6 +657,7 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
if (skin.magic == MAGIC_SKIN) {
|
if (skin.magic == MAGIC_SKIN) {
|
||||||
const uint16_t *skin_idx = safe_array<uint16_t>(skin_buf, skin.ofsIndices, skin.nIndices);
|
const uint16_t *skin_idx = safe_array<uint16_t>(skin_buf, skin.ofsIndices, skin.nIndices);
|
||||||
const uint16_t *skin_tri = safe_array<uint16_t>(skin_buf, skin.ofsTriangles, skin.nTriangles);
|
const uint16_t *skin_tri = safe_array<uint16_t>(skin_buf, skin.ofsTriangles, skin.nTriangles);
|
||||||
|
const uint8_t *skin_bones = safe_array<uint8_t>(skin_buf, skin.ofsProperties, skin.nProperties * 4);
|
||||||
const SkinSubMesh *sms = safe_array<SkinSubMesh> (skin_buf, skin.ofsSubMeshes, skin.nSubMeshes);
|
const SkinSubMesh *sms = safe_array<SkinSubMesh> (skin_buf, skin.ofsSubMeshes, skin.nSubMeshes);
|
||||||
const SkinTextureUnit *tu = safe_array<SkinTextureUnit>(skin_buf, skin.ofsTextureUnits, skin.nTextureUnits);
|
const SkinTextureUnit *tu = safe_array<SkinTextureUnit>(skin_buf, skin.ofsTextureUnits, skin.nTextureUnits);
|
||||||
|
|
||||||
@@ -303,7 +696,113 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
batch["index_count"] = idx_count;
|
batch["index_count"] = idx_count;
|
||||||
batch["material_id"] = (int)tu[u].materialIndex;
|
batch["material_id"] = (int)tu[u].materialIndex;
|
||||||
batch["texture_combo_index"] = (int)tu[u].textureComboIndex;
|
batch["texture_combo_index"] = (int)tu[u].textureComboIndex;
|
||||||
|
batch["texture_count"] = (int)tu[u].textureCount;
|
||||||
|
batch["shader_id"] = (int)tu[u].shaderID;
|
||||||
|
batch["texture_coord_combo_index"] = (int)tu[u].textureCoordComboIndex;
|
||||||
|
batch["texture_weight_combo_index"] = (int)tu[u].textureWeightComboIndex;
|
||||||
|
batch["texture_transform_combo_index"] = (int)tu[u].textureTransformComboIndex;
|
||||||
|
batch["material_layer"] = (int)tu[u].materialLayer;
|
||||||
|
batch["color_index"] = (int)tu[u].colorIndex;
|
||||||
|
batch["priority_plane"] = (int)tu[u].priority;
|
||||||
|
batch["skin_flags"] = (int)tu[u].flags;
|
||||||
|
batch["skin_flags2"] = (int)tu[u].flags2;
|
||||||
|
batch["bone_count"] = (int)sm.boneCount;
|
||||||
|
batch["bone_combo_index"] = (int)sm.boneStart;
|
||||||
batches.push_back(batch);
|
batches.push_back(batch);
|
||||||
|
|
||||||
|
if (include_animation && verts) {
|
||||||
|
PackedVector3Array surface_vertices;
|
||||||
|
PackedVector3Array surface_normals;
|
||||||
|
PackedVector2Array surface_uvs, surface_uvs2;
|
||||||
|
PackedInt32Array surface_bones;
|
||||||
|
PackedFloat32Array surface_weights;
|
||||||
|
PackedInt32Array surface_indices;
|
||||||
|
uint32_t expanded_count = 0;
|
||||||
|
surface_vertices.resize(tri_count);
|
||||||
|
surface_normals.resize(tri_count);
|
||||||
|
surface_uvs.resize(tri_count);
|
||||||
|
surface_uvs2.resize(tri_count);
|
||||||
|
surface_bones.resize(tri_count * 4);
|
||||||
|
surface_weights.resize(tri_count * 4);
|
||||||
|
surface_indices.resize(tri_count);
|
||||||
|
|
||||||
|
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];
|
||||||
|
if (global_vertex_index >= hdr.nVertices) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const auto &v = verts[global_vertex_index];
|
||||||
|
surface_vertices[expanded_count] = Vector3(v.pos[0], v.pos[2], -v.pos[1]);
|
||||||
|
surface_normals[expanded_count] = Vector3(v.normal[0], v.normal[2], -v.normal[1]);
|
||||||
|
surface_uvs[expanded_count] = Vector2(v.texCoords[0], v.texCoords[1]);
|
||||||
|
surface_uvs2[expanded_count] = Vector2(v.texCoords2[0], v.texCoords2[1]);
|
||||||
|
surface_indices[expanded_count] = (int)expanded_count;
|
||||||
|
|
||||||
|
uint32_t weight_sum = 0;
|
||||||
|
for (uint32_t j = 0; j < 4; ++j) {
|
||||||
|
weight_sum += (uint32_t)v.boneWeights[j];
|
||||||
|
}
|
||||||
|
if (weight_sum == 0) {
|
||||||
|
weight_sum = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t j = 0; j < 4; ++j) {
|
||||||
|
int local_bone = (int)v.boneIndices[j];
|
||||||
|
if (skin_bones && vertex_lookup < skin.nProperties) {
|
||||||
|
local_bone = (int)skin_bones[vertex_lookup * 4 + j];
|
||||||
|
}
|
||||||
|
int global_bone = local_bone;
|
||||||
|
uint32_t lookup_index = (uint32_t)sm.boneStart + (uint32_t)std::max(local_bone, 0);
|
||||||
|
if (bone_combo_arr && lookup_index < hdr.nBoneCombos) {
|
||||||
|
global_bone = (int)bone_combo_arr[lookup_index];
|
||||||
|
}
|
||||||
|
surface_bones[expanded_count * 4 + j] = global_bone;
|
||||||
|
surface_weights[expanded_count * 4 + j] = (float)v.boneWeights[j] / (float)weight_sum;
|
||||||
|
}
|
||||||
|
expanded_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expanded_count > 0) {
|
||||||
|
surface_vertices.resize(expanded_count);
|
||||||
|
surface_normals.resize(expanded_count);
|
||||||
|
surface_uvs.resize(expanded_count);
|
||||||
|
surface_uvs2.resize(expanded_count);
|
||||||
|
surface_bones.resize(expanded_count * 4);
|
||||||
|
surface_weights.resize(expanded_count * 4);
|
||||||
|
surface_indices.resize(expanded_count);
|
||||||
|
Dictionary surface;
|
||||||
|
surface["vertices"] = surface_vertices;
|
||||||
|
surface["normals"] = surface_normals;
|
||||||
|
surface["uvs"] = surface_uvs;
|
||||||
|
surface["uvs2"] = surface_uvs2;
|
||||||
|
surface["bones"] = surface_bones;
|
||||||
|
surface["weights"] = surface_weights;
|
||||||
|
surface["indices"] = surface_indices;
|
||||||
|
surface["material_id"] = (int)tu[u].materialIndex;
|
||||||
|
surface["texture_combo_index"] = (int)tu[u].textureComboIndex;
|
||||||
|
surface["texture_count"] = (int)tu[u].textureCount;
|
||||||
|
surface["shader_id"] = (int)tu[u].shaderID;
|
||||||
|
surface["texture_coord_combo_index"] = (int)tu[u].textureCoordComboIndex;
|
||||||
|
surface["texture_weight_combo_index"] = (int)tu[u].textureWeightComboIndex;
|
||||||
|
surface["texture_transform_combo_index"] = (int)tu[u].textureTransformComboIndex;
|
||||||
|
surface["material_layer"] = (int)tu[u].materialLayer;
|
||||||
|
surface["color_index"] = (int)tu[u].colorIndex;
|
||||||
|
surface["bone_count"] = (int)sm.boneCount;
|
||||||
|
surface["bone_combo_index"] = (int)sm.boneStart;
|
||||||
|
animated_surfaces.push_back(surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -315,18 +814,81 @@ Dictionary M2Loader::parse_m2(const std::vector<uint8_t> &buf, const std::string
|
|||||||
to_godot(skin_path));
|
to_godot(skin_path));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Array animated_bones;
|
||||||
|
Array animated_sequences;
|
||||||
|
int animation_sequence_index = -1;
|
||||||
|
float animation_length = 0.0f;
|
||||||
|
int animation_id = -1;
|
||||||
|
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); });
|
||||||
|
bool prefer_calm_stand = lower_path.find("gryphonroost") != std::string::npos;
|
||||||
|
uint32_t chosen = choose_animated_sequence(buf, seqs, hdr.nAnimations, bones, hdr.nBones, prefer_calm_stand);
|
||||||
|
animation_sequence_index = (int)chosen;
|
||||||
|
animation_length = (float)seqs[chosen].duration / 1000.0f;
|
||||||
|
animation_id = (int)seqs[chosen].id;
|
||||||
|
animation_activity_score = sequence_activity_score(buf, bones, hdr.nBones, chosen);
|
||||||
|
for (uint32_t i = 0; i < hdr.nAnimations; ++i) {
|
||||||
|
Dictionary seq;
|
||||||
|
seq["id"] = (int)seqs[i].id;
|
||||||
|
seq["variation"] = (int)seqs[i].variationIndex;
|
||||||
|
seq["duration"] = (int)seqs[i].duration;
|
||||||
|
seq["alias_next"] = (int)seqs[i].aliasNext;
|
||||||
|
seq["activity_score"] = (int)sequence_activity_score(buf, bones, hdr.nBones, i);
|
||||||
|
animated_sequences.push_back(seq);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bones && hdr.nBones > 0 && animation_sequence_index >= 0) {
|
||||||
|
for (uint32_t i = 0; i < hdr.nBones; ++i) {
|
||||||
|
Dictionary bone;
|
||||||
|
int parent = (bones[i].parentIndex == 0xFFFF) ? -1 : (int)bones[i].parentIndex;
|
||||||
|
bone["id"] = (int)bones[i].boneId;
|
||||||
|
bone["flags"] = (int)bones[i].flags;
|
||||||
|
bone["parent"] = parent;
|
||||||
|
bone["pivot"] = wow_vec3_to_godot(bones[i].pivot);
|
||||||
|
bone["translation"] = make_vec3_track(buf, bones[i].translation, (uint32_t)animation_sequence_index, false);
|
||||||
|
bone["rotation"] = make_quat_track(buf, bones[i].rotation, (uint32_t)animation_sequence_index);
|
||||||
|
bone["scale"] = make_vec3_track(buf, bones[i].scale, (uint32_t)animation_sequence_index, true);
|
||||||
|
animated_bones.push_back(bone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Dictionary result;
|
Dictionary result;
|
||||||
result["textures"] = textures;
|
result["textures"] = textures;
|
||||||
result["texture_types"] = texture_types;
|
result["texture_types"] = texture_types;
|
||||||
result["texture_flags"] = texture_flags;
|
result["texture_flags"] = texture_flags;
|
||||||
result["materials"] = materials;
|
result["materials"] = materials;
|
||||||
result["texture_combos"] = texture_combos;
|
result["texture_combos"] = texture_combos;
|
||||||
|
result["texture_coord_combos"] = texture_coord_combos;
|
||||||
|
result["texture_weight_combos"] = texture_weight_combos;
|
||||||
|
result["texture_transform_combos"] = texture_transform_combos;
|
||||||
|
result["texture_combiner_combos"] = texture_combiner_combos;
|
||||||
|
result["m2_colors"] = m2_colors;
|
||||||
|
result["texture_weights"] = texture_weights;
|
||||||
|
result["texture_transforms"] = texture_transforms;
|
||||||
|
result["m2_flags"] = (int)hdr.flags;
|
||||||
result["model_path"] = to_godot(path);
|
result["model_path"] = to_godot(path);
|
||||||
result["vertices"] = vertices;
|
result["vertices"] = vertices;
|
||||||
result["normals"] = normals;
|
result["normals"] = normals;
|
||||||
result["uvs"] = uvs;
|
result["uvs"] = uvs;
|
||||||
|
result["uvs2"] = uvs2;
|
||||||
result["indices"] = indices;
|
result["indices"] = indices;
|
||||||
result["batches"] = batches;
|
result["batches"] = batches;
|
||||||
|
if (include_animation) {
|
||||||
|
result["animated_surfaces"] = animated_surfaces;
|
||||||
|
result["bone_lookup_table"] = bone_lookup_table;
|
||||||
|
result["bones"] = animated_bones;
|
||||||
|
result["sequences"] = animated_sequences;
|
||||||
|
result["animation_sequence_index"] = animation_sequence_index;
|
||||||
|
result["animation_id"] = animation_id;
|
||||||
|
result["animation_length"] = animation_length;
|
||||||
|
result["animation_activity_score"] = (int)animation_activity_score;
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,9 +902,20 @@ Dictionary M2Loader::load_m2(const String &path) {
|
|||||||
UtilityFunctions::push_error("M2Loader: cannot read ", path);
|
UtilityFunctions::push_error("M2Loader: cannot read ", path);
|
||||||
return Dictionary();
|
return Dictionary();
|
||||||
}
|
}
|
||||||
return parse_m2(buf, spath);
|
return parse_m2(buf, spath, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dictionary M2Loader::load_m2_animated(const String &path) {
|
||||||
|
std::string spath = to_std(path);
|
||||||
|
auto buf = read_file(spath);
|
||||||
|
if (buf.empty()) {
|
||||||
|
UtilityFunctions::push_error("M2Loader: cannot read ", path);
|
||||||
|
return Dictionary();
|
||||||
|
}
|
||||||
|
return parse_m2(buf, spath, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void M2Loader::_bind_methods() {
|
void M2Loader::_bind_methods() {
|
||||||
ClassDB::bind_method(D_METHOD("load_m2", "path"), &M2Loader::load_m2);
|
ClassDB::bind_method(D_METHOD("load_m2", "path"), &M2Loader::load_m2);
|
||||||
|
ClassDB::bind_method(D_METHOD("load_m2_animated", "path"), &M2Loader::load_m2_animated);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,14 @@ namespace godot {
|
|||||||
// "texture_flags": PackedInt32Array, # M2Texture.flags per texture slot
|
// "texture_flags": PackedInt32Array, # M2Texture.flags per texture slot
|
||||||
// "materials": Array[Dictionary], # [{flags, blend_mode}]
|
// "materials": Array[Dictionary], # [{flags, blend_mode}]
|
||||||
// "texture_combos": PackedInt32Array, # textureCombos[i] = index into textures
|
// "texture_combos": PackedInt32Array, # textureCombos[i] = index into textures
|
||||||
|
// "texture_coord_combos": PackedInt32Array,
|
||||||
|
// "texture_combiner_combos": PackedInt32Array,
|
||||||
|
// "m2_flags": int,
|
||||||
// "model_path": String, # absolute source .m2 path
|
// "model_path": String, # absolute source .m2 path
|
||||||
// "vertices": PackedVector3Array, # Godot-space positions
|
// "vertices": PackedVector3Array, # Godot-space positions
|
||||||
// "normals": PackedVector3Array,
|
// "normals": PackedVector3Array,
|
||||||
// "uvs": PackedVector2Array,
|
// "uvs": PackedVector2Array,
|
||||||
|
// "uvs2": PackedVector2Array,
|
||||||
// "indices": PackedInt32Array, # flat triangle list into vertex array
|
// "indices": PackedInt32Array, # flat triangle list into vertex array
|
||||||
// "batches": Array[Dictionary], # render batches
|
// "batches": Array[Dictionary], # render batches
|
||||||
// }
|
// }
|
||||||
@@ -41,6 +45,9 @@ namespace godot {
|
|||||||
// "index_start": int, # offset into indices array
|
// "index_start": int, # offset into indices array
|
||||||
// "index_count": int, # number of indices for this batch
|
// "index_count": int, # number of indices for this batch
|
||||||
// "material_id": int, # index into materials
|
// "material_id": int, # index into materials
|
||||||
|
// "texture_count": int,
|
||||||
|
// "shader_id": int,
|
||||||
|
// "texture_coord_combo_index": int,
|
||||||
// "texture_combo_index": int, # index into texture_combos → texture
|
// "texture_combo_index": int, # index into texture_combos → texture
|
||||||
// }
|
// }
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -49,12 +56,13 @@ class M2Loader : public RefCounted {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
Dictionary load_m2(const String &path);
|
Dictionary load_m2(const String &path);
|
||||||
|
Dictionary load_m2_animated(const String &path);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
static void _bind_methods();
|
static void _bind_methods();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Dictionary parse_m2(const std::vector<uint8_t> &buf, const std::string &path);
|
Dictionary parse_m2(const std::vector<uint8_t> &buf, const std::string &path, bool include_animation);
|
||||||
|
|
||||||
static std::vector<uint8_t> read_file(const std::string &path);
|
static std::vector<uint8_t> read_file(const std::string &path);
|
||||||
static std::string to_std(const String &s);
|
static std::string to_std(const String &s);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "wow_chunk_reader.h"
|
#include "wow_chunk_reader.h"
|
||||||
|
|
||||||
#include <godot_cpp/core/class_db.hpp>
|
#include <godot_cpp/core/class_db.hpp>
|
||||||
|
#include <godot_cpp/variant/color.hpp>
|
||||||
#include <godot_cpp/variant/utility_functions.hpp>
|
#include <godot_cpp/variant/utility_functions.hpp>
|
||||||
#include <godot_cpp/variant/packed_string_array.hpp>
|
#include <godot_cpp/variant/packed_string_array.hpp>
|
||||||
#include <godot_cpp/variant/quaternion.hpp>
|
#include <godot_cpp/variant/quaternion.hpp>
|
||||||
@@ -141,6 +142,22 @@ String WMOLoader::to_godot(const std::string &s) {
|
|||||||
return String(s.c_str());
|
return String(s.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Color color_from_bgra_u32(uint32_t value) {
|
||||||
|
const float b = (value & 0xFFu) / 255.0f;
|
||||||
|
const float g = ((value >> 8) & 0xFFu) / 255.0f;
|
||||||
|
const float r = ((value >> 16) & 0xFFu) / 255.0f;
|
||||||
|
const float a = ((value >> 24) & 0xFFu) / 255.0f;
|
||||||
|
return Color(r, g, b, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Color color_from_rgba_u32(uint32_t value) {
|
||||||
|
const float r = (value & 0xFFu) / 255.0f;
|
||||||
|
const float g = ((value >> 8) & 0xFFu) / 255.0f;
|
||||||
|
const float b = ((value >> 16) & 0xFFu) / 255.0f;
|
||||||
|
const float a = ((value >> 24) & 0xFFu) / 255.0f;
|
||||||
|
return Color(r, g, b, a);
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Root parser
|
// Root parser
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
@@ -180,6 +197,10 @@ Dictionary WMOLoader::parse_root(const std::vector<uint8_t> &buf) {
|
|||||||
mat["flags"] = (int)mt[i].flags;
|
mat["flags"] = (int)mt[i].flags;
|
||||||
mat["shader"] = (int)mt[i].shader;
|
mat["shader"] = (int)mt[i].shader;
|
||||||
mat["blend_mode"] = (int)mt[i].blendMode;
|
mat["blend_mode"] = (int)mt[i].blendMode;
|
||||||
|
mat["emissive_color"] = color_from_bgra_u32(mt[i].sidnColor);
|
||||||
|
mat["diffuse_color"] = color_from_rgba_u32(mt[i].diffColor);
|
||||||
|
mat["color2"] = color_from_rgba_u32(mt[i].color2);
|
||||||
|
mat["flags2"] = (int)mt[i].flags2;
|
||||||
// texUnit offsets → find index in tex_strings
|
// texUnit offsets → find index in tex_strings
|
||||||
auto tex_idx = [&](uint32_t ofs) -> int {
|
auto tex_idx = [&](uint32_t ofs) -> int {
|
||||||
uint32_t cur = 0;
|
uint32_t cur = 0;
|
||||||
@@ -191,6 +212,7 @@ Dictionary WMOLoader::parse_root(const std::vector<uint8_t> &buf) {
|
|||||||
};
|
};
|
||||||
mat["texture0"] = tex_idx(mt[i].texUnit0);
|
mat["texture0"] = tex_idx(mt[i].texUnit0);
|
||||||
mat["texture1"] = tex_idx(mt[i].texUnit1);
|
mat["texture1"] = tex_idx(mt[i].texUnit1);
|
||||||
|
mat["texture2"] = tex_idx(mt[i].texUnit2);
|
||||||
materials.push_back(mat);
|
materials.push_back(mat);
|
||||||
}
|
}
|
||||||
} else if (chunk.is("MODS")) {
|
} else if (chunk.is("MODS")) {
|
||||||
|
|||||||
@@ -57,8 +57,12 @@ namespace godot {
|
|||||||
// {
|
// {
|
||||||
// "texture0": int, # index into textures array (-1 = none)
|
// "texture0": int, # index into textures array (-1 = none)
|
||||||
// "texture1": int,
|
// "texture1": int,
|
||||||
|
// "texture2": int,
|
||||||
// "blend_mode": int, # 0=opaque 1=alpha key 2=alpha blend
|
// "blend_mode": int, # 0=opaque 1=alpha key 2=alpha blend
|
||||||
// "flags": int,
|
// "flags": int,
|
||||||
|
// "flags2": int,
|
||||||
|
// "diffuse_color": Color,
|
||||||
|
// "emissive_color": Color,
|
||||||
// }
|
// }
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
class WMOLoader : public RefCounted {
|
class WMOLoader : public RefCounted {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
extends Resource
|
extends Resource
|
||||||
class_name WMOStreamingResource
|
class_name WMOStreamingResource
|
||||||
|
|
||||||
const FORMAT_VERSION := 1
|
const FORMAT_VERSION := 2
|
||||||
|
|
||||||
@export var format_version: int = FORMAT_VERSION
|
@export var format_version: int = FORMAT_VERSION
|
||||||
@export var source_path: String = ""
|
@export var source_path: String = ""
|
||||||
|
|||||||
@@ -0,0 +1,654 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
const GEOSET_CONTROLLER_SCRIPT := preload("res://src/scenes/character/character_geoset_controller.gd")
|
||||||
|
const TEXTURE_COMPOSITOR_SCRIPT := preload("res://src/scenes/character/character_texture_compositor.gd")
|
||||||
|
const OUTFIT_RESOLVER_SCRIPT := preload("res://src/scenes/character/wow_character_outfit_resolver.gd")
|
||||||
|
const CHARACTER_ROOT := "res://src/resources/characters"
|
||||||
|
const EXTRACTED_DIR := "res://data/extracted"
|
||||||
|
const CHARACTER_YAW_OFFSET_DEGREES := 90.0
|
||||||
|
|
||||||
|
const CLASS_IDS := {
|
||||||
|
"Warrior": 1,
|
||||||
|
"Paladin": 2,
|
||||||
|
"Hunter": 3,
|
||||||
|
"Rogue": 4,
|
||||||
|
"Priest": 5,
|
||||||
|
"Death Knight": 6,
|
||||||
|
"Shaman": 7,
|
||||||
|
"Mage": 8,
|
||||||
|
"Warlock": 9,
|
||||||
|
"Druid": 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
const GEOSET_CONTROLS := [
|
||||||
|
{"cat": "hair", "label": "Hair"},
|
||||||
|
{"cat": "facial1", "label": "Facial 1"},
|
||||||
|
{"cat": "facial2", "label": "Facial 2"},
|
||||||
|
{"cat": "facial3", "label": "Facial 3"},
|
||||||
|
{"cat": "ears", "label": "Ears"},
|
||||||
|
{"cat": "eye_effects", "label": "Eyes"},
|
||||||
|
{"cat": "wristbands", "label": "Wrist"},
|
||||||
|
]
|
||||||
|
|
||||||
|
const TOGGLE_CONTROLS := [
|
||||||
|
{"property": "show_gloves", "label": "Gloves"},
|
||||||
|
{"property": "show_boots", "label": "Boots"},
|
||||||
|
{"property": "show_shirt", "label": "Shirt"},
|
||||||
|
{"property": "show_kneepads", "label": "Knees"},
|
||||||
|
{"property": "show_chest", "label": "Chest"},
|
||||||
|
{"property": "show_pants", "label": "Pants"},
|
||||||
|
{"property": "show_tabard", "label": "Tabard"},
|
||||||
|
{"property": "show_legs", "label": "Legs"},
|
||||||
|
{"property": "show_cape", "label": "Cape"},
|
||||||
|
{"property": "show_belt", "label": "Belt"},
|
||||||
|
{"property": "show_feet", "label": "Feet"},
|
||||||
|
]
|
||||||
|
|
||||||
|
var _models: Dictionary = {}
|
||||||
|
var _race_names: Array[String] = []
|
||||||
|
var _selected_race := ""
|
||||||
|
var _selected_gender := ""
|
||||||
|
var _selected_model: Dictionary = {}
|
||||||
|
|
||||||
|
var _viewport: SubViewport
|
||||||
|
var _preview_anchor: Node3D
|
||||||
|
var _camera: Camera3D
|
||||||
|
var _model_root: Node3D
|
||||||
|
var _controller: Node
|
||||||
|
var _compositor: Node
|
||||||
|
var _outfit_resolver: RefCounted
|
||||||
|
var _current_outfit: Dictionary = {}
|
||||||
|
|
||||||
|
var _race_option: OptionButton
|
||||||
|
var _gender_option: OptionButton
|
||||||
|
var _model_option: OptionButton
|
||||||
|
var _class_option: OptionButton
|
||||||
|
var _skin_spin: SpinBox
|
||||||
|
var _face_spin: SpinBox
|
||||||
|
var _hair_color_spin: SpinBox
|
||||||
|
var _flags_spin: SpinBox
|
||||||
|
var _rotation_slider: HSlider
|
||||||
|
var _status_label: Label
|
||||||
|
var _control_container: VBoxContainer
|
||||||
|
var _geoset_options: Dictionary = {}
|
||||||
|
var _toggle_checks: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_outfit_resolver = OUTFIT_RESOLVER_SCRIPT.new()
|
||||||
|
if not _outfit_resolver.call("load_dbcs", EXTRACTED_DIR):
|
||||||
|
push_warning("CharacterCreator: CharStartOutfit/ItemDisplayInfo DBCs not loaded")
|
||||||
|
_scan_models()
|
||||||
|
_build_ui()
|
||||||
|
_populate_races()
|
||||||
|
if not _race_names.is_empty():
|
||||||
|
_select_race(_race_names[0])
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if _preview_anchor:
|
||||||
|
_preview_anchor.rotation.y = deg_to_rad(float(_rotation_slider.value))
|
||||||
|
|
||||||
|
|
||||||
|
func _scan_models() -> void:
|
||||||
|
_models.clear()
|
||||||
|
_race_names.clear()
|
||||||
|
var root := DirAccess.open(CHARACTER_ROOT)
|
||||||
|
if root == null:
|
||||||
|
push_warning("CharacterCreator: missing character root: %s" % CHARACTER_ROOT)
|
||||||
|
return
|
||||||
|
|
||||||
|
root.list_dir_begin()
|
||||||
|
var race_dir := root.get_next()
|
||||||
|
while not race_dir.is_empty():
|
||||||
|
if root.current_is_dir() and not race_dir.begins_with("."):
|
||||||
|
_scan_race(race_dir)
|
||||||
|
race_dir = root.get_next()
|
||||||
|
root.list_dir_end()
|
||||||
|
_race_names.sort_custom(func(a: String, b: String) -> bool: return _display_name(a) < _display_name(b))
|
||||||
|
|
||||||
|
|
||||||
|
func _scan_race(race: String) -> void:
|
||||||
|
var race_path := CHARACTER_ROOT.path_join(race)
|
||||||
|
var race_access := DirAccess.open(race_path)
|
||||||
|
if race_access == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
var race_data: Dictionary = {}
|
||||||
|
race_access.list_dir_begin()
|
||||||
|
var gender_dir := race_access.get_next()
|
||||||
|
while not gender_dir.is_empty():
|
||||||
|
if race_access.current_is_dir() and not gender_dir.begins_with("."):
|
||||||
|
var models := _scan_gender_models(race, gender_dir)
|
||||||
|
if not models.is_empty():
|
||||||
|
race_data[gender_dir] = models
|
||||||
|
gender_dir = race_access.get_next()
|
||||||
|
race_access.list_dir_end()
|
||||||
|
|
||||||
|
if not race_data.is_empty():
|
||||||
|
_models[race] = race_data
|
||||||
|
_race_names.append(race)
|
||||||
|
|
||||||
|
|
||||||
|
func _scan_gender_models(race: String, gender: String) -> Array:
|
||||||
|
var result: Array = []
|
||||||
|
var gender_path := CHARACTER_ROOT.path_join(race).path_join(gender)
|
||||||
|
var access := DirAccess.open(gender_path)
|
||||||
|
if access == null:
|
||||||
|
return result
|
||||||
|
|
||||||
|
access.list_dir_begin()
|
||||||
|
var file := access.get_next()
|
||||||
|
while not file.is_empty():
|
||||||
|
if not access.current_is_dir() and file.get_extension().to_lower() == "glb":
|
||||||
|
var glb_path := gender_path.path_join(file)
|
||||||
|
result.append({
|
||||||
|
"race": race,
|
||||||
|
"gender": gender,
|
||||||
|
"name": file.get_basename(),
|
||||||
|
"path": glb_path,
|
||||||
|
"textures_dir": gender_path.path_join(file.get_basename() + "_textures"),
|
||||||
|
})
|
||||||
|
file = access.get_next()
|
||||||
|
access.list_dir_end()
|
||||||
|
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return String(a["name"]) < String(b["name"]))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
|
||||||
|
var root := HSplitContainer.new()
|
||||||
|
root.name = "Layout"
|
||||||
|
root.split_offset = 780
|
||||||
|
root.set_anchors_preset(Control.PRESET_FULL_RECT)
|
||||||
|
add_child(root)
|
||||||
|
|
||||||
|
var viewport_container := SubViewportContainer.new()
|
||||||
|
viewport_container.name = "Preview"
|
||||||
|
viewport_container.stretch = true
|
||||||
|
viewport_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
viewport_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
root.add_child(viewport_container)
|
||||||
|
|
||||||
|
_viewport = SubViewport.new()
|
||||||
|
_viewport.render_target_update_mode = SubViewport.UPDATE_ALWAYS
|
||||||
|
_viewport.size = Vector2i(900, 720)
|
||||||
|
viewport_container.add_child(_viewport)
|
||||||
|
_build_preview_world()
|
||||||
|
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.name = "Controls"
|
||||||
|
panel.custom_minimum_size = Vector2(360, 0)
|
||||||
|
root.add_child(panel)
|
||||||
|
|
||||||
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
panel.add_child(scroll)
|
||||||
|
|
||||||
|
_control_container = VBoxContainer.new()
|
||||||
|
_control_container.add_theme_constant_override("separation", 8)
|
||||||
|
scroll.add_child(_control_container)
|
||||||
|
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "Character Creator"
|
||||||
|
title.add_theme_font_size_override("font_size", 22)
|
||||||
|
_control_container.add_child(title)
|
||||||
|
|
||||||
|
_race_option = _add_option("Race", _on_race_selected)
|
||||||
|
_gender_option = _add_option("Gender", _on_gender_selected)
|
||||||
|
_model_option = _add_option("Model", _on_model_selected)
|
||||||
|
_class_option = _add_option("Class", _on_class_selected)
|
||||||
|
for class_label in CLASS_IDS.keys():
|
||||||
|
_class_option.add_item(class_label)
|
||||||
|
_class_option.set_item_metadata(_class_option.item_count - 1, int(CLASS_IDS[class_label]))
|
||||||
|
|
||||||
|
_skin_spin = _add_spin("Skin", 0, 0, 1, _on_skin_changed)
|
||||||
|
_face_spin = _add_spin("Face", 0, 0, 1, _on_face_changed)
|
||||||
|
_hair_color_spin = _add_spin("Hair Color", 0, 15, 1, _on_hair_color_changed)
|
||||||
|
_flags_spin = _add_spin("Flags", 0, 31, 1, _on_flags_changed)
|
||||||
|
|
||||||
|
for spec in GEOSET_CONTROLS:
|
||||||
|
var option := _add_option(String(spec["label"]), _on_geoset_selected.bind(String(spec["cat"])))
|
||||||
|
_geoset_options[String(spec["cat"])] = option
|
||||||
|
|
||||||
|
var equipment_label := Label.new()
|
||||||
|
equipment_label.text = "Equipment Geosets"
|
||||||
|
equipment_label.add_theme_font_size_override("font_size", 16)
|
||||||
|
_control_container.add_child(equipment_label)
|
||||||
|
|
||||||
|
var toggle_grid := GridContainer.new()
|
||||||
|
toggle_grid.columns = 2
|
||||||
|
_control_container.add_child(toggle_grid)
|
||||||
|
for spec in TOGGLE_CONTROLS:
|
||||||
|
var check := CheckBox.new()
|
||||||
|
check.text = String(spec["label"])
|
||||||
|
check.toggled.connect(_on_toggle_changed.bind(String(spec["property"])))
|
||||||
|
toggle_grid.add_child(check)
|
||||||
|
_toggle_checks[String(spec["property"])] = check
|
||||||
|
|
||||||
|
_rotation_slider = _add_slider("Rotation", -180.0, 180.0, 0.0)
|
||||||
|
|
||||||
|
var reload := Button.new()
|
||||||
|
reload.text = "Rescan Models"
|
||||||
|
reload.pressed.connect(_on_rescan_pressed)
|
||||||
|
_control_container.add_child(reload)
|
||||||
|
|
||||||
|
var starter := Button.new()
|
||||||
|
starter.text = "Apply Starter Outfit"
|
||||||
|
starter.pressed.connect(_apply_starter_outfit)
|
||||||
|
_control_container.add_child(starter)
|
||||||
|
|
||||||
|
_status_label = Label.new()
|
||||||
|
_status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
_status_label.text = ""
|
||||||
|
_control_container.add_child(_status_label)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_preview_world() -> void:
|
||||||
|
var world := Node3D.new()
|
||||||
|
world.name = "PreviewWorld"
|
||||||
|
_viewport.add_child(world)
|
||||||
|
|
||||||
|
var env := WorldEnvironment.new()
|
||||||
|
var environment := Environment.new()
|
||||||
|
environment.background_mode = Environment.BG_COLOR
|
||||||
|
environment.background_color = Color(0.04, 0.045, 0.055)
|
||||||
|
environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||||
|
environment.ambient_light_color = Color(0.55, 0.58, 0.62)
|
||||||
|
environment.ambient_light_energy = 1.0
|
||||||
|
env.environment = environment
|
||||||
|
world.add_child(env)
|
||||||
|
|
||||||
|
var key_light := DirectionalLight3D.new()
|
||||||
|
key_light.rotation_degrees = Vector3(-35, -35, 0)
|
||||||
|
key_light.light_energy = 2.4
|
||||||
|
world.add_child(key_light)
|
||||||
|
|
||||||
|
var fill_light := OmniLight3D.new()
|
||||||
|
fill_light.position = Vector3(-2.5, 1.8, 2.2)
|
||||||
|
fill_light.light_energy = 1.2
|
||||||
|
fill_light.omni_range = 5.0
|
||||||
|
world.add_child(fill_light)
|
||||||
|
|
||||||
|
_preview_anchor = Node3D.new()
|
||||||
|
_preview_anchor.name = "CharacterAnchor"
|
||||||
|
world.add_child(_preview_anchor)
|
||||||
|
|
||||||
|
_camera = Camera3D.new()
|
||||||
|
_camera.name = "Camera"
|
||||||
|
_camera.position = Vector3(0, 1.25, 3.2)
|
||||||
|
_camera.rotation_degrees = Vector3(-7, 0, 0)
|
||||||
|
_camera.fov = 38.0
|
||||||
|
_camera.current = true
|
||||||
|
world.add_child(_camera)
|
||||||
|
|
||||||
|
|
||||||
|
func _add_option(label_text: String, callback: Callable) -> OptionButton:
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = label_text
|
||||||
|
_control_container.add_child(label)
|
||||||
|
var option := OptionButton.new()
|
||||||
|
option.item_selected.connect(callback)
|
||||||
|
_control_container.add_child(option)
|
||||||
|
return option
|
||||||
|
|
||||||
|
|
||||||
|
func _add_spin(label_text: String, min_value: float, max_value: float, step: float, callback: Callable) -> SpinBox:
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = label_text
|
||||||
|
_control_container.add_child(label)
|
||||||
|
var spin := SpinBox.new()
|
||||||
|
spin.min_value = min_value
|
||||||
|
spin.max_value = max_value
|
||||||
|
spin.step = step
|
||||||
|
spin.value_changed.connect(callback)
|
||||||
|
_control_container.add_child(spin)
|
||||||
|
return spin
|
||||||
|
|
||||||
|
|
||||||
|
func _add_slider(label_text: String, min_value: float, max_value: float, value: float) -> HSlider:
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = label_text
|
||||||
|
_control_container.add_child(label)
|
||||||
|
var slider := HSlider.new()
|
||||||
|
slider.min_value = min_value
|
||||||
|
slider.max_value = max_value
|
||||||
|
slider.step = 1.0
|
||||||
|
slider.value = value
|
||||||
|
_control_container.add_child(slider)
|
||||||
|
return slider
|
||||||
|
|
||||||
|
|
||||||
|
func _populate_races() -> void:
|
||||||
|
_race_option.clear()
|
||||||
|
for race in _race_names:
|
||||||
|
_race_option.add_item(_display_name(race))
|
||||||
|
_race_option.set_item_metadata(_race_option.item_count - 1, race)
|
||||||
|
|
||||||
|
|
||||||
|
func _select_race(race: String) -> void:
|
||||||
|
_selected_race = race
|
||||||
|
_gender_option.clear()
|
||||||
|
var genders: Array[String] = []
|
||||||
|
for gender in (_models.get(race, {}) as Dictionary).keys():
|
||||||
|
genders.append(String(gender))
|
||||||
|
genders.sort()
|
||||||
|
for gender in genders:
|
||||||
|
_gender_option.add_item(_display_name(gender))
|
||||||
|
_gender_option.set_item_metadata(_gender_option.item_count - 1, gender)
|
||||||
|
if not genders.is_empty():
|
||||||
|
_select_gender(genders[0])
|
||||||
|
|
||||||
|
|
||||||
|
func _select_gender(gender: String) -> void:
|
||||||
|
_selected_gender = gender
|
||||||
|
_model_option.clear()
|
||||||
|
var models: Array = (_models.get(_selected_race, {}) as Dictionary).get(gender, [])
|
||||||
|
for model in models:
|
||||||
|
_model_option.add_item(String(model["name"]))
|
||||||
|
_model_option.set_item_metadata(_model_option.item_count - 1, model)
|
||||||
|
if not models.is_empty():
|
||||||
|
_select_model(models[0])
|
||||||
|
|
||||||
|
|
||||||
|
func _select_model(model: Dictionary) -> void:
|
||||||
|
_selected_model = model
|
||||||
|
_load_model(model)
|
||||||
|
|
||||||
|
|
||||||
|
func _load_model(model: Dictionary) -> void:
|
||||||
|
if _model_root and is_instance_valid(_model_root):
|
||||||
|
_model_root.queue_free()
|
||||||
|
_model_root = null
|
||||||
|
_controller = null
|
||||||
|
_compositor = null
|
||||||
|
|
||||||
|
var scene: PackedScene = load(String(model["path"]))
|
||||||
|
if scene == null:
|
||||||
|
_status_label.text = "Failed to load model: %s" % String(model["path"])
|
||||||
|
return
|
||||||
|
|
||||||
|
_model_root = Node3D.new()
|
||||||
|
_model_root.name = "%s_%s" % [String(model["race"]), String(model["gender"])]
|
||||||
|
_model_root.set_script(GEOSET_CONTROLLER_SCRIPT)
|
||||||
|
_model_root.rotation_degrees.y = CHARACTER_YAW_OFFSET_DEGREES
|
||||||
|
|
||||||
|
var instance := scene.instantiate()
|
||||||
|
instance.name = String(model["name"])
|
||||||
|
_model_root.add_child(instance)
|
||||||
|
|
||||||
|
var compositor := Node.new()
|
||||||
|
compositor.name = "CharacterTextureCompositor"
|
||||||
|
compositor.set_script(TEXTURE_COMPOSITOR_SCRIPT)
|
||||||
|
compositor.set("textures_dir", String(model["textures_dir"]))
|
||||||
|
compositor.set("extracted_dir", EXTRACTED_DIR)
|
||||||
|
_model_root.add_child(compositor)
|
||||||
|
|
||||||
|
_preview_anchor.add_child(_model_root)
|
||||||
|
_controller = _model_root
|
||||||
|
_compositor = compositor
|
||||||
|
_fit_camera_to_model()
|
||||||
|
await get_tree().process_frame
|
||||||
|
_refresh_dynamic_controls()
|
||||||
|
_apply_all_controls()
|
||||||
|
_apply_starter_outfit()
|
||||||
|
_update_status()
|
||||||
|
|
||||||
|
|
||||||
|
func _fit_camera_to_model() -> void:
|
||||||
|
if _model_root == null:
|
||||||
|
return
|
||||||
|
var aabb := _calculate_aabb(_model_root)
|
||||||
|
if aabb.size == Vector3.ZERO:
|
||||||
|
_camera.position = Vector3(0, 1.25, 3.2)
|
||||||
|
return
|
||||||
|
var center := aabb.get_center()
|
||||||
|
_model_root.position = Vector3(-center.x, -aabb.position.y, -center.z)
|
||||||
|
var height := maxf(aabb.size.y, 1.0)
|
||||||
|
_camera.position = Vector3(0, height * 0.55, height * 1.85)
|
||||||
|
_camera.look_at(Vector3(0, height * 0.52, 0), Vector3.UP)
|
||||||
|
|
||||||
|
|
||||||
|
func _calculate_aabb(root: Node) -> AABB:
|
||||||
|
var result := AABB()
|
||||||
|
var has_aabb := false
|
||||||
|
for mesh_inst in _iter_mesh_instances(root):
|
||||||
|
if mesh_inst.mesh == null:
|
||||||
|
continue
|
||||||
|
var local_aabb := mesh_inst.mesh.get_aabb()
|
||||||
|
var global_aabb := mesh_inst.global_transform * local_aabb
|
||||||
|
if not has_aabb:
|
||||||
|
result = global_aabb
|
||||||
|
has_aabb = true
|
||||||
|
else:
|
||||||
|
result = result.merge(global_aabb)
|
||||||
|
return result if has_aabb else AABB()
|
||||||
|
|
||||||
|
|
||||||
|
func _iter_mesh_instances(root: Node) -> Array[MeshInstance3D]:
|
||||||
|
var result: Array[MeshInstance3D] = []
|
||||||
|
_collect_mesh_instances(root, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
|
||||||
|
if node is MeshInstance3D:
|
||||||
|
result.append(node)
|
||||||
|
for child in node.get_children():
|
||||||
|
_collect_mesh_instances(child, result)
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_dynamic_controls() -> void:
|
||||||
|
if _controller == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
_skin_spin.max_value = maxi(0, int(_controller.call("get_skin_color_count")) - 1)
|
||||||
|
_face_spin.max_value = maxi(0, int(_controller.call("get_face_style_count")) - 1)
|
||||||
|
_skin_spin.value = clampf(_skin_spin.value, _skin_spin.min_value, _skin_spin.max_value)
|
||||||
|
_face_spin.value = clampf(_face_spin.value, _face_spin.min_value, _face_spin.max_value)
|
||||||
|
|
||||||
|
for spec in GEOSET_CONTROLS:
|
||||||
|
var cat := String(spec["cat"])
|
||||||
|
var option: OptionButton = _geoset_options[cat]
|
||||||
|
option.clear()
|
||||||
|
var ids: Array = _controller.call("get_ids", cat)
|
||||||
|
for id in ids:
|
||||||
|
option.add_item(str(id))
|
||||||
|
option.set_item_metadata(option.item_count - 1, int(id))
|
||||||
|
option.disabled = ids.is_empty()
|
||||||
|
if not ids.is_empty():
|
||||||
|
option.select(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_all_controls() -> void:
|
||||||
|
_on_skin_changed(_skin_spin.value)
|
||||||
|
_on_face_changed(_face_spin.value)
|
||||||
|
_on_hair_color_changed(_hair_color_spin.value)
|
||||||
|
_on_flags_changed(_flags_spin.value)
|
||||||
|
for cat in _geoset_options.keys():
|
||||||
|
var option: OptionButton = _geoset_options[cat]
|
||||||
|
if option.item_count > 0:
|
||||||
|
_apply_geoset(cat, int(option.get_item_metadata(option.selected)))
|
||||||
|
for property in _toggle_checks.keys():
|
||||||
|
var check: CheckBox = _toggle_checks[property]
|
||||||
|
_set_controller_property(property, check.button_pressed)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_race_selected(index: int) -> void:
|
||||||
|
_select_race(String(_race_option.get_item_metadata(index)))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_gender_selected(index: int) -> void:
|
||||||
|
_select_gender(String(_gender_option.get_item_metadata(index)))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_model_selected(index: int) -> void:
|
||||||
|
_select_model(_model_option.get_item_metadata(index))
|
||||||
|
|
||||||
|
|
||||||
|
func _on_class_selected(_index: int) -> void:
|
||||||
|
_apply_starter_outfit()
|
||||||
|
_update_status()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_skin_changed(value: float) -> void:
|
||||||
|
_set_controller_property("skin_color", int(value))
|
||||||
|
_update_status()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_face_changed(value: float) -> void:
|
||||||
|
_set_controller_property("face_style", int(value))
|
||||||
|
_update_status()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_hair_color_changed(_value: float) -> void:
|
||||||
|
_update_status()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_flags_changed(_value: float) -> void:
|
||||||
|
_update_status()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_geoset_selected(index: int, cat: String) -> void:
|
||||||
|
var option: OptionButton = _geoset_options[cat]
|
||||||
|
if index < 0 or index >= option.item_count:
|
||||||
|
return
|
||||||
|
_apply_geoset(cat, int(option.get_item_metadata(index)))
|
||||||
|
_update_status()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_toggle_changed(pressed: bool, property: String) -> void:
|
||||||
|
_set_controller_property(property, pressed)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_rescan_pressed() -> void:
|
||||||
|
_scan_models()
|
||||||
|
_populate_races()
|
||||||
|
if _models.has(_selected_race):
|
||||||
|
_select_race(_selected_race)
|
||||||
|
elif not _race_names.is_empty():
|
||||||
|
_select_race(_race_names[0])
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_starter_outfit() -> void:
|
||||||
|
_current_outfit.clear()
|
||||||
|
if _outfit_resolver == null or _controller == null or _compositor == null:
|
||||||
|
return
|
||||||
|
var class_id := int(_class_option.get_item_metadata(maxi(0, _class_option.selected)))
|
||||||
|
var outfit: Dictionary = _outfit_resolver.call(
|
||||||
|
"resolve_start_outfit",
|
||||||
|
_selected_race,
|
||||||
|
_selected_gender,
|
||||||
|
class_id)
|
||||||
|
if outfit.is_empty():
|
||||||
|
_update_status()
|
||||||
|
return
|
||||||
|
_current_outfit = outfit
|
||||||
|
if _compositor.has_method("set_equipment_components"):
|
||||||
|
_compositor.call("set_equipment_components", outfit.get("textures", PackedStringArray()))
|
||||||
|
if _controller.has_method("apply_outfit_defaults"):
|
||||||
|
_controller.call("apply_outfit_defaults", outfit)
|
||||||
|
_sync_outfit_controls()
|
||||||
|
_update_status()
|
||||||
|
|
||||||
|
|
||||||
|
func _sync_outfit_controls() -> void:
|
||||||
|
if _controller == null:
|
||||||
|
return
|
||||||
|
for property in _toggle_checks.keys():
|
||||||
|
var check: CheckBox = _toggle_checks[property]
|
||||||
|
check.set_pressed_no_signal(bool(_controller.get(property)))
|
||||||
|
var wrist: int = int(_controller.get("wristband_style"))
|
||||||
|
if _geoset_options.has("wristbands"):
|
||||||
|
_select_option_by_metadata(_geoset_options["wristbands"], wrist)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_geoset(cat: String, id: int) -> void:
|
||||||
|
match cat:
|
||||||
|
"hair":
|
||||||
|
_set_controller_property("hair_style", id)
|
||||||
|
"facial1":
|
||||||
|
_set_controller_property("facial1", id)
|
||||||
|
"facial2":
|
||||||
|
_set_controller_property("facial2", id)
|
||||||
|
"facial3":
|
||||||
|
_set_controller_property("facial3", id)
|
||||||
|
"ears":
|
||||||
|
_set_controller_property("ears", id)
|
||||||
|
"eye_effects":
|
||||||
|
_set_controller_property("eye_effects", id)
|
||||||
|
"wristbands":
|
||||||
|
_set_controller_property("wristband_style", id)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_controller_property(property: String, value: Variant) -> void:
|
||||||
|
if _controller == null:
|
||||||
|
return
|
||||||
|
_controller.set(property, value)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_status() -> void:
|
||||||
|
if _status_label == null:
|
||||||
|
return
|
||||||
|
var packed := _pack_appearance()
|
||||||
|
var equipment := _pack_equipment()
|
||||||
|
var outfit_display_count := 0
|
||||||
|
var outfit_texture_count := 0
|
||||||
|
if not _current_outfit.is_empty():
|
||||||
|
outfit_display_count = (_current_outfit.get("display_ids", PackedInt32Array()) as PackedInt32Array).size()
|
||||||
|
for rel_path in (_current_outfit.get("textures", PackedStringArray()) as PackedStringArray):
|
||||||
|
if not String(rel_path).is_empty():
|
||||||
|
outfit_texture_count += 1
|
||||||
|
_status_label.text = "Appearance 0x%08X\nEquipment 0x%08X\nStarter outfit: %d displays, %d textures\n%s / %s / %s" % [
|
||||||
|
packed,
|
||||||
|
equipment,
|
||||||
|
outfit_display_count,
|
||||||
|
outfit_texture_count,
|
||||||
|
_display_name(_selected_race),
|
||||||
|
_display_name(_selected_gender),
|
||||||
|
String(_selected_model.get("name", "")),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func _pack_appearance() -> int:
|
||||||
|
var skin := int(_skin_spin.value) & 0x1f
|
||||||
|
var face := int(_face_spin.value) & 0x1f
|
||||||
|
var hair := _selected_local_geoset_id("hair", 1) & 0x1f
|
||||||
|
var hair_color := int(_hair_color_spin.value) & 0x0f
|
||||||
|
var facial := _selected_local_geoset_id("facial1", 101) & 0x0f
|
||||||
|
var class_id := int(_class_option.get_item_metadata(maxi(0, _class_option.selected))) & 0x0f
|
||||||
|
var flags := int(_flags_spin.value) & 0x1f
|
||||||
|
return skin | (face << 5) | (hair << 10) | (hair_color << 15) | (facial << 19) | (class_id << 23) | (flags << 27)
|
||||||
|
|
||||||
|
|
||||||
|
func _pack_equipment() -> int:
|
||||||
|
var upper := 1 if bool((_toggle_checks.get("show_chest") as CheckBox).button_pressed) else 0
|
||||||
|
var lower := 1 if bool((_toggle_checks.get("show_pants") as CheckBox).button_pressed) else 0
|
||||||
|
var hands := 1 if bool((_toggle_checks.get("show_gloves") as CheckBox).button_pressed) else 0
|
||||||
|
var feet := 1 if bool((_toggle_checks.get("show_boots") as CheckBox).button_pressed) else 0
|
||||||
|
return upper | (lower << 8) | (hands << 16) | (feet << 24)
|
||||||
|
|
||||||
|
|
||||||
|
func _selected_local_geoset_id(cat: String, base_id: int) -> int:
|
||||||
|
if not _geoset_options.has(cat):
|
||||||
|
return 0
|
||||||
|
var option: OptionButton = _geoset_options[cat]
|
||||||
|
if option.item_count <= 0 or option.selected < 0:
|
||||||
|
return 0
|
||||||
|
return maxi(0, int(option.get_item_metadata(option.selected)) - base_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _select_option_by_metadata(option: OptionButton, metadata: int) -> void:
|
||||||
|
for i in option.item_count:
|
||||||
|
if int(option.get_item_metadata(i)) == metadata:
|
||||||
|
option.select(i)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
func _display_name(value: String) -> String:
|
||||||
|
var s := value.replace("_", " ").to_lower()
|
||||||
|
var parts := s.split(" ", false)
|
||||||
|
for i in parts.size():
|
||||||
|
parts[i] = String(parts[i]).capitalize()
|
||||||
|
return " ".join(parts)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cwvvg0npx5vlr
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://src/scenes/character/character_creator.gd" id="1_creator"]
|
||||||
|
|
||||||
|
[node name="CharacterCreator" type="Control"]
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_creator")
|
||||||
@@ -181,7 +181,7 @@ func _apply_variant(cat: String, wanted: int) -> void:
|
|||||||
for child in _geoset_nodes():
|
for child in _geoset_nodes():
|
||||||
var gid := _geoset_id(child.name)
|
var gid := _geoset_id(child.name)
|
||||||
if _category(gid) == cat:
|
if _category(gid) == cat:
|
||||||
child.visible = (gid == wanted)
|
child.visible = (gid == wanted) and _geoset_renderable(child, cat)
|
||||||
|
|
||||||
## Show or hide all geosets in a category.
|
## Show or hide all geosets in a category.
|
||||||
func _apply_toggle(cat: String, on: bool) -> void:
|
func _apply_toggle(cat: String, on: bool) -> void:
|
||||||
@@ -204,7 +204,7 @@ func _apply_all(nodes: Array[Node] = []) -> void:
|
|||||||
"facial2": child.visible = (gid == facial2)
|
"facial2": child.visible = (gid == facial2)
|
||||||
"facial3": child.visible = (gid == facial3)
|
"facial3": child.visible = (gid == facial3)
|
||||||
"ears": child.visible = (gid == ears)
|
"ears": child.visible = (gid == ears)
|
||||||
"eye_effects": child.visible = (gid == eye_effects)
|
"eye_effects": child.visible = (gid == eye_effects) and _geoset_renderable(child, cat)
|
||||||
"gloves": child.visible = show_gloves
|
"gloves": child.visible = show_gloves
|
||||||
"boots": child.visible = show_boots
|
"boots": child.visible = show_boots
|
||||||
"shirt": child.visible = show_shirt
|
"shirt": child.visible = show_shirt
|
||||||
@@ -301,17 +301,19 @@ func _apply_eye_glow_shader(nodes: Array[Node] = []) -> void:
|
|||||||
if not is_eye_glow:
|
if not is_eye_glow:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Replace every surface material with the glow shader
|
if not _geoset_renderable(mesh_inst, "eye_effects"):
|
||||||
for si in range(mesh_inst.get_surface_override_material_count()):
|
mesh_inst.visible = false
|
||||||
var orig_mat := mesh_inst.mesh.surface_get_material(si)
|
continue
|
||||||
|
|
||||||
|
for si in range(mesh_inst.mesh.get_surface_count()):
|
||||||
|
var orig_mat := _surface_material(mesh_inst, si)
|
||||||
|
if not _material_has_albedo_texture(orig_mat):
|
||||||
|
continue
|
||||||
var shader_mat := ShaderMaterial.new()
|
var shader_mat := ShaderMaterial.new()
|
||||||
shader_mat.shader = EYE_GLOW_SHADER
|
shader_mat.shader = EYE_GLOW_SHADER
|
||||||
shader_mat.set_shader_parameter("glow_color", Vector3(glow_color.r, glow_color.g, glow_color.b))
|
shader_mat.set_shader_parameter("glow_color", Vector3(glow_color.r, glow_color.g, glow_color.b))
|
||||||
shader_mat.set_shader_parameter("glow_intensity", eye_glow_intensity)
|
shader_mat.set_shader_parameter("glow_intensity", eye_glow_intensity)
|
||||||
# Carry over the original albedo texture if it was a StandardMaterial3D
|
|
||||||
if orig_mat is StandardMaterial3D:
|
|
||||||
var std := orig_mat as StandardMaterial3D
|
var std := orig_mat as StandardMaterial3D
|
||||||
if std.albedo_texture:
|
|
||||||
shader_mat.set_shader_parameter("albedo_texture", std.albedo_texture)
|
shader_mat.set_shader_parameter("albedo_texture", std.albedo_texture)
|
||||||
mesh_inst.set_surface_override_material(si, shader_mat)
|
mesh_inst.set_surface_override_material(si, shader_mat)
|
||||||
|
|
||||||
@@ -338,6 +340,34 @@ func get_skin_color_count() -> int: return _skin_color_count
|
|||||||
## Returns valid face style count for this model (0 if compositor not found yet).
|
## Returns valid face style count for this model (0 if compositor not found yet).
|
||||||
func get_face_style_count() -> int: return _face_style_count
|
func get_face_style_count() -> int: return _face_style_count
|
||||||
|
|
||||||
|
|
||||||
|
func apply_outfit_defaults(outfit: Dictionary = {}) -> void:
|
||||||
|
show_gloves = true
|
||||||
|
show_boots = true
|
||||||
|
show_shirt = false
|
||||||
|
show_kneepads = true
|
||||||
|
show_chest = false
|
||||||
|
show_pants = false
|
||||||
|
show_tabard = false
|
||||||
|
show_legs = (int(outfit.get("flags", 0)) & 0x4) == 0
|
||||||
|
show_cape = true
|
||||||
|
show_belt = false
|
||||||
|
show_feet = false
|
||||||
|
wristband_style = _clamp_geoset("wristbands", 802)
|
||||||
|
_apply_exact_or_first("gloves", 401)
|
||||||
|
_apply_exact_or_first("boots", 501)
|
||||||
|
_apply_exact_or_first("ears", 702)
|
||||||
|
_apply_exact_or_first("wristbands", wristband_style)
|
||||||
|
_apply_exact_or_first("kneepads", 902)
|
||||||
|
_apply_exact_or_first("legs", 1301)
|
||||||
|
_apply_exact_or_first("cape", 1501)
|
||||||
|
_apply_toggle("shirt", false)
|
||||||
|
_apply_toggle("chest", false)
|
||||||
|
_apply_toggle("pants", false)
|
||||||
|
_apply_toggle("tabard", false)
|
||||||
|
_apply_toggle("belt", false)
|
||||||
|
_apply_toggle("feet", false)
|
||||||
|
|
||||||
## Clamp/snap v to the nearest available geoset ID in cat.
|
## Clamp/snap v to the nearest available geoset ID in cat.
|
||||||
## Returns v unchanged if _available is not populated yet or cat is missing.
|
## Returns v unchanged if _available is not populated yet or cat is missing.
|
||||||
func _clamp_geoset(cat: String, v: int) -> int:
|
func _clamp_geoset(cat: String, v: int) -> int:
|
||||||
@@ -357,3 +387,31 @@ func _clamp_geoset(cat: String, v: int) -> int:
|
|||||||
best_dist = d
|
best_dist = d
|
||||||
best = id
|
best = id
|
||||||
return best
|
return best
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_exact_or_first(cat: String, wanted: int) -> void:
|
||||||
|
var id := _clamp_geoset(cat, wanted)
|
||||||
|
_apply_variant(cat, id)
|
||||||
|
|
||||||
|
|
||||||
|
func _geoset_renderable(node: Node, cat: String) -> bool:
|
||||||
|
if cat != "eye_effects":
|
||||||
|
return true
|
||||||
|
var mesh_inst := node as MeshInstance3D
|
||||||
|
if mesh_inst == null or mesh_inst.mesh == null:
|
||||||
|
return false
|
||||||
|
for si in range(mesh_inst.mesh.get_surface_count()):
|
||||||
|
if _material_has_albedo_texture(_surface_material(mesh_inst, si)):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _surface_material(mesh_inst: MeshInstance3D, surface: int) -> Material:
|
||||||
|
var mat := mesh_inst.get_surface_override_material(surface)
|
||||||
|
if mat == null and mesh_inst.mesh != null:
|
||||||
|
mat = mesh_inst.mesh.surface_get_material(surface)
|
||||||
|
return mat
|
||||||
|
|
||||||
|
|
||||||
|
func _material_has_albedo_texture(mat: Material) -> bool:
|
||||||
|
return mat is StandardMaterial3D and (mat as StandardMaterial3D).albedo_texture != null
|
||||||
|
|||||||
@@ -20,6 +20,16 @@ const NAKED_TORSO_POS := Vector2i(0, 0)
|
|||||||
const NAKED_PELVIS_POS := Vector2i(0, 128)
|
const NAKED_PELVIS_POS := Vector2i(0, 128)
|
||||||
const FACE_UPPER_POS := Vector2i(0, 320)
|
const FACE_UPPER_POS := Vector2i(0, 320)
|
||||||
const FACE_LOWER_POS := Vector2i(0, 384)
|
const FACE_LOWER_POS := Vector2i(0, 384)
|
||||||
|
const COMPONENT_RECTS := [
|
||||||
|
Rect2i(0, 0, 256, 128),
|
||||||
|
Rect2i(0, 128, 256, 128),
|
||||||
|
Rect2i(0, 256, 256, 64),
|
||||||
|
Rect2i(256, 0, 256, 128),
|
||||||
|
Rect2i(256, 128, 256, 64),
|
||||||
|
Rect2i(256, 192, 256, 128),
|
||||||
|
Rect2i(256, 320, 256, 128),
|
||||||
|
Rect2i(256, 448, 256, 64),
|
||||||
|
]
|
||||||
|
|
||||||
# ── Exports ──────────────────────────────────────────────────────────────────
|
# ── Exports ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -27,6 +37,9 @@ const FACE_LOWER_POS := Vector2i(0, 384)
|
|||||||
## Example: "res://src/resources/characters/BloodElf/Female/BloodElfFemale_textures"
|
## Example: "res://src/resources/characters/BloodElf/Female/BloodElfFemale_textures"
|
||||||
@export var textures_dir: String = "" : set = _set_textures_dir
|
@export var textures_dir: String = "" : set = _set_textures_dir
|
||||||
|
|
||||||
|
## Root folder containing extracted WoW data. Used for DBC item component BLPs.
|
||||||
|
@export var extracted_dir: String = "res://data/extracted"
|
||||||
|
|
||||||
## Skin colour index (selects skin_XX.png / naked_torso_XX.png /
|
## Skin colour index (selects skin_XX.png / naked_torso_XX.png /
|
||||||
## naked_pelvis_XX.png).
|
## naked_pelvis_XX.png).
|
||||||
@export var skin_color: int = 0 : set = _set_skin_color
|
@export var skin_color: int = 0 : set = _set_skin_color
|
||||||
@@ -38,6 +51,8 @@ const FACE_LOWER_POS := Vector2i(0, 384)
|
|||||||
|
|
||||||
# MeshInstance3D surfaces to override: Array of {node, surface_idx}
|
# MeshInstance3D surfaces to override: Array of {node, surface_idx}
|
||||||
var _skin_surfaces: Array = []
|
var _skin_surfaces: Array = []
|
||||||
|
var _equipment_components := PackedStringArray()
|
||||||
|
var _blp_image_cache: Dictionary = {}
|
||||||
|
|
||||||
# True once _ready has run (prevents setters from firing before init)
|
# True once _ready has run (prevents setters from firing before init)
|
||||||
var _ready_done := false
|
var _ready_done := false
|
||||||
@@ -111,7 +126,7 @@ func _scan_skin_surfaces() -> void:
|
|||||||
|
|
||||||
## A surface is a "skin surface" if:
|
## A surface is a "skin surface" if:
|
||||||
## - its active material is a StandardMaterial3D (not ShaderMaterial = eye glow)
|
## - its active material is a StandardMaterial3D (not ShaderMaterial = eye glow)
|
||||||
## - AND its albedo texture is 512x512 (the composited body skin)
|
## - AND its albedo texture is square body texture, usually 256x256 or 512x512
|
||||||
func _is_skin_surface(mesh_inst: MeshInstance3D, si: int) -> bool:
|
func _is_skin_surface(mesh_inst: MeshInstance3D, si: int) -> bool:
|
||||||
# Prefer surface override, fall back to mesh material
|
# Prefer surface override, fall back to mesh material
|
||||||
var mat := mesh_inst.get_surface_override_material(si)
|
var mat := mesh_inst.get_surface_override_material(si)
|
||||||
@@ -123,11 +138,10 @@ func _is_skin_surface(mesh_inst: MeshInstance3D, si: int) -> bool:
|
|||||||
var tex := std.albedo_texture
|
var tex := std.albedo_texture
|
||||||
if tex == null:
|
if tex == null:
|
||||||
return false
|
return false
|
||||||
# Skin texture is 512x512; hair/other textures are smaller
|
|
||||||
var img := tex.get_image()
|
var img := tex.get_image()
|
||||||
if img == null:
|
if img == null:
|
||||||
return false
|
return false
|
||||||
return img.get_width() == 512 and img.get_height() == 512
|
return img.get_width() == img.get_height() and img.get_width() >= 256
|
||||||
|
|
||||||
|
|
||||||
# ── Compositing ──────────────────────────────────────────────────────────────
|
# ── Compositing ──────────────────────────────────────────────────────────────
|
||||||
@@ -151,13 +165,20 @@ func _build_skin_texture() -> ImageTexture:
|
|||||||
return null
|
return null
|
||||||
|
|
||||||
# 2. Naked overlays (underwear)
|
# 2. Naked overlays (underwear)
|
||||||
_blit(base, _load_layer("naked_torso_%02d.png" % skin_color), NAKED_TORSO_POS)
|
_blit_scaled(base, _load_layer("naked_torso_%02d.png" % skin_color), NAKED_TORSO_POS)
|
||||||
_blit(base, _load_layer("naked_pelvis_%02d.png" % skin_color), NAKED_PELVIS_POS)
|
_blit_scaled(base, _load_layer("naked_pelvis_%02d.png" % skin_color), NAKED_PELVIS_POS)
|
||||||
|
|
||||||
# 3. Face
|
# 3. Face
|
||||||
_blit(base, _load_layer("face_upper_%02d_%02d.png" % [face_style, skin_color]), FACE_UPPER_POS)
|
_blit_scaled(base, _load_layer("face_upper_%02d_%02d.png" % [face_style, skin_color]), FACE_UPPER_POS)
|
||||||
_blit(base, _load_layer("face_lower_%02d_%02d.png" % [face_style, skin_color]), FACE_LOWER_POS)
|
_blit_scaled(base, _load_layer("face_lower_%02d_%02d.png" % [face_style, skin_color]), FACE_LOWER_POS)
|
||||||
|
|
||||||
|
for slot in mini(_equipment_components.size(), COMPONENT_RECTS.size()):
|
||||||
|
var rel_path := String(_equipment_components[slot])
|
||||||
|
if rel_path.is_empty():
|
||||||
|
continue
|
||||||
|
_blit_reference_rect(base, _load_blp_layer(rel_path), COMPONENT_RECTS[slot])
|
||||||
|
|
||||||
|
base.generate_mipmaps()
|
||||||
return ImageTexture.create_from_image(base)
|
return ImageTexture.create_from_image(base)
|
||||||
|
|
||||||
|
|
||||||
@@ -184,10 +205,15 @@ func _apply_texture(tex: ImageTexture) -> void:
|
|||||||
|
|
||||||
func _load_layer(filename: String) -> Image:
|
func _load_layer(filename: String) -> Image:
|
||||||
var path := textures_dir.path_join(filename)
|
var path := textures_dir.path_join(filename)
|
||||||
if not FileAccess.file_exists(path):
|
if not ResourceLoader.exists(path) and not FileAccess.file_exists(path):
|
||||||
|
return null
|
||||||
|
var texture := ResourceLoader.load(path, "Texture2D", ResourceLoader.CACHE_MODE_REUSE) as Texture2D
|
||||||
|
if texture != null:
|
||||||
|
var image := texture.get_image()
|
||||||
|
return image.duplicate() if image != null else null
|
||||||
|
if FileAccess.file_exists(path):
|
||||||
|
return Image.load_from_file(path)
|
||||||
return null
|
return null
|
||||||
var img := Image.load_from_file(path)
|
|
||||||
return img
|
|
||||||
|
|
||||||
|
|
||||||
func _blit(dst: Image, src: Image, pos: Vector2i) -> void:
|
func _blit(dst: Image, src: Image, pos: Vector2i) -> void:
|
||||||
@@ -196,6 +222,58 @@ func _blit(dst: Image, src: Image, pos: Vector2i) -> void:
|
|||||||
dst.blend_rect(src, Rect2i(Vector2i.ZERO, src.get_size()), pos)
|
dst.blend_rect(src, Rect2i(Vector2i.ZERO, src.get_size()), pos)
|
||||||
|
|
||||||
|
|
||||||
|
func _blit_scaled(dst: Image, src: Image, reference_pos: Vector2i) -> void:
|
||||||
|
if src == null:
|
||||||
|
return
|
||||||
|
var scale := float(dst.get_width()) / 512.0
|
||||||
|
var target_pos := Vector2i(
|
||||||
|
int(round(float(reference_pos.x) * scale)),
|
||||||
|
int(round(float(reference_pos.y) * scale)))
|
||||||
|
if is_equal_approx(scale, 1.0):
|
||||||
|
_blit(dst, src, target_pos)
|
||||||
|
return
|
||||||
|
var target_size := Vector2i(
|
||||||
|
maxi(1, int(round(float(src.get_width()) * scale))),
|
||||||
|
maxi(1, int(round(float(src.get_height()) * scale))))
|
||||||
|
var scaled := src.duplicate()
|
||||||
|
scaled.resize(target_size.x, target_size.y, Image.INTERPOLATE_LANCZOS)
|
||||||
|
_blit(dst, scaled, target_pos)
|
||||||
|
|
||||||
|
|
||||||
|
func _blit_reference_rect(dst: Image, src: Image, reference_rect: Rect2i) -> void:
|
||||||
|
if src == null:
|
||||||
|
return
|
||||||
|
var scale_x := float(dst.get_width()) / 512.0
|
||||||
|
var scale_y := float(dst.get_height()) / 512.0
|
||||||
|
var target_rect := Rect2i(
|
||||||
|
Vector2i(
|
||||||
|
int(round(float(reference_rect.position.x) * scale_x)),
|
||||||
|
int(round(float(reference_rect.position.y) * scale_y))),
|
||||||
|
Vector2i(
|
||||||
|
maxi(1, int(round(float(reference_rect.size.x) * scale_x))),
|
||||||
|
maxi(1, int(round(float(reference_rect.size.y) * scale_y)))))
|
||||||
|
var scaled := src.duplicate()
|
||||||
|
scaled.resize(target_rect.size.x, target_rect.size.y, Image.INTERPOLATE_LANCZOS)
|
||||||
|
_blit(dst, scaled, target_rect.position)
|
||||||
|
|
||||||
|
|
||||||
|
func _load_blp_layer(rel_path: String) -> Image:
|
||||||
|
if rel_path.is_empty():
|
||||||
|
return null
|
||||||
|
if _blp_image_cache.has(rel_path):
|
||||||
|
return (_blp_image_cache[rel_path] as Image).duplicate() if _blp_image_cache[rel_path] != null else null
|
||||||
|
if not ClassDB.class_exists("BLPLoader"):
|
||||||
|
_blp_image_cache[rel_path] = null
|
||||||
|
return null
|
||||||
|
var abs_path := ProjectSettings.globalize_path(extracted_dir.trim_suffix("/").path_join(rel_path.replace("\\", "/")))
|
||||||
|
if not FileAccess.file_exists(abs_path):
|
||||||
|
_blp_image_cache[rel_path] = null
|
||||||
|
return null
|
||||||
|
var img: Image = ClassDB.instantiate("BLPLoader").call("load_image", abs_path)
|
||||||
|
_blp_image_cache[rel_path] = img
|
||||||
|
return img.duplicate() if img != null else null
|
||||||
|
|
||||||
|
|
||||||
func _iter_mesh_instances(root: Node) -> Array[MeshInstance3D]:
|
func _iter_mesh_instances(root: Node) -> Array[MeshInstance3D]:
|
||||||
var result: Array[MeshInstance3D] = []
|
var result: Array[MeshInstance3D] = []
|
||||||
_collect_meshes(root, result)
|
_collect_meshes(root, result)
|
||||||
@@ -217,6 +295,11 @@ func refresh() -> void:
|
|||||||
_recomposite()
|
_recomposite()
|
||||||
|
|
||||||
|
|
||||||
|
func set_equipment_components(components: PackedStringArray) -> void:
|
||||||
|
_equipment_components = components
|
||||||
|
_recomposite()
|
||||||
|
|
||||||
|
|
||||||
## Returns the number of skin surfaces found (useful for debugging).
|
## Returns the number of skin surfaces found (useful for debugging).
|
||||||
func get_skin_surface_count() -> int:
|
func get_skin_surface_count() -> int:
|
||||||
return _skin_surfaces.size()
|
return _skin_surfaces.size()
|
||||||
|
|||||||
@@ -0,0 +1,252 @@
|
|||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const SLOT_UPPER_ARM := 0
|
||||||
|
const SLOT_LOWER_ARM := 1
|
||||||
|
const SLOT_HAND := 2
|
||||||
|
const SLOT_UPPER_TORSO := 3
|
||||||
|
const SLOT_LOWER_TORSO := 4
|
||||||
|
const SLOT_UPPER_LEG := 5
|
||||||
|
const SLOT_LOWER_LEG := 6
|
||||||
|
const SLOT_FOOT := 7
|
||||||
|
const SLOT_COUNT := 8
|
||||||
|
|
||||||
|
const COMPONENT_FOLDERS := [
|
||||||
|
"ArmUpperTexture",
|
||||||
|
"ArmLowerTexture",
|
||||||
|
"HandTexture",
|
||||||
|
"TorsoUpperTexture",
|
||||||
|
"TorsoLowerTexture",
|
||||||
|
"LegUpperTexture",
|
||||||
|
"LegLowerTexture",
|
||||||
|
"FootTexture",
|
||||||
|
]
|
||||||
|
|
||||||
|
const RACE_IDS := {
|
||||||
|
"human": 1,
|
||||||
|
"orc": 2,
|
||||||
|
"dwarf": 3,
|
||||||
|
"nightelf": 4,
|
||||||
|
"scourge": 5,
|
||||||
|
"undead": 5,
|
||||||
|
"tauren": 6,
|
||||||
|
"gnome": 7,
|
||||||
|
"troll": 8,
|
||||||
|
"bloodelf": 10,
|
||||||
|
"draenei": 11,
|
||||||
|
}
|
||||||
|
|
||||||
|
var extracted_dir := "res://data/extracted"
|
||||||
|
var _char_start := {}
|
||||||
|
var _item_display := {}
|
||||||
|
var _item_display_by_id := {}
|
||||||
|
var _texture_cache := {}
|
||||||
|
|
||||||
|
|
||||||
|
func load_dbcs(p_extracted_dir: String) -> bool:
|
||||||
|
extracted_dir = p_extracted_dir.trim_suffix("/")
|
||||||
|
var base := extracted_dir.path_join("DBFilesClient")
|
||||||
|
_char_start = _load_wdbc(base.path_join("CharStartOutfit.dbc"))
|
||||||
|
_item_display = _load_wdbc(base.path_join("ItemDisplayInfo.dbc"))
|
||||||
|
_index_item_display()
|
||||||
|
return not _char_start.is_empty() and not _item_display.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func resolve_start_outfit(race_name: String, gender_name: String, class_id: int) -> Dictionary:
|
||||||
|
if _char_start.is_empty() or _item_display.is_empty():
|
||||||
|
return {}
|
||||||
|
var race_id := race_id_for_name(race_name)
|
||||||
|
var gender_id := gender_id_for_name(gender_name)
|
||||||
|
if race_id <= 0 or gender_id < 0:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
var wanted_key := race_id | ((class_id & 0xff) << 8) | ((gender_id & 0xff) << 16)
|
||||||
|
for i in int(_char_start["records"]):
|
||||||
|
if int(_dbc_u32(_char_start, i, 1)) != wanted_key:
|
||||||
|
continue
|
||||||
|
return _resolve_start_record(i, gender_id)
|
||||||
|
for i in int(_char_start["records"]):
|
||||||
|
var race_class_gender := int(_dbc_u32(_char_start, i, 1))
|
||||||
|
if (race_class_gender & 0xff) == race_id and ((race_class_gender >> 16) & 0xff) == gender_id:
|
||||||
|
return _resolve_start_record(i, gender_id)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func _resolve_start_record(record: int, gender_id: int) -> Dictionary:
|
||||||
|
var display_start := _char_start_display_field()
|
||||||
|
var display_count := mini(24, int(_char_start["fields"]) - display_start)
|
||||||
|
var display_ids := PackedInt32Array()
|
||||||
|
for n in display_count:
|
||||||
|
var display_id := int(_dbc_u32(_char_start, record, display_start + n))
|
||||||
|
if display_id > 0 and display_id != 0xffffffff:
|
||||||
|
display_ids.append(display_id)
|
||||||
|
var outfit := resolve_display_ids(display_ids, gender_id)
|
||||||
|
outfit["source_record"] = record
|
||||||
|
outfit["source_class"] = (int(_dbc_u32(_char_start, record, 1)) >> 8) & 0xff
|
||||||
|
return outfit
|
||||||
|
|
||||||
|
|
||||||
|
func resolve_display_ids(display_ids: PackedInt32Array, gender_id: int) -> Dictionary:
|
||||||
|
var textures := PackedStringArray()
|
||||||
|
textures.resize(SLOT_COUNT)
|
||||||
|
var geosets := PackedInt32Array()
|
||||||
|
geosets.resize(3)
|
||||||
|
var flags := 0
|
||||||
|
var used_display_ids := PackedInt32Array()
|
||||||
|
|
||||||
|
var texture_base := _item_display_texture_base()
|
||||||
|
var geoset_base := _item_display_geoset_base()
|
||||||
|
var flags_field := _item_display_flags_field()
|
||||||
|
for display_id in display_ids:
|
||||||
|
var record := int(_item_display_by_id.get(display_id, -1))
|
||||||
|
if record < 0:
|
||||||
|
continue
|
||||||
|
used_display_ids.append(display_id)
|
||||||
|
for i in 3:
|
||||||
|
var geoset_group := int(_dbc_u32(_item_display, record, geoset_base + i))
|
||||||
|
if geoset_group != 0:
|
||||||
|
geosets[i] = geoset_group
|
||||||
|
flags |= int(_dbc_u32(_item_display, record, flags_field))
|
||||||
|
for slot in SLOT_COUNT:
|
||||||
|
var stem := _dbc_string(_item_display, record, texture_base + slot)
|
||||||
|
if stem.is_empty():
|
||||||
|
continue
|
||||||
|
var rel_path := component_texture_path(stem, slot, gender_id)
|
||||||
|
if not rel_path.is_empty():
|
||||||
|
textures[slot] = rel_path
|
||||||
|
|
||||||
|
return {
|
||||||
|
"display_ids": used_display_ids,
|
||||||
|
"textures": textures,
|
||||||
|
"geosets": geosets,
|
||||||
|
"flags": flags,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func component_texture_path(stem: String, slot: int, gender_id: int) -> String:
|
||||||
|
if stem.is_empty() or slot < 0 or slot >= SLOT_COUNT:
|
||||||
|
return ""
|
||||||
|
var suffix := "F" if gender_id == 1 else "M"
|
||||||
|
var folder := String(COMPONENT_FOLDERS[slot])
|
||||||
|
var candidates := [
|
||||||
|
"ITEM/TEXTURECOMPONENTS/%s/%s_%s.blp" % [folder, stem, suffix],
|
||||||
|
"Item/TextureComponents/%s/%s_%s.blp" % [folder, stem, suffix],
|
||||||
|
"ITEM/TEXTURECOMPONENTS/%s/%s_U.blp" % [folder, stem],
|
||||||
|
"Item/TextureComponents/%s/%s_U.blp" % [folder, stem],
|
||||||
|
]
|
||||||
|
for rel_path in candidates:
|
||||||
|
if _texture_exists(rel_path):
|
||||||
|
return rel_path
|
||||||
|
return candidates[0]
|
||||||
|
|
||||||
|
|
||||||
|
func race_id_for_name(race_name: String) -> int:
|
||||||
|
var normalized := race_name.replace("_", "").replace(" ", "").to_lower()
|
||||||
|
return int(RACE_IDS.get(normalized, 0))
|
||||||
|
|
||||||
|
|
||||||
|
func gender_id_for_name(gender_name: String) -> int:
|
||||||
|
var normalized := gender_name.to_lower()
|
||||||
|
if normalized == "male" or normalized == "m":
|
||||||
|
return 0
|
||||||
|
if normalized == "female" or normalized == "f":
|
||||||
|
return 1
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
func infer_race_gender_from_model_path(model_path: String) -> Dictionary:
|
||||||
|
var parts := model_path.replace("\\", "/").split("/", false)
|
||||||
|
for i in range(parts.size() - 2):
|
||||||
|
if String(parts[i]).to_lower() == "characters":
|
||||||
|
return {"race": String(parts[i + 1]), "gender": String(parts[i + 2])}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
func _index_item_display() -> void:
|
||||||
|
_item_display_by_id.clear()
|
||||||
|
if _item_display.is_empty():
|
||||||
|
return
|
||||||
|
for i in int(_item_display["records"]):
|
||||||
|
var display_id := int(_dbc_u32(_item_display, i, 0))
|
||||||
|
if display_id > 0:
|
||||||
|
_item_display_by_id[display_id] = i
|
||||||
|
|
||||||
|
|
||||||
|
func _char_start_display_field() -> int:
|
||||||
|
if int(_char_start.get("fields", 0)) >= 77:
|
||||||
|
return 26
|
||||||
|
return 14
|
||||||
|
|
||||||
|
|
||||||
|
func _item_display_texture_base() -> int:
|
||||||
|
return 15 if int(_item_display.get("fields", 0)) >= 25 else 14
|
||||||
|
|
||||||
|
|
||||||
|
func _item_display_geoset_base() -> int:
|
||||||
|
return 7 if int(_item_display.get("fields", 0)) >= 25 else 6
|
||||||
|
|
||||||
|
|
||||||
|
func _item_display_flags_field() -> int:
|
||||||
|
return 10 if int(_item_display.get("fields", 0)) >= 25 else 9
|
||||||
|
|
||||||
|
|
||||||
|
func _texture_exists(rel_path: String) -> bool:
|
||||||
|
if _texture_cache.has(rel_path):
|
||||||
|
return bool(_texture_cache[rel_path])
|
||||||
|
var exists := FileAccess.file_exists(ProjectSettings.globalize_path(extracted_dir.path_join(rel_path)))
|
||||||
|
_texture_cache[rel_path] = exists
|
||||||
|
return exists
|
||||||
|
|
||||||
|
|
||||||
|
func _load_wdbc(path: String) -> Dictionary:
|
||||||
|
var abs_path := ProjectSettings.globalize_path(path)
|
||||||
|
if not FileAccess.file_exists(abs_path):
|
||||||
|
return {}
|
||||||
|
var file := FileAccess.open(abs_path, FileAccess.READ)
|
||||||
|
if not file:
|
||||||
|
return {}
|
||||||
|
var bytes := file.get_buffer(file.get_length())
|
||||||
|
if bytes.size() < 20 or bytes[0] != 0x57 or bytes[1] != 0x44 or bytes[2] != 0x42 or bytes[3] != 0x43:
|
||||||
|
return {}
|
||||||
|
var records := int(bytes.decode_u32(4))
|
||||||
|
var fields := int(bytes.decode_u32(8))
|
||||||
|
var record_size := int(bytes.decode_u32(12))
|
||||||
|
var string_size := int(bytes.decode_u32(16))
|
||||||
|
var required := 20 + records * record_size + string_size
|
||||||
|
if records < 0 or fields <= 0 or record_size <= 0 or required > bytes.size():
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
"bytes": bytes,
|
||||||
|
"records": records,
|
||||||
|
"fields": fields,
|
||||||
|
"record_size": record_size,
|
||||||
|
"records_offset": 20,
|
||||||
|
"strings_offset": 20 + records * record_size,
|
||||||
|
"string_size": string_size,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _dbc_u32(dbc: Dictionary, record: int, field: int) -> int:
|
||||||
|
if record < 0 or record >= int(dbc["records"]) or field < 0:
|
||||||
|
return 0
|
||||||
|
var record_size := int(dbc["record_size"])
|
||||||
|
var field_offset := field * 4
|
||||||
|
if field_offset + 4 > record_size:
|
||||||
|
return 0
|
||||||
|
var bytes: PackedByteArray = dbc["bytes"]
|
||||||
|
return int(bytes.decode_u32(int(dbc["records_offset"]) + record * record_size + field_offset))
|
||||||
|
|
||||||
|
|
||||||
|
func _dbc_string(dbc: Dictionary, record: int, field: int) -> String:
|
||||||
|
var offset := _dbc_u32(dbc, record, field)
|
||||||
|
var string_size := int(dbc.get("string_size", 0))
|
||||||
|
if offset <= 0 or offset >= string_size:
|
||||||
|
return ""
|
||||||
|
var bytes: PackedByteArray = dbc["bytes"]
|
||||||
|
var pos := int(dbc["strings_offset"]) + offset
|
||||||
|
var end := pos
|
||||||
|
var max_end := int(dbc["strings_offset"]) + string_size
|
||||||
|
while end < max_end and bytes[end] != 0:
|
||||||
|
end += 1
|
||||||
|
if end <= pos:
|
||||||
|
return ""
|
||||||
|
return bytes.slice(pos, end).get_string_from_utf8()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://d3e8tcysnsliv
|
||||||
@@ -1,5 +1,9 @@
|
|||||||
extends CharacterBody3D
|
extends CharacterBody3D
|
||||||
|
|
||||||
|
const GEOSET_CONTROLLER_SCRIPT := preload("res://src/scenes/character/character_geoset_controller.gd")
|
||||||
|
const TEXTURE_COMPOSITOR_SCRIPT := preload("res://src/scenes/character/character_texture_compositor.gd")
|
||||||
|
const OUTFIT_RESOLVER_SCRIPT := preload("res://src/scenes/character/wow_character_outfit_resolver.gd")
|
||||||
|
|
||||||
const TILE_SIZE := 533.33333
|
const TILE_SIZE := 533.33333
|
||||||
const CHUNK_SIZE := TILE_SIZE / 16.0
|
const CHUNK_SIZE := TILE_SIZE / 16.0
|
||||||
const UNIT_SIZE := CHUNK_SIZE / 8.0
|
const UNIT_SIZE := CHUNK_SIZE / 8.0
|
||||||
@@ -14,26 +18,39 @@ const UNIT_SIZE := CHUNK_SIZE / 8.0
|
|||||||
@export var backward_speed: float = 4.5
|
@export var backward_speed: float = 4.5
|
||||||
@export var strafe_speed: float = 4.5
|
@export var strafe_speed: float = 4.5
|
||||||
@export var sprint_multiplier: float = 6.0
|
@export var sprint_multiplier: float = 6.0
|
||||||
|
@export var flight_vertical_speed: float = 7.0
|
||||||
@export var acceleration: float = 28.0
|
@export var acceleration: float = 28.0
|
||||||
@export var mouse_sensitivity: float = 0.003
|
@export var mouse_sensitivity: float = 0.003
|
||||||
@export var camera_pitch_min: float = deg_to_rad(-65.0)
|
@export var camera_pitch_min: float = deg_to_rad(-65.0)
|
||||||
@export var camera_pitch_max: float = deg_to_rad(35.0)
|
@export var camera_pitch_max: float = deg_to_rad(35.0)
|
||||||
@export var camera_height: float = 1.7
|
@export var camera_height: float = 1.7
|
||||||
@export var camera_distance: float = 8.0
|
@export var camera_distance: float = 8.0
|
||||||
|
@export var camera_min_distance: float = 2.0
|
||||||
|
@export var camera_max_distance: float = 18.0
|
||||||
|
@export var camera_zoom_step: float = 1.0
|
||||||
@export var ground_offset: float = 0.05
|
@export var ground_offset: float = 0.05
|
||||||
@export var ground_snap_speed: float = 24.0
|
@export var ground_snap_speed: float = 24.0
|
||||||
|
|
||||||
@export var camera_pivot_path: NodePath = NodePath("CameraPivot")
|
@export var camera_pivot_path: NodePath = NodePath("CameraPivot")
|
||||||
@export var camera_path: NodePath = NodePath("CameraPivot/Camera3D")
|
@export var camera_path: NodePath = NodePath("CameraPivot/Camera3D")
|
||||||
@export var visual_path: NodePath = NodePath("Visual")
|
@export var visual_path: NodePath = NodePath("Visual")
|
||||||
|
@export var character_model_path: String = "res://src/resources/characters/HUMAN/MALE/HumanMale.glb"
|
||||||
|
@export var character_class_id: int = 1
|
||||||
|
@export var character_scale: float = 1.0
|
||||||
|
@export var character_yaw_offset_degrees: float = 90.0
|
||||||
|
@export var animation_blend_time: float = 0.15
|
||||||
|
|
||||||
var _camera_pivot: Node3D
|
var _camera_pivot: Node3D
|
||||||
var _camera: Camera3D
|
var _camera: Camera3D
|
||||||
var _visual: Node3D
|
var _visual: Node3D
|
||||||
|
var _character_root: Node3D
|
||||||
|
var _animation_player: AnimationPlayer
|
||||||
|
var _active_animation := ""
|
||||||
var _captured := false
|
var _captured := false
|
||||||
var _yaw := 0.0
|
var _yaw := 0.0
|
||||||
var _pitch := deg_to_rad(-18.0)
|
var _pitch := deg_to_rad(-18.0)
|
||||||
var _horizontal_velocity := Vector3.ZERO
|
var _horizontal_velocity := Vector3.ZERO
|
||||||
|
var _flight_enabled := false
|
||||||
var _adt_cache: Dictionary = {}
|
var _adt_cache: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
@@ -47,6 +64,7 @@ func _ready() -> void:
|
|||||||
if spawn_at_tile_center:
|
if spawn_at_tile_center:
|
||||||
global_position.x = (float(spawn_tile_x) + 0.5) * TILE_SIZE
|
global_position.x = (float(spawn_tile_x) + 0.5) * TILE_SIZE
|
||||||
global_position.z = (float(spawn_tile_y) + 0.5) * TILE_SIZE
|
global_position.z = (float(spawn_tile_y) + 0.5) * TILE_SIZE
|
||||||
|
_load_character_visual()
|
||||||
var ground := _sample_ground_height(global_position)
|
var ground := _sample_ground_height(global_position)
|
||||||
if is_finite(ground):
|
if is_finite(ground):
|
||||||
global_position.y = ground + ground_offset
|
global_position.y = ground + ground_offset
|
||||||
@@ -58,9 +76,18 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
|
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT:
|
||||||
_captured = event.pressed
|
_captured = event.pressed
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if _captured else Input.MOUSE_MODE_VISIBLE
|
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if _captured else Input.MOUSE_MODE_VISIBLE
|
||||||
|
elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||||
|
camera_distance = clampf(camera_distance - camera_zoom_step, camera_min_distance, camera_max_distance)
|
||||||
|
_apply_camera_transform()
|
||||||
|
elif event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||||
|
camera_distance = clampf(camera_distance + camera_zoom_step, camera_min_distance, camera_max_distance)
|
||||||
|
_apply_camera_transform()
|
||||||
elif event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
elif event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||||
_captured = false
|
_captured = false
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
elif event is InputEventKey and event.pressed and not event.echo and event.keycode == KEY_SPACE:
|
||||||
|
_flight_enabled = not _flight_enabled
|
||||||
|
_horizontal_velocity = Vector3.ZERO
|
||||||
|
|
||||||
if _captured and event is InputEventMouseMotion:
|
if _captured and event is InputEventMouseMotion:
|
||||||
_yaw -= event.relative.x * mouse_sensitivity
|
_yaw -= event.relative.x * mouse_sensitivity
|
||||||
@@ -74,18 +101,24 @@ func _physics_process(delta: float) -> void:
|
|||||||
var target_velocity := Vector3.ZERO
|
var target_velocity := Vector3.ZERO
|
||||||
if input_dir.length_squared() > 0.0:
|
if input_dir.length_squared() > 0.0:
|
||||||
target_velocity = _movement_vector(input_dir)
|
target_velocity = _movement_vector(input_dir)
|
||||||
|
if _flight_enabled:
|
||||||
|
target_velocity.y += _flight_vertical_velocity()
|
||||||
|
|
||||||
_horizontal_velocity = _horizontal_velocity.move_toward(target_velocity, acceleration * delta)
|
_horizontal_velocity = _horizontal_velocity.move_toward(target_velocity, acceleration * delta)
|
||||||
global_position += _horizontal_velocity * delta
|
global_position += _horizontal_velocity * delta
|
||||||
|
|
||||||
|
if not _flight_enabled:
|
||||||
var ground := _sample_ground_height(global_position)
|
var ground := _sample_ground_height(global_position)
|
||||||
if is_finite(ground):
|
if is_finite(ground):
|
||||||
var target_y := ground + ground_offset
|
var target_y := ground + ground_offset
|
||||||
global_position.y = lerpf(global_position.y, target_y, clampf(ground_snap_speed * delta, 0.0, 1.0))
|
global_position.y = lerpf(global_position.y, target_y, clampf(ground_snap_speed * delta, 0.0, 1.0))
|
||||||
|
|
||||||
if _visual and _horizontal_velocity.length_squared() > 0.01:
|
var horizontal_motion := Vector2(_horizontal_velocity.x, _horizontal_velocity.z)
|
||||||
|
if _visual and horizontal_motion.length_squared() > 0.01:
|
||||||
_visual.global_rotation.y = atan2(-_horizontal_velocity.x, -_horizontal_velocity.z)
|
_visual.global_rotation.y = atan2(-_horizontal_velocity.x, -_horizontal_velocity.z)
|
||||||
|
|
||||||
|
_update_character_animation(horizontal_motion.length_squared() > 0.04)
|
||||||
|
|
||||||
_apply_camera_transform()
|
_apply_camera_transform()
|
||||||
|
|
||||||
|
|
||||||
@@ -105,6 +138,10 @@ func _get_input_dir() -> Vector2:
|
|||||||
func _movement_vector(input_dir: Vector2) -> Vector3:
|
func _movement_vector(input_dir: Vector2) -> Vector3:
|
||||||
var forward := -global_basis.z
|
var forward := -global_basis.z
|
||||||
var right := global_basis.x
|
var right := global_basis.x
|
||||||
|
if _flight_enabled and _camera_pivot:
|
||||||
|
forward = -_camera_pivot.global_basis.z
|
||||||
|
right = _camera_pivot.global_basis.x
|
||||||
|
else:
|
||||||
forward.y = 0.0
|
forward.y = 0.0
|
||||||
right.y = 0.0
|
right.y = 0.0
|
||||||
forward = forward.normalized()
|
forward = forward.normalized()
|
||||||
@@ -116,6 +153,167 @@ func _movement_vector(input_dir: Vector2) -> Vector3:
|
|||||||
return (forward * -input_dir.y * speed_z + right * input_dir.x * speed_x) * sprint
|
return (forward * -input_dir.y * speed_z + right * input_dir.x * speed_x) * sprint
|
||||||
|
|
||||||
|
|
||||||
|
func _flight_vertical_velocity() -> float:
|
||||||
|
var dir := 0.0
|
||||||
|
if Input.is_key_pressed(KEY_E):
|
||||||
|
dir += 1.0
|
||||||
|
if Input.is_key_pressed(KEY_Q):
|
||||||
|
dir -= 1.0
|
||||||
|
var sprint := sprint_multiplier if Input.is_key_pressed(KEY_SHIFT) else 1.0
|
||||||
|
return dir * flight_vertical_speed * sprint
|
||||||
|
|
||||||
|
|
||||||
|
func _load_character_visual() -> void:
|
||||||
|
if character_model_path.is_empty() or _visual == null:
|
||||||
|
return
|
||||||
|
if _visual is MeshInstance3D:
|
||||||
|
(_visual as MeshInstance3D).mesh = null
|
||||||
|
_visual.position = Vector3.ZERO
|
||||||
|
|
||||||
|
if _character_root and is_instance_valid(_character_root):
|
||||||
|
_character_root.queue_free()
|
||||||
|
_character_root = null
|
||||||
|
_animation_player = null
|
||||||
|
_active_animation = ""
|
||||||
|
|
||||||
|
var scene: PackedScene = load(character_model_path)
|
||||||
|
if scene == null:
|
||||||
|
push_warning("ThirdPersonWowController: cannot load character model: %s" % character_model_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
_character_root = Node3D.new()
|
||||||
|
_character_root.name = "CharacterModel"
|
||||||
|
_character_root.set_script(GEOSET_CONTROLLER_SCRIPT)
|
||||||
|
_character_root.scale = Vector3.ONE * maxf(character_scale, 0.001)
|
||||||
|
_character_root.rotation_degrees.y = character_yaw_offset_degrees
|
||||||
|
|
||||||
|
var instance := scene.instantiate()
|
||||||
|
instance.name = character_model_path.get_file().get_basename()
|
||||||
|
_character_root.add_child(instance)
|
||||||
|
|
||||||
|
var compositor := Node.new()
|
||||||
|
compositor.name = "CharacterTextureCompositor"
|
||||||
|
compositor.set_script(TEXTURE_COMPOSITOR_SCRIPT)
|
||||||
|
compositor.set("textures_dir", _character_textures_dir(character_model_path))
|
||||||
|
compositor.set("extracted_dir", extracted_dir)
|
||||||
|
_character_root.add_child(compositor)
|
||||||
|
|
||||||
|
_visual.add_child(_character_root)
|
||||||
|
await get_tree().process_frame
|
||||||
|
_fit_character_to_ground()
|
||||||
|
_apply_character_starter_outfit(compositor)
|
||||||
|
_animation_player = _find_animation_player(_character_root)
|
||||||
|
if _animation_player:
|
||||||
|
_prepare_animation_player(_animation_player)
|
||||||
|
_update_character_animation(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _fit_character_to_ground() -> void:
|
||||||
|
if _character_root == null:
|
||||||
|
return
|
||||||
|
var aabb := _calculate_aabb(_character_root)
|
||||||
|
if aabb.size == Vector3.ZERO:
|
||||||
|
return
|
||||||
|
var local_min_y := aabb.position.y - _character_root.global_position.y
|
||||||
|
_character_root.position.y -= local_min_y
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_character_starter_outfit(compositor: Node) -> void:
|
||||||
|
if _character_root == null or compositor == null:
|
||||||
|
return
|
||||||
|
var resolver: RefCounted = OUTFIT_RESOLVER_SCRIPT.new()
|
||||||
|
if not resolver.call("load_dbcs", extracted_dir):
|
||||||
|
return
|
||||||
|
var inferred: Dictionary = resolver.call("infer_race_gender_from_model_path", character_model_path)
|
||||||
|
var race := String(inferred.get("race", "Human"))
|
||||||
|
var gender := String(inferred.get("gender", "Male"))
|
||||||
|
var outfit: Dictionary = resolver.call("resolve_start_outfit", race, gender, character_class_id)
|
||||||
|
if outfit.is_empty():
|
||||||
|
return
|
||||||
|
if compositor.has_method("set_equipment_components"):
|
||||||
|
compositor.call("set_equipment_components", outfit.get("textures", PackedStringArray()))
|
||||||
|
if _character_root.has_method("apply_outfit_defaults"):
|
||||||
|
_character_root.call("apply_outfit_defaults", outfit)
|
||||||
|
|
||||||
|
|
||||||
|
func _update_character_animation(is_moving: bool) -> void:
|
||||||
|
if _animation_player == null:
|
||||||
|
return
|
||||||
|
var wanted := _choose_animation(["Run", "Walk"]) if is_moving else _choose_animation(["Stand", "Idle"])
|
||||||
|
if wanted.is_empty() or wanted == _active_animation:
|
||||||
|
return
|
||||||
|
_active_animation = wanted
|
||||||
|
_animation_player.play(wanted, animation_blend_time)
|
||||||
|
|
||||||
|
|
||||||
|
func _prepare_animation_player(player: AnimationPlayer) -> void:
|
||||||
|
player.playback_default_blend_time = animation_blend_time
|
||||||
|
for animation_name in player.get_animation_list():
|
||||||
|
var lower := String(animation_name).to_lower()
|
||||||
|
if lower == "stand" or lower == "idle" or lower == "run" or lower == "walk" or lower.contains("stand") or lower.contains("run") or lower.contains("walk"):
|
||||||
|
var animation := player.get_animation(animation_name)
|
||||||
|
if animation:
|
||||||
|
animation.loop_mode = Animation.LOOP_LINEAR
|
||||||
|
|
||||||
|
|
||||||
|
func _choose_animation(candidates: Array[String]) -> String:
|
||||||
|
if _animation_player == null:
|
||||||
|
return ""
|
||||||
|
for candidate in candidates:
|
||||||
|
if _animation_player.has_animation(candidate):
|
||||||
|
return candidate
|
||||||
|
var list := _animation_player.get_animation_list()
|
||||||
|
for candidate in candidates:
|
||||||
|
var lower := candidate.to_lower()
|
||||||
|
for animation_name in list:
|
||||||
|
if String(animation_name).to_lower().contains(lower):
|
||||||
|
return String(animation_name)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
func _find_animation_player(root: Node) -> AnimationPlayer:
|
||||||
|
if root is AnimationPlayer:
|
||||||
|
return root
|
||||||
|
for child in root.get_children():
|
||||||
|
var found := _find_animation_player(child)
|
||||||
|
if found:
|
||||||
|
return found
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func _calculate_aabb(root: Node) -> AABB:
|
||||||
|
var result := AABB()
|
||||||
|
var has_aabb := false
|
||||||
|
for mesh_inst in _iter_mesh_instances(root):
|
||||||
|
if mesh_inst.mesh == null:
|
||||||
|
continue
|
||||||
|
var local_aabb := mesh_inst.mesh.get_aabb()
|
||||||
|
var global_aabb := mesh_inst.global_transform * local_aabb
|
||||||
|
if not has_aabb:
|
||||||
|
result = global_aabb
|
||||||
|
has_aabb = true
|
||||||
|
else:
|
||||||
|
result = result.merge(global_aabb)
|
||||||
|
return result if has_aabb else AABB()
|
||||||
|
|
||||||
|
|
||||||
|
func _iter_mesh_instances(root: Node) -> Array[MeshInstance3D]:
|
||||||
|
var result: Array[MeshInstance3D] = []
|
||||||
|
_collect_mesh_instances(root, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
|
||||||
|
if node is MeshInstance3D:
|
||||||
|
result.append(node)
|
||||||
|
for child in node.get_children():
|
||||||
|
_collect_mesh_instances(child, result)
|
||||||
|
|
||||||
|
|
||||||
|
func _character_textures_dir(model_path: String) -> String:
|
||||||
|
return model_path.get_base_dir().path_join(model_path.get_file().get_basename() + "_textures")
|
||||||
|
|
||||||
|
|
||||||
func _apply_camera_transform() -> void:
|
func _apply_camera_transform() -> void:
|
||||||
if _camera_pivot:
|
if _camera_pivot:
|
||||||
_camera_pivot.position = Vector3(0.0, camera_height, 0.0)
|
_camera_pivot.position = Vector3(0.0, camera_height, 0.0)
|
||||||
|
|||||||
@@ -95,11 +95,13 @@ var _active_skybox_node: Node3D
|
|||||||
var _last_logged_area_id := -1
|
var _last_logged_area_id := -1
|
||||||
var _last_logged_zone_id := -1
|
var _last_logged_zone_id := -1
|
||||||
var _last_logged_profile_signature := ""
|
var _last_logged_profile_signature := ""
|
||||||
|
var _last_global_light_signature := ""
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
_resolve_nodes()
|
_resolve_nodes()
|
||||||
_prepare_environment()
|
_prepare_environment()
|
||||||
|
_ensure_wow_shader_globals()
|
||||||
_loaded = _load_lighting_dbcs()
|
_loaded = _load_lighting_dbcs()
|
||||||
if _loaded:
|
if _loaded:
|
||||||
print("WowSkyController: loaded %d LightParams profiles, %d area records and %d %s light volumes" % [
|
print("WowSkyController: loaded %d LightParams profiles, %d area records and %d %s light volumes" % [
|
||||||
@@ -271,6 +273,43 @@ func _apply_sky(delta: float) -> void:
|
|||||||
_sun.light_energy = lerpf(_sun.light_energy, clamp(_color_luma(diffuse) * lerpf(0.22, 1.45, sun_elevation), 0.05, 1.6), blend)
|
_sun.light_energy = lerpf(_sun.light_energy, clamp(_color_luma(diffuse) * lerpf(0.22, 1.45, sun_elevation), 0.05, 1.6), blend)
|
||||||
_apply_sun_direction(time_hours)
|
_apply_sun_direction(time_hours)
|
||||||
|
|
||||||
|
_update_wow_shader_globals(sun_elevation)
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_wow_shader_globals() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func _update_wow_shader_globals(sun_elevation: float) -> void:
|
||||||
|
if not _environment:
|
||||||
|
return
|
||||||
|
var light_dir := Vector3(-0.35, 0.82, -0.45).normalized()
|
||||||
|
if _sun:
|
||||||
|
light_dir = -_sun.global_transform.basis.z.normalized()
|
||||||
|
var fog_range := Vector2(_environment.fog_depth_begin, _environment.fog_depth_end)
|
||||||
|
var raw_light: Color = _sun.light_color if _sun else Color(1.0, 0.91, 0.78, 1.0)
|
||||||
|
var shader_ambient := _world_shader_color(_environment.ambient_light_color, 0.65, Color(0.72, 0.70, 0.64, 1.0), 0.22)
|
||||||
|
var shader_light := _world_shader_color(raw_light, 0.45, Color(1.0, 0.92, 0.78, 1.0), 0.16)
|
||||||
|
var shader_fog := _world_shader_color(_environment.fog_light_color, 0.75, Color(0.62, 0.66, 0.66, 1.0), 0.38)
|
||||||
|
var density: float = clampf(_environment.fog_density * 2200.0, 0.0, 0.42)
|
||||||
|
var signature := "%s|%s|%s|%s|%.4f|%.3f" % [
|
||||||
|
str(shader_ambient),
|
||||||
|
str(shader_light),
|
||||||
|
str(light_dir.snapped(Vector3(0.001, 0.001, 0.001))),
|
||||||
|
str(fog_range.snapped(Vector2(1.0, 1.0))),
|
||||||
|
density,
|
||||||
|
sun_elevation]
|
||||||
|
if signature == _last_global_light_signature:
|
||||||
|
return
|
||||||
|
_last_global_light_signature = signature
|
||||||
|
RenderingServer.global_shader_parameter_set(&"wow_ambient_color", shader_ambient)
|
||||||
|
RenderingServer.global_shader_parameter_set(&"wow_light_color", shader_light)
|
||||||
|
RenderingServer.global_shader_parameter_set(&"wow_light_dir", light_dir)
|
||||||
|
RenderingServer.global_shader_parameter_set(&"wow_fog_color", shader_fog)
|
||||||
|
RenderingServer.global_shader_parameter_set(&"wow_fog_range", fog_range)
|
||||||
|
RenderingServer.global_shader_parameter_set(&"wow_fog_density", density)
|
||||||
|
RenderingServer.global_shader_parameter_set(&"wow_sun_elevation", sun_elevation)
|
||||||
|
|
||||||
|
|
||||||
func _load_lighting_dbcs() -> bool:
|
func _load_lighting_dbcs() -> bool:
|
||||||
var base := _res_path(extracted_dir).path_join("DBFilesClient")
|
var base := _res_path(extracted_dir).path_join("DBFilesClient")
|
||||||
@@ -860,6 +899,18 @@ func _color_luma(color: Color) -> float:
|
|||||||
return color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722
|
return color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722
|
||||||
|
|
||||||
|
|
||||||
|
func _world_shader_color(color: Color, desaturate: float, neutral: Color, neutral_mix: float) -> Color:
|
||||||
|
var luma := clampf(_color_luma(color), 0.0, 1.25)
|
||||||
|
var gray := Color(luma, luma, luma, color.a)
|
||||||
|
var result := color.lerp(gray, clampf(desaturate, 0.0, 1.0))
|
||||||
|
result = result.lerp(neutral, clampf(neutral_mix, 0.0, 1.0))
|
||||||
|
return Color(
|
||||||
|
clampf(result.r, 0.0, 1.25),
|
||||||
|
clampf(result.g, 0.0, 1.25),
|
||||||
|
clampf(result.b, 0.0, 1.25),
|
||||||
|
color.a)
|
||||||
|
|
||||||
|
|
||||||
func _get_light_volume_count(id: int) -> int:
|
func _get_light_volume_count(id: int) -> int:
|
||||||
return int((_light_volumes_by_map.get(id, []) as Array).size())
|
return int((_light_volumes_by_map.get(id, []) as Array).size())
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ max_concurrent_tile_tasks = 1
|
|||||||
tile_lod_remove_ops_per_tick = 1
|
tile_lod_remove_ops_per_tick = 1
|
||||||
m2_build_groups_per_tick = 1
|
m2_build_groups_per_tick = 1
|
||||||
m2_multimesh_batch_size = 64
|
m2_multimesh_batch_size = 64
|
||||||
|
m2_animated_denylist_patterns = PackedStringArray("gryphonroost")
|
||||||
|
m2_animated_allowlist_patterns = PackedStringArray("creature/fish/", "creature/eagle/", "world/critter/")
|
||||||
wmo_render_group_ops_per_tick = 16
|
wmo_render_group_ops_per_tick = 16
|
||||||
cached_tile_mesh_limit = 48
|
cached_tile_mesh_limit = 48
|
||||||
terrain_quality_mesh_cache_limit = 48
|
terrain_quality_mesh_cache_limit = 48
|
||||||
@@ -85,13 +87,14 @@ m2_tile_radius = 3
|
|||||||
wmo_tile_radius = 5
|
wmo_tile_radius = 5
|
||||||
m2_visibility_range = 1600.0
|
m2_visibility_range = 1600.0
|
||||||
wmo_visibility_range = 3600.0
|
wmo_visibility_range = 3600.0
|
||||||
|
debug_streaming = true
|
||||||
hitch_profiler_enabled = true
|
hitch_profiler_enabled = true
|
||||||
|
|
||||||
[node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687]
|
[node name="ThirdPersonPlayer" type="CharacterBody3D" parent="." unique_id=502573687]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 22666, 80, 15200)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 16800, 80, 26400)
|
||||||
script = ExtResource("2_player")
|
script = ExtResource("2_player")
|
||||||
spawn_tile_x = 31
|
spawn_tile_x = 31
|
||||||
spawn_tile_y = 31
|
spawn_tile_y = 49
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="ThirdPersonPlayer" unique_id=1297880621]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="ThirdPersonPlayer" unique_id=1297880621]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.05, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.05, 0)
|
||||||
|
|||||||
@@ -0,0 +1,245 @@
|
|||||||
|
extends Node
|
||||||
|
class_name M2NativeAnimator
|
||||||
|
|
||||||
|
var mesh_instance_path: NodePath = NodePath("../Mesh")
|
||||||
|
var mesh_instance: MeshInstance3D
|
||||||
|
var mesh: ArrayMesh
|
||||||
|
var bones: Array = []
|
||||||
|
var surfaces: Array = []
|
||||||
|
var animation_length: float = 0.0
|
||||||
|
var playback_speed: float = 1.0
|
||||||
|
|
||||||
|
var _time: float = 0.0
|
||||||
|
var _materials: Array[Material] = []
|
||||||
|
var _unique_mesh_ready := false
|
||||||
|
var _prepared := false
|
||||||
|
|
||||||
|
|
||||||
|
func setup(target_mesh_instance: MeshInstance3D, bone_data: Array, surface_data: Array, length_seconds: float) -> void:
|
||||||
|
mesh_instance = target_mesh_instance
|
||||||
|
if mesh_instance != null:
|
||||||
|
mesh_instance_path = get_path_to(mesh_instance)
|
||||||
|
mesh = mesh_instance.mesh as ArrayMesh
|
||||||
|
bones = bone_data
|
||||||
|
surfaces = surface_data
|
||||||
|
animation_length = maxf(length_seconds, 0.0)
|
||||||
|
_capture_materials()
|
||||||
|
_make_mesh_unique()
|
||||||
|
_rebuild_mesh(0.0)
|
||||||
|
set_process(mesh != null and not bones.is_empty() and not surfaces.is_empty() and animation_length > 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
prepare_runtime()
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if mesh == null or bones.is_empty() or surfaces.is_empty() or animation_length <= 0.0:
|
||||||
|
return
|
||||||
|
_time = fmod(_time + delta * playback_speed, animation_length)
|
||||||
|
_rebuild_mesh(_time)
|
||||||
|
|
||||||
|
|
||||||
|
func set_phase(phase: float) -> void:
|
||||||
|
if animation_length <= 0.0:
|
||||||
|
_time = 0.0
|
||||||
|
else:
|
||||||
|
_time = fposmod(animation_length * phase, animation_length)
|
||||||
|
_rebuild_mesh(_time)
|
||||||
|
|
||||||
|
|
||||||
|
func prepare_runtime() -> bool:
|
||||||
|
_resolve_mesh_instance()
|
||||||
|
_capture_materials()
|
||||||
|
_unique_mesh_ready = false
|
||||||
|
_make_mesh_unique()
|
||||||
|
_rebuild_mesh(_time)
|
||||||
|
_prepared = mesh != null and not bones.is_empty() and not surfaces.is_empty() and animation_length > 0.0
|
||||||
|
set_process(_prepared)
|
||||||
|
return _prepared
|
||||||
|
|
||||||
|
|
||||||
|
func runtime_debug_state() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"prepared": _prepared,
|
||||||
|
"processing": is_processing(),
|
||||||
|
"has_mesh": mesh != null,
|
||||||
|
"bones": bones.size(),
|
||||||
|
"surfaces": surfaces.size(),
|
||||||
|
"length": animation_length,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _resolve_mesh_instance() -> void:
|
||||||
|
var resolved := get_node_or_null(mesh_instance_path)
|
||||||
|
if resolved is MeshInstance3D:
|
||||||
|
mesh_instance = resolved as MeshInstance3D
|
||||||
|
if mesh_instance != null:
|
||||||
|
mesh = mesh_instance.mesh as ArrayMesh
|
||||||
|
|
||||||
|
|
||||||
|
func _make_mesh_unique() -> void:
|
||||||
|
if _unique_mesh_ready or mesh_instance == null or mesh_instance.mesh == null:
|
||||||
|
return
|
||||||
|
var duplicated := mesh_instance.mesh.duplicate(true) as ArrayMesh
|
||||||
|
if duplicated == null:
|
||||||
|
return
|
||||||
|
mesh_instance.mesh = duplicated
|
||||||
|
mesh = duplicated
|
||||||
|
_unique_mesh_ready = true
|
||||||
|
|
||||||
|
|
||||||
|
func _capture_materials() -> void:
|
||||||
|
if mesh == null:
|
||||||
|
return
|
||||||
|
if not _materials.is_empty() and _materials.size() == mesh.get_surface_count():
|
||||||
|
return
|
||||||
|
_materials.clear()
|
||||||
|
for surface_index in mesh.get_surface_count():
|
||||||
|
_materials.append(mesh.surface_get_material(surface_index))
|
||||||
|
|
||||||
|
|
||||||
|
func _rebuild_mesh(time: float) -> void:
|
||||||
|
if mesh == null:
|
||||||
|
return
|
||||||
|
var bone_matrices := _build_bone_matrices(time)
|
||||||
|
if bone_matrices.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
mesh.clear_surfaces()
|
||||||
|
for surface_index in surfaces.size():
|
||||||
|
var source = surfaces[surface_index]
|
||||||
|
if not (source is Dictionary):
|
||||||
|
continue
|
||||||
|
var surface: Dictionary = source
|
||||||
|
var base_vertices: PackedVector3Array = surface.get("vertices", PackedVector3Array())
|
||||||
|
var base_normals: PackedVector3Array = surface.get("normals", PackedVector3Array())
|
||||||
|
var uvs: PackedVector2Array = surface.get("uvs", PackedVector2Array())
|
||||||
|
var uvs2: PackedVector2Array = surface.get("uvs2", PackedVector2Array())
|
||||||
|
var indices: PackedInt32Array = surface.get("indices", PackedInt32Array())
|
||||||
|
var bone_indices: PackedInt32Array = surface.get("bones", PackedInt32Array())
|
||||||
|
var weights: PackedFloat32Array = surface.get("weights", PackedFloat32Array())
|
||||||
|
if base_vertices.is_empty() or indices.is_empty():
|
||||||
|
continue
|
||||||
|
|
||||||
|
var vertices := PackedVector3Array()
|
||||||
|
var normals := PackedVector3Array()
|
||||||
|
vertices.resize(base_vertices.size())
|
||||||
|
if base_normals.size() == base_vertices.size():
|
||||||
|
normals.resize(base_normals.size())
|
||||||
|
|
||||||
|
var can_skin := bone_indices.size() == base_vertices.size() * 4 and weights.size() == base_vertices.size() * 4
|
||||||
|
for vertex_index in base_vertices.size():
|
||||||
|
if can_skin:
|
||||||
|
var skinned_pos := Vector3.ZERO
|
||||||
|
var skinned_nrm := Vector3.ZERO
|
||||||
|
var total_weight := 0.0
|
||||||
|
for influence in 4:
|
||||||
|
var weight := weights[vertex_index * 4 + influence]
|
||||||
|
if weight <= 0.0:
|
||||||
|
continue
|
||||||
|
var bone_index := bone_indices[vertex_index * 4 + influence]
|
||||||
|
if bone_index < 0 or bone_index >= bone_matrices.size():
|
||||||
|
continue
|
||||||
|
var transform: Transform3D = bone_matrices[bone_index]
|
||||||
|
skinned_pos += transform * base_vertices[vertex_index] * weight
|
||||||
|
if normals.size() == base_normals.size():
|
||||||
|
skinned_nrm += (transform.basis * base_normals[vertex_index]) * weight
|
||||||
|
total_weight += weight
|
||||||
|
if total_weight > 0.0:
|
||||||
|
vertices[vertex_index] = skinned_pos / total_weight
|
||||||
|
if normals.size() == base_normals.size():
|
||||||
|
normals[vertex_index] = (skinned_nrm / total_weight).normalized()
|
||||||
|
else:
|
||||||
|
vertices[vertex_index] = base_vertices[vertex_index]
|
||||||
|
if normals.size() == base_normals.size():
|
||||||
|
normals[vertex_index] = base_normals[vertex_index]
|
||||||
|
else:
|
||||||
|
vertices[vertex_index] = base_vertices[vertex_index]
|
||||||
|
if normals.size() == base_normals.size():
|
||||||
|
normals[vertex_index] = base_normals[vertex_index]
|
||||||
|
|
||||||
|
var arrays := []
|
||||||
|
arrays.resize(Mesh.ARRAY_MAX)
|
||||||
|
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||||
|
if normals.size() == vertices.size():
|
||||||
|
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||||
|
if uvs.size() == vertices.size():
|
||||||
|
arrays[Mesh.ARRAY_TEX_UV] = uvs
|
||||||
|
if uvs2.size() == vertices.size():
|
||||||
|
arrays[Mesh.ARRAY_TEX_UV2] = uvs2
|
||||||
|
arrays[Mesh.ARRAY_INDEX] = indices
|
||||||
|
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||||
|
if surface_index < _materials.size() and _materials[surface_index] != null:
|
||||||
|
mesh.surface_set_material(mesh.get_surface_count() - 1, _materials[surface_index])
|
||||||
|
|
||||||
|
|
||||||
|
func _build_bone_matrices(time: float) -> Array[Transform3D]:
|
||||||
|
var result: Array[Transform3D] = []
|
||||||
|
result.resize(bones.size())
|
||||||
|
for i in range(bones.size()):
|
||||||
|
var bone: Dictionary = bones[i]
|
||||||
|
var pivot: Vector3 = bone.get("pivot", Vector3.ZERO)
|
||||||
|
var translation := _sample_vec3_track(bone.get("translation", {}), time, Vector3.ZERO)
|
||||||
|
var rotation := _sample_quat_track(bone.get("rotation", {}), time, Quaternion.IDENTITY)
|
||||||
|
var scale := _sample_vec3_track(bone.get("scale", {}), time, Vector3.ONE)
|
||||||
|
|
||||||
|
var local := Transform3D(Basis.IDENTITY, pivot + translation)
|
||||||
|
local = local * Transform3D(Basis(rotation).scaled(scale), Vector3.ZERO)
|
||||||
|
local = local * Transform3D(Basis.IDENTITY, -pivot)
|
||||||
|
|
||||||
|
var parent := int(bone.get("parent", -1))
|
||||||
|
if parent >= 0 and parent < i:
|
||||||
|
result[i] = result[parent] * local
|
||||||
|
else:
|
||||||
|
result[i] = local
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _sample_vec3_track(track: Variant, time: float, fallback: Vector3) -> Vector3:
|
||||||
|
if not (track is Dictionary):
|
||||||
|
return fallback
|
||||||
|
var times: PackedFloat32Array = track.get("times", PackedFloat32Array())
|
||||||
|
var values: PackedVector3Array = track.get("values", PackedVector3Array())
|
||||||
|
var index := _track_index(times, time)
|
||||||
|
if index < 0 or values.is_empty():
|
||||||
|
return fallback
|
||||||
|
if index >= values.size() - 1 or index >= times.size() - 1:
|
||||||
|
return values[mini(index, values.size() - 1)]
|
||||||
|
var t0 := times[index]
|
||||||
|
var t1 := times[index + 1]
|
||||||
|
var alpha := 0.0 if is_equal_approx(t0, t1) else clampf((time - t0) / (t1 - t0), 0.0, 1.0)
|
||||||
|
return values[index].lerp(values[index + 1], alpha)
|
||||||
|
|
||||||
|
|
||||||
|
func _sample_quat_track(track: Variant, time: float, fallback: Quaternion) -> Quaternion:
|
||||||
|
if not (track is Dictionary):
|
||||||
|
return fallback
|
||||||
|
var times: PackedFloat32Array = track.get("times", PackedFloat32Array())
|
||||||
|
var raw_values: PackedVector4Array = track.get("values", PackedVector4Array())
|
||||||
|
var index := _track_index(times, time)
|
||||||
|
if index < 0 or raw_values.is_empty():
|
||||||
|
return fallback
|
||||||
|
var q0 := _quat_from_vec4(raw_values[mini(index, raw_values.size() - 1)])
|
||||||
|
if index >= raw_values.size() - 1 or index >= times.size() - 1:
|
||||||
|
return q0.normalized()
|
||||||
|
var q1 := _quat_from_vec4(raw_values[index + 1])
|
||||||
|
var t0 := times[index]
|
||||||
|
var t1 := times[index + 1]
|
||||||
|
var alpha := 0.0 if is_equal_approx(t0, t1) else clampf((time - t0) / (t1 - t0), 0.0, 1.0)
|
||||||
|
return q0.slerp(q1, alpha).normalized()
|
||||||
|
|
||||||
|
|
||||||
|
func _track_index(times: PackedFloat32Array, time: float) -> int:
|
||||||
|
if times.is_empty():
|
||||||
|
return -1
|
||||||
|
if times.size() == 1 or time <= times[0]:
|
||||||
|
return 0
|
||||||
|
for i in range(times.size() - 1):
|
||||||
|
if time >= times[i] and time < times[i + 1]:
|
||||||
|
return i
|
||||||
|
return times.size() - 1
|
||||||
|
|
||||||
|
|
||||||
|
func _quat_from_vec4(v: Vector4) -> Quaternion:
|
||||||
|
return Quaternion(v.x, v.y, v.z, v.w).normalized()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b8pxshlr85g2t
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -3,6 +3,8 @@
|
|||||||
## Usage:
|
## Usage:
|
||||||
## godot --headless --path <project> --script res://src/tools/bake_m2_cache.gd -- \
|
## godot --headless --path <project> --script res://src/tools/bake_m2_cache.gd -- \
|
||||||
## --map Azeroth --extracted res://data/extracted --output res://data/cache/m2_glb --force
|
## --map Azeroth --extracted res://data/extracted --output res://data/cache/m2_glb --force
|
||||||
|
## Add --only-pattern waterfall to rebuild only matching model paths.
|
||||||
|
## Add --glb-animations to also emit animated GLB scenes through src/tools/m2_to_gltf.py.
|
||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
|
||||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||||
@@ -13,6 +15,10 @@ func _initialize() -> void:
|
|||||||
var extracted := _res(_arg(args, "--extracted", "res://data/extracted"))
|
var extracted := _res(_arg(args, "--extracted", "res://data/extracted"))
|
||||||
var output_dir := _res(_arg(args, "--output", "res://data/cache/m2_glb"))
|
var output_dir := _res(_arg(args, "--output", "res://data/cache/m2_glb"))
|
||||||
var force := args.has("--force")
|
var force := args.has("--force")
|
||||||
|
var glb_animations := args.has("--glb-animations")
|
||||||
|
var only_patterns := _arg_values(args, "--only-pattern")
|
||||||
|
var python_exe := _arg(args, "--python", "python")
|
||||||
|
var converter := _res(_arg(args, "--converter", "res://src/tools/m2_to_gltf.py"))
|
||||||
|
|
||||||
if not ClassDB.class_exists("ADTLoader") or not ClassDB.class_exists("M2Loader"):
|
if not ClassDB.class_exists("ADTLoader") or not ClassDB.class_exists("M2Loader"):
|
||||||
push_error("GDExtension not loaded. Rebuild first.")
|
push_error("GDExtension not loaded. Rebuild first.")
|
||||||
@@ -47,7 +53,7 @@ func _initialize() -> void:
|
|||||||
var norm := str(rel).replace("\\", "/").to_lower()
|
var norm := str(rel).replace("\\", "/").to_lower()
|
||||||
if norm.ends_with(".mdx") or norm.ends_with(".mdl"):
|
if norm.ends_with(".mdx") or norm.ends_with(".mdl"):
|
||||||
norm = norm.get_basename() + ".m2"
|
norm = norm.get_basename() + ".m2"
|
||||||
if not norm.is_empty():
|
if not norm.is_empty() and _matches_only_patterns(norm, only_patterns):
|
||||||
unique[norm] = true
|
unique[norm] = true
|
||||||
scanned += 1
|
scanned += 1
|
||||||
|
|
||||||
@@ -55,8 +61,10 @@ func _initialize() -> void:
|
|||||||
|
|
||||||
var m2_loader = ClassDB.instantiate("M2Loader")
|
var m2_loader = ClassDB.instantiate("M2Loader")
|
||||||
var baked := 0
|
var baked := 0
|
||||||
|
var baked_glb := 0
|
||||||
var skipped := 0
|
var skipped := 0
|
||||||
var failed := 0
|
var failed := 0
|
||||||
|
var failed_glb := 0
|
||||||
var total := unique.size()
|
var total := unique.size()
|
||||||
var i := 0
|
var i := 0
|
||||||
|
|
||||||
@@ -64,16 +72,22 @@ func _initialize() -> void:
|
|||||||
i += 1
|
i += 1
|
||||||
var stem: String = rel_path.get_basename()
|
var stem: String = rel_path.get_basename()
|
||||||
var out_path := output_dir.path_join(stem + ".tscn")
|
var out_path := output_dir.path_join(stem + ".tscn")
|
||||||
|
var out_glb_path := output_dir.path_join(stem + ".glb")
|
||||||
if not force and ResourceLoader.exists(out_path):
|
|
||||||
skipped += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
var abs_m2 := ProjectSettings.globalize_path(extracted.path_join(rel_path))
|
var abs_m2 := ProjectSettings.globalize_path(extracted.path_join(rel_path))
|
||||||
if not FileAccess.file_exists(abs_m2):
|
if not FileAccess.file_exists(abs_m2):
|
||||||
failed += 1
|
failed += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
if not force and ResourceLoader.exists(out_path):
|
||||||
|
if glb_animations:
|
||||||
|
if _bake_glb_animation_cache(python_exe, converter, abs_m2, output_dir, out_glb_path, false):
|
||||||
|
baked_glb += 1
|
||||||
|
else:
|
||||||
|
failed_glb += 1
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
var data: Dictionary = m2_loader.call("load_m2", abs_m2)
|
var data: Dictionary = m2_loader.call("load_m2", abs_m2)
|
||||||
if data.is_empty() or data.get("vertices", PackedVector3Array()).is_empty():
|
if data.is_empty() or data.get("vertices", PackedVector3Array()).is_empty():
|
||||||
failed += 1
|
failed += 1
|
||||||
@@ -102,10 +116,17 @@ func _initialize() -> void:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
baked += 1
|
baked += 1
|
||||||
|
if glb_animations:
|
||||||
|
if _bake_glb_animation_cache(python_exe, converter, abs_m2, output_dir, out_glb_path, force):
|
||||||
|
baked_glb += 1
|
||||||
|
else:
|
||||||
|
failed_glb += 1
|
||||||
if i % 50 == 0 or i == total:
|
if i % 50 == 0 or i == total:
|
||||||
print("[%d/%d] baked=%d skipped=%d failed=%d" % [i, total, baked, skipped, failed])
|
print("[%d/%d] baked=%d glb=%d skipped=%d failed=%d glb_failed=%d" % [
|
||||||
|
i, total, baked, baked_glb, skipped, failed, failed_glb])
|
||||||
|
|
||||||
print("Done. baked=%d skipped=%d failed=%d" % [baked, skipped, failed])
|
print("Done. baked=%d glb=%d skipped=%d failed=%d glb_failed=%d" % [
|
||||||
|
baked, baked_glb, skipped, failed, failed_glb])
|
||||||
quit(0)
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
@@ -116,12 +137,65 @@ func _arg(args: PackedStringArray, name: String, default: String) -> String:
|
|||||||
return default
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
func _arg_values(args: PackedStringArray, name: String) -> PackedStringArray:
|
||||||
|
var result := PackedStringArray()
|
||||||
|
for i in args.size():
|
||||||
|
if args[i] != name or i + 1 >= args.size():
|
||||||
|
continue
|
||||||
|
for part in String(args[i + 1]).split(",", false):
|
||||||
|
var value := part.strip_edges().to_lower()
|
||||||
|
if not value.is_empty():
|
||||||
|
result.append(value)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _matches_only_patterns(path: String, patterns: PackedStringArray) -> bool:
|
||||||
|
if patterns.is_empty():
|
||||||
|
return true
|
||||||
|
var lower := path.to_lower()
|
||||||
|
for pattern in patterns:
|
||||||
|
if lower.contains(pattern):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
func _res(path: String) -> String:
|
func _res(path: String) -> String:
|
||||||
if path.begins_with("res://") or path.begins_with("user://"):
|
if path.begins_with("res://") or path.begins_with("user://"):
|
||||||
return path
|
return path
|
||||||
return "res://" + path.trim_prefix("./").trim_prefix("/")
|
return "res://" + path.trim_prefix("./").trim_prefix("/")
|
||||||
|
|
||||||
|
|
||||||
|
func _bake_glb_animation_cache(
|
||||||
|
python_exe: String,
|
||||||
|
converter: String,
|
||||||
|
abs_m2: String,
|
||||||
|
output_dir: String,
|
||||||
|
out_glb_path: String,
|
||||||
|
force: bool) -> bool:
|
||||||
|
var abs_out_glb := ProjectSettings.globalize_path(out_glb_path)
|
||||||
|
if not force and FileAccess.file_exists(abs_out_glb):
|
||||||
|
return true
|
||||||
|
var abs_converter := ProjectSettings.globalize_path(converter)
|
||||||
|
var abs_output := ProjectSettings.globalize_path(output_dir)
|
||||||
|
if not FileAccess.file_exists(abs_converter):
|
||||||
|
push_warning("M2 GLB converter not found: %s" % converter)
|
||||||
|
return false
|
||||||
|
var stdout := []
|
||||||
|
var exit_code := OS.execute(
|
||||||
|
python_exe,
|
||||||
|
[abs_converter, abs_m2, abs_output],
|
||||||
|
stdout,
|
||||||
|
true,
|
||||||
|
false)
|
||||||
|
if exit_code != 0:
|
||||||
|
push_warning("animated GLB bake failed for %s exit=%d\n%s" % [
|
||||||
|
abs_m2,
|
||||||
|
exit_code,
|
||||||
|
"\n".join(stdout)])
|
||||||
|
return false
|
||||||
|
return FileAccess.file_exists(abs_out_glb)
|
||||||
|
|
||||||
|
|
||||||
func _set_owner_recursive(node: Node, owner_root: Node) -> void:
|
func _set_owner_recursive(node: Node, owner_root: Node) -> void:
|
||||||
for child in node.get_children():
|
for child in node.get_children():
|
||||||
child.owner = owner_root
|
child.owner = owner_root
|
||||||
|
|||||||
+29
-28
@@ -650,7 +650,11 @@ class SkinParser:
|
|||||||
|
|
||||||
class GltfBuilder:
|
class GltfBuilder:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.asset = {"version": "2.0", "generator": "WoW M2 to GLTF converter"}
|
self.asset = {
|
||||||
|
"version": "2.0",
|
||||||
|
"generator": "WoW M2 to GLTF converter",
|
||||||
|
"extras": {"openwc_m2_anim_schema": "pivot_prefix_v1"},
|
||||||
|
}
|
||||||
self.nodes = []
|
self.nodes = []
|
||||||
self.meshes = []
|
self.meshes = []
|
||||||
self.skins = []
|
self.skins = []
|
||||||
@@ -1027,36 +1031,41 @@ class GltfBuilder:
|
|||||||
|
|
||||||
def _build_skeleton(self, m2: M2Parser):
|
def _build_skeleton(self, m2: M2Parser):
|
||||||
bone_node_indices = []
|
bone_node_indices = []
|
||||||
|
pivot_node_indices = []
|
||||||
|
|
||||||
for i, bone in enumerate(m2.bones):
|
for i, bone in enumerate(m2.bones):
|
||||||
node_idx = len(self.nodes)
|
pivot_idx = len(self.nodes)
|
||||||
bone_node_indices.append(node_idx)
|
anim_idx = pivot_idx + 1
|
||||||
|
pivot_node_indices.append(pivot_idx)
|
||||||
|
bone_node_indices.append(anim_idx)
|
||||||
|
|
||||||
# Pivot in GLTF space (world-space first; parent-relative below)
|
# Pivot node holds the bind-pose offset. The child bone node stays
|
||||||
|
# at identity and receives animation channels. This mirrors WoW's
|
||||||
|
# T(pivot) * anim * T(-pivot) behavior closely enough for GLTF skinning.
|
||||||
px, py, pz = wow_pos_to_gltf(*bone['pivot'])
|
px, py, pz = wow_pos_to_gltf(*bone['pivot'])
|
||||||
|
self.nodes.append({
|
||||||
node = {
|
"name": f"bone_{i}_pivot",
|
||||||
"name": f"bone_{i}",
|
|
||||||
"translation": [px, py, pz],
|
"translation": [px, py, pz],
|
||||||
"rotation": [0.0, 0.0, 0.0, 1.0],
|
"children": [anim_idx],
|
||||||
"scale": [1.0, 1.0, 1.0],
|
})
|
||||||
}
|
self.nodes.append({
|
||||||
self.nodes.append(node)
|
"name": f"bone_{i}",
|
||||||
|
})
|
||||||
|
|
||||||
# Wire up children
|
# Wire pivot nodes into the hierarchy.
|
||||||
for i, bone in enumerate(m2.bones):
|
for i, bone in enumerate(m2.bones):
|
||||||
parent = bone['parent']
|
parent = bone['parent']
|
||||||
if parent >= 0 and parent < len(m2.bones):
|
if parent >= 0 and parent < len(m2.bones):
|
||||||
parent_node = bone_node_indices[parent]
|
parent_node = bone_node_indices[parent]
|
||||||
child_node = bone_node_indices[i]
|
child_node = pivot_node_indices[i]
|
||||||
self.nodes[parent_node].setdefault("children", []).append(child_node)
|
self.nodes[parent_node].setdefault("children", []).append(child_node)
|
||||||
|
|
||||||
# Convert world-space pivots to parent-relative translations
|
# Convert world-space pivots to parent-relative pivot translations.
|
||||||
for i, bone in enumerate(m2.bones):
|
for i, bone in enumerate(m2.bones):
|
||||||
parent = bone['parent']
|
parent = bone['parent']
|
||||||
if parent >= 0 and parent < len(m2.bones):
|
if parent >= 0 and parent < len(m2.bones):
|
||||||
ppx, ppy, ppz = wow_pos_to_gltf(*m2.bones[parent]['pivot'])
|
ppx, ppy, ppz = wow_pos_to_gltf(*m2.bones[parent]['pivot'])
|
||||||
node = self.nodes[bone_node_indices[i]]
|
node = self.nodes[pivot_node_indices[i]]
|
||||||
t = node["translation"]
|
t = node["translation"]
|
||||||
node["translation"] = [t[0]-ppx, t[1]-ppy, t[2]-ppz]
|
node["translation"] = [t[0]-ppx, t[1]-ppy, t[2]-ppz]
|
||||||
|
|
||||||
@@ -1064,7 +1073,7 @@ class GltfBuilder:
|
|||||||
# WoW models have multiple root bones (main skeleton + attachment/socket
|
# WoW models have multiple root bones (main skeleton + attachment/socket
|
||||||
# bones each with parent=-1). Wrap them all under a single virtual
|
# bones each with parent=-1). Wrap them all under a single virtual
|
||||||
# "Armature" node at the origin so the skin has a valid common root.
|
# "Armature" node at the origin so the skin has a valid common root.
|
||||||
root_bone_nodes = [bone_node_indices[i]
|
root_bone_nodes = [pivot_node_indices[i]
|
||||||
for i, b in enumerate(m2.bones) if b['parent'] < 0]
|
for i, b in enumerate(m2.bones) if b['parent'] < 0]
|
||||||
armature_idx = len(self.nodes)
|
armature_idx = len(self.nodes)
|
||||||
self.nodes.append({
|
self.nodes.append({
|
||||||
@@ -1310,20 +1319,12 @@ class GltfBuilder:
|
|||||||
node_idx = bone_node_indices[bone_idx]
|
node_idx = bone_node_indices[bone_idx]
|
||||||
|
|
||||||
# Translation
|
# Translation
|
||||||
# M2 t_track values are OFFSETS from the bone's rest position (pivot
|
# Pivot offset lives on the parent pivot node. The animated bone node
|
||||||
# relative to parent pivot). GLTF animation overwrites node.translation
|
# receives only the M2 translation offset.
|
||||||
# entirely, so we must emit: rest_local + m2_offset (both in GLTF space).
|
|
||||||
ts, vals = m2._resolve_track(bone['t_track'], seq_idx, 'vec3')
|
ts, vals = m2._resolve_track(bone['t_track'], seq_idx, 'vec3')
|
||||||
if ts and vals:
|
if ts and vals:
|
||||||
rest = self.nodes[node_idx].get("translation", [0.0, 0.0, 0.0])
|
s_idx = self._anim_sampler(ts, vals, 'VEC3',
|
||||||
rx, ry, rz = rest[0], rest[1], rest[2]
|
lambda v: wow_pos_to_gltf(*v))
|
||||||
# Use a factory to capture rx/ry/rz by value (avoid late-binding bug)
|
|
||||||
def _make_trans_fn(rx, ry, rz):
|
|
||||||
def fn(v):
|
|
||||||
cx, cy, cz = wow_pos_to_gltf(*v)
|
|
||||||
return (rx + cx, ry + cy, rz + cz)
|
|
||||||
return fn
|
|
||||||
s_idx = self._anim_sampler(ts, vals, 'VEC3', _make_trans_fn(rx, ry, rz))
|
|
||||||
if s_idx is not None:
|
if s_idx is not None:
|
||||||
samplers.append(s_idx)
|
samplers.append(s_idx)
|
||||||
channels.append({"sampler": len(samplers)-1,
|
channels.append({"sampler": len(samplers)-1,
|
||||||
|
|||||||
Reference in New Issue
Block a user