Records wear real sleeves: Nano Banana art atlases + UV-tile skinning
- assets/art/sleeves_{a,b}.jpg: two 3x3 sheets of ORIGINAL fictional album
covers (18 total — dub, techno, metal, jazz, punk, synthwave, hip-hop,
psych, ambient, northern soul, krautrock, disco, white-label, bossa,
UKG, country, noise) generated with Gemini gemini-3-pro-image. No real
artists/albums — copyright-clean destructible content.
- assets/art/keyart.jpg: DESTROYULATOR title key art ("CLOCK OUT. SMASH
EVERYTHING.") for a future title screen.
- Main.gd _skin_record(): records duplicate their M_Sleeve_Front/Back
materials and window one random atlas tile via uv1_scale/uv1_offset —
one texture in VRAM serves nine covers, every record looks different.
- Verified headless: boots clean, "[art] records skinned: 15" (all of
them). The two dummy-renderer "material is null" prints are headless-
only (override materials have no RID in the dummy backend). Eyeball a
sleeve in the editor — if the art renders rotated, flip uv1 here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7c88e8371d
commit
b78ea04183
BIN
assets/art/keyart.jpg
Normal file
BIN
assets/art/keyart.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 766 KiB |
40
assets/art/keyart.jpg.import
Normal file
40
assets/art/keyart.jpg.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://drqh8c73yn7c3"
|
||||||
|
path="res://.godot/imported/keyart.jpg-a17068a646e54c982c021f2d854b8d99.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/art/keyart.jpg"
|
||||||
|
dest_files=["res://.godot/imported/keyart.jpg-a17068a646e54c982c021f2d854b8d99.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
assets/art/sleeves_a.jpg
Normal file
BIN
assets/art/sleeves_a.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
40
assets/art/sleeves_a.jpg.import
Normal file
40
assets/art/sleeves_a.jpg.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dkqsrlw0qbq8q"
|
||||||
|
path="res://.godot/imported/sleeves_a.jpg-c3eb7828a71203698e54664c9ba6c8ac.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/art/sleeves_a.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeves_a.jpg-c3eb7828a71203698e54664c9ba6c8ac.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
BIN
assets/art/sleeves_b.jpg
Normal file
BIN
assets/art/sleeves_b.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.6 MiB |
40
assets/art/sleeves_b.jpg.import
Normal file
40
assets/art/sleeves_b.jpg.import
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dxq5sjamvdegc"
|
||||||
|
path="res://.godot/imported/sleeves_b.jpg-5f25abc3a10a57693e8df03cba6dbd6d.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://assets/art/sleeves_b.jpg"
|
||||||
|
dest_files=["res://.godot/imported/sleeves_b.jpg-5f25abc3a10a57693e8df03cba6dbd6d.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
@ -82,6 +82,16 @@ const RACK_GLB := "res://assets/store/rack.glb"
|
|||||||
const CRATE_GLB := "res://assets/store/crate.glb"
|
const CRATE_GLB := "res://assets/store/crate.glb"
|
||||||
const RECORD_GLB := "res://assets/store/record.glb"
|
const RECORD_GLB := "res://assets/store/record.glb"
|
||||||
|
|
||||||
|
# Generated sleeve art (Nano Banana): each file is a 3x3 sheet of nine original
|
||||||
|
# fictional album covers. A record picks one tile at random via uv1 scale/offset —
|
||||||
|
# one texture in VRAM, eighteen different-looking records on the floor.
|
||||||
|
const SLEEVE_ATLASES: Array[String] = [
|
||||||
|
"res://assets/art/sleeves_a.jpg",
|
||||||
|
"res://assets/art/sleeves_b.jpg",
|
||||||
|
]
|
||||||
|
var _sleeve_textures: Array[Texture2D] = []
|
||||||
|
var _records_skinned := 0
|
||||||
|
|
||||||
func _build_rack(origin: Vector3) -> void:
|
func _build_rack(origin: Vector3) -> void:
|
||||||
# two racks form a smashable back wall of the booth
|
# two racks form a smashable back wall of the booth
|
||||||
_glb_piece(RACK_GLB, "wood", Vector2(origin.x - 0.95, origin.z - 0.7), true)
|
_glb_piece(RACK_GLB, "wood", Vector2(origin.x - 0.95, origin.z - 0.7), true)
|
||||||
@ -101,6 +111,7 @@ func _build_rack(origin: Vector3) -> void:
|
|||||||
var jitter := Vector2(randf_range(-0.06, 0.06), randf_range(-0.06, 0.06))
|
var jitter := Vector2(randf_range(-0.06, 0.06), randf_range(-0.06, 0.06))
|
||||||
var rec := _glb_piece(RECORD_GLB, "vinyl", xz + jitter, false, crate_top + float(i) * 0.03)
|
var rec := _glb_piece(RECORD_GLB, "vinyl", xz + jitter, false, crate_top + float(i) * 0.03)
|
||||||
(crate["piece"] as Smashable).supports.append(rec["piece"])
|
(crate["piece"] as Smashable).supports.append(rec["piece"])
|
||||||
|
print("[art] records skinned with sleeve art: ", _records_skinned)
|
||||||
|
|
||||||
## Instance a GLB as the VISUAL of a Smashable, give it a box collider auto-sized to
|
## Instance a GLB as the VISUAL of a Smashable, give it a box collider auto-sized to
|
||||||
## the mesh AABB, and drop it so its bottom rests at height `sit_on`. Returns
|
## the mesh AABB, and drop it so its bottom rests at height `sit_on`. Returns
|
||||||
@ -112,6 +123,8 @@ func _glb_piece(path: String, kind: String, xz: Vector2, frozen: bool, sit_on :=
|
|||||||
var packed := load(path) as PackedScene
|
var packed := load(path) as PackedScene
|
||||||
var vis: Node3D = packed.instantiate()
|
var vis: Node3D = packed.instantiate()
|
||||||
s.add_child(vis)
|
s.add_child(vis)
|
||||||
|
if kind == "vinyl":
|
||||||
|
_skin_record(vis) # random sleeve art on the jacket faces
|
||||||
_world.add_child(s) # in-tree so transforms/AABBs resolve
|
_world.add_child(s) # in-tree so transforms/AABBs resolve
|
||||||
var ab := _body_local_aabb(s, vis)
|
var ab := _body_local_aabb(s, vis)
|
||||||
if ab.size == Vector3.ZERO:
|
if ab.size == Vector3.ZERO:
|
||||||
@ -126,6 +139,43 @@ func _glb_piece(path: String, kind: String, xz: Vector2, frozen: bool, sit_on :=
|
|||||||
s.smashed.connect(_on_smashed)
|
s.smashed.connect(_on_smashed)
|
||||||
return {"piece": s, "top": sit_on + ab.size.y}
|
return {"piece": s, "top": sit_on + ab.size.y}
|
||||||
|
|
||||||
|
## Give a record's sleeve faces a random cover from the generated 3x3 sheets.
|
||||||
|
## The record.glb sleeve cube carries M_Sleeve_Front / M_Sleeve_Back materials
|
||||||
|
## with real UVs — we duplicate them and window one tile of the atlas via
|
||||||
|
## uv1_scale/uv1_offset (no image slicing, one texture serves nine covers).
|
||||||
|
func _skin_record(vis: Node3D) -> void:
|
||||||
|
if _sleeve_textures.is_empty():
|
||||||
|
for p in SLEEVE_ATLASES:
|
||||||
|
var t := load(p) as Texture2D
|
||||||
|
if t != null:
|
||||||
|
_sleeve_textures.append(t)
|
||||||
|
if _sleeve_textures.is_empty():
|
||||||
|
return # art not imported yet — records stay plain
|
||||||
|
var tex: Texture2D = _sleeve_textures[randi() % _sleeve_textures.size()]
|
||||||
|
var tile_col := randi() % 3
|
||||||
|
var tile_row := randi() % 3
|
||||||
|
var mis: Array[MeshInstance3D] = []
|
||||||
|
_collect_meshes(vis, mis)
|
||||||
|
var did := false
|
||||||
|
for mi in mis:
|
||||||
|
if mi.mesh == null:
|
||||||
|
continue
|
||||||
|
for si in mi.mesh.get_surface_count():
|
||||||
|
var mat := mi.mesh.surface_get_material(si)
|
||||||
|
if mat == null or not (mat is BaseMaterial3D):
|
||||||
|
continue
|
||||||
|
var nm: String = mat.resource_name
|
||||||
|
if nm.contains("Sleeve_Front") or nm.contains("Sleeve_Back"):
|
||||||
|
var dup := mat.duplicate() as BaseMaterial3D
|
||||||
|
dup.albedo_color = Color.WHITE
|
||||||
|
dup.albedo_texture = tex
|
||||||
|
dup.uv1_scale = Vector3(1.0 / 3.0, 1.0 / 3.0, 1.0)
|
||||||
|
dup.uv1_offset = Vector3(tile_col / 3.0, tile_row / 3.0, 0.0)
|
||||||
|
mi.set_surface_override_material(si, dup)
|
||||||
|
did = true
|
||||||
|
if did:
|
||||||
|
_records_skinned += 1
|
||||||
|
|
||||||
## Combined AABB of every MeshInstance3D under `visual_root`, in `body`'s local space
|
## Combined AABB of every MeshInstance3D under `visual_root`, in `body`'s local space
|
||||||
## (independent of where `body` currently sits in the world).
|
## (independent of where `body` currently sits in the world).
|
||||||
func _body_local_aabb(body: Node3D, visual_root: Node3D) -> AABB:
|
func _body_local_aabb(body: Node3D, visual_root: Node3D) -> AABB:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user