diff --git a/renderer/bodymenu.cpp b/renderer/bodymenu.cpp index 5ae160d..34f0a0e 100644 --- a/renderer/bodymenu.cpp +++ b/renderer/bodymenu.cpp @@ -28,6 +28,8 @@ static float spawnPos[3] = {0.0f, 3.0f, 0.0f}; static float spawnSpeed[3] = {0.0f, 0.0f, 0.0f}; static float spawnForce[3] = {0.0f, 0.0f, 0.0f}; static float spawnMass = 1.0f; +static float spawnDensity = 1.0f; +static float spawnVolume = 0.5235988f; static float sphereRadius = 0.5f; static float boxHalfSize[3] = {0.5f, 0.5f, 0.5f}; @@ -37,6 +39,106 @@ static float rampHalfWidthZ = 1.5f; static std::vector> ownedColliders; +static constexpr float kPi = 3.14159265358979323846f; + +static float sphereVolumeFromRadius(float radius) +{ + radius = std::max(0.0f, radius); + return (4.0f / 3.0f) * kPi * radius * radius * radius; +} + +static float sphereRadiusFromVolume(float volume) +{ + volume = std::max(0.0f, volume); + if (volume <= 0.0f) + return 0.0f; + return std::cbrt((3.0f * volume) / (4.0f * kPi)); +} + +static void setBodyMassAndInertia(Rigidbody &body, float mass) +{ + const float MIN_MASS = PHYSICS_EPSILON; + float effective_mass = mass; + + if (mass <= 0.0f) + { + effective_mass = 0.0f; + } + else if (mass < MIN_MASS) + { + effective_mass = MIN_MASS; + } + + body.inverse_mass = (effective_mass > 0.0f) ? (1.0f / effective_mass) : 0.0f; + const float actual_mass = (body.inverse_mass > 0.0f) ? (1.0f / body.inverse_mass) : 0.0f; + + if (!body.collider || actual_mass <= 0.0f) + { + body.inverse_inertia_body = Mat3::identity() * 0.0f; + body.updateworldinvinertia(); + return; + } + + if (body.collider->type == ShapeType::Sphere) + { + const auto *sphere = static_cast(body.collider); + const float I = 0.4f * actual_mass * sphere->radius * sphere->radius; + const float invI = (I > PHYSICS_EPSILON) ? (1.0f / I) : 0.0f; + body.inverse_inertia_body = Mat3::diag(invI, invI, invI); + } + else if (body.collider->type == ShapeType::Box) + { + const auto *box = static_cast(body.collider); + const Vec3 e = box->halfsize * 2.0f; + + const float Ix = (actual_mass / 12.0f) * (e.y * e.y + e.z * e.z); + const float Iy = (actual_mass / 12.0f) * (e.x * e.x + e.z * e.z); + const float Iz = (actual_mass / 12.0f) * (e.x * e.x + e.y * e.y); + + body.inverse_inertia_body = Mat3::diag( + Ix > PHYSICS_EPSILON ? (1.0f / Ix) : 0.0f, + Iy > PHYSICS_EPSILON ? (1.0f / Iy) : 0.0f, + Iz > PHYSICS_EPSILON ? (1.0f / Iz) : 0.0f); + } + else + { + body.inverse_inertia_body = Mat3::identity() * 0.0f; + } + + body.updateworldinvinertia(); +} + +static bool isBuoyancyHelperWall(const PhysicsWorld &world, const Rigidbody &body) +{ + if (!world.enable_buoyancy) + return false; + if (body.inverse_mass != 0.0f) + return false; + if (!body.collider || body.collider->type != ShapeType::Box) + return false; + + const Fluid &fluid = world.water_fluid; + const float halfSize = fluid.beaker_half_size; + constexpr float wallThickness = 0.2f; + constexpr float wallHalf = wallThickness / 2.0f; + const float tol = 0.02f; + + const auto *box = static_cast(body.collider); + const Vec3 hs = box->halfsize; + + const bool matchesXWall = (std::abs(hs.x - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.z - halfSize) < tol); + const bool matchesZWall = (std::abs(hs.z - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.x - halfSize) < tol); + + const float bottomHalfX = std::max(0.01f, halfSize - wallThickness); + const float bottomHalfY = wallHalf; + const float bottomHalfZ = std::max(0.01f, halfSize - wallThickness); + const bool matchesBottom = (std::abs(hs.x - bottomHalfX) < tol) && + (std::abs(hs.y - bottomHalfY) < tol) && + (std::abs(hs.z - bottomHalfZ) < tol); + + return matchesXWall || matchesZWall || matchesBottom; +} + static void show_tooltip(const char *text) { if (ImGui::IsItemHovered(ImGuiHoveredFlags_DelayShort)) @@ -148,7 +250,29 @@ static float approx_radius_for_new_shape() static void spawn_body(PhysicsWorld &world) { Collider *collider_ptr = nullptr; + int shapeChoice = shapeIndex; + if (world.enable_buoyancy) + { + if (shapeChoice < 0 || shapeChoice > 1) + shapeChoice = 0; + shapeIndex = shapeChoice; + + spawnMass = spawnDensity * spawnVolume; + spawnSpeed[0] = spawnSpeed[1] = spawnSpeed[2] = 0.0f; + spawnForce[0] = spawnForce[1] = spawnForce[2] = 0.0f; + + if (shapeChoice == 0) + { + sphereRadius = sphereRadiusFromVolume(spawnVolume); + } + else if (shapeChoice == 1) + { + const float h = std::cbrt(std::max(0.0f, spawnVolume) / 8.0f); + boxHalfSize[0] = boxHalfSize[1] = boxHalfSize[2] = h; + } + } + if (world.thermal_spawn_controls.lock_to_basic_shapes && shapeChoice > 1) { shapeChoice = std::min(shapeChoice, 1); @@ -195,6 +319,90 @@ static void spawn_body(PhysicsWorld &world) void RenderAddBodyMenuContent(PhysicsWorld &world) { ImGui::SeparatorText("Spawn Body"); + if (world.enable_buoyancy) + { + if (shapeIndex < 0 || shapeIndex > 1) + shapeIndex = 0; + const char *buoyShapes[] = {"Sphere", "Box"}; + ImGui::Combo("Add Shape", &shapeIndex, buoyShapes, 2); + + const Vec3 fluidCenter = world.water_fluid.beaker_center; + const float halfSize = 4.0f; + const float minX = fluidCenter.x - halfSize; + const float maxX = fluidCenter.x + halfSize; + const float minZ = fluidCenter.z - halfSize; + const float maxZ = fluidCenter.z + halfSize; + + ImGui::DragFloat("X", &spawnPos[0], 0.1f); + ImGui::DragFloat("Y", &spawnPos[1], 0.1f); + ImGui::DragFloat("Z", &spawnPos[2], 0.1f); + spawnPos[0] = std::max(minX, std::min(maxX, spawnPos[0])); + spawnPos[2] = std::max(minZ, std::min(maxZ, spawnPos[2])); + + ImGui::DragFloat("Density", &spawnDensity, 0.05f, 0.01f, 100000.0f); + const float prevVolume = spawnVolume; + ImGui::DragFloat("Volume", &spawnVolume, 0.01f, 0.0001f, 100.0f); + if (spawnVolume > 100.0f) + spawnVolume = prevVolume; + + if (ImGui::Button("Add Body")) + { + static float last_add_time = -1000.0f; + float now = ImGui::GetTime(); + if (now - last_add_time < 2.0f) + RequestEngineToast("Too frequent requests"); + else + { + last_add_time = now; + Vec3 pos(spawnPos[0], spawnPos[1], spawnPos[2]); + if (std::abs(pos.x - fluidCenter.x) > halfSize || std::abs(pos.z - fluidCenter.z) > halfSize) + { + RequestEngineToast("Body spawn must be inside beaker X/Z."); + } + else + { + if (shapeIndex == 0) + sphereRadius = sphereRadiusFromVolume(spawnVolume); + else + { + const float h = std::cbrt(std::max(0.0f, spawnVolume) / 8.0f); + boxHalfSize[0] = boxHalfSize[1] = boxHalfSize[2] = h; + } + const float newRad = approx_radius_for_new_shape(); + const auto &bodies = world.getBodies(); + int overlap = 0; + for (auto &b : bodies) + { + if (!b.collider) + continue; + if (b.inverse_mass == 0.0f) + continue; + const float r = approx_radius_from_collider(b.collider); + const float dx = b.position.x - pos.x; + const float dy = b.position.y - pos.y; + const float dz = b.position.z - pos.z; + const float dist2 = dx * dx + dy * dy + dz * dz; + const float rsum = newRad + r; + const float thresh = rsum * 0.85f; + if (dist2 < thresh * thresh) + ++overlap; + if (overlap >= 4) + break; + } + if (overlap >= 4) + RequestEngineToast("area too clustered to add a body"); + else + { + spawn_body(world); + RequestEngineToast("Body was added successfully"); + } + } + } + } + show_tooltip("Creates one rigid body with the current spawn parameters."); + return; + } + const bool restrictShapes = world.thermal_spawn_controls.lock_to_basic_shapes; const char *shapeNamesFull[] = {"Sphere", "Box", "Ramp"}; const char *shapeNamesLimited[] = {"Sphere", "Box"}; @@ -270,6 +478,8 @@ void RenderAddBodyMenuContent(PhysicsWorld &world) { if (!b.collider) continue; + if (world.enable_buoyancy && b.inverse_mass == 0.0f) + continue; const float r = approx_radius_from_collider(b.collider); const float dx = b.position.x - pos.x; const float dy = b.position.y - pos.y; @@ -416,6 +626,12 @@ void RenderBodyInspectorContent(PhysicsWorld &world, bool showCloseButton) { ImGui::PushID(body.id); + if (isBuoyancyHelperWall(world, body)) + { + ImGui::PopID(); + continue; + } + if (!body.collider) { ImGui::PopID(); @@ -452,13 +668,63 @@ void RenderBodyInspectorContent(PhysicsWorld &world, bool showCloseButton) if (isSelected && isLive) { - float editSpeed[3] = {body.velocity.x, body.velocity.y, body.velocity.z}; - if (ImGui::DragFloat3("Edit Speed", editSpeed, 0.1f)) - body.velocity = Vec3(editSpeed[0], editSpeed[1], editSpeed[2]); + if (world.enable_buoyancy) + { + if (body.collider->type == ShapeType::Sphere) + { + SphereCollider *sphere = static_cast(body.collider); + const float currentVolume = sphereVolumeFromRadius(sphere->radius); + const float currentMass = (body.inverse_mass > 0.0f) ? (1.0f / body.inverse_mass) : 0.0f; + const float currentDensity = (currentVolume > 1e-6f) ? (currentMass / currentVolume) : 0.0f; + + float editVolume = currentVolume; + float editDensity = currentDensity; + const float prevVolume = editVolume; + bool volChanged = ImGui::DragFloat("Volume", &editVolume, 0.01f, 0.0001f, 100.0f); + if (editVolume > 100.0f) + editVolume = prevVolume; + bool densChanged = ImGui::DragFloat("Density", &editDensity, 0.05f, 0.01f, 100000.0f); + + if (volChanged || densChanged) + { + sphere->radius = sphereRadiusFromVolume(editVolume); + setBodyMassAndInertia(body, editDensity * editVolume); + } + } + else if (body.collider->type == ShapeType::Box) + { + BoxCollider *box = static_cast(body.collider); + const Vec3 hs = box->halfsize; + const float currentVolume = (2.0f * hs.x) * (2.0f * hs.y) * (2.0f * hs.z); + const float currentMass = (body.inverse_mass > 0.0f) ? (1.0f / body.inverse_mass) : 0.0f; + const float currentDensity = (currentVolume > 1e-6f) ? (currentMass / currentVolume) : 0.0f; + + float editVolume = currentVolume; + float editDensity = currentDensity; + const float prevVolume = editVolume; + bool volChanged = ImGui::DragFloat("Volume", &editVolume, 0.01f, 0.0001f, 100.0f); + if (editVolume > 100.0f) + editVolume = prevVolume; + bool densChanged = ImGui::DragFloat("Density", &editDensity, 0.05f, 0.01f, 100000.0f); + + if (volChanged || densChanged) + { + const float scale = std::cbrt(std::max(0.0001f, editVolume) / std::max(1e-6f, currentVolume)); + box->halfsize = Vec3(hs.x * scale, hs.y * scale, hs.z * scale); + setBodyMassAndInertia(body, editDensity * editVolume); + } + } + } + else + { + float editSpeed[3] = {body.velocity.x, body.velocity.y, body.velocity.z}; + if (ImGui::DragFloat3("Edit Speed", editSpeed, 0.1f)) + body.velocity = Vec3(editSpeed[0], editSpeed[1], editSpeed[2]); - float editForce[3] = {body.force_accum.x, body.force_accum.y, body.force_accum.z}; - if (ImGui::DragFloat3("Edit Force", editForce, 0.1f)) - body.force_accum = Vec3(editForce[0], editForce[1], editForce[2]); + float editForce[3] = {body.force_accum.x, body.force_accum.y, body.force_accum.z}; + if (ImGui::DragFloat3("Edit Force", editForce, 0.1f)) + body.force_accum = Vec3(editForce[0], editForce[1], editForce[2]); + } if (body.thermal_enabled) { float editTemp = body.temperature; @@ -497,7 +763,8 @@ void RenderBodyMenu(PhysicsWorld &world) { ImGui::Begin("Body Menu"); RenderAddBodyMenuContent(world); - RenderConstraintMenuContent(world); + if (!world.enable_buoyancy) + RenderConstraintMenuContent(world); RenderWorldMenuContent(world); RenderBodyInspectorContent(world, false); ImGui::End(); diff --git a/renderer/drawbodies.cpp b/renderer/drawbodies.cpp index 8eb6bd4..1152523 100644 --- a/renderer/drawbodies.cpp +++ b/renderer/drawbodies.cpp @@ -16,6 +16,7 @@ #include "thermal_palette.hpp" #include #include +#include #include #include @@ -450,6 +451,110 @@ static void drawVelocityArrows(PhysicsWorld &world, GLuint prog, GLuint vao, GLu void RenderBodies(PhysicsWorld &world, const Camera &camera, float aspectRatio) { + if (world.enable_buoyancy) + { + const Fluid &fluid = world.water_fluid; + const float halfSize = fluid.beaker_half_size; + constexpr float wallThickness = 0.2f; + constexpr float wallHalf = wallThickness / 2.0f; + + float beakerCenterY = fluid.beaker_center.y; + if (std::abs(beakerCenterY) < 1e-4f) + beakerCenterY = halfSize; + + auto isWallLike = [&](const Rigidbody &body) -> bool + { + if (body.inverse_mass != 0.0f) + return false; + if (!body.collider || body.collider->type != ShapeType::Box) + return false; + const BoxCollider *box = static_cast(body.collider); + const Vec3 hs = box->halfsize; + const float tol = 0.02f; + + const bool matchesXWall = (std::abs(hs.x - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.z - halfSize) < tol); + const bool matchesZWall = (std::abs(hs.z - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.x - halfSize) < tol); + + const float bottomHalfX = std::max(0.01f, halfSize - wallThickness); + const float bottomHalfY = wallHalf; + const float bottomHalfZ = std::max(0.01f, halfSize - wallThickness); + const bool matchesBottom = (std::abs(hs.x - bottomHalfX) < tol) && + (std::abs(hs.y - bottomHalfY) < tol) && + (std::abs(hs.z - bottomHalfZ) < tol); + + return matchesXWall || matchesZWall || matchesBottom; + }; + + bool alreadyHas = false; + for (const auto &b : world.getBodies()) + { + if (isWallLike(b)) + { + alreadyHas = true; + break; + } + } + + static std::vector> s_owned; + if (!alreadyHas) + { + s_owned.clear(); + const float cx = fluid.beaker_center.x; + const float cz = fluid.beaker_center.z; + const float yCenter = beakerCenterY; + const float yHalf = halfSize; + + auto addBox = [&](const Vec3 &pos, const Vec3 &hs) + { + auto c = std::make_unique(hs); + Collider *ptr = c.get(); + s_owned.push_back(std::move(c)); + world.addBody(Rigidbody(pos, Vec3(), ptr, 0.0f)); + }; + + addBox(Vec3(cx - halfSize + wallHalf, yCenter, cz), Vec3(wallHalf, yHalf, halfSize)); + addBox(Vec3(cx + halfSize - wallHalf, yCenter, cz), Vec3(wallHalf, yHalf, halfSize)); + addBox(Vec3(cx, yCenter, cz - halfSize + wallHalf), Vec3(halfSize, yHalf, wallHalf)); + addBox(Vec3(cx, yCenter, cz + halfSize - wallHalf), Vec3(halfSize, yHalf, wallHalf)); + + const float bottomHalfX = std::max(0.01f, halfSize - wallThickness); + const float bottomHalfY = wallHalf; + const float bottomHalfZ = std::max(0.01f, halfSize - wallThickness); + const float bottomCenterY = yCenter - halfSize + wallHalf; + addBox(Vec3(cx, bottomCenterY, cz), Vec3(bottomHalfX, bottomHalfY, bottomHalfZ)); + } + } + + auto isBuoyancyHelperWallBody = [&](const Rigidbody &body) -> bool + { + if (!world.enable_buoyancy) + return false; + if (body.inverse_mass != 0.0f) + return false; + if (!body.collider || body.collider->type != ShapeType::Box) + return false; + + const Fluid &fluid = world.water_fluid; + const float halfSize = fluid.beaker_half_size; + constexpr float wallThickness = 0.2f; + constexpr float wallHalf = wallThickness / 2.0f; + const float tol = 0.02f; + const BoxCollider *box = static_cast(body.collider); + const Vec3 hs = box->halfsize; + + const bool matchesXWall = (std::abs(hs.x - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.z - halfSize) < tol); + const bool matchesZWall = (std::abs(hs.z - wallHalf) < tol) && (std::abs(hs.y - halfSize) < tol) && (std::abs(hs.x - halfSize) < tol); + + const float bottomHalfX = std::max(0.01f, halfSize - wallThickness); + const float bottomHalfY = wallHalf; + const float bottomHalfZ = std::max(0.01f, halfSize - wallThickness); + const bool matchesBottom = (std::abs(hs.x - bottomHalfX) < tol) && + (std::abs(hs.y - bottomHalfY) < tol) && + (std::abs(hs.z - bottomHalfZ) < tol); + + return matchesXWall || matchesZWall || matchesBottom; + }; + // Separate vertex lists so we can color axes and body outlines differently std::vector axisVertices; axisVertices.reserve(18); // 3 axes * 2 endpoints * 3 components @@ -633,10 +738,130 @@ void RenderBodies(PhysicsWorld &world, const Camera &camera, float aspectRatio) glDrawArrays(GL_TRIANGLES, 0, static_cast(solidVerts.size() / 6)); }; + auto pushBoxSolidOpenTop = [&](std::vector &v, const glm::vec3 &c, const glm::vec3 &h, const Mat3 &R) + { + const glm::vec3 p000 = c + rotateOffset(R, glm::vec3(-h.x, -h.y, -h.z)); + const glm::vec3 p001 = c + rotateOffset(R, glm::vec3(-h.x, -h.y, +h.z)); + const glm::vec3 p010 = c + rotateOffset(R, glm::vec3(-h.x, +h.y, -h.z)); + const glm::vec3 p011 = c + rotateOffset(R, glm::vec3(-h.x, +h.y, +h.z)); + const glm::vec3 p100 = c + rotateOffset(R, glm::vec3(+h.x, -h.y, -h.z)); + const glm::vec3 p101 = c + rotateOffset(R, glm::vec3(+h.x, -h.y, +h.z)); + const glm::vec3 p110 = c + rotateOffset(R, glm::vec3(+h.x, +h.y, -h.z)); + const glm::vec3 p111 = c + rotateOffset(R, glm::vec3(+h.x, +h.y, +h.z)); + const glm::vec3 nxp = rotateOffset(R, glm::vec3(-1.0f, 0.0f, 0.0f)); + const glm::vec3 nx = rotateOffset(R, glm::vec3(1.0f, 0.0f, 0.0f)); + const glm::vec3 nyn = rotateOffset(R, glm::vec3(0.0f, -1.0f, 0.0f)); + const glm::vec3 nzn = rotateOffset(R, glm::vec3(0.0f, 0.0f, -1.0f)); + const glm::vec3 nz = rotateOffset(R, glm::vec3(0.0f, 0.0f, 1.0f)); + pushFace4(v, p000, nxp, p010, nxp, p011, nxp, p001, nxp); + pushFace4(v, p100, nx, p110, nx, p111, nx, p101, nx); + pushFace4(v, p000, nyn, p100, nyn, p101, nyn, p001, nyn); + pushFace4(v, p000, nzn, p100, nzn, p110, nzn, p010, nzn); + pushFace4(v, p001, nz, p011, nz, p111, nz, p101, nz); + }; + + auto drawTransparentBoxOpenTop = [&](const glm::vec3 ¢er, const glm::vec3 &half, + const Mat3 &R, + float r, float g, float b, float a) + { + std::vector solidVerts; + solidVerts.reserve(4096); + pushBoxSolidOpenTop(solidVerts, center, half, R); + if (solidVerts.empty()) + return; + glBufferData(GL_ARRAY_BUFFER, solidVerts.size() * sizeof(float), solidVerts.data(), GL_DYNAMIC_DRAW); + if (smFloor >= 0) + glUniform1f(smFloor, 0.0f); + if (smCol >= 0) + glUniform4f(smCol, r, g, b, a); + if (smMatAmbient >= 0) + glUniform3f(smMatAmbient, r, g, b); + if (smMatDiffuse >= 0) + glUniform3f(smMatDiffuse, r, g, b); + if (smMatSpecular >= 0) + glUniform3fv(smMatSpecular, 1, glm::value_ptr(specularColor)); + if (smMatShininess >= 0) + glUniform1f(smMatShininess, shininess); + glDrawArrays(GL_TRIANGLES, 0, static_cast(solidVerts.size() / 6)); + }; + + auto drawTransparentBox = [&](const glm::vec3 ¢er, const glm::vec3 &half, + const Mat3 &R, + float r, float g, float b, float a) + { + std::vector solidVerts; + solidVerts.reserve(4096); + pushBoxSolid(solidVerts, center, half, R); + if (solidVerts.empty()) + return; + glBufferData(GL_ARRAY_BUFFER, solidVerts.size() * sizeof(float), solidVerts.data(), GL_DYNAMIC_DRAW); + if (smFloor >= 0) + glUniform1f(smFloor, 0.0f); + if (smCol >= 0) + glUniform4f(smCol, r, g, b, a); + if (smMatAmbient >= 0) + glUniform3f(smMatAmbient, r, g, b); + if (smMatDiffuse >= 0) + glUniform3f(smMatDiffuse, r, g, b); + if (smMatSpecular >= 0) + glUniform3fv(smMatSpecular, 1, glm::value_ptr(specularColor)); + if (smMatShininess >= 0) + glUniform1f(smMatShininess, shininess); + glDrawArrays(GL_TRIANGLES, 0, static_cast(solidVerts.size() / 6)); + }; + + if (world.enable_buoyancy) + { + const Fluid &fluid = world.water_fluid; + const float halfSize = fluid.beaker_half_size; + constexpr float wallThickness = 0.2f; + + float beakerCenterY = fluid.beaker_center.y; + if (std::abs(beakerCenterY) < 1e-4f) + beakerCenterY = halfSize; + + const Vec3 beakerCenter(fluid.beaker_center.x, beakerCenterY, fluid.beaker_center.z); + + const float innerHalfX = std::max(0.01f, halfSize - wallThickness); + const float innerHalfZ = std::max(0.01f, halfSize - wallThickness); + const float innerHalfY = std::max(0.01f, halfSize - wallThickness); + const float innerBottomY = beakerCenterY - innerHalfY; + + const float waterTopY = fluid.height; + const float waterHeight = std::max(0.0f, std::min(waterTopY - innerBottomY, 2.0f * innerHalfY)); + + if (waterHeight > 1e-4f) + { + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); + + const float waterHalfY = waterHeight * 0.5f; + const float waterCenterY = innerBottomY + waterHalfY; + drawTransparentBox(glm::vec3(beakerCenter.x, waterCenterY, beakerCenter.z), + glm::vec3(innerHalfX, waterHalfY, innerHalfZ), + Mat3::identity(), + 0.18f, 0.55f, 1.00f, 0.34f); + + constexpr float surfaceThickness = 0.08f; + const float surfaceHalfY = std::min(surfaceThickness * 0.5f, waterHalfY); + const float surfaceCenterY = waterTopY - surfaceHalfY; + drawTransparentBox(glm::vec3(beakerCenter.x, surfaceCenterY, beakerCenter.z), + glm::vec3(innerHalfX, surfaceHalfY, innerHalfZ), + Mat3::identity(), + 0.22f, 0.65f, 1.00f, 0.24f); + + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); + } + } + for (auto &body : world.getBodies()) { if (looksLikeFloor(body)) continue; + if (isBuoyancyHelperWallBody(body)) + continue; BodyID key = body.id; float r = ((key * 73u) % 100) / 100.0f; float g = ((key * 37u) % 100) / 100.0f; @@ -674,6 +899,26 @@ void RenderBodies(PhysicsWorld &world, const Camera &camera, float aspectRatio) continue; drawSolidBody(body, 1.0f, 0.26f, 0.28f, 0.31f, 0.78f); } + + if (world.enable_buoyancy) + { + const Fluid &fluid = world.water_fluid; + const float halfSize = fluid.beaker_half_size; + float beakerCenterY = fluid.beaker_center.y; + if (std::abs(beakerCenterY) < 1e-4f) + beakerCenterY = halfSize; + const Vec3 beakerCenter(fluid.beaker_center.x, beakerCenterY, fluid.beaker_center.z); + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); + drawTransparentBoxOpenTop(glm::vec3(beakerCenter.x, beakerCenter.y, beakerCenter.z), + glm::vec3(halfSize, halfSize, halfSize), + Mat3::identity(), + 0.75f, 0.92f, 1.00f, 0.08f); + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); + } glDisable(GL_POLYGON_OFFSET_FILL); glDepthMask(GL_TRUE); glDisable(GL_BLEND); @@ -700,6 +945,8 @@ void RenderBodies(PhysicsWorld &world, const Camera &camera, float aspectRatio) { if (!body.collider) continue; + if (isBuoyancyHelperWallBody(body)) + continue; const glm::vec3 c(body.position.x, body.position.y, body.position.z); Mat3 R = body.orientation.toMat3(); diff --git a/renderer/window.cpp b/renderer/window.cpp index 324202a..ff07ebd 100644 --- a/renderer/window.cpp +++ b/renderer/window.cpp @@ -371,10 +371,13 @@ void CreateWindow(PhysicsWorld &world) ImGui::EndMenu(); } - if (ImGui::BeginMenu("Constraints")) + if (!world.enable_buoyancy) { - RenderConstraintMenuContent(world); - ImGui::EndMenu(); + if (ImGui::BeginMenu("Constraints")) + { + RenderConstraintMenuContent(world); + ImGui::EndMenu(); + } } if (ImGui::BeginMenu("World"))