diff options
Diffstat (limited to 'godot')
| -rw-r--r-- | godot/project.godot | 13 | ||||
| -rw-r--r-- | godot/sim.gd | 223 | ||||
| -rw-r--r-- | godot/sim.tscn | 6 |
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") |