#include "../../../shared_cpp/Renderer2d.h" #include "../../../shared_cpp/types.h" #include "../../../shared_cpp/mathlib.h" struct PointMassUpdateData { int32 index = 0; Vector2 restingPosition; // Position is in world coordinates Vector2 currentPosition; // Position is in world coordinates Vector2 velocity; Vector2 acceleration; Vector2 force; bool isHovered = false; PointMassUpdateData* neighbors[4]; }; struct SoftbodyRectangle { // User defined float32 width = 200; float32 height = 200; int32 springDensity = 16; float32 k = 10000000.f; // in N /m float32 c = 9000.f; float32 jointMassKg = 10.f; float32 floorPosition = 0; // Calculated before runtime Vector2 springDimensions; // Runtime data PointMassUpdateData* updateData = NULL; // Render data Mesh2d mesh; Mesh2d pointsMesh; Mesh2d floorMesh; Vertex2d* vertices = NULL; Vertex2d* pointsVertices = NULL; void load(Renderer2d* renderer) { auto defaultPosition = Vector2(800 / 2 - width / 2, 400); springDimensions = Vector2(width / springDensity, height / springDensity); int32 numVertices = springDensity * springDensity; // Each subdivision is a square. int32 numIndices = 6 * ((springDensity - 1) * (springDensity - 1)); vertices = new Vertex2d[numVertices]; updateData = new PointMassUpdateData[numVertices]; pointsVertices = new Vertex2d[numVertices]; auto indices = new GLuint[numIndices]; // -- Load a square with the desired density int32 vIdx = 0; int32 iIdx = 0; float32 inverseDensity = 1.f / springDensity; float32 halfInv = inverseDensity / 2.f; for (int32 y = 0; y < springDensity; y++) { // Rows for (int32 x = 0; x < springDensity; x++) { // Columns Vector2 vpos = Vector2(x * inverseDensity - halfInv, y * inverseDensity- halfInv); vpos.x = vpos.x * width + defaultPosition.x; vpos.y = vpos.y * height + defaultPosition.y; vertices[vIdx] = { vpos, Vector4(1, 0, 0, 1) }; updateData[vIdx].index = vIdx; updateData[vIdx].restingPosition = vpos; updateData[vIdx].currentPosition = vpos; updateData[vIdx].force = Vector2(0, 0); updateData[vIdx].velocity = Vector2(0, 0); updateData[vIdx].acceleration = Vector2(0, 0); if (x != springDensity - 1) updateData[vIdx].neighbors[0] = &updateData[vIdx + 1]; // Right else updateData[vIdx].neighbors[0] = NULL; if (y != springDensity - 1) updateData[vIdx].neighbors[1] = &updateData[vIdx + springDensity]; // Bottom else updateData[vIdx].neighbors[1] = NULL; if (x != 0) updateData[vIdx].neighbors[2] = &updateData[vIdx - 1]; // Left else updateData[vIdx].neighbors[2] = NULL; if (y != 0) updateData[vIdx].neighbors[3] = &updateData[vIdx - springDensity]; // Top else updateData[vIdx].neighbors[3] = NULL; if (y != springDensity - 1 && x != springDensity - 1) { indices[iIdx++] = vIdx; indices[iIdx++] = vIdx + 1; indices[iIdx++] = vIdx + springDensity; indices[iIdx++] = vIdx + springDensity; indices[iIdx++] = vIdx + springDensity + 1; indices[iIdx++] = vIdx + 1; } pointsVertices[vIdx].position = vpos; pointsVertices[vIdx].color = Vector4(0, 0, 0, 1); vIdx++; } } mesh.load(vertices, numVertices, indices, numIndices, renderer, GL_DYNAMIC_DRAW); pointsMesh.load(pointsVertices, numVertices, renderer, GL_DYNAMIC_DRAW); delete [] indices; // -- Load the floor line; Vector2 floorDimensions = Vector2(renderer->context->width, 8); floorPosition = 100.f; Vector4 floorColor = Vector4(0.5, 0.5, 0.5, 1); Vertex2d floorVertices[6]; floorVertices[0] = { Vector4(0, floorPosition, 0, 1), floorColor }; floorVertices[1] = { Vector4(floorDimensions.x, floorPosition, 0, 1), floorColor }; floorVertices[2] = { Vector4(0, floorPosition - floorDimensions.y, 0, 1), floorColor }; floorVertices[3] = { Vector4(0, floorPosition - floorDimensions.y, 0, 1), floorColor }; floorVertices[4] = { Vector4(floorDimensions.x, floorPosition - floorDimensions.y, 0, 1), floorColor }; floorVertices[5] = { Vector4(floorDimensions.x, floorPosition, 0, 1), floorColor }; floorMesh.load(floorVertices, 6, renderer); } Vector2 getForceBetweenPointMasses(PointMassUpdateData* first, PointMassUpdateData* second) { auto relativeVelocity = second->velocity - first->velocity; auto restLength = (second->restingPosition - first->restingPosition).length(); auto relativePosition = second->currentPosition - first->currentPosition; auto currentLength = relativePosition.length(); auto positionDir = relativePosition.normalize(); auto velDotProduct = positionDir.dot(relativeVelocity); auto accelDotProduct = positionDir.dot(second->acceleration - first->acceleration); float32 springForce = k * (currentLength - restLength); float32 dampingForce = c * velDotProduct; float32 accelerationForce = jointMassKg * accelDotProduct; float32 totalForce = accelerationForce + springForce + dampingForce; return positionDir * totalForce; } void update(float32 dtSeconds) { for (int32 v = 0; v < pointsMesh.numVertices; v++) { auto pointMass = &updateData[v]; // -- Add the forces from it's neighbors. Note that we only do the first two // neighbors, which are the right and bottom neighbors. for (int32 n = 0; n < 2; n++) { auto neighbor = pointMass->neighbors[n]; if (neighbor == NULL) continue; auto forceBetween = getForceBetweenPointMasses(pointMass, neighbor); pointMass->force = pointMass->force + forceBetween; neighbor->force = neighbor->force- forceBetween; } } // -- Update the local position of each vertex. for (int32 v = 0; v < pointsMesh.numVertices; v++) { auto pointMass = &updateData[v]; auto prevPos = pointMass->currentPosition; // -- Gravity Vector2 g = Vector2(0, -9.8 * jointMassKg) * dtSeconds; // -- Euler integration to find the current velocity and position pointMass->acceleration = (pointMass->force / jointMassKg) * dtSeconds; pointMass->velocity = pointMass->velocity + pointMass->acceleration * dtSeconds + g; pointMass->restingPosition = pointMass->restingPosition + g * dtSeconds; pointMass->currentPosition = pointMass->currentPosition + (pointMass->velocity * dtSeconds); pointMass->force = Vector2(0, 0); // Reset the force for the next update particleFloorCollision(pointMass, prevPos, dtSeconds); // -- Collision detection const float32 COLLISION_DISTANCE = 0.3f; for (int32 n = 0; n < 4; n++) { auto neighbor = pointMass->neighbors[n]; if (neighbor == NULL) continue; if ((neighbor->currentPosition - pointMass->currentPosition).length() < COLLISION_DISTANCE) { auto positionNormal = (neighbor->currentPosition - pointMass->currentPosition).normalize(); pointMass->currentPosition = neighbor->currentPosition - positionNormal * COLLISION_DISTANCE; float32 dotProduct = pointMass->velocity.dot(positionNormal); pointMass->velocity = pointMass->velocity - positionNormal * (2 * dotProduct); } } vertices[v].position = pointMass->currentPosition; pointsVertices[v].position = pointMass->currentPosition; } // -- Update vertices mesh.updateVertices(vertices); pointsMesh.updateVertices(pointsVertices); } void particleFloorCollision(PointMassUpdateData* ud, Vector2 prevPos, float32 dtSeconds) { // We assume that the floor is always horizontal for this simulation auto dotProduct = ud->velocity.dot(Vector2(0, 1)); if (dotProduct >= 0) { return; // Not moving in the same direction } if (ud->currentPosition.y - floorPosition < 0.1f) { // Find the point in the simulation at which we began intersecting, and then reflect. Vector2 newPosition; do { dtSeconds = dtSeconds - 0.02f; newPosition = prevPos + ud->velocity * dtSeconds; } while (newPosition.y < floorPosition); ud->currentPosition = newPosition; ud->velocity = (ud->velocity - Vector2(0, 1) * (2 * dotProduct)) * 0.5f; } } void render(Renderer2d* renderer) { mesh.render(renderer); pointsMesh.render(renderer, GL_POINTS); floorMesh.render(renderer); } void unload() { mesh.unload(); pointsMesh.unload(); delete [] vertices; delete [] pointsVertices; } };