aboutsummaryrefslogtreecommitdiffstats
path: root/godot/sim.gd
diff options
context:
space:
mode:
authorElvis Claros Castro <elvis@claros.ar>2026-09-26 20:50:41 -0300
committerElvis Claros Castro <elvis@claros.ar>2026-09-26 20:50:41 -0300
commitfafaebb051907a848a9406f9da19669c81a83a3b (patch)
treec30ea26e6b549e5523af2bae5c39569e9a946b12 /godot/sim.gd
parent59355909f2de9236af8168a26c70bcf6caa3b285 (diff)
download100cia-videos-fafaebb051907a848a9406f9da19669c81a83a3b.tar.gz
100cia-videos-fafaebb051907a848a9406f9da19669c81a83a3b.zip
Translate code, comments and logs to English; English README; configurable paths and env varsHEADmain
Identifiers, docstrings, comments and console messages are now in English. Narration, subtitles and on-screen text stay in Spanish (they are the video content). The Blender <-> Godot physics protocol uses English keys and body prefixes chosen to keep the original creation order, so cached simulations and renders stay bit-identical. The old Spanish environment variable names are still accepted.
Diffstat (limited to 'godot/sim.gd')
-rw-r--r--godot/sim.gd281
1 files changed, 144 insertions, 137 deletions
diff --git a/godot/sim.gd b/godot/sim.gd
index 7e860dc..5b0e868 100644
--- a/godot/sim.gd
+++ b/godot/sim.gd
@@ -1,223 +1,230 @@
extends Node3D
-## Simulador generico: Blender disena, Godot (Jolt) hace la fisica.
+## Generic simulator: Blender designs, Godot (Jolt) does the physics.
##
## godot --headless --path godot --fixed-fps 240 -- <config.json>
##
-## El .glb exportado por Blender se carga en tiempo de ejecucion y cada objeto
-## se convierte en cuerpo segun el prefijo de su nombre:
-## fijo_* estatico, malla exacta (ConcavePolygonShape3D)
-## caja_* estatico, caja con el tamano de la malla
-## compuerta_* estatico, caja que un evento "quitar" saca del mundo
-## bola_* rigido, esfera
-## bloque_* rigido, caja
-## Las trayectorias se graban a 30 fps en coordenadas de Blender (Z arriba),
-## asi el script de render las usa sin convertir nada.
-
-var hz := 240 # pasos de fisica por segundo
-var pasos_por_frame := 8 # hz / 30 fps de video
+## The .glb exported by Blender is loaded at runtime and every object becomes
+## a body according to its name prefix:
+## static_* static, exact mesh (ConcavePolygonShape3D)
+## box_* static, box the size of the mesh
+## gate_* static, box that a "remove" event takes out of the world
+## ball_* rigid, sphere
+## block_* rigid, box
+## Trajectories are recorded in Blender coordinates (Z up), so the render
+## script uses them without converting anything.
+##
+## The prefixes also fix the order bodies are created in: Blender exports
+## objects alphabetically, and in a chaotic scene (hundreds of balls on pegs)
+## a different creation order gives different trajectories. Keep new prefixes
+## in the same alphabetical order (ball_ < box_ < gate_ < static_) or cached
+## simulations will no longer match.
+
+var hz := 240 # physics steps per second
+var steps_per_frame := 8 # hz / 30 video fps
var cfg: Dictionary
-var dinamicos: Array[RigidBody3D] = []
-var removibles := {} # nombre -> CollisionObject3D
+var dynamic_bodies: Array[RigidBody3D] = []
+var removable := {} # name -> CollisionObject3D
var tick := 0
var frames: Array = []
-var eventos: Array = []
-var esc := 1.0 # el mundo se simula esc veces mas grande
+var events: Array = []
+var scale_f := 1.0 # the world is simulated scale_f times larger
func _ready() -> void:
var args := OS.get_cmdline_user_args()
if args.is_empty():
- push_error("falta el config.json")
+ push_error("missing config.json")
get_tree().quit(1)
return
cfg = JSON.parse_string(FileAccess.get_file_as_string(args[0]))
- # Jolt tiene tolerancias en metros (penetracion admitida 2 cm): con piezas
- # chicas conviene simular a mayor escala. Gravedad y posiciones se escalan
- # igual, asi la dinamica es la misma y la salida vuelve a la escala original.
- esc = float(cfg.get("escala", 1.0))
- # mas pasos por segundo = contactos mas rigidos (pilas altas); el proceso
- # corre con --fixed-fps 240, asi que cada frame hace hz/240 pasos
+ # Jolt has tolerances in meters (2 cm of allowed penetration): with small
+ # pieces it is better to simulate at a larger scale. Gravity and positions
+ # are scaled alike, so the dynamics are the same and the output goes back
+ # to the original scale.
+ scale_f = float(cfg.get("scale", 1.0))
+ # more steps per second = stiffer contacts (tall stacks); the process runs
+ # with --fixed-fps 240, so each frame does hz/240 steps
hz = int(cfg.get("hz", 240))
- # fps de grabacion: 30 para tiempo real, mas para camara lenta
- pasos_por_frame = hz / int(cfg.get("fps", 30))
+ # recording fps: 30 for real time, more for slow motion
+ steps_per_frame = hz / int(cfg.get("fps", 30))
Engine.physics_ticks_per_second = hz
Engine.max_physics_steps_per_frame = maxi(1, hz / 240)
PhysicsServer3D.area_set_param(get_world_3d().space, PhysicsServer3D.AREA_PARAM_GRAVITY,
- float(cfg.get("gravedad", 9.81)) * esc)
- eventos = cfg.get("eventos", [])
- eventos.sort_custom(func(a, b): return a["t"] < b["t"])
+ float(cfg.get("gravity", 9.81)) * scale_f)
+ events = cfg.get("events", [])
+ events.sort_custom(func(a, b): return a["t"] < b["t"])
var doc := GLTFDocument.new()
var st := GLTFState.new()
if doc.append_from_file(cfg["glb"], st) != OK:
- push_error("no pude leer " + cfg["glb"])
+ push_error("could not read " + cfg["glb"])
get_tree().quit(1)
return
- var raiz := doc.generate_scene(st)
- add_child(raiz)
- var mallas: Array[MeshInstance3D] = []
- _juntar(raiz, mallas)
- for mi in mallas:
- _crear_cuerpo(mi)
- raiz.queue_free()
- print("[sim] %d dinamicos, %d mallas, g=%.2f" % [dinamicos.size(), mallas.size(),
- float(cfg.get("gravedad", 9.81))])
- _grabar()
-
-
-func _juntar(n: Node, out: Array[MeshInstance3D]) -> void:
+ var root := doc.generate_scene(st)
+ add_child(root)
+ var meshes: Array[MeshInstance3D] = []
+ _collect(root, meshes)
+ for mi in meshes:
+ _create_body(mi)
+ root.queue_free()
+ print("[sim] %d dynamic, %d meshes, g=%.2f" % [dynamic_bodies.size(), meshes.size(),
+ float(cfg.get("gravity", 9.81))])
+ _record()
+
+
+func _collect(n: Node, out: Array[MeshInstance3D]) -> void:
if n is MeshInstance3D:
out.append(n)
for c in n.get_children():
- _juntar(c, out)
+ _collect(c, out)
-func _regla(nombre: String) -> Dictionary:
+func _rule(body_name: String) -> Dictionary:
var r: Dictionary = {}
- var largo := -1
- for pre in cfg.get("reglas", {}):
- if nombre.begins_with(pre) and pre.length() > largo:
- r = cfg["reglas"][pre]
- largo = pre.length()
+ var longest := -1
+ for prefix in cfg.get("rules", {}):
+ if body_name.begins_with(prefix) and prefix.length() > longest:
+ r = cfg["rules"][prefix]
+ longest = prefix.length()
return r
-func _crear_cuerpo(mi: MeshInstance3D) -> void:
- var nombre := String(mi.name)
+func _create_body(mi: MeshInstance3D) -> void:
+ var body_name := String(mi.name)
var aabb := mi.get_aabb()
- aabb = AABB(aabb.position * esc, aabb.size * esc)
- var r := _regla(nombre)
+ aabb = AABB(aabb.position * scale_f, aabb.size * scale_f)
+ var r := _rule(body_name)
var mat := PhysicsMaterial.new()
- mat.friction = float(r.get("friccion", 0.5))
- mat.bounce = float(r.get("rebote", 0.0))
- var cuerpo: CollisionObject3D
- var forma := CollisionShape3D.new()
+ mat.friction = float(r.get("friction", 0.5))
+ mat.bounce = float(r.get("bounce", 0.0))
+ var body: CollisionObject3D
+ var shape := CollisionShape3D.new()
- if nombre.begins_with("fijo_"):
- cuerpo = StaticBody3D.new()
+ if body_name.begins_with("static_"):
+ body = StaticBody3D.new()
var tri := ConcavePolygonShape3D.new()
- var caras := mi.mesh.get_faces()
- for n in caras.size():
- caras[n] *= esc
- tri.set_faces(caras)
- forma.shape = tri
- elif nombre.begins_with("caja_") or nombre.begins_with("compuerta_"):
- cuerpo = StaticBody3D.new()
+ var faces := mi.mesh.get_faces()
+ for n in faces.size():
+ faces[n] *= scale_f
+ tri.set_faces(faces)
+ shape.shape = tri
+ elif body_name.begins_with("box_") or body_name.begins_with("gate_"):
+ body = StaticBody3D.new()
var b := BoxShape3D.new()
b.size = aabb.size
- forma.shape = b
- forma.position = aabb.get_center()
- elif nombre.begins_with("bola_") or nombre.begins_with("bloque_"):
+ shape.shape = b
+ shape.position = aabb.get_center()
+ elif body_name.begins_with("ball_") or body_name.begins_with("block_"):
var rb := RigidBody3D.new()
var vol: float
- if nombre.begins_with("bola_"):
+ if body_name.begins_with("ball_"):
var s := SphereShape3D.new()
s.radius = aabb.size.x * 0.5
- forma.shape = s
+ shape.shape = s
vol = 4.0 / 3.0 * PI * pow(s.radius, 3)
else:
var b := BoxShape3D.new()
b.size = aabb.size
- forma.shape = b
+ shape.shape = b
vol = aabb.size.x * aabb.size.y * aabb.size.z
- forma.position = aabb.get_center()
- rb.mass = vol * float(r.get("densidad", 1000.0))
+ shape.position = aabb.get_center()
+ rb.mass = vol * float(r.get("density", 1000.0))
rb.continuous_cd = bool(r.get("ccd", false))
- rb.can_sleep = bool(r.get("dormir", true))
- rb.linear_damp = float(r.get("amortiguar", 0.0))
- rb.angular_damp = float(r.get("amortiguar_giro", 0.0))
- if r.get("plano", false):
- # movimiento en el plano del tablero (Blender XZ = Godot XY)
+ rb.can_sleep = bool(r.get("can_sleep", true))
+ rb.linear_damp = float(r.get("damping", 0.0))
+ rb.angular_damp = float(r.get("angular_damping", 0.0))
+ if r.get("planar", false):
+ # motion in the board plane (Blender XZ = Godot XY)
rb.axis_lock_linear_z = true
rb.axis_lock_angular_x = true
rb.axis_lock_angular_y = true
- for pre in cfg.get("congelados", []):
- if nombre.begins_with(pre):
+ for prefix in cfg.get("frozen", []):
+ if body_name.begins_with(prefix):
rb.freeze = true
rb.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
- cuerpo = rb
- dinamicos.append(rb)
+ body = rb
+ dynamic_bodies.append(rb)
else:
return
- cuerpo.name = nombre
- if cuerpo is StaticBody3D:
- cuerpo.physics_material_override = mat
+ body.name = body_name
+ if body is StaticBody3D:
+ body.physics_material_override = mat
else:
- (cuerpo as RigidBody3D).physics_material_override = mat
- cuerpo.add_child(forma)
- add_child(cuerpo)
+ (body as RigidBody3D).physics_material_override = mat
+ body.add_child(shape)
+ add_child(body)
var tr := mi.global_transform
- tr.origin *= esc
- cuerpo.global_transform = tr
- removibles[nombre] = cuerpo
+ tr.origin *= scale_f
+ body.global_transform = tr
+ removable[body_name] = body
func _physics_process(_delta: float) -> void:
tick += 1
var t := tick / float(hz)
- while not eventos.is_empty() and eventos[0]["t"] <= t:
- _aplicar(eventos.pop_front())
- if tick % pasos_por_frame == 0:
- _grabar()
- if t >= float(cfg["duracion"]):
- _guardar()
+ while not events.is_empty() and events[0]["t"] <= t:
+ _apply(events.pop_front())
+ if tick % steps_per_frame == 0:
+ _record()
+ if t >= float(cfg["duration"]):
+ _save()
get_tree().quit()
-func _aplicar(ev: Dictionary) -> void:
- var pre: String = ev["prefijo"]
+func _apply(ev: Dictionary) -> void:
+ var prefix: String = ev["prefix"]
var n := 0
- for nombre in removibles.keys():
- if not String(nombre).begins_with(pre):
+ for body_name in removable.keys():
+ if not String(body_name).begins_with(prefix):
continue
- var c: CollisionObject3D = removibles[nombre]
+ var c: CollisionObject3D = removable[body_name]
if not is_instance_valid(c):
continue
n += 1
- match ev["accion"]:
- "quitar":
+ match ev["action"]:
+ "remove":
c.queue_free()
- "soltar":
+ "release":
var rb := c as RigidBody3D
rb.freeze = false
rb.sleeping = false
- if ev.has("velocidad"):
- var v: Array = ev["velocidad"] # en coordenadas de Blender
- rb.linear_velocity = Vector3(v[0], v[2], -v[1]) * esc
- if ev.has("giro"):
- var w: Array = ev["giro"] # rad/s, ejes de Blender
+ if ev.has("velocity"):
+ var v: Array = ev["velocity"] # in Blender coordinates
+ rb.linear_velocity = Vector3(v[0], v[2], -v[1]) * scale_f
+ if ev.has("spin"):
+ var w: Array = ev["spin"] # rad/s, Blender axes
rb.angular_velocity = Vector3(w[0], w[2], -w[1])
- # un cuerpo dormido no se entera de que le sacaron el apoyo: se despiertan todos
- for rb in dinamicos:
+ # a sleeping body never notices its support was removed: wake them all
+ for rb in dynamic_bodies:
if not rb.freeze:
rb.sleeping = false
- print("[sim] t=%.2f %s %s (%d)" % [tick / float(hz), ev["accion"], pre, n])
+ print("[sim] t=%.2f %s %s (%d)" % [tick / float(hz), ev["action"], prefix, n])
-func _grabar() -> void:
- var fila := PackedFloat32Array()
- for rb in dinamicos:
- var p := rb.global_position / esc
+func _record() -> void:
+ var row := PackedFloat32Array()
+ for rb in dynamic_bodies:
+ var p := rb.global_position / scale_f
var q := rb.global_transform.basis.get_rotation_quaternion()
- # Godot (Y arriba) -> Blender (Z arriba): (x, y, z) -> (x, -z, y)
- fila.append_array([p.x, -p.z, p.y, q.w, q.x, -q.z, q.y])
- frames.append(fila)
-
-
-func _guardar() -> void:
- # binario: cabecera JSON en un archivo aparte + float32 crudos
- var sal: String = cfg["salida"]
- var nombres := []
- for rb in dinamicos:
- nombres.append(String(rb.name))
- var f := FileAccess.open(sal + ".cab.json", FileAccess.WRITE)
- f.store_string(JSON.stringify({"nombres": nombres, "frames": frames.size(),
- "fps": int(cfg.get("fps", 30)), "campos": ["x", "y", "z", "qw", "qx", "qy", "qz"]}))
+ # Godot (Y up) -> Blender (Z up): (x, y, z) -> (x, -z, y)
+ row.append_array([p.x, -p.z, p.y, q.w, q.x, -q.z, q.y])
+ frames.append(row)
+
+
+func _save() -> void:
+ # binary: a JSON header in a separate file + raw float32
+ var out: String = cfg["output"]
+ var names := []
+ for rb in dynamic_bodies:
+ names.append(String(rb.name))
+ var f := FileAccess.open(out + ".header.json", FileAccess.WRITE)
+ f.store_string(JSON.stringify({"names": names, "frames": frames.size(),
+ "fps": int(cfg.get("fps", 30)), "fields": ["x", "y", "z", "qw", "qx", "qy", "qz"]}))
f.close()
- var b := FileAccess.open(sal + ".bin", FileAccess.WRITE)
- for fila in frames:
- b.store_buffer(fila.to_byte_array())
+ var b := FileAccess.open(out + ".bin", FileAccess.WRITE)
+ for row in frames:
+ b.store_buffer(row.to_byte_array())
b.close()
- print("[sim] %d frames -> %s.bin" % [frames.size(), sal])
+ print("[sim] %d frames -> %s.bin" % [frames.size(), out])