extends Node
## Bakes the procedural G1 world of the Godot reference for the Unity port (U2B): the view
## LuneWorld (scripts/luneraria/view/lune_world.gd) is built headless for the real map, and
## every static mesh it creates is written out - terrain grids, lane paving, platform, bridge,
## curbs, bases, landmarks, vegetation, grotto stairs, veins, deployment areas, water - with its
## transforms and material description, plus the height a figure stands on (0.5 m grid).
## The strategic views (towers, Ancients, grotto, tower effects) are skipped: Unity builds them
## from the FBX assets and the match state. Terrain and vegetation depend on Godot's noise and
## random generator, so baking keeps them exact. Coordinates stay Godot's (Unity: x -> -x).
## Run: HEADLESS=0 sh tools/godot-golden/run_godot.sh world_export --out=
## (a real rendering driver is needed: the headless dummy renderer returns identity transforms
## for MultiMesh instances; the tool refuses to write such a bake).
const LuneWorld = preload("res://scripts/luneraria/view/lune_world.gd")
const SKIP_SCRIPTS: Array[String] = [
"res://scripts/luneraria/view/lune_tower_view.gd", "res://scripts/luneraria/view/lune_ancient_view.gd",
"res://scripts/luneraria/view/lune_grotto_view.gd", "res://scripts/luneraria/view/lune_tower_fx.gd",
]
const STAND_STEP: float = 0.5
const STAND_EXTENT: float = 60.0
var _blob := PackedByteArray()
var _meshes: Array = []
var _mesh_ids: Dictionary = {}
var _materials: Array = []
var _material_ids: Dictionary = {}
var _world: Node3D
func _ready() -> void:
var out: String = "/tmp/lune_world"
for arg: String in OS.get_cmdline_user_args():
if arg.begins_with("--out="):
out = arg.substr(6)
var map := LuneMap.new(LuneBalance.default_balance())
_world = LuneWorld.new()
add_child(_world)
_world.build(map)
var props: Dictionary = {}
for key: String in _world._props:
props[(_world._props[key] as Mesh).get_instance_id()] = key
var surface_keys: Dictionary = {}
for key: String in _world._materials:
surface_keys[(_world._materials[key] as Material).get_instance_id()] = key
var nodes: Array = []
_collect(_world, nodes, props, surface_keys)
for entry: Dictionary in nodes:
if entry.kind == "multimesh" and (entry.transforms as Array).size() > 1 and entry.transforms[0] == entry.transforms[1]:
push_error("WORLD_EXPORT: MultiMesh %s has identical instance transforms (headless dummy renderer?)" % entry.name)
get_tree().quit(1)
return
var terrain: LuneTerrain = _world.terrain
var data: Dictionary = {
"engine": Engine.get_version_info().string,
"generator": "scripts/luneraria/view/lune_world.gd, scripts/luneraria/view/lune_terrain.gd",
"coordinates": "Godot: x right, y up, z towards the viewer; map point (x, y) = (x, h, y)",
"terrain": _terrain(terrain),
"stand": _stand(),
"lights": _lights(),
"nodes": nodes,
"meshes": _meshes,
"materials": _materials,
"constants": {"plaza_lift": LuneWorld.PLAZA_LIFT, "plaza_radius": LuneWorld.PLAZA_RADIUS, "water_level": LuneTerrain.WATER_LEVEL,
"base_height": LuneTerrain.BASE_HEIGHT, "arena_height": LuneTerrain.ARENA_HEIGHT, "arena_radius": LuneTerrain.ARENA_RADIUS,
"platform_height": LuneTerrain.PLATFORM_HEIGHT, "platform_radius": LuneTerrain.PLATFORM_RADIUS},
}
DirAccess.make_dir_recursive_absolute(out)
var json := FileAccess.open(out.path_join("g1_world.json"), FileAccess.WRITE)
json.store_string(JSON.stringify(data, "\t", false))
json.close()
var bin := FileAccess.open(out.path_join("g1_world.bytes"), FileAccess.WRITE)
bin.store_buffer(_blob)
bin.close()
print("WORLD_EXPORT nodes=%d meshes=%d materials=%d bytes=%d -> %s" % [nodes.size(), _meshes.size(), _materials.size(), _blob.size(), out])
get_tree().quit()
func _collect(node: Node, nodes: Array, props: Dictionary, surface_keys: Dictionary) -> void:
for child: Node in node.get_children():
var script: Script = child.get_script()
if script != null and SKIP_SCRIPTS.has(script.resource_path):
continue
if child is MeshInstance3D and (child as MeshInstance3D).mesh != null:
var instance := child as MeshInstance3D
var entry: Dictionary = _instance_common(instance, surface_keys)
entry["kind"] = "mesh"
entry["transforms"] = [_transform(_relative(instance))]
_mesh_ref(instance.mesh, props, entry)
nodes.append(entry)
elif child is MultiMeshInstance3D and (child as MultiMeshInstance3D).multimesh != null:
var multi := child as MultiMeshInstance3D
var entry: Dictionary = _instance_common(multi, surface_keys)
entry["kind"] = "multimesh"
var base: Transform3D = _relative(multi)
var transforms: Array = []
for index: int in range(multi.multimesh.instance_count):
transforms.append(_transform(base * multi.multimesh.get_instance_transform(index)))
entry["transforms"] = transforms
_mesh_ref(multi.multimesh.mesh, props, entry)
nodes.append(entry)
_collect(child, nodes, props, surface_keys)
func _relative(node: Node3D) -> Transform3D:
return _world.global_transform.affine_inverse() * node.global_transform
func _instance_common(instance: GeometryInstance3D, surface_keys: Dictionary) -> Dictionary:
var entry: Dictionary = {"name": str(instance.name), "visible": instance.visible,
"cast_shadow": instance.cast_shadow != GeometryInstance3D.SHADOW_CASTING_SETTING_OFF}
if instance.material_override != null:
entry["material"] = _material(instance.material_override, surface_keys)
var params: Dictionary = {}
for name: String in ["tint", "seed", "energy", "flicker"]:
var value: Variant = instance.get_instance_shader_parameter(name)
if value != null:
params[name] = _value(value)
if not params.is_empty():
entry["instance_params"] = params
return entry
func _mesh_ref(mesh: Mesh, props: Dictionary, entry: Dictionary) -> void:
if props.has(mesh.get_instance_id()):
entry["prop"] = props[mesh.get_instance_id()]
return
var id: int = mesh.get_instance_id()
if not _mesh_ids.has(id):
_mesh_ids[id] = _meshes.size()
var surfaces: Array = []
for index: int in range(mesh.get_surface_count()):
var arrays: Array = mesh.surface_get_arrays(index)
var surface: Dictionary = {"vertices": _put((arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array).to_byte_array(), (arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array).size())}
if arrays[Mesh.ARRAY_NORMAL] != null:
surface["normals"] = _put((arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array).to_byte_array(), (arrays[Mesh.ARRAY_NORMAL] as PackedVector3Array).size())
if arrays[Mesh.ARRAY_TEX_UV] != null:
surface["uvs"] = _put((arrays[Mesh.ARRAY_TEX_UV] as PackedVector2Array).to_byte_array(), (arrays[Mesh.ARRAY_TEX_UV] as PackedVector2Array).size())
if arrays[Mesh.ARRAY_COLOR] != null:
surface["colors"] = _put((arrays[Mesh.ARRAY_COLOR] as PackedColorArray).to_byte_array(), (arrays[Mesh.ARRAY_COLOR] as PackedColorArray).size())
if arrays[Mesh.ARRAY_INDEX] != null:
surface["indices"] = _put((arrays[Mesh.ARRAY_INDEX] as PackedInt32Array).to_byte_array(), (arrays[Mesh.ARRAY_INDEX] as PackedInt32Array).size())
var material: Material = mesh.surface_get_material(index)
if material != null:
surface["material"] = _material(material, {})
surfaces.append(surface)
_meshes.append({"type": mesh.get_class(), "surfaces": surfaces})
entry["mesh"] = _mesh_ids[id]
func _put(bytes: PackedByteArray, count: int) -> Array:
var offset: int = _blob.size()
_blob.append_array(bytes)
return [offset, count]
func _material(material: Material, surface_keys: Dictionary) -> int:
var id: int = material.get_instance_id()
if _material_ids.has(id):
return _material_ids[id]
var entry: Dictionary = {"name": material.resource_name}
if surface_keys.has(id):
entry["surface_key"] = surface_keys[id]
if material is ShaderMaterial:
var shader_material := material as ShaderMaterial
entry["type"] = "shader"
entry["shader"] = shader_material.shader.resource_path
var params: Dictionary = {}
for uniform: Dictionary in shader_material.shader.get_shader_uniform_list():
var value: Variant = shader_material.get_shader_parameter(uniform.name)
if value != null:
params[uniform.name] = _value(value)
entry["params"] = params
elif material is StandardMaterial3D:
var standard := material as StandardMaterial3D
entry["type"] = "standard"
entry["albedo"] = _value(standard.albedo_color)
entry["roughness"] = standard.roughness
entry["metallic"] = standard.metallic
entry["unshaded"] = standard.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED
entry["transparent"] = standard.transparency != BaseMaterial3D.TRANSPARENCY_DISABLED
entry["vertex_color_albedo"] = standard.vertex_color_use_as_albedo
if standard.emission_enabled:
entry["emission"] = _value(standard.emission)
entry["emission_energy"] = standard.emission_energy_multiplier
else:
entry["type"] = material.get_class()
_material_ids[id] = _materials.size()
_materials.append(entry)
return _material_ids[id]
func _value(value: Variant) -> Variant:
if value is Color:
return [value.r, value.g, value.b, value.a]
if value is Vector2:
return [value.x, value.y]
if value is Vector3:
return [value.x, value.y, value.z]
if value is Texture:
return (value as Texture).resource_path
return value
func _transform(t: Transform3D) -> Array:
# Column-major basis (x, y, z axes) and origin.
return [t.basis.x.x, t.basis.x.y, t.basis.x.z, t.basis.y.x, t.basis.y.y, t.basis.y.z,
t.basis.z.x, t.basis.z.y, t.basis.z.z, t.origin.x, t.origin.y, t.origin.z]
func _terrain(terrain: LuneTerrain) -> Dictionary:
var outer: Dictionary = terrain._grid(LuneTerrain.OUTER_EXTENT, LuneTerrain.OUTER_STEP, LuneTerrain.OUTER_HOLE)
return {
"inner": {"extent": LuneTerrain.EXTENT, "step": LuneTerrain.STEP, "size": terrain.size, "hole": 0.0,
"ground": _put(terrain.ground.to_byte_array(), terrain.ground.size()), "walk": _put(terrain.walk.to_byte_array(), terrain.walk.size()),
"colors": _put(terrain.colors.to_byte_array(), terrain.colors.size())},
"outer": {"extent": LuneTerrain.OUTER_EXTENT, "step": LuneTerrain.OUTER_STEP, "size": outer.size, "hole": LuneTerrain.OUTER_HOLE,
"ground": _put((outer.ground as PackedFloat32Array).to_byte_array(), (outer.ground as PackedFloat32Array).size()),
"colors": _put((outer.colors as PackedColorArray).to_byte_array(), (outer.colors as PackedColorArray).size())},
}
## Height a figure stands on (terrain walk height plus plaza, dais and grotto lifts).
func _stand() -> Dictionary:
var n: int = int(round(STAND_EXTENT * 2.0 / STAND_STEP)) + 1
var heights := PackedFloat32Array()
heights.resize(n * n)
for j: int in range(n):
for i: int in range(n):
heights[j * n + i] = _world.height_at(Vector2(-STAND_EXTENT + i * STAND_STEP, -STAND_EXTENT + j * STAND_STEP))
return {"extent": STAND_EXTENT, "step": STAND_STEP, "size": n, "heights": _put(heights.to_byte_array(), heights.size())}
func _lights() -> Dictionary:
var result: Dictionary = {}
for child: Node in _world.get_children():
if child is DirectionalLight3D:
var light := child as DirectionalLight3D
result[str(light.name) if light == _world.sun else "fill"] = {"rotation_degrees": _value(light.rotation_degrees),
"color": _value(light.light_color), "energy": light.light_energy, "shadow": light.shadow_enabled}
elif child is WorldEnvironment:
var environment: Environment = (child as WorldEnvironment).environment
result["environment"] = {"ambient_source": environment.ambient_light_source, "ambient_color": _value(environment.ambient_light_color),
"ambient_energy": environment.ambient_light_energy, "background_mode": environment.background_mode,
"background_color": _value(environment.background_color), "tonemap_mode": environment.tonemap_mode,
"tonemap_exposure": environment.tonemap_exposure, "tonemap_white": environment.tonemap_white}
return result