aboutsummaryrefslogtreecommitdiffstats
path: root/godot
diff options
context:
space:
mode:
authorElvis Claros Castro <elvis@claros.ar>2026-09-26 20:21:47 -0300
committerElvis Claros Castro <elvis@claros.ar>2026-09-26 20:21:47 -0300
commit59355909f2de9236af8168a26c70bcf6caa3b285 (patch)
tree686186e2086f81aa22ad25e78eb29fca3cbadc9a /godot
download100cia-videos-59355909f2de9236af8168a26c70bcf6caa3b285.tar.gz
100cia-videos-59355909f2de9236af8168a26c70bcf6caa3b285.zip
Import video pipeline as it was
Diffstat (limited to 'godot')
-rw-r--r--godot/project.godot13
-rw-r--r--godot/sim.gd223
-rw-r--r--godot/sim.tscn6
3 files changed, 242 insertions, 0 deletions
diff --git a/godot/project.godot b/godot/project.godot
new file mode 100644
index 0000000..c45c053
--- /dev/null
+++ b/godot/project.godot
@@ -0,0 +1,13 @@
+; Simulador de fisica del canal: carga un .glb disenado en Blender, arma los
+; cuerpos rigidos segun el prefijo de cada objeto y graba las trayectorias.
+config_version=5
+
+[application]
+config/name="fisica-videos"
+run/main_scene="res://sim.tscn"
+
+[physics]
+3d/physics_engine="Jolt Physics"
+common/physics_ticks_per_second=240
+common/max_physics_steps_per_frame=1
+common/physics_jitter_fix=0.0
diff --git a/godot/sim.gd b/godot/sim.gd
new file mode 100644
index 0000000..7e860dc
--- /dev/null
+++ b/godot/sim.gd
@@ -0,0 +1,223 @@
+extends Node3D
+## Simulador generico: Blender disena, Godot (Jolt) hace la fisica.
+##
+## 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
+
+var cfg: Dictionary
+var dinamicos: Array[RigidBody3D] = []
+var removibles := {} # nombre -> CollisionObject3D
+var tick := 0
+var frames: Array = []
+var eventos: Array = []
+var esc := 1.0 # el mundo se simula esc veces mas grande
+
+
+func _ready() -> void:
+ var args := OS.get_cmdline_user_args()
+ if args.is_empty():
+ push_error("falta el 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
+ 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))
+ 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"])
+
+ var doc := GLTFDocument.new()
+ var st := GLTFState.new()
+ if doc.append_from_file(cfg["glb"], st) != OK:
+ push_error("no pude leer " + 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:
+ if n is MeshInstance3D:
+ out.append(n)
+ for c in n.get_children():
+ _juntar(c, out)
+
+
+func _regla(nombre: 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()
+ return r
+
+
+func _crear_cuerpo(mi: MeshInstance3D) -> void:
+ var nombre := String(mi.name)
+ var aabb := mi.get_aabb()
+ aabb = AABB(aabb.position * esc, aabb.size * esc)
+ var r := _regla(nombre)
+ 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()
+
+ if nombre.begins_with("fijo_"):
+ cuerpo = 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 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_"):
+ var rb := RigidBody3D.new()
+ var vol: float
+ if nombre.begins_with("bola_"):
+ var s := SphereShape3D.new()
+ s.radius = aabb.size.x * 0.5
+ forma.shape = s
+ vol = 4.0 / 3.0 * PI * pow(s.radius, 3)
+ else:
+ var b := BoxShape3D.new()
+ b.size = aabb.size
+ forma.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))
+ 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.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):
+ rb.freeze = true
+ rb.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
+ cuerpo = rb
+ dinamicos.append(rb)
+ else:
+ return
+
+ cuerpo.name = nombre
+ if cuerpo is StaticBody3D:
+ cuerpo.physics_material_override = mat
+ else:
+ (cuerpo as RigidBody3D).physics_material_override = mat
+ cuerpo.add_child(forma)
+ add_child(cuerpo)
+ var tr := mi.global_transform
+ tr.origin *= esc
+ cuerpo.global_transform = tr
+ removibles[nombre] = cuerpo
+
+
+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()
+ get_tree().quit()
+
+
+func _aplicar(ev: Dictionary) -> void:
+ var pre: String = ev["prefijo"]
+ var n := 0
+ for nombre in removibles.keys():
+ if not String(nombre).begins_with(pre):
+ continue
+ var c: CollisionObject3D = removibles[nombre]
+ if not is_instance_valid(c):
+ continue
+ n += 1
+ match ev["accion"]:
+ "quitar":
+ c.queue_free()
+ "soltar":
+ 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
+ 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:
+ if not rb.freeze:
+ rb.sleeping = false
+ print("[sim] t=%.2f %s %s (%d)" % [tick / float(hz), ev["accion"], pre, n])
+
+
+func _grabar() -> void:
+ var fila := PackedFloat32Array()
+ for rb in dinamicos:
+ var p := rb.global_position / esc
+ 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"]}))
+ f.close()
+ var b := FileAccess.open(sal + ".bin", FileAccess.WRITE)
+ for fila in frames:
+ b.store_buffer(fila.to_byte_array())
+ b.close()
+ print("[sim] %d frames -> %s.bin" % [frames.size(), sal])
diff --git a/godot/sim.tscn b/godot/sim.tscn
new file mode 100644
index 0000000..3973f97
--- /dev/null
+++ b/godot/sim.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3]
+
+[ext_resource type="Script" path="res://sim.gd" id="1"]
+
+[node name="Sim" type="Node3D"]
+script = ExtResource("1")