новая структура проекта

This commit is contained in:
2026-04-20 21:04:25 +04:00
parent 1fe2a72ef1
commit 1a56b22e38
1932 changed files with 1886 additions and 22779 deletions
+34
View File
@@ -0,0 +1,34 @@
## Free-fly camera: WASD + QE up/down, RMB hold to look, Shift to speed up
extends Camera3D
@export var speed : float = 200.0
@export var fast_mult : float = 5.0
@export var sensitivity: float = 0.003
var _captured := false
func _ready() -> void:
current = true
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_RIGHT:
_captured = event.pressed
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if _captured else Input.MOUSE_MODE_VISIBLE
if _captured and event is InputEventMouseMotion:
rotate_y(-event.relative.x * sensitivity)
rotate_object_local(Vector3.RIGHT, -event.relative.y * sensitivity)
func _process(delta: float) -> void:
if not _captured:
return
var move := Vector3.ZERO
if Input.is_key_pressed(KEY_W): move -= basis.z
if Input.is_key_pressed(KEY_S): move += basis.z
if Input.is_key_pressed(KEY_A): move -= basis.x
if Input.is_key_pressed(KEY_D): move += basis.x
if Input.is_key_pressed(KEY_E): move += Vector3.UP
if Input.is_key_pressed(KEY_Q): move -= Vector3.UP
var s := speed * (fast_mult if Input.is_key_pressed(KEY_SHIFT) else 1.0)
position += move.normalized() * s * delta
+1
View File
@@ -0,0 +1 @@
uid://bauggobg40psr
@@ -0,0 +1,359 @@
## CharacterGeosetController WoW 3.3.5a (build 12340) character geoset manager.
##
## Attach to the root of an imported character GLB scene.
##
## Correct geoset ID ranges (from M2SkinMeshPartID, WotLK 3.3.5):
## 0 skin base body, always visible
## 1 34 hair hair style variants
## 101 124 facial1 beard
## 201 219 facial2 mustache
## 301 319 facial3 sideburns
## 401 405 gloves
## 501 510 boots
## 601 614 shirt
## 701 711 ears
## 801 804 wristbands (801 = bare wrists)
## 901 905 kneepads
## 1001 1004 chest
## 1101 1105 pants
## 1201 1204 tabard
## 1301 1303 legs
## 1401 1414 shirt_doublet
## 1501 1524 cape
## 1601 1614 facial_jewelry
## 1701 1705 eye_effects
## 1801 1804 belt
## 1901 1914 trail
## 2001 2008 feet
@tool
extends Node3D
const EYE_GLOW_SHADER := preload("res://src/scenes/character/eye_glow.gdshader")
## Customisation (0 = auto-select first available on _ready)
@export var hair_style: int = 0 : set = _set_hair
@export var facial1: int = 0 : set = _set_facial1
@export var facial2: int = 0 : set = _set_facial2
@export var facial3: int = 0 : set = _set_facial3
@export var ears: int = 0 : set = _set_ears
@export var eye_effects: int = 0 : set = _set_eye_effects
@export var eye_glow_intensity: float = 1.3
## Skin / face customisation forwarded to CharacterTextureCompositor sibling/child.
@export var skin_color: int = 0 : set = _set_skin_color
@export var face_style: int = 0 : set = _set_face_style
## Internal: available geoset IDs per category (populated in _ready from actual model).
var _available: Dictionary = {} # category_name -> Array[int] (sorted)
## Internal: valid ranges for texture-based customisation (from compositor).
var _skin_color_count: int = 0
var _face_style_count: int = 0
## Equipment toggles (off by default = naked character)
@export var show_gloves: bool = false : set = _set_gloves
@export var show_boots: bool = false : set = _set_boots
@export var show_shirt: bool = false : set = _set_shirt
## Wristband geoset ID: 801 = bare wrists (no bracer), 802+ = equipped bracers.
## 0 = auto (picks first available in model).
@export var wristband_style: int = 0 : set = _set_wristbands
@export var show_kneepads: bool = false : set = _set_kneepads
@export var show_chest: bool = false : set = _set_chest
@export var show_pants: bool = false : set = _set_pants
@export var show_tabard: bool = false : set = _set_tabard
@export var show_legs: bool = false : set = _set_legs
@export var show_cape: bool = false : set = _set_cape
@export var show_belt: bool = false : set = _set_belt
@export var show_feet: bool = false : set = _set_feet
# ── WoW 3.3.5 geoset ID ranges ───────────────────────────────────────────────
# Each entry: [start, end_exclusive, category_name]
const RANGES: Array = [
[0, 1, "skin"],
[1, 35, "hair"],
[101, 125, "facial1"],
[201, 220, "facial2"],
[301, 320, "facial3"],
[401, 406, "gloves"],
[501, 511, "boots"],
[601, 615, "shirt"],
[701, 712, "ears"],
[801, 805, "wristbands"],
[901, 906, "kneepads"],
[1001, 1005, "chest"],
[1101, 1106, "pants"],
[1201, 1205, "tabard"],
[1301, 1304, "legs"],
[1401, 1415, "shirt_doublet"],
[1501, 1525, "cape"],
[1601, 1615, "facial_jewelry"],
[1701, 1706, "eye_effects"],
[1801, 1805, "belt"],
[1901, 1915, "trail"],
[2001, 2009, "feet"],
]
func _ready() -> void:
var nodes := _geoset_nodes()
if nodes.is_empty():
return
# Build _available: actual geoset IDs present in the model, grouped by category.
_available.clear()
for child in nodes:
var gid := _geoset_id(child.name)
var cat := _category(gid)
if not _available.has(cat):
_available[cat] = []
_available[cat].append(gid)
for cat in _available:
(_available[cat] as Array).sort()
# Query compositor for skin/face counts (it knows what PNGs are on disk).
var comp := _get_compositor()
if comp:
_skin_color_count = comp.get_skin_color_count()
_face_style_count = comp.get_face_style_count()
# Clamp each export to the nearest valid value for this model.
hair_style = _clamp_geoset("hair", hair_style)
facial1 = _clamp_geoset("facial1", facial1)
facial2 = _clamp_geoset("facial2", facial2)
facial3 = _clamp_geoset("facial3", facial3)
ears = _clamp_geoset("ears", ears)
eye_effects = _clamp_geoset("eye_effects", eye_effects)
wristband_style = _clamp_geoset("wristbands", wristband_style)
skin_color = clampi(skin_color, 0, maxi(_skin_color_count - 1, 0))
face_style = clampi(face_style, 0, maxi(_face_style_count - 1, 0))
_apply_all(nodes)
_apply_eye_glow_shader(nodes)
# ── setters ───────────────────────────────────────────────────────────────────
func _set_hair(v): hair_style = _clamp_geoset("hair", v); _apply_variant("hair", hair_style)
func _set_facial1(v): facial1 = _clamp_geoset("facial1", v); _apply_variant("facial1", facial1)
func _set_facial2(v): facial2 = _clamp_geoset("facial2", v); _apply_variant("facial2", facial2)
func _set_facial3(v): facial3 = _clamp_geoset("facial3", v); _apply_variant("facial3", facial3)
func _set_ears(v): ears = _clamp_geoset("ears", v); _apply_variant("ears", ears)
func _set_eye_effects(v): eye_effects = _clamp_geoset("eye_effects", v); _apply_variant("eye_effects", eye_effects)
func _set_skin_color(v: int) -> void:
skin_color = clampi(v, 0, maxi(_skin_color_count - 1, 0))
var c := _get_compositor()
if c: c.skin_color = skin_color
func _set_face_style(v: int) -> void:
face_style = clampi(v, 0, maxi(_face_style_count - 1, 0))
var c := _get_compositor()
if c: c.face_style = face_style
func _get_compositor() -> Node:
# Look for CharacterTextureCompositor as a direct child first,
# then as a sibling (child of our parent).
for child in get_children():
if child.get_script() and child.has_method("refresh"):
return child
var p := get_parent()
if p:
for sibling in p.get_children():
if sibling != self and sibling.get_script() and sibling.has_method("refresh"):
return sibling
return null
func _set_gloves(v): show_gloves = v; _apply_toggle("gloves", v)
func _set_boots(v): show_boots = v; _apply_toggle("boots", v)
func _set_shirt(v): show_shirt = v; _apply_toggle("shirt", v)
func _set_wristbands(v): wristband_style = v; _apply_variant("wristbands", v)
func _set_kneepads(v): show_kneepads = v; _apply_toggle("kneepads", v)
func _set_chest(v): show_chest = v; _apply_toggle("chest", v)
func _set_pants(v): show_pants = v; _apply_toggle("pants", v)
func _set_tabard(v): show_tabard = v; _apply_toggle("tabard", v)
func _set_legs(v): show_legs = v; _apply_toggle("legs", v)
func _set_cape(v): show_cape = v; _apply_toggle("cape", v)
func _set_belt(v): show_belt = v; _apply_toggle("belt", v)
func _set_feet(v): show_feet = v; _apply_toggle("feet", v)
# ── visibility ────────────────────────────────────────────────────────────────
## Show the geoset matching exact ID `wanted`; hide all others in the category.
func _apply_variant(cat: String, wanted: int) -> void:
for child in _geoset_nodes():
var gid := _geoset_id(child.name)
if _category(gid) == cat:
child.visible = (gid == wanted)
## Show or hide all geosets in a category.
func _apply_toggle(cat: String, on: bool) -> void:
for child in _geoset_nodes():
if _category(_geoset_id(child.name)) == cat:
child.visible = on
func _apply_all(nodes: Array[Node] = []) -> void:
if nodes.is_empty():
nodes = _geoset_nodes()
for child in nodes:
var gid := _geoset_id(child.name)
var cat := _category(gid)
match cat:
"skin": child.visible = true
"hair": child.visible = (gid == hair_style)
"facial1": child.visible = (gid == facial1)
"facial2": child.visible = (gid == facial2)
"facial3": child.visible = (gid == facial3)
"ears": child.visible = (gid == ears)
"eye_effects": child.visible = (gid == eye_effects)
"gloves": child.visible = show_gloves
"boots": child.visible = show_boots
"shirt": child.visible = show_shirt
"wristbands": child.visible = (gid == wristband_style)
"kneepads": child.visible = show_kneepads
"chest": child.visible = show_chest
"pants": child.visible = show_pants
"tabard": child.visible = show_tabard
"legs": child.visible = show_legs
"cape": child.visible = show_cape
"belt": child.visible = show_belt
"feet": child.visible = show_feet
# shirt_doublet, facial_jewelry, trail: follow shirt / facial / trail
"shirt_doublet": child.visible = show_shirt
"facial_jewelry":child.visible = (gid == facial1 + 1500) # rough guess
"trail": child.visible = true
_: child.visible = true # unknown leave visible
# ── helpers ───────────────────────────────────────────────────────────────────
func _geoset_nodes() -> Array[Node]:
var result: Array[Node] = []
_collect(self, result)
return result
func _collect(node: Node, result: Array[Node]) -> void:
for child in node.get_children():
if child.name.begins_with("geoset_") and child is MeshInstance3D:
result.append(child)
else:
_collect(child, result)
## Extract geoset_id from node name "geoset_NNNN_category_vNN"
func _geoset_id(node_name: String) -> int:
var parts := node_name.split("_")
if parts.size() >= 2:
return parts[1].to_int()
return -1
## Map a geoset ID to its category name using the WoW 3.3.5 ranges.
func _category(gid: int) -> String:
for r in RANGES:
if gid >= r[0] and gid < r[1]:
return r[2]
return "unknown"
## Replace materials on eye-glow geosets with the multiplicative glow shader.
## Detects eye-glow blend in two ways (in priority order):
## 1. GLTF node extras["blend_mode"] == 4 (Mod) or 3 (Add) from M2 material data
## 2. Fallback: geoset ID in the eye_effects range 1701-1705
## Call once after the scene loads.
func _apply_eye_glow_shader(nodes: Array[Node] = []) -> void:
if nodes.is_empty():
nodes = _geoset_nodes()
# Per-race glow colour defaults (Mod blending: tints the eye behind it).
# With blend_mul: result = ALBEDO * background.
# Use saturated hues so the tint is strong; 1.0 on the dominant channel
# keeps that channel of the background intact while suppressing others.
var race_colors := {
"bloodelf": Color(0.1, 1.0, 0.2), # green
"nightelf": Color(0.6, 0.9, 1.0), # silver-blue
"deathknight": Color(0.1, 0.4, 1.0), # icy blue
"scourge": Color(0.1, 0.4, 1.0), # icy blue (undead DKs)
}
# Guess race from scene/model name
var scene_name := get_name().to_lower()
var glow_color := Color(0.2, 1.0, 0.3) # default green
for race in race_colors:
if race in scene_name:
glow_color = race_colors[race]
break
for child in nodes:
var mesh_inst := child as MeshInstance3D
if not mesh_inst:
continue
# Determine whether this geoset uses WoW eye-glow blending.
# Primary: check blend_mode from M2 material stored in GLTF extras.
# blend_mode=4 → Mod (DST_COLOR * src) — most races (Blood Elf, Night Elf, etc.)
# blend_mode=3 → Add (SRC_ALPHA + ONE) — pure additive (rare; DK particles etc.)
# Fallback: geoset ID in the eye_effects range (1701-1705) for older GLBs.
var is_eye_glow := false
if mesh_inst.has_meta("extras"):
var extras = mesh_inst.get_meta("extras")
if extras is Dictionary and extras.has("blend_mode"):
var bm: int = extras["blend_mode"]
is_eye_glow = (bm == 4 or bm == 3)
if not is_eye_glow:
is_eye_glow = (_category(_geoset_id(child.name)) == "eye_effects")
if not is_eye_glow:
continue
# Replace every surface material with the glow shader
for si in range(mesh_inst.get_surface_override_material_count()):
var orig_mat := mesh_inst.mesh.surface_get_material(si)
var shader_mat := ShaderMaterial.new()
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_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
if std.albedo_texture:
shader_mat.set_shader_parameter("albedo_texture", std.albedo_texture)
mesh_inst.set_surface_override_material(si, shader_mat)
## Returns sorted list of geoset IDs available for a given category.
## After _ready() this uses the cached _available dict; before _ready() it scans nodes.
func get_ids(cat: String) -> Array[int]:
if not _available.is_empty():
var ids: Array = _available.get(cat, [])
var result: Array[int] = []
for id in ids:
result.append(id)
return result
# Fallback: scan live (called before _ready in edge cases)
var result: Array[int] = []
for child in _geoset_nodes():
var gid := _geoset_id(child.name)
if _category(gid) == cat and not gid in result:
result.append(gid)
result.sort()
return result
## Returns valid skin colour count for this model (0 if compositor not found yet).
func get_skin_color_count() -> int: return _skin_color_count
## Returns valid face style count for this model (0 if compositor not found yet).
func get_face_style_count() -> int: return _face_style_count
## Clamp/snap v to the nearest available geoset ID in cat.
## Returns v unchanged if _available is not populated yet or cat is missing.
func _clamp_geoset(cat: String, v: int) -> int:
if not _available.has(cat):
return v
var ids: Array = _available[cat] # already sorted
if ids.is_empty():
return v
if v in ids:
return v
# Pick the nearest available ID
var best: int = ids[0]
var best_dist: int = abs(v - best)
for id in ids:
var d: int = abs(v - id)
if d < best_dist:
best_dist = d
best = id
return best
@@ -0,0 +1 @@
uid://cb1cwcj4p6d6p
@@ -0,0 +1,16 @@
[gd_scene format=3 uid="uid://c71mitsmy1vq1"]
[ext_resource type="Script" uid="uid://cb1cwcj4p6d6p" path="res://src/scenes/character/character_geoset_controller.gd" id="1_vyw27"]
[ext_resource type="PackedScene" uid="uid://cf4xxu36nrmnq" path="res://src/resources/characters/Draenei/Female/DraeneiFemale.glb" id="2_vyw27"]
[node name="CharacterPreview" type="Node3D" unique_id=733111432]
script = ExtResource("1_vyw27")
hair_style = 3
facial1 = 104
facial2 = 1021
facial3 = 83
ears = 724
eye_effects = 1702
wristband_style = 802
[node name="DraeneiFemale" parent="." unique_id=999499863 instance=ExtResource("2_vyw27")]
@@ -0,0 +1,242 @@
## CharacterTextureCompositor
##
## Runtime skin-texture compositing for WoW 3.3.5a character models.
##
## Attach as a child of the character's root node (next to
## CharacterGeosetController). Set [member textures_dir] to the
## {ModelName}_textures/ folder that was exported alongside the GLB.
##
## UV layout of the 512x512 WoW character skin texture:
## (0, 0) 256x128 NakedTorsoSkin bra area
## (0, 128) 256x128 NakedPelvisSkin panties area
## (0, 320) 256x64 FaceUpper forehead / upper face
## (0, 384) 256x128 FaceLower eyes, mouth, nose
@tool
extends Node
# ── Layer paste positions (pixels, top-left origin) ─────────────────────────
const NAKED_TORSO_POS := Vector2i(0, 0)
const NAKED_PELVIS_POS := Vector2i(0, 128)
const FACE_UPPER_POS := Vector2i(0, 320)
const FACE_LOWER_POS := Vector2i(0, 384)
# ── Exports ──────────────────────────────────────────────────────────────────
## Path to the {ModelName}_textures/ folder.
## Example: "res://src/resources/characters/BloodElf/Female/BloodElfFemale_textures"
@export var textures_dir: String = "" : set = _set_textures_dir
## Skin colour index (selects skin_XX.png / naked_torso_XX.png /
## naked_pelvis_XX.png).
@export var skin_color: int = 0 : set = _set_skin_color
## Face style index (first part of face_lower_SS_CC.png filename).
@export var face_style: int = 0 : set = _set_face_style
# ── Internal state ───────────────────────────────────────────────────────────
# MeshInstance3D surfaces to override: Array of {node, surface_idx}
var _skin_surfaces: Array = []
# True once _ready has run (prevents setters from firing before init)
var _ready_done := false
func _ready() -> void:
_ready_done = true
if textures_dir.is_empty():
_auto_detect_textures_dir()
_scan_skin_surfaces()
_recomposite()
# ── Setters ──────────────────────────────────────────────────────────────────
func _set_textures_dir(v: String) -> void:
textures_dir = v
if _ready_done:
_scan_skin_surfaces()
_recomposite()
func _set_skin_color(v: int) -> void:
skin_color = v
if _ready_done: _recomposite()
func _set_face_style(v: int) -> void:
face_style = v
if _ready_done: _recomposite()
# ── Auto-detection ───────────────────────────────────────────────────────────
func _auto_detect_textures_dir() -> void:
# Convention: GLB is at res://src/resources/characters/Race/Gender/Model.glb
# Textures: same folder / Model_textures/
# We walk the scene tree to find the first MeshInstance3D and infer from
# the mesh resource path.
var root := get_parent() if get_parent() else get_tree().current_scene
for mesh_inst in _iter_mesh_instances(root):
var mesh := mesh_inst.mesh
if mesh == null:
continue
var rpath := mesh.resource_path # e.g. res://src/resources/.../BloodElfFemale.glb::Mesh_0
if rpath.is_empty():
continue
# Extract the GLB path (everything before "::")
var glb_path := rpath.get_slice("::", 0)
var dir := glb_path.get_base_dir()
var stem := glb_path.get_file().get_basename()
var candidate := dir.path_join(stem + "_textures")
if DirAccess.dir_exists_absolute(candidate):
textures_dir = candidate
print("CharacterTextureCompositor: auto-detected textures_dir = ", textures_dir)
return
push_warning("CharacterTextureCompositor: could not auto-detect textures_dir; set it manually.")
# ── Surface discovery ────────────────────────────────────────────────────────
func _scan_skin_surfaces() -> void:
_skin_surfaces.clear()
var root := get_parent() if get_parent() else self
for mesh_inst in _iter_mesh_instances(root):
var mesh := mesh_inst.mesh
if mesh == null:
continue
for si in range(mesh.get_surface_count()):
if _is_skin_surface(mesh_inst, si):
_skin_surfaces.append({"node": mesh_inst, "surface": si})
## A surface is a "skin surface" if:
## - its active material is a StandardMaterial3D (not ShaderMaterial = eye glow)
## - AND its albedo texture is 512x512 (the composited body skin)
func _is_skin_surface(mesh_inst: MeshInstance3D, si: int) -> bool:
# Prefer surface override, fall back to mesh material
var mat := mesh_inst.get_surface_override_material(si)
if mat == null:
mat = mesh_inst.mesh.surface_get_material(si)
if not (mat is StandardMaterial3D):
return false
var std := mat as StandardMaterial3D
var tex := std.albedo_texture
if tex == null:
return false
# Skin texture is 512x512; hair/other textures are smaller
var img := tex.get_image()
if img == null:
return false
return img.get_width() == 512 and img.get_height() == 512
# ── Compositing ──────────────────────────────────────────────────────────────
func _recomposite() -> void:
if textures_dir.is_empty() or _skin_surfaces.is_empty():
return
var skin_tex := _build_skin_texture()
if skin_tex == null:
return
_apply_texture(skin_tex)
func _build_skin_texture() -> ImageTexture:
# 1. Base skin
var base := _load_layer("skin_%02d.png" % skin_color)
if base == null:
push_warning("CharacterTextureCompositor: missing skin_%02d.png" % skin_color)
return null
# 2. Naked overlays (underwear)
_blit(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)
# 3. Face
_blit(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)
return ImageTexture.create_from_image(base)
func _apply_texture(tex: ImageTexture) -> void:
for entry in _skin_surfaces:
var mesh_inst: MeshInstance3D = entry["node"]
var si: int = entry["surface"]
if not is_instance_valid(mesh_inst):
continue
# Clone the existing material so we don't mutate the shared resource
var orig_mat := mesh_inst.get_surface_override_material(si)
if orig_mat == null:
orig_mat = mesh_inst.mesh.surface_get_material(si)
if not (orig_mat is StandardMaterial3D):
continue
var mat := orig_mat.duplicate() as StandardMaterial3D
mat.albedo_texture = tex
mesh_inst.set_surface_override_material(si, mat)
# ── Helpers ───────────────────────────────────────────────────────────────────
func _load_layer(filename: String) -> Image:
var path := textures_dir.path_join(filename)
if not FileAccess.file_exists(path):
return null
var img := Image.load_from_file(path)
return img
func _blit(dst: Image, src: Image, pos: Vector2i) -> void:
if src == null:
return
dst.blend_rect(src, Rect2i(Vector2i.ZERO, src.get_size()), pos)
func _iter_mesh_instances(root: Node) -> Array[MeshInstance3D]:
var result: Array[MeshInstance3D] = []
_collect_meshes(root, result)
return result
func _collect_meshes(node: Node, result: Array[MeshInstance3D]) -> void:
if node is MeshInstance3D:
result.append(node)
for child in node.get_children():
_collect_meshes(child, result)
# ── Public API ────────────────────────────────────────────────────────────────
## Force a full rescan + recomposite (call after swapping the character GLB).
func refresh() -> void:
_scan_skin_surfaces()
_recomposite()
## Returns the number of skin surfaces found (useful for debugging).
func get_skin_surface_count() -> int:
return _skin_surfaces.size()
## Returns how many skin colour variants exist (i.e. max valid skin_color + 1).
func get_skin_color_count() -> int:
if textures_dir.is_empty():
return 0
var n := 0
while FileAccess.file_exists(textures_dir.path_join("skin_%02d.png" % n)):
n += 1
return n
## Returns how many face styles exist (i.e. max valid face_style + 1).
func get_face_style_count() -> int:
if textures_dir.is_empty():
return 0
var n := 0
while FileAccess.file_exists(textures_dir.path_join("face_lower_%02d_00.png" % n)):
n += 1
return n
@@ -0,0 +1 @@
uid://dowc3tg8h7avb
@@ -0,0 +1,75 @@
[gd_scene format=3 uid="uid://b76a81ecfo3rs"]
[ext_resource type="PackedScene" uid="uid://uc8ihrw2g0hy" path="res://src/resources/characters/BloodElf/Female/BloodElfFemale.glb" id="1_22k5x"]
[node name="BloodElfFemale" unique_id=396948181 instance=ExtResource("1_22k5x")]
[node name="Skeleton3D" parent="Armature" parent_id_path=PackedInt32Array(1689329618) index="0" unique_id=1185793760]
bones/1/position = Vector3(0.020567639, 1.1418878, -0.017966472)
bones/2/position = Vector3(-5.5879354e-09, 0.013204693, 0.00061981526)
bones/2/rotation = Quaternion(0.123804875, 0.11940578, 0.013367673, 0.9850056)
bones/3/rotation = Quaternion(-0.06487095, 0.021112822, -0.035487678, 0.99703896)
bones/4/rotation = Quaternion(-0.11656414, -0.009169256, 0.012765532, 0.9930588)
bones/5/rotation = Quaternion(0.051051795, -0.04647367, 0.0915637, 0.9934033)
bones/6/rotation = Quaternion(-0.0043863687, -0.08180441, -0.15909226, 0.98385894)
bones/9/position = Vector3(0.095388055, 0.06224084, 0)
bones/13/rotation = Quaternion(0.0036348682, -0.0020478186, -0.04413144, 0.99901706)
bones/14/rotation = Quaternion(0.0050126333, -0.0004394381, 0.046115804, 0.99892354)
bones/15/rotation = Quaternion(-0.045602754, -0.019244853, -0.1461395, 0.98802495)
bones/16/rotation = Quaternion(-0.0010522516, 0.0012768576, 0.006581648, 0.999977)
bones/17/rotation = Quaternion(-0.007964021, 0.00088885747, 0.03926191, 0.9991969)
bones/21/position = Vector3(0.116049744, 0.0758859, -0.01846384)
bones/21/rotation = Quaternion(-0.05670909, -0.0046792068, -0.03128321, 0.99788964)
bones/22/rotation = Quaternion(-0.09271755, -0.025453072, 0.047640584, 0.99422634)
bones/24/position = Vector3(0.11551966, -0.0015778318, -0.021920012)
bones/25/position = Vector3(0.116256274, 0.06848195, 0.02165297)
bones/25/rotation = Quaternion(0.02255213, -0.0075094164, -0.0022095616, 0.9997151)
bones/26/rotation = Quaternion(0.09265718, 0.025392218, 0.047640927, 0.9942335)
bones/27/rotation = Quaternion(0.0009912114, -0.0013378978, 0.0065816464, 0.999977)
bones/28/rotation = Quaternion(0.007905513, -0.0009485561, 0.039257552, 0.9991975)
bones/32/position = Vector3(0.1155197, -0.0015768784, 0.021421462)
bones/37/rotation = Quaternion(0.06507223, 0.09674508, 0.068695635, 0.9908012)
bones/38/rotation = Quaternion(-0.022194698, -0.06936834, -0.09758068, 0.9925591)
bones/39/rotation = Quaternion(-0.03300783, -0.013267887, 0.03763607, 0.9986581)
bones/40/rotation = Quaternion(0.033775434, -0.016210906, -0.057038154, 0.9976689)
bones/51/rotation = Quaternion(-0.029908726, 0.020630918, -0.016968625, 0.99919564)
bones/56/rotation = Quaternion(0.0024567072, -0.013335031, -0.0029236332, 0.9999038)
bones/60/rotation = Quaternion(-0.016449975, -0.0067556957, 0.017745575, 0.99968445)
bones/63/rotation = Quaternion(0.026935987, -0.05643349, 0.049124315, 0.9968333)
bones/64/rotation = Quaternion(-0.027013818, 0.0563725, 0.04912438, 0.99683464)
bones/65/rotation = Quaternion(-0.09290248, -0.15748595, 0.06828833, 0.98076713)
bones/66/rotation = Quaternion(0.14272292, -0.011203573, -0.11163199, 0.9833834)
bones/67/rotation = Quaternion(0.06000255, 0.01980045, 0.04261177, 0.9970917)
bones/68/rotation = Quaternion(-0.022882408, 0.006796895, -0.1232713, 0.99208593)
bones/69/position = Vector3(0.0413239, -0.06549978, -0.020981606)
bones/69/rotation = Quaternion(-0.023896394, -0.10211665, 0.037965663, 0.9937604)
bones/71/rotation = Quaternion(-0.08642865, -0.023835018, -0.0076296474, 0.99594367)
bones/73/rotation = Quaternion(-0.053072903, -0.012879106, -0.0046084006, 0.99849695)
bones/75/rotation = Quaternion(-0.032319825, -0.007080453, -0.0044558025, 0.9994426)
bones/77/rotation = Quaternion(0.0020142787, 0.00033571312, 0.0003967519, 0.99999785)
bones/79/position = Vector3(0.014056671, -0.1078162, -0.01205897)
bones/79/rotation = Quaternion(0.0065921065, 0.017426355, -0.06521913, 0.997697)
bones/88/rotation = Quaternion(-0.0022131095, 0.011718128, -0.0025471686, 0.99992573)
bones/90/rotation = Quaternion(0.029425986, 0.010602127, 0.021910178, 0.99927056)
bones/116/position = Vector3(5.5879354e-09, -0.013204574, -0.00061981526)
bones/116/rotation = Quaternion(0.061444823, 0.11480759, -0.11069672, 0.9852868)
bones/117/rotation = Quaternion(-0.15319423, 0.0766162, 0.067507334, 0.98290604)
bones/118/rotation = Quaternion(0.0016076086, -0.0010537306, 0.0116005195, 0.99993086)
bones/119/rotation = Quaternion(0.073131986, 0.04742621, 0.024243427, 0.995899)
bones/120/rotation = Quaternion(-0.012878833, 0.0033570419, 0.0015869653, 0.9999102)
bones/122/rotation = Quaternion(0.00078478147, -0.000550218, 0.0058596972, 0.9999825)
bones/124/rotation = Quaternion(-0.13583149, 0.09406532, 0.17353375, 0.97086954)
bones/125/rotation = Quaternion(0.01501317, -0.009498554, -0.10665566, 0.9941373)
bones/126/rotation = Quaternion(0.04327209, -0.18378712, 0.019996213, 0.9818096)
bones/127/rotation = Quaternion(0.0030824, -0.005432348, 0.0014954217, 0.9999794)
bones/129/rotation = Quaternion(0.0074802367, -0.004755205, -0.05334622, 0.9985368)
bones/131/position = Vector3(-0.15702619, -0.063200355, -0.00018678745)
bones/131/rotation = Quaternion(-0.08914321, 0.0019654154, 0.059140634, 0.9942596)
bones/132/rotation = Quaternion(0.04012855, 0.005028783, -0.012988713, 0.99909747)
bones/133/rotation = Quaternion(-0.077178694, 0.076181, 0.12862553, 0.98574615)
bones/134/rotation = Quaternion(-0.024023479, -0.006217299, -0.013663187, 0.99959874)
bones/135/rotation = Quaternion(0.009281775, 0.039282277, 0.052435584, 0.9978082)
bones/136/position = Vector3(-0.18500036, 0.052093893, 0.009707565)
bones/136/rotation = Quaternion(-0.061086744, -0.06087173, 0.17241617, 0.98124194)
bones/137/rotation = Quaternion(-0.024402294, -0.0006531618, 0.047712527, 0.99856275)
bones/138/rotation = Quaternion(-0.018057052, -0.030023871, 0.06942246, 0.9969719)
+35
View File
@@ -0,0 +1,35 @@
// WoW 3.3.5 character eye glow effect
// Replicates WoW M2 blendingMode=4 (Add: ONE + ONE — pure additive).
//
// render_mode breakdown:
// blend_add — additive blending: eye adds light, makes area brighter
// unshaded — no lighting response, fully self-illuminated
// depth_draw_never — don't write depth, renders on top without occlusion
// cull_disabled — visible from both sides (eye meshes are thin)
//
// How it works:
// result = shader_output + background. The eye glow mesh sits over the
// eye base and adds bright colored light in the shape of the glow texture.
// Darker texture areas contribute nothing; bright areas max out the glow.
shader_type spatial;
render_mode blend_add, unshaded, depth_draw_never, cull_disabled;
// Base texture from the M2 eye-glow BLP (e.g. BloodElfFemaleEyeGlowGreen.blp)
uniform sampler2D albedo_texture : source_color, hint_default_white;
// Tint + brightness multiplier. Defaults match Blood Elf green glow.
uniform vec3 glow_color : source_color = vec3(0.1, 1.0, 0.2);
uniform float glow_intensity : hint_range(0.0, 4.0, 0.05) = 1.3;
void fragment() {
vec4 tex = texture(albedo_texture, UV);
// Use texture luminance as the glow mask; multiply by tint and intensity.
// High-luminance areas of the texture produce bright colored glow.
float lum = dot(tex.rgb, vec3(0.299, 0.587, 0.114));
ALBEDO = glow_color * glow_intensity * lum;
// Alpha drives blend contribution (SRC_ALPHA + ONE in additive mode).
ALPHA = tex.a * lum;
}
@@ -0,0 +1 @@
uid://0p15euukhw1p
+80
View File
@@ -0,0 +1,80 @@
@tool
## Editor-time preview for a single ADT tile.
extends Node3D
const PREVIEW_NODE_NAME := "__adt_tile_preview__"
const ADT_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/adt_builder.gd")
@export var extracted_dir: String = "res://data/extracted"
@export var map_name: String = "Azeroth"
@export var tile_x: int = 32
@export var tile_y: int = 48
@export var auto_rebuild_in_editor: bool = true
@export var reload_now: bool = false
var _last_signature := ""
var _rebuild_queued := false
func _ready() -> void:
set_process(true)
_queue_rebuild()
func _process(_delta: float) -> void:
if reload_now:
reload_now = false
_queue_rebuild()
if Engine.is_editor_hint() and auto_rebuild_in_editor:
var current_signature := _make_signature()
if current_signature != _last_signature:
_queue_rebuild()
func _queue_rebuild() -> void:
if _rebuild_queued or not is_inside_tree():
return
_rebuild_queued = true
call_deferred("_rebuild_preview")
func _rebuild_preview() -> void:
_rebuild_queued = false
_clear_preview()
_last_signature = _make_signature()
if not ClassDB.class_exists("ADTLoader"):
push_warning("ADTLoader not found. Rebuild GDExtension first.")
return
var abs_extracted := ProjectSettings.globalize_path(extracted_dir)
var adt_path := "%s/World/Maps/%s/%s_%d_%d.adt" % [
abs_extracted, map_name, map_name, tile_x, tile_y
]
if not FileAccess.file_exists(adt_path):
push_warning("ADT not found: %s" % adt_path)
return
var loader = ClassDB.instantiate("ADTLoader")
var data: Dictionary = loader.call("load_adt", adt_path)
if data.is_empty() or not data.has("chunks"):
push_warning("Failed to load ADT: %s" % adt_path)
return
var builder := ADT_BUILDER_SCRIPT.new()
var terrain: Node3D = builder.build_scene(data, abs_extracted)
terrain.name = PREVIEW_NODE_NAME
add_child(terrain)
func _clear_preview() -> void:
var preview := get_node_or_null(PREVIEW_NODE_NAME)
if preview:
remove_child(preview)
preview.free()
func _make_signature() -> String:
return "%s|%s|%d|%d" % [extracted_dir, map_name, tile_x, tile_y]
@@ -0,0 +1 @@
uid://cacewewk7xgw4
+30
View File
@@ -0,0 +1,30 @@
[gd_scene format=3 uid="uid://cbgbp30flxdgx"]
[ext_resource type="Script" uid="uid://cacewewk7xgw4" path="res://src/scenes/preview/adt_tile_preview.gd" id="1_preview"]
[ext_resource type="Script" uid="uid://bauggobg40psr" path="res://src/scenes/camera/fly_camera.gd" id="2_flycam"]
[sub_resource type="Sky" id="sky_1"]
[sub_resource type="Environment" id="env_1"]
background_mode = 2
sky = SubResource("sky_1")
ambient_light_source = 3
ambient_light_color = Color(1, 1, 1, 1)
ambient_light_energy = 0.5
[node name="ADTTilePreview" type="Node3D" unique_id=623058517]
script = ExtResource("1_preview")
[node name="Camera3D" type="Camera3D" parent="." unique_id=597499101]
transform = Transform3D(1, 0, 0, 0, 0.906308, 0.422618, 0, -0.422618, 0.906308, 18.5112, 168.016, 111.689)
current = true
far = 20000.0
script = ExtResource("2_flycam")
[node name="Sun" type="DirectionalLight3D" parent="." unique_id=697026372]
transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 0, 0, 0)
light_energy = 1.5
shadow_enabled = true
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1785163215]
environment = SubResource("env_1")
@@ -0,0 +1,41 @@
[gd_scene format=3 uid="uid://f1nqi4emji47"]
[ext_resource type="Script" uid="uid://yi6lawwjgocg" path="res://src/scenes/streaming/streaming_world_loader.gd" id="1_stream"]
[ext_resource type="Script" uid="uid://bauggobg40psr" path="res://src/scenes/camera/fly_camera.gd" id="2_flycam"]
[sub_resource type="Sky" id="Sky_1"]
[sub_resource type="Environment" id="Environment_1"]
background_mode = 2
sky = SubResource("Sky_1")
ambient_light_source = 3
ambient_light_color = Color(1, 1, 1, 1)
ambient_light_energy = 0.5
[node name="StreamingWorld" type="Node3D" unique_id=1063159974]
script = ExtResource("1_stream")
camera_path = NodePath("Camera3D")
update_interval = 0.1
max_concurrent_tile_tasks = 4
chunk_ops_per_tick = 64
cached_tile_mesh_limit = 48
use_baked_tile_cache = false
enable_water = true
terrain_cast_shadows = true
m2_cast_shadows = null
[node name="Camera3D" type="Camera3D" parent="." unique_id=502573687]
transform = Transform3D(1, 0, 0, 0, 0.707107, 0.707107, 0, -0.707107, 0.707107, 0, 300, 300)
current = true
far = 50000.0
script = ExtResource("2_flycam")
speed = 1200.0
fast_mult = 8.0
[node name="Sun" type="DirectionalLight3D" parent="." unique_id=1436804627]
transform = Transform3D(0.866025, -0.353553, 0.353553, 0, 0.707107, 0.707107, -0.5, -0.612372, 0.612372, 0, 0, 0)
light_energy = 1.5
shadow_enabled = true
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=12906896]
environment = SubResource("Environment_1")
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
uid://yi6lawwjgocg
+26
View File
@@ -0,0 +1,26 @@
[gd_scene format=3 uid="uid://chk8oerov1xve"]
[ext_resource type="Script" uid="uid://dq3t6tfy5wa0r" path="res://src/scenes/streaming/test_world_loader.gd" id="1_gyc4i"]
[ext_resource type="Script" path="res://src/scenes/camera/fly_camera.gd" id="2_flycam"]
[sub_resource type="Sky" id="sky_1"]
[sub_resource type="Environment" id="env_1"]
background_mode = 2
sky = SubResource("sky_1")
[node name="World" type="Node3D"]
script = ExtResource("1_gyc4i")
[node name="Camera3D" type="Camera3D" parent="."]
transform = Transform3D(1, 0, 0, 0, 0.857, 0.515, 0, -0.515, 0.857, 0, 500, 500)
far = 20000.0
current = true
script = ExtResource("2_flycam")
[node name="Sun" type="DirectionalLight3D" parent="."]
transform = Transform3D(0.866, -0.353, 0.354, 0, 0.707, 0.707, -0.5, -0.612, 0.612, 0, 0, 0)
light_energy = 1.5
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
environment = SubResource("env_1")
+85
View File
@@ -0,0 +1,85 @@
## Загрузчик Элвиннского леса — 3×3 ADT тайла из extracted/
extends Node3D
const ADT_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/adt_builder.gd")
@export var extracted_dir : String = "res://data/extracted"
@export var map_name : String = "Azeroth"
## Центр Элвиннского леса (тайл 32,48), загружаем 3×3 вокруг него
@export var center_x : int = 32
@export var center_y : int = 48
@export var radius : int = 1 ## 1 = 3×3, 2 = 5×5
func _ready() -> void:
if not ClassDB.class_exists("ADTLoader"):
push_error("ADTLoader не найден — пересобери GDExtension")
return
var abs_extracted := ProjectSettings.globalize_path(extracted_dir)
print("Папка extracted: ", abs_extracted)
var loader = ClassDB.instantiate("ADTLoader")
var loaded := 0
for dy in range(-radius, radius + 1):
for dx in range(-radius, radius + 1):
var tx := center_x + dx
var ty := center_y + dy
var path := "%s/World/Maps/%s/%s_%d_%d.adt" % [
abs_extracted, map_name, map_name, tx, ty]
print("Проверяю: ", path, "", FileAccess.file_exists(path))
if not FileAccess.file_exists(path):
continue
var data: Dictionary = loader.call("load_adt", path)
if data.is_empty() or not data.has("chunks"):
push_warning("Не удалось загрузить: ", path)
continue
# Debug first chunk of first tile only
if loaded == 0:
var chunks: Array = data.get("chunks", [])
if chunks.size() > 0:
var c0 = chunks[0]
var origin0: Vector3 = c0.get("origin", Vector3.ZERO)
var h: PackedFloat32Array = c0.get("heights", PackedFloat32Array())
print("DEBUG chunk[0] origin=", origin0)
if h.size() > 0:
print("DEBUG heights[0]=", h[0], " [72]=", h[72] if h.size()>72 else 0.0)
var builder := ADT_BUILDER_SCRIPT.new()
var terrain: Node3D = builder.build_scene(data, abs_extracted)
add_child(terrain)
loaded += 1
print("Загружен тайл %d,%d (%d чанков)" % [tx, ty, terrain.get_child_count()])
print("Всего тайлов загружено: ", loaded)
# Print terrain AABB so you know where to fly
await get_tree().process_frame
var aabb := AABB(); var first := true
for child in get_children():
if not (child is Node3D): continue
for chunk in child.get_children():
if chunk is MeshInstance3D and chunk.mesh:
var b: AABB = chunk.get_aabb()
b.position += chunk.global_position
if first: aabb = b; first = false
else: aabb = aabb.merge(b)
var bad_count := 0
for child in get_children():
if not (child is Node3D): continue
for chunk in child.get_children():
if not (chunk is MeshInstance3D): continue
var gpos: Vector3 = chunk.global_position
var local_aabb: AABB = chunk.get_aabb()
if abs(gpos.y) > 1000.0 or abs(local_aabb.position.y) > 1000.0 or local_aabb.size.y > 1000.0:
if bad_count < 5:
print("BAD chunk ", chunk.name, " gpos=", gpos, " local_aabb=", local_aabb)
bad_count += 1
if bad_count > 0:
print("Total bad chunks: ", bad_count)
if not first:
print("Terrain AABB center=", aabb.get_center(), " size=", aabb.size)
print("Fly to: ", aabb.get_center() + Vector3(0, aabb.size.length() * 0.3, 0))
@@ -0,0 +1 @@
uid://dq3t6tfy5wa0r