public static void lua_AddCharacterHair(int entID, int setupIndex) { var ent = EngineWorld.GetCharacterByID((uint)entID); if (ent != null) { var hairSetup = new HairSetup(); hairSetup.GetSetup((uint)setupIndex); ent.Hairs.Add(new Hair()); if (!ent.Hairs.Last().Create(hairSetup, ent)) { ConsoleInfo.Instance.Warning(SYSWARN_CANT_CREATE_HAIR, entID); ent.Hairs.RemoveAt(ent.Hairs.Count - 1); } } else { ConsoleInfo.Instance.Warning(SYSWARN_NO_CHARACTER, entID); } }
/// <summary> /// Creates hair into allocated hair structure, using previously defined setup and entity index. /// </summary> public bool Create(HairSetup setup, Entity parentEntity) { // No setup or parent to link to - bypass function. if (parentEntity == null || setup == null || setup.LinkBody >= parentEntity.Bf.BoneTags.Count || parentEntity.Bt.BtBody[(int)setup.LinkBody] == null) return false; var model = EngineWorld.GetModelByID(setup.Model); // No model to link to - bypass function. if (model == null || model.MeshCount == 0) return false; // Setup engine container. FIXME: DOESN'T WORK PROPERLY ATM. Container = new EngineContainer(); Container.Room = parentEntity.Self.Room; Container.ObjectType = OBJECT_TYPE.Hair; Container.Object = this; // Setup initial hair parameters. OwnerChar = parentEntity; // Entity to refer to. OwnerBody = setup.LinkBody; // Entity body to refer to. // Setup initial position / angles. var ownerBodyTransform = parentEntity.Transform * parentEntity.Bf.BoneTags[(int) OwnerBody].FullTransform; // Number of elements (bodies) is equal to number of hair meshes. Elements = new List<HairElement>(); Elements.Resize(model.MeshCount, () => new HairElement()); // Root index should be always zero, as it is how engine determines that it is // connected to head and renders it properly. Tail index should be always the // last element of the hair, as it indicates absence of "child" constraint. RootIndex = 0; TailIndex = (byte)(Elements.Count - 1); // Weight step is needed to determine the weight of each hair body. // It is derived from root body weight and tail body weight. var weightStep = (setup.RootWeight - setup.TailWeight) / Elements.Count; var currentWeight = setup.RootWeight; for (var i = 0; i < Elements.Count; i++) { // Point to corresponding mesh. Elements[i].Mesh = model.MeshTree[i].MeshBase; // Begin creating ACTUAL physical hair mesh. var localInertia = BulletSharp.Math.Vector3.Zero; // Make collision shape out of mesh. Elements[i].Shape = BT_CSfromMesh(Elements[i].Mesh, true, true, false); Elements[i].Shape.CalculateLocalInertia(currentWeight * setup.HairInertia, out localInertia); // Decrease next body weight to weight_step parameter. currentWeight -= weightStep; // Initialize motion state for body. var startTransform = ownerBodyTransform; var motionState = new DefaultMotionState(((Matrix4)startTransform).ToBullet()); // Make rigid body. Elements[i].Body = new RigidBody(new RigidBodyConstructionInfo(currentWeight, motionState, Elements[i].Shape, localInertia)); // Damping makes body stop in space by itself, to prevent it from continous movement. Elements[i].Body.SetDamping(setup.HairDamping[0], setup.HairDamping[1]); // Restitution and friction parameters define "bounciness" and "dullness" of hair. Elements[i].Body.Restitution = setup.HairRestitution; Elements[i].Body.Friction = setup.HairFriction; // Since hair is always moving with Lara, even if she's in still state (like, hanging // on a ledge), hair bodies shouldn't deactivate over time. Elements[i].Body.ForceActivationState(ActivationState.DisableDeactivation); // Hair bodies must not collide with each other, and also collide ONLY with kinematic // bodies (e. g. animated meshes), or else Lara's ghost object or anything else will be able to // collide with hair! Elements[i].Body.UserObject = Container; BtEngineDynamicsWorld.AddRigidBody(Elements[i].Body, CollisionFilterGroups.CharacterFilter, CollisionFilterGroups.KinematicFilter); Elements[i].Body.Activate(); } // GENERATE CONSTRAINTS. // All constraints are generic 6-DOF type, as they seem perfect fit for hair. // Joint count is calculated from overall body amount multiplied by per-body constraint // count. Joints = new List<Generic6DofConstraint>(); Joints.Resize(Elements.Count); // If multiple joints per body is specified, joints are placed in circular manner, // with obvious step of (SIMD_2_PI) / joint count. It means that all joints will form // circle-like figure. var currJoint = 0; for (var i = 0; i < Elements.Count; i++) { float bodyLength; var localA = new Transform(); localA.SetIdentity(); var localB = new Transform(); localB.SetIdentity(); var jointX = 0.0f; var jointY = 0.0f; RigidBody prevBody; if(i == 0) // First joint group { // Adjust pivot point A to parent body. localA.Origin = setup.HeadOffset + new Vector3(jointX, 0.0f, jointY); Helper.SetEulerZYX(ref localA.Basis, setup.RootAngle.X, setup.RootAngle.Y, setup.RootAngle.Z); // Stealing this calculation because I need it for drawing OwnerBodyHairRoot = localA; localB.Origin = new Vector3(jointX, 0.0f, jointY); Helper.SetEulerZYX(ref localB.Basis, 0, -HalfPI, 0); prevBody = parentEntity.Bt.BtBody[(int) OwnerBody]; // Previous body is parent body. } else { // Adjust pivot point A to previous mesh's length, considering mesh overlap multiplier. bodyLength = Math.Abs(Elements[i - 1].Mesh.BBMax.Y - Elements[i - 1].Mesh.BBMin.Y) * setup.JointOverlap; localA.Origin = new Vector3(jointX, bodyLength, jointY); Helper.SetEulerZYX(ref localA.Basis, 0, -HalfPI, 0); // Pivot point B is automatically adjusted by Bullet. localB.Origin = new Vector3(jointX, 0.0f, jointY); Helper.SetEulerZYX(ref localB.Basis, 0, -HalfPI, 0); prevBody = Elements[i - 1].Body; // Previous body is preceding hair mesh. } // Create 6DOF constraint. Joints[currJoint] = new Generic6DofConstraint(prevBody, Elements[i].Body, ((Matrix4) localA).ToBullet(), ((Matrix4) localB).ToBullet(), true); // CFM and ERP parameters are critical for making joint "hard" and link // to Lara's head. With wrong values, constraints may become "elastic". for (var axis = 0; axis < 6; axis++) { Joints[currJoint].SetParam(ConstraintParam.StopCfm, setup.JointCfm, axis); Joints[currJoint].SetParam(ConstraintParam.StopErp, setup.JointErp, axis); } Joints[currJoint].LinearLowerLimit = BulletSharp.Math.Vector3.Zero; Joints[currJoint].LinearUpperLimit = BulletSharp.Math.Vector3.Zero; if(i == 0) { // First joint group should be more limited in motion, as it is connected // right to the head. NB: Should we make it scriptable as well? Joints[currJoint].AngularLowerLimit = new BulletSharp.Math.Vector3(-HalfPI, 0.0f, -HalfPI * 0.4f); Joints[currJoint].AngularLowerLimit = new BulletSharp.Math.Vector3(-HalfPI * 0.3f, 0.0f, HalfPI * 0.4f); // Increased solver iterations make constraint even more stable. Joints[currJoint].OverrideNumSolverIterations = 100; } else { // Normal joint with more movement freedom. Joints[currJoint].AngularLowerLimit = new BulletSharp.Math.Vector3(-HalfPI * 0.5f, 0.0f, -HalfPI * 0.5f); Joints[currJoint].AngularLowerLimit = new BulletSharp.Math.Vector3(HalfPI * 0.5f, 0.0f, HalfPI * 0.5f); } Joints[currJoint].DebugDrawSize = 5.0f; // Draw constraint axes. // Add constraint to the world. BtEngineDynamicsWorld.AddConstraint(Joints[currJoint], true); currJoint++; // Point to the next joint. } createHairMesh(model); return true; }