Initial commit

This commit is contained in:
scorpion21211-cmd
2026-10-03 23:29:41 -04:00
commit 965f0d7584
10586 changed files with 1030294 additions and 0 deletions
@@ -0,0 +1,114 @@
@tool
extends RefCounted
class_name CghFlipbookImport
## Builds a PLAYING flipbook from a sprite-sheet (NxM grid) and adds it to the
## open scene. Uses Sprite3D (hframes/vframes) + an AnimationPlayer that keyframes
## the `frame` property — the official, non-deprecated path that animates BOTH in
## the editor viewport and at runtime (AnimatedTexture is deprecated/broken).
## Handles .exr HDR sheets (load_exr_from_buffer) as well as png/jpg/webp.
static func from_sheet(sheet_path: String, h_frames: int, v_frames: int, fps := 30.0) -> Sprite3D:
# SNIFF the real format from magic bytes rather than trusting the extension — CDN sheets
# are sometimes served as webp/jpg but named ".png", and Godot's extension-based img.load()
# then fails with a misleading "PNG corrupt" (png_driver_common). Sniffing decodes them.
var img := _decode_sheet(FileAccess.get_file_as_bytes(sheet_path), sheet_path.get_extension().to_lower())
if img == null:
push_error("[CGHEVEN] Could not load flipbook sheet: " + sheet_path)
return null
var tex := ImageTexture.create_from_image(img)
var cols: int = max(1, h_frames)
var rows: int = max(1, v_frames)
var total: int = cols * rows
# Sprite3D slices the sheet natively via hframes/vframes; `frame` indexes the grid.
var spr := Sprite3D.new()
spr.name = "CghFlipbook"
spr.texture = tex
spr.hframes = cols
spr.vframes = rows
spr.frame = 0
spr.billboard = BaseMaterial3D.BILLBOARD_ENABLED # face the camera (VFX)
spr.alpha_cut = SpriteBase3D.ALPHA_CUT_DISABLED # default = alpha blend
var edited := EditorInterface.get_edited_scene_root()
if edited == null:
push_warning("[CGHEVEN] No open scene — flipbook node created but not added. Open a scene and re-import.")
return spr
# A Sprite3D only lives/renders in a 3D world. If the open scene is a UI
# (Control) or 2D (Node2D) scene, nesting it inside that tree makes it
# invisible. So guarantee a 3D host: use the root if it is Node3D, else drop
# the flipbook under a dedicated Node3D wrapper (reused across imports).
var parent_3d: Node = _ensure_3d_parent(edited)
parent_3d.add_child(spr)
spr.owner = edited
# Single-frame sheet (no NxM in the filename) — nothing to animate.
if total <= 1:
return spr
# Build a discrete, looping animation that steps `frame` 0..total-1.
var anim := Animation.new()
var track := anim.add_track(Animation.TYPE_VALUE)
anim.track_set_path(track, NodePath(".:frame"))
anim.value_track_set_update_mode(track, Animation.UPDATE_DISCRETE)
anim.length = float(total) / fps
anim.loop_mode = Animation.LOOP_LINEAR
for i in total:
anim.track_insert_key(track, float(i) / fps, i)
var lib := AnimationLibrary.new()
lib.add_animation("play", anim)
var ap := AnimationPlayer.new()
ap.name = "FlipbookPlayer"
spr.add_child(ap)
ap.owner = edited
ap.add_animation_library("", lib)
ap.autoplay = "play" # plays automatically when the scene runs
ap.play("play") # and starts immediately in the editor viewport
return spr
## Decode a sprite-sheet from raw bytes, sniffing the real format from magic bytes (the file
## extension often lies). PNG=89 50 4E 47, JPEG=FF D8, WEBP=RIFF....WEBP, EXR=76 2F 31 01.
## Returns null on an unrecognised/corrupt body.
static func _decode_sheet(body: PackedByteArray, ext: String) -> Image:
if body.size() < 12:
return null
var img := Image.new()
var ok := ERR_INVALID_DATA
if body[0] == 0x89 and body[1] == 0x50 and body[2] == 0x4E and body[3] == 0x47:
ok = img.load_png_from_buffer(body)
elif body[0] == 0xFF and body[1] == 0xD8:
ok = img.load_jpg_from_buffer(body)
elif body[0] == 0x52 and body[1] == 0x49 and body[2] == 0x46 and body[3] == 0x46 \
and body[8] == 0x57 and body[9] == 0x45 and body[10] == 0x42 and body[11] == 0x50:
ok = img.load_webp_from_buffer(body)
elif body[0] == 0x76 and body[1] == 0x2F and body[2] == 0x31 and body[3] == 0x01:
ok = img.load_exr_from_buffer(body)
elif ext == "exr":
ok = img.load_exr_from_buffer(body) # magic missing but named .exr — try anyway
else:
return null
return img if ok == OK else null
## Returns a 3D-capable parent for the flipbook. If the edited scene root is a
## Node3D we use it directly; otherwise (Control / Node2D / plain Node scene) we
## create — or reuse — a "CghevenFlipbooks" Node3D so the sprite always sits in a
## real 3D context and renders in the 3D viewport instead of vanishing into UI.
static func _ensure_3d_parent(edited: Node) -> Node:
if edited is Node3D:
return edited
var existing := edited.get_node_or_null(NodePath("CghevenFlipbooks"))
if existing != null and existing is Node3D:
return existing
var host := Node3D.new()
host.name = "CghevenFlipbooks"
edited.add_child(host)
host.owner = edited
push_warning("[CGHEVEN] Open scene is not 3D — flipbook placed under a new "
+ "'CghevenFlipbooks' Node3D. Switch to the 3D viewport (or open a 3D "
+ "scene) to see it play.")
return host
@@ -0,0 +1 @@
uid://d06uk4ol66bf2
@@ -0,0 +1,26 @@
@tool
extends RefCounted
class_name CghGltfImport
## Imports a downloaded .glb/.gltf into the currently edited scene.
static func import_into_scene(glb_path: String) -> Node:
var doc := GLTFDocument.new()
var state := GLTFState.new()
var err := doc.append_from_file(glb_path, state)
if err != OK:
push_error("[CGHEVEN] glTF parse failed: %d" % err)
return null
var scene_root := doc.generate_scene(state)
if scene_root == null:
push_error("[CGHEVEN] glTF generate_scene returned null")
return null
var edited := EditorInterface.get_edited_scene_root()
if edited == null:
push_warning("[CGHEVEN] No open scene to import into.")
return scene_root
edited.add_child(scene_root)
# own the node so it saves with the scene
scene_root.owner = edited
for c in scene_root.find_children("*", "", true, false):
c.owner = edited
return scene_root
@@ -0,0 +1 @@
uid://c2th51vfweq7f
@@ -0,0 +1,48 @@
@tool
extends RefCounted
class_name CghHdriImport
## Sets the scene's world environment from an equirectangular HDRI.
## Builds WorldEnvironment → Environment(BG_SKY) → Sky → PanoramaSkyMaterial,
## with ambient + reflections taken from the sky (official Godot sky workflow).
## Reuses an existing WorldEnvironment if the scene already has one (Godot allows
## only one active per scene tree). HDRIs are served as .exr (load_exr_from_buffer).
static func apply_as_world(exr_path: String) -> WorldEnvironment:
var img := Image.new()
var ext := exr_path.get_extension().to_lower()
var ok: int = img.load_exr_from_buffer(FileAccess.get_file_as_bytes(exr_path)) if ext == "exr" \
else img.load(exr_path)
if ok != OK:
push_error("[CGHEVEN] Could not load HDRI: " + exr_path)
return null
var tex := ImageTexture.create_from_image(img)
var sky_mat := PanoramaSkyMaterial.new()
sky_mat.panorama = tex
var sky := Sky.new()
sky.sky_material = sky_mat
var env := Environment.new()
env.background_mode = Environment.BG_SKY
env.sky = sky
env.ambient_light_source = Environment.AMBIENT_SOURCE_SKY
env.reflected_light_source = Environment.REFLECTION_SOURCE_SKY
var edited := EditorInterface.get_edited_scene_root()
if edited == null:
push_warning("[CGHEVEN] No open scene — WorldEnvironment created but not added.")
var orphan := WorldEnvironment.new()
orphan.environment = env
return orphan
# Reuse an existing WorldEnvironment (only one may be active per scene tree).
var existing := edited.find_children("*", "WorldEnvironment", true, false)
var we: WorldEnvironment
if existing.is_empty():
we = WorldEnvironment.new()
we.name = "WorldEnvironment"
edited.add_child(we)
we.owner = edited
else:
we = existing[0]
we.environment = env
return we
@@ -0,0 +1 @@
uid://b6ro7gtcoafbj
@@ -0,0 +1,42 @@
@tool
extends RefCounted
class_name CghMaterialImport
## Builds a StandardMaterial3D from a PBR texture set and assigns it to the
## selected MeshInstance3D. NOTE: Blender procedural node-graphs do NOT import;
## only the baked-PBR subset is reproduced here.
static func build_pbr(tex_paths: Dictionary) -> StandardMaterial3D:
# tex_paths keys: albedo, normal, roughness, metallic, emission, ao
var m := StandardMaterial3D.new()
if tex_paths.has("albedo"):
m.albedo_texture = _load(tex_paths["albedo"])
if tex_paths.has("normal"):
m.normal_enabled = true
m.normal_texture = _load(tex_paths["normal"])
if tex_paths.has("roughness"):
m.roughness_texture = _load(tex_paths["roughness"])
if tex_paths.has("metallic"):
m.metallic = 1.0
m.metallic_texture = _load(tex_paths["metallic"])
if tex_paths.has("ao"):
m.ao_enabled = true
m.ao_texture = _load(tex_paths["ao"])
if tex_paths.has("emission"):
m.emission_enabled = true
m.emission_texture = _load(tex_paths["emission"])
return m
static func _load(path: String) -> Texture2D:
var img := Image.new()
if img.load(path) != OK:
return null
return ImageTexture.create_from_image(img)
static func assign_to_selection(mat: Material) -> bool:
var sel := EditorInterface.get_selection().get_selected_nodes()
for n in sel:
if n is MeshInstance3D:
n.material_override = mat
return true
push_warning("[CGHEVEN] Select a MeshInstance3D to apply the material.")
return false
@@ -0,0 +1 @@
uid://binrjwgiv4o85
+231
View File
@@ -0,0 +1,231 @@
@tool
extends RefCounted
class_name CghImportRouter
## Routes a downloaded file to the right importer. CRITICAL: it routes by the
## asset's CATEGORY first, not just the extension — an `.exr` can be EITHER an
## HDRI panorama (hdri category) OR a flipbook sprite-sheet (flipbooks category),
## so extension alone is ambiguous and used to send flipbook .exr sheets to the
## HDRI importer (sky), which is why flipbooks "imported but never played".
##
## After a successful live-instance into the open scene, the source file is ALSO
## copied into res://CGHEVEN/<category>/ so it shows up in the FileSystem dock and
## the user can find / reuse it (Godot only imports files that live under res://).
const RES_ROOT := "res://CGHEVEN"
# Model formats Godot's EDITOR can import (into a PackedScene, or a Mesh for .obj) — far
# more than the runtime GLTFDocument path, which only handles glTF. Order = most reliable
# first (used to pick the best model inside a zip, like the Blender addon's priority).
const EDITOR_MODEL_PRIORITY := ["glb", "gltf", "fbx", "dae", "obj"]
static func import_file(path: String, category_slug: String) -> String:
var ext := path.get_extension().to_lower()
# unpack archives first, then re-route on the inner importable file
if ext == "zip":
var inner := _extract_zip(path)
if inner == "":
return "Extracted zip, but found no importable file inside."
return import_file(inner, category_slug)
var cat := category_slug.to_lower()
var msg := ""
# Category decides intent. Match the TOKEN, not exact equality, because the slug can be a
# SUBCATEGORY ("muzzle-flashes-flipbooks", "space-hdr", …) — matching "flipbooks" exactly
# used to send subcategory flipbook .exr sheets to the HDRI importer (wrong). Fall back to
# extension when the category is unknown.
if cat.contains("flipbook"):
msg = _do_flipbook(path)
elif cat.contains("hdr"):
msg = _do_hdri(path)
elif cat == "3d-models" or cat.contains("model"):
msg = _do_3d(path)
elif ext in CghConfig.EXT_3D:
msg = _do_3d(path)
elif ext in CghConfig.EXT_HDRI:
msg = _do_hdri(path)
elif ext in CghConfig.EXT_IMAGE:
msg = _do_flipbook(path)
else:
# Not auto-importable — still drop it in the project so the user has it.
var rp0 := copy_into_project(path, cat)
return "Saved to %s (no auto-import for .%s)." % [rp0, ext] if rp0 != "" else \
"Downloaded to %s (no auto-import for .%s)." % [path, ext]
# Copy a reusable source into the project (FileSystem dock) and tell the user.
var rp := copy_into_project(path, cat)
if rp != "":
msg += " Saved in %s/" % rp.get_base_dir()
return msg
# ---------------- per-type importers ----------------
static func _do_flipbook(path: String) -> String:
var hv := _flipbook_grid(path)
var spr := CghFlipbookImport.from_sheet(path, hv.x, hv.y)
if spr == null:
if path.get_extension().to_lower() == "exr":
return "Flipbook import failed — this .exr uses a compression Godot can't read. Pick the PNG version from the ▾ menu."
return "Flipbook import failed."
if hv == Vector2i(1, 1):
return "Flipbook added (single frame — grid NxM not in filename, can't auto-play)."
return "Flipbook added & playing (%dx%d grid)." % [hv.x, hv.y]
static func _do_hdri(path: String) -> String:
var we := CghHdriImport.apply_as_world(path)
return "HDRI applied as the scene's world/sky." if we else \
"HDRI import failed — this .exr uses a compression Godot can't read (e.g. DWAA/PXR24). Try another HDRI."
static func _do_3d(path: String) -> String:
var ext := path.get_extension().to_lower()
var n: Node = _import_fbx(path) if ext == "fbx" else CghGltfImport.import_into_scene(path)
return "3D model added to the scene." if n else "3D import failed (open a scene first?)."
static func _import_fbx(path: String) -> Node:
# ufbx (4.3+) — FBXDocument mirrors GLTFDocument
var doc := FBXDocument.new()
var state := FBXState.new()
if doc.append_from_file(path, state) != OK:
return null
var root := doc.generate_scene(state)
var edited := EditorInterface.get_edited_scene_root()
if edited and root:
edited.add_child(root)
root.owner = edited
for c in root.find_children("*", "", true, false):
c.owner = edited
return root
# ---------------- project copy (FileSystem dock) ----------------
# Copy the downloaded source into res://CGHEVEN/<category>/<filename> so Godot
# imports it and it appears in the FileSystem dock (only files under res:// are
# importable/usable). Returns the res:// path, or "" on failure.
static func copy_into_project(path: String, category_slug: String) -> String:
var bytes := FileAccess.get_file_as_bytes(path)
if bytes.is_empty():
return ""
var sub := category_slug if category_slug != "" else "assets"
var dir := RES_ROOT.path_join(sub)
DirAccess.make_dir_recursive_absolute(dir)
var dest := dir.path_join(CghConfig.safe_download_name(path.get_file()))
var f := FileAccess.open(dest, FileAccess.WRITE)
if f == null:
return ""
f.store_buffer(bytes)
f.close()
# Make the editor pick up + import the new file so it shows in the dock.
if Engine.is_editor_hint():
var efs := EditorInterface.get_resource_filesystem()
if efs and not efs.is_scanning():
efs.scan()
return dest
## Extract EVERY file from a zip into a sibling temp dir and return the best
## editor-importable MODEL inside (glb/gltf > fbx > dae > obj), or "" if none. Extracts
## the whole tree (not just the model) so .bin/.mtl/textures land next to it and survive
## the import. Mirrors the Blender addon's _pick_best_model_in_tree.
static func extract_model_from_zip(zip_path: String) -> String:
var reader := ZIPReader.new()
if reader.open(zip_path) != OK:
return ""
var out_dir := zip_path.get_basename() + "_unzip"
DirAccess.make_dir_recursive_absolute(out_dir)
var best := ""
var best_rank := 999
for entry in reader.get_files():
if entry.ends_with("/"):
continue
var rel := entry.replace("\\", "/")
var out := out_dir.path_join(rel)
DirAccess.make_dir_recursive_absolute(out.get_base_dir())
var f := FileAccess.open(out, FileAccess.WRITE)
if f:
f.store_buffer(reader.read_file(entry))
f.close()
var rank: int = EDITOR_MODEL_PRIORITY.find(rel.get_extension().to_lower())
if rank != -1 and rank < best_rank:
best_rank = rank
best = out
reader.close()
return best
## Copy a model file AND its sibling files (textures / .bin / .mtl in the SAME folder)
## into res://CGHEVEN/<category>/<modelname>/ so the editor import keeps materials. Use
## this for models extracted from a zip (isolated temp dir); for a bare downloaded file
## use copy_into_project (single file). Returns the model's res:// path, or "" on failure.
static func copy_model_into_project(model_path: String, category_slug: String) -> String:
var src_dir := model_path.get_base_dir()
var sub := category_slug if category_slug != "" else "assets"
var stem := CghConfig.safe_download_name(model_path.get_file()).get_basename()
var dest_dir := RES_ROOT.path_join(sub).path_join(stem)
DirAccess.make_dir_recursive_absolute(dest_dir)
var model_dest := ""
var da := DirAccess.open(src_dir)
if da:
da.list_dir_begin()
var fn := da.get_next()
while fn != "":
if not da.current_is_dir():
var bytes := FileAccess.get_file_as_bytes(src_dir.path_join(fn))
var d := dest_dir.path_join(CghConfig.safe_download_name(fn))
var wf := FileAccess.open(d, FileAccess.WRITE)
if wf:
wf.store_buffer(bytes)
wf.close()
if fn == model_path.get_file():
model_dest = d
fn = da.get_next()
da.list_dir_end()
if model_dest == "":
# fallback: copy just the model file (distinct var names — GDScript locals are
# function-scoped, so re-declaring bytes/wf from the loop above would error).
var fbytes := FileAccess.get_file_as_bytes(model_path)
if fbytes.is_empty():
return ""
model_dest = dest_dir.path_join(CghConfig.safe_download_name(model_path.get_file()))
var fwf := FileAccess.open(model_dest, FileAccess.WRITE)
if fwf == null:
return ""
fwf.store_buffer(fbytes)
fwf.close()
if Engine.is_editor_hint():
var efs := EditorInterface.get_resource_filesystem()
if efs and not efs.is_scanning():
efs.scan()
return model_dest
static func _extract_zip(zip_path: String) -> String:
var reader := ZIPReader.new()
if reader.open(zip_path) != OK:
return ""
var ok := CghConfig.EXT_3D + CghConfig.EXT_HDRI + CghConfig.EXT_IMAGE
var out_dir := zip_path.get_basename() + "_unzip"
DirAccess.make_dir_recursive_absolute(out_dir)
var best := ""
var best_rank := 99
for entry in reader.get_files():
if entry.ends_with("/"):
continue
var e_ext := entry.get_extension().to_lower()
if not (e_ext in ok):
continue
var out := out_dir + "/" + entry.get_file()
var f := FileAccess.open(out, FileAccess.WRITE)
if f:
f.store_buffer(reader.read_file(entry))
f.close()
# prefer 3D > hdri > image when several importable files exist
var rank := 0 if e_ext in CghConfig.EXT_3D else (1 if e_ext in CghConfig.EXT_HDRI else 2)
if rank < best_rank:
best_rank = rank
best = out
reader.close()
return best
static func _flipbook_grid(path: String) -> Vector2i:
# look for an NxM token in the filename, e.g. fire_8x8.png / Portal_04_4K_8X8.exr
var rx := RegEx.new()
rx.compile("(\\d+)\\s*[xX]\\s*(\\d+)")
var m := rx.search(path.get_file())
if m:
return Vector2i(int(m.get_string(1)), int(m.get_string(2)))
return Vector2i(1, 1)
@@ -0,0 +1 @@
uid://cyrdsmgv6xlmi