diff --git a/armory/Sources/armory/trait/physics/bullet/RigidBody.hx b/armory/Sources/armory/trait/physics/bullet/RigidBody.hx index 7a4586e75..417d7a887 100644 --- a/armory/Sources/armory/trait/physics/bullet/RigidBody.hx +++ b/armory/Sources/armory/trait/physics/bullet/RigidBody.hx @@ -55,6 +55,10 @@ class RigidBody extends iron.Trait { public var onReady: Void->Void = null; public var onContact: ArrayVoid> = null; public var heightData: haxe.io.Bytes = null; + + // Compound shape children (baked from exporter) + var compoundChildren: Array = null; + #if js static var ammoArray: Int = -1; #end @@ -144,6 +148,9 @@ class RigidBody extends iron.Trait { this.staticObj = flags.staticObj; this.useDeactivation = flags.useDeactivation; + // Store compound children data if provided + this.compoundChildren = params.compoundChildren; + notifyOnAdd(init); } @@ -245,6 +252,25 @@ class RigidBody extends iron.Trait { btshape.setLocalScaling(vec1); #end } + else if (shape == Shape.Compound) { + // Create compound shape and add all child shapes + var compound = new bullet.Bt.CompoundShape(true); + if (compoundChildren != null) { + for (child in compoundChildren) { + var childShape = createChildShape(child); + if (childShape != null) { + // Set child local transform + trans2.setIdentity(); + vec1.setValue(child.posX, child.posY, child.posZ); + trans2.setOrigin(vec1); + quat1.setValue(child.rotX, child.rotY, child.rotZ, child.rotW); + trans2.setRotation(quat1); + compound.addChildShape(trans2, childShape); + } + } + } + btshape = compound; + } trans1.setIdentity(); vec1.setX(transform.worldx()); @@ -332,6 +358,46 @@ class RigidBody extends iron.Trait { #end } + /** + * Creates a child collision shape for compound rigidbodies from baked export data. + * @param child The compound child data containing shape type and dimensions + * @return The created Bullet collision shape, or null if shape type is unsupported + */ + function createChildShape(child: CompoundChild): bullet.Bt.CollisionShape { + var childShapeType: Int = child.shape; + + if (childShapeType == Shape.Box) { + vec1.setValue(withMargin(child.dimX / 2), withMargin(child.dimY / 2), withMargin(child.dimZ / 2)); + return new bullet.Bt.BoxShape(vec1); + } + else if (childShapeType == Shape.Sphere) { + return new bullet.Bt.SphereShape(withMargin(child.dimX / 2)); + } + else if (childShapeType == Shape.Cone) { + var coneZ = new bullet.Bt.ConeShapeZ( + withMargin(child.dimX / 2), // Radius + withMargin(child.dimZ)); // Height + return coneZ; + } + else if (childShapeType == Shape.Cylinder) { + vec1.setValue(withMargin(child.dimX / 2), withMargin(child.dimY / 2), withMargin(child.dimZ / 2)); + var cylZ = new bullet.Bt.CylinderShapeZ(vec1); + return cylZ; + } + else if (childShapeType == Shape.Capsule) { + var r = child.dimX / 2; + var capsZ = new bullet.Bt.CapsuleShapeZ( + withMargin(r), // Radius + withMargin(child.dimZ - r * 2)); // Height between 2 sphere centers + return capsZ; + } + else { + // Unsupported shape type for compound children (ConvexHull, Mesh, Terrain) + trace("Warning: Unsupported compound child shape type: " + childShapeType); + return null; + } + } + function update() { if (interpolate) { var t: Float = Time.fixedStepInterpolation; @@ -732,6 +798,7 @@ class RigidBody extends iron.Trait { var Cylinder = 5; var Capsule = 6; var Terrain = 7; + var Compound = 8; } typedef RigidBodyParams = { @@ -748,6 +815,21 @@ typedef RigidBodyParams = { var linearDeactivationThreshold: Float; var angularDeactivationThreshold: Float; var deactivationTime: Float; + @:optional var compoundChildren: Array; +} + +typedef CompoundChild = { + var shape: Int; // 0=Box, 1=Sphere, 2=ConvexHull, 3=Mesh, 4=Cone, 5=Cylinder, 6=Capsule + var posX: Float; // Local position relative to parent + var posY: Float; + var posZ: Float; + var rotX: Float; // Local rotation quaternion + var rotY: Float; + var rotZ: Float; + var rotW: Float; + var dimX: Float; // Dimensions for shape creation + var dimY: Float; + var dimZ: Float; } typedef RigidBodyFlags = { diff --git a/armory/Sources/iron/Scene.hx b/armory/Sources/iron/Scene.hx index 7aad72da8..7c0a00d75 100644 --- a/armory/Sources/iron/Scene.hx +++ b/armory/Sources/iron/Scene.hx @@ -78,6 +78,9 @@ class Scene { public var traitRemoves: ArrayVoid> = []; var initializing: Bool; // Is the scene in its initialization phase? + var spawnDepth: Int = 0; // Nested spawn counter (defer trait creation while > 0) + var spawning(get, never): Bool; + inline function get_spawning(): Bool return spawnDepth > 0; public function new() { uid = uidCounter++; @@ -437,7 +440,7 @@ class Scene { var result = objects.length; for (o in objects) { if (discardNoSpawn && o.spawn != null && o.spawn == false) continue; // Do not count children of non-spawned objects - if (o.children != null) result += getObjectsCount(o.children); + if (o.children != null) result += getObjectsCount(o.children, discardNoSpawn); } return result; } @@ -451,11 +454,16 @@ class Scene { @param srcRaw If not `null`, spawn the object from the given scene data instead of using the scene this function is called on. Useful to spawn objects from other scenes. **/ public function spawnObject(name: String, parent: Null, done: NullVoid>, spawnChildren = true, srcRaw: Null = null) { + spawnObjectInternal(name, parent, done, spawnChildren, srcRaw, true); + } + + function spawnObjectInternal(name: String, parent: Null, done: NullVoid>, spawnChildren: Bool, srcRaw: Null, createTraits: Bool) { if (srcRaw == null) srcRaw = raw; var objectsTraversed = 0; var obj = getRawObjectByName(srcRaw, name); var objectsCount = spawnChildren ? getObjectsCount([obj], false) : 1; var rootId = -1; + spawnDepth++; // Defer trait creation until all objects are ready function spawnObjectTree(obj: TObj, parent: Object, parentObject: TObj, done: Object->Void) { createObject(obj, srcRaw, parent, parentObject, function(object: Object) { if (rootId == -1) { @@ -471,6 +479,11 @@ class Scene { while (object.uid != rootId) { object = object.parent; } + // Create traits bottom-up after all objects are ready + spawnDepth--; + if (createTraits) { + createTraitsBottomUp(object); + } // Then call user callback if (done != null) done(object); } @@ -604,7 +617,7 @@ class Scene { else { for (object_ref in object_refs) { // Spawn top-level collection objects and their children - spawnObject(object_ref, groupOwner, function(spawnedObject: Object) { + spawnObjectInternal(object_ref, groupOwner, function(spawnedObject: Object) { // Apply collection/group instance offset to all // top-level parents of that group if (!isObjectInGroup(groupRef, spawnedObject.parent, format)) { @@ -624,7 +637,7 @@ class Scene { groupOwner.transform.reset(); done(); } - }, true, format); + }, true, format, false); } } } @@ -874,9 +887,9 @@ class Scene { } } - // If the scene is still initializing, traits will be created later + // If the scene is still initializing or spawning, traits will be created later // to ensure that object references for trait properties are valid - if (!active.initializing) createTraits(o.traits, object); + if (!active.initializing && !active.spawning) createTraits(o.traits, object); } done(object); } diff --git a/armory/Sources/iron/object/Object.hx b/armory/Sources/iron/object/Object.hx index 8101935df..f569666d2 100644 --- a/armory/Sources/iron/object/Object.hx +++ b/armory/Sources/iron/object/Object.hx @@ -212,7 +212,7 @@ class Object { public function getTraitFromChildren(c: Class): T { var t: T = getTrait(c); if (t != null) return t; - for (child in getChildren()) { + for (child in getChildren(true)) { t = child.getTraitFromChildren(c); if (t != null) return t; } diff --git a/armory/blender/arm/assets.py b/armory/blender/arm/assets.py index 51acce913..d481879c1 100644 --- a/armory/blender/arm/assets.py +++ b/armory/blender/arm/assets.py @@ -55,6 +55,16 @@ def reset(): shader_cons['voxel_frag'] = [] shader_cons['voxel_geom'] = [] +def reset_shader_cons(): + # Reset shader comparison arrays to prevent cross-scene shader merging + global shader_cons + shader_cons['mesh_vert'] = [] + shader_cons['depth_vert'] = [] + shader_cons['depth_frag'] = [] + shader_cons['voxel_vert'] = [] + shader_cons['voxel_frag'] = [] + shader_cons['voxel_geom'] = [] + def add(asset_file): global assets diff --git a/armory/blender/arm/exporter.py b/armory/blender/arm/exporter.py index 430d85208..14cfcb9bb 100644 --- a/armory/blender/arm/exporter.py +++ b/armory/blender/arm/exporter.py @@ -62,7 +62,18 @@ def get_bobject_type(cls, bobject: bpy.types.Object) -> "NodeType": if bobject.type == "MESH": if bobject.data.polygons or bobject.data.edges or bobject.data.vertices: return cls.MESH - elif bobject.type in ('FONT', 'META'): + elif bobject.type in ('FONT', 'META', 'CURVE'): + mesh = bobject.to_mesh() + if mesh is not None: + has_geometry = mesh.polygons or mesh.edges or mesh.vertices + elif bobject.type in ('FONT', 'META', 'CURVE'): # FIXME: curves with meshes shouldn't be used in modifiers for now. + if bobject.type == 'CURVE': + mesh = bobject.to_mesh() + if mesh is not None: + has_geometry = len(mesh.polygons) > 0 + bobject.to_mesh_clear() + if not has_geometry: + return cls.EMPTY return cls.MESH elif bobject.type == "LIGHT": return cls.LIGHT @@ -2929,89 +2940,136 @@ def post_export_object(self, bobject: bpy.types.Object, o, type): # Rigid body trait if bobject.rigid_body is not None and phys_enabled: - ArmoryExporter.export_physics = True - rb = bobject.rigid_body - shape = 0 # BOX - - if rb.collision_shape == 'SPHERE': - shape = 1 - elif rb.collision_shape == 'CONVEX_HULL': - shape = 2 - elif rb.collision_shape == 'MESH': - shape = 3 - elif rb.collision_shape == 'CONE': - shape = 4 - elif rb.collision_shape == 'CYLINDER': - shape = 5 - elif rb.collision_shape == 'CAPSULE': - shape = 6 - - body_mass = rb.mass - is_static = self.rigid_body_static(rb) - if is_static: - body_mass = 0 - x = {} - x['type'] = 'Script' - x['class_name'] = 'armory.trait.physics.' + phys_pkg + '.RigidBody' - col_group = '' - for b in rb.collision_collections: - col_group = ('1' if b else '0') + col_group - col_mask = '' - for b in bobject.arm_rb_collision_filter_mask: - col_mask = ('1' if b else '0') + col_mask - - x['parameters'] = [str(shape), str(body_mass), str(rb.friction), str(rb.restitution), str(int(col_group, 2)), str(int(col_mask, 2)) ] - lx = bobject.arm_rb_linear_factor[0] - ly = bobject.arm_rb_linear_factor[1] - lz = bobject.arm_rb_linear_factor[2] - ax = bobject.arm_rb_angular_factor[0] - ay = bobject.arm_rb_angular_factor[1] - az = bobject.arm_rb_angular_factor[2] - if bobject.lock_location[0]: - lx = 0 - if bobject.lock_location[1]: - ly = 0 - if bobject.lock_location[2]: - lz = 0 - if bobject.lock_rotation[0]: - ax = 0 - if bobject.lock_rotation[1]: - ay = 0 - if bobject.lock_rotation[2]: - az = 0 - col_margin = rb.collision_margin if rb.use_margin else 0.0 - if rb.use_deactivation: - deact_lv = rb.deactivate_linear_velocity - deact_av = rb.deactivate_angular_velocity - deact_time = bobject.arm_rb_deactivation_time + # Skip children of compound parents - their shapes are baked into the parent + is_compound_child = (bobject.parent is not None and + bobject.parent.rigid_body is not None and + bobject.parent.rigid_body.collision_shape == 'COMPOUND') + if is_compound_child: + pass # Don't export RigidBody trait for compound children else: - deact_lv = 0.0 - deact_av = 0.0 - deact_time = 0.0 - body_params = {} - body_params['linearDamping'] = rb.linear_damping - body_params['angularDamping'] = rb.angular_damping - body_params['linearFactorsX'] = lx - body_params['linearFactorsY'] = ly - body_params['linearFactorsZ'] = lz - body_params['angularFactorsX'] = ax - body_params['angularFactorsY'] = ay - body_params['angularFactorsZ'] = az - body_params['angularFriction'] = bobject.arm_rb_angular_friction - body_params['collisionMargin'] = col_margin - body_params['linearDeactivationThreshold'] = deact_lv - body_params['angularDeactivationThrshold'] = deact_av - body_params['deactivationTime'] = deact_time - body_flags = {} - body_flags['animated'] = rb.kinematic - body_flags['trigger'] = bobject.arm_rb_trigger - body_flags['ccd'] = bobject.arm_rb_ccd - body_flags['interpolate'] = bobject.arm_rb_interpolate - body_flags['staticObj'] = is_static - body_flags['useDeactivation'] = rb.use_deactivation - x['parameters'].append(arm.utils.get_haxe_json_string(body_params)) - x['parameters'].append(arm.utils.get_haxe_json_string(body_flags)) - o['traits'].append(x) + ArmoryExporter.export_physics = True + rb = bobject.rigid_body + shape = 0 # BOX + + if rb.collision_shape == 'SPHERE': + shape = 1 + elif rb.collision_shape == 'CONVEX_HULL': + shape = 2 + elif rb.collision_shape == 'MESH': + shape = 3 + elif rb.collision_shape == 'CONE': + shape = 4 + elif rb.collision_shape == 'CYLINDER': + shape = 5 + elif rb.collision_shape == 'CAPSULE': + shape = 6 + elif rb.collision_shape == 'COMPOUND': + shape = 8 + + body_mass = rb.mass + is_static = self.rigid_body_static(rb) + if is_static: + body_mass = 0 + x = {} + x['type'] = 'Script' + x['class_name'] = 'armory.trait.physics.' + phys_pkg + '.RigidBody' + col_group = '' + for b in rb.collision_collections: + col_group = ('1' if b else '0') + col_group + col_mask = '' + for b in bobject.arm_rb_collision_filter_mask: + col_mask = ('1' if b else '0') + col_mask + + x['parameters'] = [str(shape), str(body_mass), str(rb.friction), str(rb.restitution), str(int(col_group, 2)), str(int(col_mask, 2)) ] + lx = bobject.arm_rb_linear_factor[0] + ly = bobject.arm_rb_linear_factor[1] + lz = bobject.arm_rb_linear_factor[2] + ax = bobject.arm_rb_angular_factor[0] + ay = bobject.arm_rb_angular_factor[1] + az = bobject.arm_rb_angular_factor[2] + if bobject.lock_location[0]: + lx = 0 + if bobject.lock_location[1]: + ly = 0 + if bobject.lock_location[2]: + lz = 0 + if bobject.lock_rotation[0]: + ax = 0 + if bobject.lock_rotation[1]: + ay = 0 + if bobject.lock_rotation[2]: + az = 0 + col_margin = rb.collision_margin if rb.use_margin else 0.0 + if rb.use_deactivation: + deact_lv = rb.deactivate_linear_velocity + deact_av = rb.deactivate_angular_velocity + deact_time = bobject.arm_rb_deactivation_time + else: + deact_lv = 0.0 + deact_av = 0.0 + deact_time = 0.0 + body_params = {} + body_params['linearDamping'] = rb.linear_damping + body_params['angularDamping'] = rb.angular_damping + body_params['linearFactorsX'] = lx + body_params['linearFactorsY'] = ly + body_params['linearFactorsZ'] = lz + body_params['angularFactorsX'] = ax + body_params['angularFactorsY'] = ay + body_params['angularFactorsZ'] = az + body_params['angularFriction'] = bobject.arm_rb_angular_friction + body_params['collisionMargin'] = col_margin + body_params['linearDeactivationThreshold'] = deact_lv + body_params['angularDeactivationThrshold'] = deact_av + body_params['deactivationTime'] = deact_time + + # Collect compound children shapes if this is a compound parent + if rb.collision_shape == 'COMPOUND': + compound_children = [] + for child in bobject.children: + if child.rigid_body is not None: + child_rb = child.rigid_body + # Map child collision shape to int + child_shape = 0 # BOX default + if child_rb.collision_shape == 'SPHERE': + child_shape = 1 + elif child_rb.collision_shape == 'CONVEX_HULL': + child_shape = 2 + elif child_rb.collision_shape == 'MESH': + child_shape = 3 + elif child_rb.collision_shape == 'CONE': + child_shape = 4 + elif child_rb.collision_shape == 'CYLINDER': + child_shape = 5 + elif child_rb.collision_shape == 'CAPSULE': + child_shape = 6 + + # Get child's local transform relative to parent + local_matrix = bobject.matrix_world.inverted() @ child.matrix_world + loc = local_matrix.to_translation() + rot = local_matrix.to_quaternion() + + # Get child dimensions + child_dim = child.dimensions + + compound_children.append({ + 'shape': child_shape, + 'posX': loc.x, 'posY': loc.y, 'posZ': loc.z, + 'rotX': rot.x, 'rotY': rot.y, 'rotZ': rot.z, 'rotW': rot.w, + 'dimX': child_dim.x, 'dimY': child_dim.y, 'dimZ': child_dim.z + }) + body_params['compoundChildren'] = compound_children + + body_flags = {} + body_flags['animated'] = rb.kinematic + body_flags['trigger'] = bobject.arm_rb_trigger + body_flags['ccd'] = bobject.arm_rb_ccd + body_flags['interpolate'] = bobject.arm_rb_interpolate + body_flags['staticObj'] = is_static + body_flags['useDeactivation'] = rb.use_deactivation + x['parameters'].append(arm.utils.get_haxe_json_string(body_params)) + x['parameters'].append(arm.utils.get_haxe_json_string(body_flags)) + o['traits'].append(x) # Phys traits if phys_enabled: diff --git a/armory/blender/arm/make.py b/armory/blender/arm/make.py index 2622bf6e6..51cfc1bf2 100644 --- a/armory/blender/arm/make.py +++ b/armory/blender/arm/make.py @@ -258,6 +258,8 @@ def export_data(fp, sdk_path): for scene in bpy.data.scenes: if scene.arm_export: + # Reset shader comparison arrays to prevent cross-scene shader merging + assets.reset_shader_cons() ext = '.lz4' if ArmoryExporter.compress_enabled else '.arm' asset_path = build_dir + '/compiled/Assets/' + arm.utils.safestr(scene.name + "_" + os.path.basename(scene.library.filepath).replace(".blend", "") if scene.library else scene.name) + ext ArmoryExporter.export_scene(bpy.context, asset_path, scene=scene, depsgraph=depsgraph) diff --git a/armory/blender/arm/material/mat_batch.py b/armory/blender/arm/material/mat_batch.py index 862a18962..299f96ec6 100644 --- a/armory/blender/arm/material/mat_batch.py +++ b/armory/blender/arm/material/mat_batch.py @@ -59,6 +59,7 @@ def get_signature(mat, object: bpy.types.Object): sign += mat.arm_billboard sign += '_skin' if arm_utils.export_bone_data(object) else '0' sign += '_morph' if arm_utils.export_morph_targets(object) else '0' + # sign += '_tilesheet' if mat.arm_tilesheet_flag else '0' return sign def traverse_tree2(node, ar):