extends Node3D ## Simulador generico: Blender disena, Godot (Jolt) hace la fisica. ## ## godot --headless --path godot --fixed-fps 240 -- ## ## 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])