#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 force; bool isHovered = false; PointMassUpdateData* neighbors[4]; }; EM_BOOL onMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); EM_BOOL onMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); EM_BOOL onMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData); struct SoftbodyRectangle { // User defined float32 width = 200; float32 height = 200; int32 springDensity = 10; float32 k = 3.f; // in N /m float32 c = 3.f; float32 jointMassKg = 1.f; // Calculated before runtime Vector2 springDimensions; // Runtime data float32 totalTimeSeconds = 0.f; PointMassUpdateData* updateData = NULL; bool hasLastPositionChanged = false; Vector2 lastMousePosition; PointMassUpdateData* draggedVertex = NULL; // Render data Mesh2d mesh; Mesh2d pointsMesh; Vertex2d* vertices = NULL; Vertex2d* pointsVertices = NULL; void load(Renderer2d* renderer) { Vector2 position = Vector2(800 / 2 - width / 2, 600 / 2 - height / 2); 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]; 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 + position.x; vpos.y = vpos.y * height + position.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); if (x != 0) updateData[vIdx].neighbors[0] = &updateData[vIdx - 1]; // Left else updateData[vIdx].neighbors[0] = NULL; if (x != springDensity - 1) updateData[vIdx].neighbors[1] = &updateData[vIdx + 1]; // Right else updateData[vIdx].neighbors[1] = NULL; if (y != 0) updateData[vIdx].neighbors[2] = &updateData[vIdx - springDensity]; // Top else updateData[vIdx].neighbors[2] = NULL; if (y != springDensity - 1) updateData[vIdx].neighbors[3] = &updateData[vIdx + springDensity]; // Bottom 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; } vIdx++; } } mesh.load(vertices, numVertices, indices, numIndices, renderer, GL_DYNAMIC_DRAW); pointsVertices = new Vertex2d[numVertices]; for (int32 v = 0; v < numVertices; v++) { pointsVertices[v].position = vertices[v].position; pointsVertices[v].color = Vector4(0, 0, 0, 1); } pointsMesh.load(pointsVertices, numVertices, renderer, GL_DYNAMIC_DRAW); delete [] indices; // Setup callbacks emscripten_set_mousemove_callback("#gl_canvas", this, false, onMouseMove); emscripten_set_mousedown_callback("#gl_canvas", this, false, onMouseDown); emscripten_set_mouseup_callback("#gl_canvas", this, false, onMouseUp); } Vector2 getForceBetweenPointMasses(PointMassUpdateData* first, PointMassUpdateData* second) { auto relativeVelocity = second->velocity - first->velocity; auto restLength = (first->restingPosition - second->restingPosition).length(); auto relativePosition = second->currentPosition - first->currentPosition; auto currentLength = relativePosition.length(); auto positionDir = relativePosition.normalize(); auto dotProduct = positionDir.dot(relativeVelocity); float32 springForce = k * (currentLength - restLength); float32 dampingForce = c * dotProduct; float32 totalForce = springForce + dampingForce; return positionDir * totalForce; } void update(float32 dtSeconds) { totalTimeSeconds += dtSeconds; for (int32 v = 0; v < pointsMesh.numVertices; v++) { auto pointMass = &updateData[v]; if (draggedVertex != NULL) { if (pointMass == draggedVertex && hasLastPositionChanged) { hasLastPositionChanged = false; Vector2 displacement = lastMousePosition - pointMass->restingPosition; // We need to limit the new position based off of the triangle if (displacement.x > springDimensions.x) { displacement.x = springDimensions.x; } else if (displacement.x < -springDimensions.x) { displacement.x = -springDimensions.x; } if (displacement.y > springDimensions.y) { displacement.y = springDimensions.y; } else if (displacement.y < -springDimensions.y) { displacement.y = -springDimensions.y; } pointMass->currentPosition = pointMass->restingPosition + displacement; vertices[v].position = pointMass->currentPosition; pointsVertices[v].position = pointMass->currentPosition; } } else { // Add the forces from it's neighbors for (int32 n = 0; n < 4; n++) { auto neighbor = pointMass->neighbors[n]; if (neighbor == NULL) continue; pointMass->force += getForceBetweenPointMasses(pointMass, neighbor); } pointMass->velocity = pointMass->velocity + (pointMass->force / jointMassKg) * dtSeconds; pointMass->currentPosition = pointMass->currentPosition + (pointMass->velocity * dtSeconds); const float32 COLLISION_DISTANCE = 4.f; for (int32 n = 0; n < pointsMesh.numVertices; n++) { if (n == v) continue; auto neighbor = &updateData[n]; 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; // Hovering highlights behavior if (hasLastPositionChanged) { if ((pointMass->currentPosition - lastMousePosition).length() < 10.f) { pointsVertices[v].color = Vector4(1, 1, 0, 1); pointMass->isHovered = true; for (int32 n = 0; n < 4; n++) { if (pointMass->neighbors[n]) pointsVertices[pointMass->neighbors[n]->index].color = Vector4(0, 0, 1, 1); } } else if (pointMass->isHovered) { pointsVertices[v].color = Vector4(0, 0, 0, 1); for (int32 n = 0; n < 4; n++) { if (pointMass->neighbors[n] && !pointMass->neighbors[n]->isHovered) pointsVertices[pointMass->neighbors[n]->index].color = Vector4(0, 0, 0, 1); } pointMass->isHovered = false; } } } } if (hasLastPositionChanged) hasLastPositionChanged = false; mesh.updateVertices(vertices); pointsMesh.updateVertices(pointsVertices); } void render(Renderer2d* renderer) { mesh.render(renderer); pointsMesh.render(renderer, GL_POINTS); } void unload() { mesh.unload(); pointsMesh.unload(); delete [] vertices; delete [] pointsVertices; } }; EM_BOOL onMouseMove(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) { SoftbodyRectangle* rectangle = (SoftbodyRectangle*)userData; rectangle->hasLastPositionChanged = true; rectangle->lastMousePosition = Vector2(static_cast(mouseEvent->targetX), static_cast(600 - mouseEvent->targetY)); return true; } EM_BOOL onMouseDown(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) { SoftbodyRectangle* rectangle = (SoftbodyRectangle*)userData; for (int32 v = 0; v < rectangle->pointsMesh.numVertices; v++) { auto pointMass = &rectangle->updateData[v]; if (pointMass->isHovered) { rectangle->draggedVertex = pointMass; break; } } return true; } EM_BOOL onMouseUp(int eventType, const EmscriptenMouseEvent *mouseEvent, void *userData) { SoftbodyRectangle* rectangle = (SoftbodyRectangle*)userData; rectangle->draggedVertex = NULL; return true; }