Levy (blogpost)

The physics solution because everything else sucked. (note: this is naught but a blog post. it doesn't actually include all of Levy.)

Me reconnaissez-vous? C'est moi le crisse de fou qui marche au-dessus de la ville!
     - Serge Fiori

Request

I am currently working on a small parkour/racing game for school called Vehicular. I wrote the engine, Jossec, named after Yann Le Jossec, founder of PEGASE Pilotage, for the game as well as some other features like the level select screen, the radio, and the FPS controller. I'm also writing the entire soundtrack including sound effects. For this specific game, I needed a specific engine that could do specific stuff. I knew I needed GameBox. But, as I knew Ubisoft Nadeo wouldn't really be keen on licensing their in-house engine for standalone release, I decided instead to write a GameBox-like engine. It would use meshes either from a set of meshes or from the map itself, so basically an Item/Block system like TrackMania has. Since the game is part FPS, part vehicle simulation, I decided to reuse my old unnormalized FPS controller that I made for Trans Girl Toolkit. And then came the problems...

Problems

The first problem came when I decided to look into how the industry did it. Of course, Quake, even though it has physics, doesn't really have an advanced enough physics engine for the complex simulations that came from it in later forks like Source. Source uses both Quake collisions and Havok physics, choosing which system to trust depending on the context. I then started looking for physics engine to hook my raylib BoundingBox to. I first tried ReactPhysics3D. I am not even going to talk about it in further detail, it just didn't want to work; I couldn't even get the examples to work properly. I then tried JoltPhysics which worked, then didn't, then worked, then stopped working. I managed to get to a point where Jolt would cooperate somewhat. Except that forces wouldn't be applied unless the character was in the air or they were so strong that they catapulted the car off of the starting platform at 1000kph. Furthermore, it felt unnecessarily verbose to use and was really way more than I realistically needed for such a simple thing as "knowing when stuff is inside of other stuff". I decided I'd get the physics situation in my own hands.

Temporary Solutions

I created a new physicscollision engine named Levy, after Sylvain Levy. It went through 3 stages; full rigidbody simulation, voxelization, and pure mesh raycasting.

Rigidbody simulation

My first thought was: "let's try to make a rigidbody simulation". So I started by adding game object registration and force application.

int Levy::PhysicsEngine::Register(RigidBodyBase* rb)
{
  rigidbodies.push_back(rb);
  return rigidbodies.size() - 1;
}

void Levy::PhysicsEngine::Update(float deltaTime)
{
  for (int i = 0; i < rigidbodies.size(); i++) {
    RigidBodyBase* rigid = rigidbodies.at(i);
    for (int f = 0; f < rigid->forces.size(); f++) {
      rigid->velocity = Vector3Add(rigid->velocity, rigid->forces.at(f));
    }
    rigid->forces.clear();
    rigid->position = Vector3Add(rigid->position, rigid->velocity);
    rigid->velocity.x = (rigid->velocity.x < phvarMovingThreshold && rigid->velocity.x > - phvarMovingThreshold) ? 0 :
rigid->velocity.x > 0 ? rigid->velocity.x - phvarFriction * deltaTime : rigid->velocity.x < 0 ? rigid->velocity.x + phvarFriction * deltaTime : rigid->velocity.x; rigid->velocity.y = (rigid->velocity.y < phvarMovingThreshold && rigid->velocity.y > -phvarMovingThreshold) ? 0 :
rigid->velocity.y > 0 ? rigid->velocity.y - phvarFriction * deltaTime : rigid->velocity.y < 0 ? rigid->velocity.y + phvarFriction * deltaTime : rigid->velocity.y; rigid->velocity.z = (rigid->velocity.z < phvarMovingThreshold && rigid->velocity.z > -phvarMovingThreshold) ? 0 :
rigid->velocity.z > 0 ? rigid->velocity.z - phvarFriction * deltaTime : rigid->velocity.z < 0 ? rigid->velocity.x + phvarFriction * deltaTime : rigid->velocity.z; } }

I quickly realized that this would be total garbage for a few reasons:

Voxelization

So then I thought that I could voxelize meshes at runtime and collide with them as BoundingBox instances. Seems good enough, so what are the issues with it?

How much code? It can't possibly be that bad... right?

bool qSubCollide(BoundingBox a, BoundingBox b) {
  __m128d aminx = _mm_set_pd(a.min.x, a.min.x),
    amaxx = _mm_set_pd(a.max.x, a.max.x),
    aminy = _mm_set_pd(a.min.y, a.min.y),
    amaxy = _mm_set_pd(a.max.y, a.max.y),
    aminz = _mm_set_pd(a.min.z, a.min.z),
    amaxz = _mm_set_pd(a.max.z, a.max.z);

  __m128d bminx = _mm_set_pd(b.min.x, b.min.x),
    bmaxx = _mm_set_pd(b.max.x, b.max.x),
    bminy = _mm_set_pd(b.min.y, b.min.y),
    bmaxy = _mm_set_pd(b.max.y, b.max.y),
    bminz = _mm_set_pd(b.min.z, b.min.z),
    bmaxz = _mm_set_pd(b.max.z, b.max.z);

  bool b1 = _mm_cmple_pd(aminx, bmaxx).m128d_f64[1] != 0;
  bool b2 = _mm_cmpge_pd(amaxx, bminx).m128d_f64[1] != 0;
  bool b3 = _mm_cmple_pd(aminy, bmaxy).m128d_f64[1] != 0;
  bool b4 = _mm_cmpge_pd(amaxy, bminy).m128d_f64[1] != 0;
  bool b5 = _mm_cmple_pd(aminz, bmaxz).m128d_f64[1] != 0;
  bool b6 = _mm_cmpge_pd(amaxz, bminz).m128d_f64[1] != 0;
  return (b1 && b2 && b3 && b4 && b5 && b6);
}
bool qSubCollide_double (BoundingBox a, BoundingBox b, BoundingBox c) {
  __m128d aminx = _mm_set_pd(a.min.x, a.min.x),
      amaxx = _mm_set_pd(a.max.x, a.max.x),
      aminy = _mm_set_pd(a.min.y, a.min.y),
      amaxy = _mm_set_pd(a.max.y, a.max.y),
      aminz = _mm_set_pd(a.min.z, a.min.z),
      amaxz = _mm_set_pd(a.max.z, a.max.z);

  __m128d bminx = _mm_set_pd(b.min.x, c.min.x),
      bmaxx = _mm_set_pd(b.max.x, c.max.x),
      bminy = _mm_set_pd(b.min.y, c.min.y),
      bmaxy = _mm_set_pd(b.max.y, c.max.y),
      bminz = _mm_set_pd(b.min.z, c.min.z),
      bmaxz = _mm_set_pd(b.max.z, c.max.z);

  bool b1 = _mm_cmple_pd(aminx, bmaxx).m128d_f64[1] != 0;
  bool b2 = _mm_cmpge_pd(amaxx, bminx).m128d_f64[1] != 0;
  bool b3 = _mm_cmple_pd(aminy, bmaxy).m128d_f64[1] != 0;
  bool b4 = _mm_cmpge_pd(amaxy, bminy).m128d_f64[1] != 0;
  bool b5 = _mm_cmple_pd(aminz, bmaxz).m128d_f64[1] != 0;
  bool b6 = _mm_cmpge_pd(amaxz, bminz).m128d_f64[1] != 0;
  return (b1 && b2 && b3 && b4 && b5 && b6);
}

bool qSubCollide_quad(BoundingBox a, BoundingBox b, BoundingBox c, BoundingBox d, BoundingBox e) {
  __m128 aminx = _mm_set_ps(a.min.x, a.min.x, a.min.x, a.min.x),
       amaxx = _mm_set_ps(a.max.x, a.max.x, a.max.x, a.max.x),
       aminy = _mm_set_ps(a.min.y, a.min.y, a.min.y, a.min.y),
       amaxy = _mm_set_ps(a.max.y, a.max.y, a.max.y, a.max.y),
       aminz = _mm_set_ps(a.min.z, a.min.z, a.min.z, a.min.z),
       amaxz = _mm_set_ps(a.max.z, a.max.z, a.max.z, a.max.z);

  __m128 bminx = _mm_set_ps(b.min.x, c.min.x, d.min.x, e.min.x),
    bmaxx = _mm_set_ps(b.max.x, c.max.x, d.max.x, e.max.x),
    bminy = _mm_set_ps(b.min.y, c.min.y, d.min.y, e.min.y),
    bmaxy = _mm_set_ps(b.max.y, c.max.y, d.max.y, e.max.y),
    bminz = _mm_set_ps(b.min.z, c.min.z, d.min.z, e.min.z),
    bmaxz = _mm_set_ps(b.max.z, c.max.z, d.max.z, e.max.z);

  bool b1 = _mm_cmple_ps(aminx, bmaxx).m128_u64[1] != 0;
  bool b2 = _mm_cmpge_ps(amaxx, bminx).m128_u64[1] != 0;
  bool b3 = _mm_cmple_ps(aminy, bmaxy).m128_u64[1] != 0;
  bool b4 = _mm_cmpge_ps(amaxy, bminy).m128_u64[1] != 0;
  bool b5 = _mm_cmple_ps(aminz, bmaxz).m128_u64[1] != 0;
  bool b6 = _mm_cmpge_ps(amaxz, bminz).m128_u64[1] != 0;
  return (b1 && b2 && b3 && b4 && b5 && b6);
}


void SubThreadCollide(std::vector<BoundingBox> mapsect, BoundingBox collider, bool* colliding) {
  for (int i = 0; i < mapsect.size()-2; i+=2) {
#ifndef _MSC_VER
    if (CheckCollisionBoxes(mapsect.at(i), collider)) {
      *colliding = true;
      return;
    }
#else
    
    if (qSubCollide_double(collider, mapsect.at(i), mapsect.at(i+1))) {
      // std::cout << "Collided_subthread\n";
      *colliding = true;
      return;
    }
#endif

  }
  if(mapsect.size() %2 != 0){
#ifndef _MSC_VER
    if (CheckCollisionBoxes(mapsect.at(mapsect.size()-1), collider)) {
      *colliding = true;
      return;
    }
#else
    if (qSubCollide(mapsect.at(mapsect.size() - 1), collider)) {
      *colliding = true;
      return;
    }
#endif
  }
}

#define _LEVY_ACCURATE_VOXELIZATION_ALGORITHM

std::vector<std::vector<BoundingBox>> Voxelize(Mesh sbd, Matrix transform, float definition, std::vector<Vector3> directions, Vector3 position, float threshold){
  std::vector<std::vector<BoundingBox>> voxelSubVectors;
  std::vector<BoundingBox> voxels;
#ifdef _LEVY_ACCURATE_VOXELIZATION_ALGORITHM

  for (float z = -10; z < 10; z += definition) {
    for (float y = 0; y < 5; y += definition) {
      for (float x = -10; x < 10; x += definition) {
        for (int c = 0; c < directions.size(); c++) {
          Ray r;
          r.position = { x,y,z };
          r.direction = Vector3Multiply({ definition, definition, definition }, directions.at(c));
          RayCollision rc = GetRayCollisionMesh(r, sbd, transform);
          if (rc.hit) {
            if (rc.distance <= definition) {
              BoundingBox b;
              b.min = { x,y,z };
              b.max = Vector3Add(b.min, { definition, definition, definition });
              b.min = Vector3Add(b.min, position);
              b.max = Vector3Add(b.max, position);
              if (b.min.x > b.max.x) std::swap(b.min.x, b.max.x);
              if (b.min.y > b.max.y) std::swap(b.min.y, b.max.y);
              if (b.min.z > b.max.z) std::swap(b.min.z, b.max.z);
              voxels.push_back(b);
              if (voxels.size() > threshold) {
                voxelSubVectors.push_back(voxels);
                voxels.clear();
              }

              // DrawBoundingBox(b, YELLOW);
            }
          }
        }
      }
    }
  }
#else

  for (float x = -10; x < 10; x++) {
    for (float z = -10; z < 10; z++) {
      {
        Ray r;
        r.position = { x,5,z };
        r.direction = Vector3Multiply({ definition, definition, definition }, { 0, -1, 0 });
        RayCollision rc = GetRayCollisionMesh(r, sbd, transform);
        if (rc.hit) {
          BoundingBox b;
          b.min = rc.point;
          b.max = Vector3Add(b.min, { definition, definition, definition });
          b.min = Vector3Add(b.min, position);
          b.max = Vector3Add(b.max, position);
          voxels.push_back(b);
          if (voxels.size() > threshold) {
            voxelSubVectors.push_back(voxels);
            voxels.clear();
          }

          // DrawBoundingBox(b, YELLOW);
        }
      }
      {
        Ray r;
        r.position = { x,-1,z };
        r.direction = Vector3Multiply({ definition, definition, definition }, { 0, 1, 0 });
        RayCollision rc = GetRayCollisionMesh(r, sbd, transform);
        if (rc.hit) {
          BoundingBox b;
          b.min = rc.point;
          b.max = Vector3Add(b.min, { definition, definition, definition });
          b.min = Vector3Add(b.min, position);
          b.max = Vector3Add(b.max, position);
          voxels.push_back(b);
          if (voxels.size() > threshold) {
            voxelSubVectors.push_back(voxels);
            voxels.clear();
          }

          // DrawBoundingBox(b, YELLOW);
        }
      }
    }
  }
  for (float x = -10; x < 10; x++) {
    for (float y = 0; y < 5; y++) {
      {
        Ray r;
        r.position = { x,y,-10 };
        r.direction = Vector3Multiply({ definition, definition, definition }, { 0, 0, 1 });
        RayCollision rc = GetRayCollisionMesh(r, sbd, transform);
        if (rc.hit) {
          BoundingBox b;
          b.min = rc.point;
          b.max = Vector3Add(b.min, { definition, definition, definition });
          b.min = Vector3Add(b.min, position);
          b.max = Vector3Add(b.max, position);
          voxels.push_back(b);
          if (voxels.size() > threshold) {
            voxelSubVectors.push_back(voxels);
            voxels.clear();
          }

          // DrawBoundingBox(b, YELLOW);
        }
      }
      {
        Ray r;
        r.position = { x,y,10 };
        r.direction = Vector3Multiply({ definition, definition, definition }, { 0, 0, -1 });
        RayCollision rc = GetRayCollisionMesh(r, sbd, transform);
        if (rc.hit) {
          BoundingBox b;
          b.min = rc.point;
          b.max = Vector3Add(b.min, { definition, definition, definition });
          b.min = Vector3Add(b.min, position);
          b.max = Vector3Add(b.max, position);
          voxels.push_back(b);
          if (voxels.size() > threshold) {
            voxelSubVectors.push_back(voxels);
            voxels.clear();
          }

          // DrawBoundingBox(b, YELLOW);
        }
      }
    }
  }
  for (float z = -10; z < 10; z++) {
    for (float y = 0; y < 5; y++) {
      {
        Ray r;
        r.position = { -10,y,z };
        r.direction = Vector3Multiply({ definition, definition, definition }, { 1, 0, 0 });
        RayCollision rc = GetRayCollisionMesh(r, sbd, transform);
        if (rc.hit) {
          BoundingBox b;
          b.min = rc.point;
          b.max = Vector3Add(b.min, { definition, definition, definition });
          b.min = Vector3Add(b.min, position);
          b.max = Vector3Add(b.max, position);
          voxels.push_back(b);
          if (voxels.size() > threshold) {
            voxelSubVectors.push_back(voxels);
            voxels.clear();
          }

          // DrawBoundingBox(b, YELLOW);
        }
      }
      {
        Ray r;
        r.position = { 10,y,z };
        r.direction = Vector3Multiply({ definition, definition, definition }, { -1, 0, 0 });
        RayCollision rc = GetRayCollisionMesh(r, sbd, transform);
        if (rc.hit) {
          BoundingBox b;
          b.min = rc.point;
          b.max = Vector3Add(b.min, { definition, definition, definition });
          b.min = Vector3Add(b.min, position);
          b.max = Vector3Add(b.max, position);
          voxels.push_back(b);
          if (voxels.size() > threshold) {
            voxelSubVectors.push_back(voxels);
            voxels.clear();
          }

          // DrawBoundingBox(b, YELLOW);
        }
      }
    }
  }
  
  

#endif // _LEVY_ACCURATE_VOXELIZATION_ALGORITHM
  voxelSubVectors.push_back(voxels);
  voxels.clear();
  return voxelSubVectors;
}


void Levy::PhysicsEngine::_CollidesThread(Levy::PhysicsEngine*phen, Levy::StandardMapBlockDefinition smbd, BoundingBox collider, bool* collides, int cachenum, BoundingBox blockCollider) {
  // std::cout << "block " << i << "!\n";
  // BoundingBox blockCollider;
  // blockCollider.min = Vector3Add(blocks.at(i).position, { -10, -2.5, -10 });
  // blockCollider.max = Vector3Add(blocks.at(i).position, { 10, 2.5, 10 });
  StandardBlockDefinition sbd = phen->blockset.at(smbd.id);
  // std::cout << sbd.transform.m0;
  // blockCollider = GetModelBoundingBox(sbd);
  // blockCollider.min = Vector3Add(blockCollider.min, smbd.position);
  // blockCollider.max = Vector3Add(blockCollider.max, smbd.position);
  // DrawBoundingBox(blockCollider, RED);
  BoundingBox largeCollider = collider;
  // largeCollider.min = Vector3Multiply(largeCollider.min, { 0.1,0.1,0.1 });
  // largeCollider.max = Vector3Multiply(largeCollider.min, { 3,3,3 });
  // DrawBoundingBox(collider, GREEN);
  // DrawBoundingBox(largeCollider, YELLOW);
  

  if (CheckCollisionBoxes(collider, blockCollider)) {
    // std::cout << "Collided_thread\n";
    std::vector<std::thread> subthreads;
    std::vector<std::vector<BoundingBox>> voxelSubVectors;
    if (phen->cache.find(cachenum) != phen->cache.end()) voxelSubVectors = phen->cache.at(cachenum);
    else{
      if (sbd.meshCount > 1) {

        for (int j = 0; j < sbd.meshCount; j++) {
          voxelSubVectors = Voxelize(sbd.meshes[j], sbd.transform, phen->phvarModelVoxelizationDefinition, phen->directions, smbd.position, phen->phvarModelVoxelizationSubthreadCreationThreshold);
        }
      }
      else {
        voxelSubVectors = Voxelize(*sbd.meshes, sbd.transform, phen->phvarModelVoxelizationDefinition, phen->directions, smbd.position, phen->phvarModelVoxelizationSubthreadCreationThreshold);
      }
      phen->cache.insert_or_assign(cachenum, voxelSubVectors);
    // last += voxels.size();
    }
    bool colliding = false;

    for (int i = 0; i < voxelSubVectors.size(); i++) {
      subthreads.push_back(std::thread(SubThreadCollide, voxelSubVectors.at(i), collider, &colliding));
      
    }
    for (int i = 0; i < subthreads.size(); i++) {
       if (subthreads.at(i).joinable()) subthreads.at(i).join();
    }

    if (colliding) *collides = true;

    /*for (int i = 0; i < voxels.size(); i++) {
      if (CheckCollisionBoxes(collider, voxels.at(i))) {
        *collides = true;
      }
    }*/
  }
}



bool Levy::PhysicsEngine::CollidesWithMap(BoundingBox collider)
{
  if (collider.min.x > collider.max.x) std::swap(collider.min.x, collider.max.x);
  if (collider.min.y > collider.max.y) std::swap(collider.min.y, collider.max.y);
  if (collider.min.z > collider.max.z) std::swap(collider.min.z, collider.max.z);
  // std::cout << "called!\n";
  bool collides = false;
  std::vector<std::thread> threads;
  // uint64_t last = 0;
  for (int i = 0; i < blocks.size(); i++) {
    
    BoundingBox blockCollider = GetModelBoundingBox(blockset.at(blocks.at(i).id));
    blockCollider.min = Vector3Add(blocks.at(i).position, blockCollider.min);
    blockCollider.max = Vector3Add(blocks.at(i).position, blockCollider.max);
    if (blockCollider.min.x > blockCollider.max.x) std::swap(blockCollider.min.x, blockCollider.max.x);
    if (blockCollider.min.y > blockCollider.max.y) std::swap(blockCollider.min.y, blockCollider.max.y);
    if (blockCollider.min.z > blockCollider.max.z) std::swap(blockCollider.min.z, blockCollider.max.z);
    // std::cout << "Comparing " << collider.min.x << " " << collider.min.y << " " << collider.min.z << " " << collider.max.x << " " << collider.max.y << " " << collider.max.z << " with "
    // 	<< blockCollider.min.x << " " << blockCollider.min.y << " " << blockCollider.min.z << " " << blockCollider.max.x << " " << blockCollider.max.y << " " << blockCollider.max.z << "\n";
    // DrawBoundingBox(blockCollider, RED);
#ifdef _LEVY_VOXALIZE
#ifndef _MSC_VER
    if(CheckCollisionBoxes(collider, blockCollider)) threads.push_back(std::thread(Levy::PhysicsEngine::_CollidesThread, this, blocks.at(i), collider, &collides, i, blockCollider));
    
#else
    __m128d aminx = _mm_set_pd(blockCollider.min.x, blockCollider.min.x),
          amaxx = _mm_set_pd(blockCollider.max.x, blockCollider.max.x),
          aminy = _mm_set_pd(blockCollider.min.y, blockCollider.min.y), 
          amaxy = _mm_set_pd(blockCollider.max.y, blockCollider.max.y), 
          aminz = _mm_set_pd(blockCollider.min.z, blockCollider.min.z), 
          amaxz = _mm_set_pd(blockCollider.max.z, blockCollider.max.z);

    __m128d bminx = _mm_set_pd(collider.min.x, collider.min.x),
          bmaxx = _mm_set_pd(collider.max.x, collider.max.x),
          bminy = _mm_set_pd(collider.min.y, collider.min.y),
          bmaxy = _mm_set_pd(collider.max.y, collider.max.y),
          bminz = _mm_set_pd(collider.min.z, collider.min.z),
          bmaxz = _mm_set_pd(collider.max.z, collider.max.z);

    bool b1 = _mm_cmple_pd(aminx, bmaxx).m128d_f64[1] != 0;
    bool b2 = _mm_cmpge_pd(amaxx, bminx).m128d_f64[1] != 0;
    bool b3 = _mm_cmple_pd(aminy, bmaxy).m128d_f64[1] != 0;
    bool b4 = _mm_cmpge_pd(amaxy, bminy).m128d_f64[1] != 0;
    bool b5 = _mm_cmple_pd(aminz, bmaxz).m128d_f64[1] != 0;
    bool b6 = _mm_cmpge_pd(amaxz, bminz).m128d_f64[1] != 0;
    
    if (b1 && b2 && b3 && b4 && b5 && b6) {
      // std::cout << "Collided\n";
      threads.push_back(std::thread(Levy::PhysicsEngine::_CollidesThread, this, blocks.at(i), collider, &collides, i, blockCollider));
    }
#endif
#else 
    Ray r1, r2, r3, r4, r5, r6, r7, r8;
    RayCollision rc1, rc2, rc3, rc4, rc5, rc6, rc7, rc8;
    r1.position = collider.max;
    r1.direction = Vector3Multiply({ -1,-1,-1 }, Vector3Normalize(Vector3Subtract(collider.max, collider.min)));
    
    r2.position = collider.max; r2.position.x = collider.min.x;
    r2.direction = Vector3Multiply({ 1,-1,-1 }, Vector3Normalize(Vector3Subtract(collider.max, collider.min)));

    r3.position = collider.max; r3.position.z = collider.min.z;
    r3.direction = Vector3Multiply({ -1,-1,1 }, Vector3Normalize(Vector3Subtract(collider.max, collider.min)));

    r4.position = collider.max; r4.position.x = collider.min.x; r4.position.z = collider.min.z;
    r4.direction = Vector3Multiply({ 1,-1,1 }, Vector3Normalize(Vector3Subtract(collider.max, collider.min)));



    rc1 = GetRayCollisionMesh(r1, *blockset.at(blocks.at(i).id).meshes, MatrixMultiply(blockset.at(blocks.at(i).id).transform, MatrixTranslate(blocks.at(i).position.x, blocks.at(i).position.y, blocks.at(i).position.z)));
    rc2 = GetRayCollisionMesh(r2, *blockset.at(blocks.at(i).id).meshes, MatrixMultiply(blockset.at(blocks.at(i).id).transform, MatrixTranslate(blocks.at(i).position.x, blocks.at(i).position.y, blocks.at(i).position.z)));
    rc3 = GetRayCollisionMesh(r3, *blockset.at(blocks.at(i).id).meshes, MatrixMultiply(blockset.at(blocks.at(i).id).transform, MatrixTranslate(blocks.at(i).position.x, blocks.at(i).position.y, blocks.at(i).position.z)));
    rc4 = GetRayCollisionMesh(r4, *blockset.at(blocks.at(i).id).meshes, MatrixMultiply(blockset.at(blocks.at(i).id).transform, MatrixTranslate(blocks.at(i).position.x, blocks.at(i).position.y, blocks.at(i).position.z)));
#ifdef LEVY_DEBUG_RENDER
    DrawRay(r1, RED);
    DrawRay(r2, RED);
    DrawRay(r3, RED);
    DrawRay(r4, RED);
#endif
    if (rc1.hit) {
      if (rc1.distance <= Vector3Distance(collider.min, collider.max) + 0.01) return true;
    }

    
    if (rc2.hit) {
      if (rc2.distance <= Vector3Distance(collider.min, collider.max) + 0.01) return true;
    }

    
    if (rc3.hit) {
      if (rc3.distance <= Vector3Distance(collider.min, collider.max) + 0.01) return true;
    }

    
    if (rc4.hit) {
      if (rc4.distance <= Vector3Distance(collider.min, collider.max) + 0.01) return true;
    }

#endif
  }
#ifdef _LEVY_VOXALIZE
  for (int i = 0; i < threads.size(); i++) {
    threads.at(i).join();
  }
#endif

  // dbgCollStats.totalVoxelsGenerated += last;
  // if (last > dbgCollStats.maximumVoxelsGenerated) dbgCollStats.maximumVoxelsGenerated = last;
  // dbgCollStats.lastVoxelsGenerated = last;
  return collides;
}

Levy::PhysicsEngine::~PhysicsEngine()
{
  
}

std::map<int, std::vector<std::vector<BoundingBox>>> Levy::PhysicsEngine::dbg_getVoxelCache()
{
  return cache;
}
    
  

Yeah... That's not optimal.

Final Solution

The final solution is actually hidden in the code above. In fact, the final solution IS what runs in the above code's current state. Voxelization is disabled through the use of preprocessor macros. Instead of subdividing the mesh as in voxelization, why not collide directly with the shape? After all, voxelization works by casting raylib Rays at the mesh to map out where the ray collides. Why not keep the same BoundingBox but instead cast rays from the corners of the bounding box? So that's what I did. If you remove voxel code from the Levy::PhysicsEngine::CollidesWithMap() function, it looks like this:

bool Levy::PhysicsEngine::CollidesWithMap(BoundingBox collider)
{
  if (collider.min.x > collider.max.x) std::swap(collider.min.x, collider.max.x);
  if (collider.min.y > collider.max.y) std::swap(collider.min.y, collider.max.y);
  if (collider.min.z > collider.max.z) std::swap(collider.min.z, collider.max.z);
  bool collides = false;
  for (int i = 0; i < blocks.size(); i++) {
    
    BoundingBox blockCollider = GetModelBoundingBox(blockset.at(blocks.at(i).id));
    blockCollider.min = Vector3Add(blocks.at(i).position, blockCollider.min);
    blockCollider.max = Vector3Add(blocks.at(i).position, blockCollider.max);
    if (blockCollider.min.x > blockCollider.max.x) std::swap(blockCollider.min.x, blockCollider.max.x);
    if (blockCollider.min.y > blockCollider.max.y) std::swap(blockCollider.min.y, blockCollider.max.y);
    if (blockCollider.min.z > blockCollider.max.z) std::swap(blockCollider.min.z, blockCollider.max.z);

    Ray r1, r2, r3, r4, r5, r6, r7, r8;
    RayCollision rc1, rc2, rc3, rc4, rc5, rc6, rc7, rc8;
    r1.position = collider.max;
    r1.direction = Vector3Multiply({ -1,-1,-1 }, Vector3Normalize(Vector3Subtract(collider.max, collider.min)));
    
    r2.position = collider.max; r2.position.x = collider.min.x;
    r2.direction = Vector3Multiply({ 1,-1,-1 }, Vector3Normalize(Vector3Subtract(collider.max, collider.min)));

    r3.position = collider.max; r3.position.z = collider.min.z;
    r3.direction = Vector3Multiply({ -1,-1,1 }, Vector3Normalize(Vector3Subtract(collider.max, collider.min)));

    r4.position = collider.max; r4.position.x = collider.min.x; r4.position.z = collider.min.z;
    r4.direction = Vector3Multiply({ 1,-1,1 }, Vector3Normalize(Vector3Subtract(collider.max, collider.min)));



    rc1 = GetRayCollisionMesh(r1, *blockset.at(blocks.at(i).id).meshes, MatrixMultiply(blockset.at(blocks.at(i).id).transform, MatrixTranslate(blocks.at(i).position.x, blocks.at(i).position.y, blocks.at(i).position.z)));
    rc2 = GetRayCollisionMesh(r2, *blockset.at(blocks.at(i).id).meshes, MatrixMultiply(blockset.at(blocks.at(i).id).transform, MatrixTranslate(blocks.at(i).position.x, blocks.at(i).position.y, blocks.at(i).position.z)));
    rc3 = GetRayCollisionMesh(r3, *blockset.at(blocks.at(i).id).meshes, MatrixMultiply(blockset.at(blocks.at(i).id).transform, MatrixTranslate(blocks.at(i).position.x, blocks.at(i).position.y, blocks.at(i).position.z)));
    rc4 = GetRayCollisionMesh(r4, *blockset.at(blocks.at(i).id).meshes, MatrixMultiply(blockset.at(blocks.at(i).id).transform, MatrixTranslate(blocks.at(i).position.x, blocks.at(i).position.y, blocks.at(i).position.z)));
#ifdef LEVY_DEBUG_RENDER
    DrawRay(r1, RED);
    DrawRay(r2, RED);
    DrawRay(r3, RED);
    DrawRay(r4, RED);
#endif
    if (rc1.hit) {
      if (rc1.distance <= Vector3Distance(collider.min, collider.max) + 0.01) return true;
    }

    
    if (rc2.hit) {
      if (rc2.distance <= Vector3Distance(collider.min, collider.max) + 0.01) return true;
    }

    
    if (rc3.hit) {
      if (rc3.distance <= Vector3Distance(collider.min, collider.max) + 0.01) return true;
    }

    
    if (rc4.hit) {
      if (rc4.distance <= Vector3Distance(collider.min, collider.max) + 0.01) return true;
    }

  return collides;
}

  

Why it still sucks

It doesn't account for collision responses

In Levy, collision responses are left as an exercise for the one using the library. This makes Levy more of a collision engine than a physics engine. Which in itself is okay, some people might need just that, it's just that it makes it less ideal for more physics intensive games or applications.

It has quite a heavy dependency

raylib is fantastical. It's majestic. It's the best f*cking thing to be invented after sliced bread. However, it is an entire game framework. Sure, you could rewrite the structs and functions from raylib into Levy to make it standalone, but that would come at the cost of more of my time that I don't have nor want to spend on this f*cking thing, and also compatibility with raylib.

How in the sweet loving Cinnamon Toast f*ck do you server-side that.

In multiplayer titles with a client/server, it's useful to have a physics simulation running both on the server and on the client. This is framerate-dependent, has no sense of collisions, has no sense of movement, has no sense of time, has no sense of forces, has no sense of what should be possible or not, has no sense of anything really. This is not at all built for competitive multiplayer, trust me, I've tried. This is certified garbage when it comes to networked games and validation of player movement. Please don't try to build multiplayer games with collision engines instead of physics engines. Okay. Thanks. Bye.


< back to ~safariminer
< back to tilde.town