Over the weekend we worked really hard on a submission for Ludum Dare 38, a 72 hour game jam where developers make new games from scratch while racing against the clock. Our end product is Gravity Architect, a puzzle game using gravity fields to manipulate particles into a goal.

You can vote for Gravity Architect on the the LDJam site here:
https://ldjam.com/events/ludum-dare/38/gravity-architect
Download and play it (Vive, Oculus, and Daydream) here:

Read on if you want to hear a technical breakdown of Gravity Architect’s dynamic gravity field engine!

It supports tens of millions of voxels all exerting gravitational force onto hundreds of particles (512 on PC, 128 on mobile) simulating at 90fps (60fps on mobile) on a single CPU core.

In Gravity Architect, you can place any amount of mass with massive amounts of detail into world. Every tiny piece of matter is summed up to produce a dynamic, warped gravity field.

Here’s a quick video showing a particularly complicated gravity field:

 

The gravity field tree was built with SculptrVR’s dynamic Sparse Voxel Octree (SVO) as a starting point. SculptrVR’s SVO typically only store 13 bytes of data in each voxel: 4 for RGBA, 1 for VoxelType, and 8 for a pointer to a block of 8 children. For Gravity Fields we added 7 more bytes bringing the total up to 20: 4 bytes for float Density and 3 for center of mass (one byte for each of X/Y/Z).

Most games with dynamic or planetary gravity have a small, finite number of gravity sources, and each one usually has limited range with discrete hand-off between gravity sources. This is because gravity calculations usually scale in complexity with the number of sources. To keep computation manageable, these games limit the number of gravity sources in the play-field.

In Gravity Architect, we use the stored density and center of mass in the octree to compute accurate local gravity independent of the total number of gravity sources!

The key thing enabling this is SculptrVR’s built-in “Filter” step when making changes to the octree. Every time a voxel is changed, that change is filtered up through all its parents. The built-in filter step only averages color (to make a sort of mipmap as you reduce detail), and decides based on number of on children whether to render a parent as solid or empty. For Gravity Architect we extended the filter function to average density and also do a density-weighted average of center of mass. So if a Child’s density is increased from 0 to 1, the parent’s density will increase by 0.125 (1/8th), and its center of mass is shifted toward that child in X/Y/Z.

Now that we have variable resolution of density and center of mass in the tree, we can cheaply compute an approximate local gravity! For each particle, we do a tree traversal, and only subdivide nodes near the particle. That way, a higher resolution of voxels is used near the particle, and a low resolution is used far away. Here’s some pseudocode for this traversal:

Vector ComputeLocalGravity(Vector ParticleLocation, float Precision) const
{
    //Start with gravity zeroed and accumulate from voxel sources.
    Vector TotalGravity = (0,0,0);

    //Begin tree traversal at the root node
    NodeStack NS = {TreeRoot};
    while (NodeStack.HasNodes())
    {
        TreeNode CurrentNode = NodeStack.AdvanceToNextNode();

        Vector ParticleToNodeVector = CurrentNode.GetLocationOffsetByCenterOfMass() - ParticleLocation;

        float GravityCoef = CurrentNode.GetVolume() / ParticleToNodeVector.LengthSquared();

        float NodeAngularArea = CurrentNode.GetWidthSquared() / ParticleToNodeVector.LengthSquared();

        //Compare the approximate angular area of the node to the desired Precision. Subdivide if the 
        //A) the node has children and can be subdivided and B) The node's angular area is bigger than desired
        bool ShouldSubdivide = NodeAngularArea > Precision && CurrentNode.HasChildren();

        if (!ShouldSubdivide)
        {
            //Accumulate this node's gravitational pull into the total
            TotalGravity += CurrentNode.GetDensity() * GravityCoef * ParticleToNodeVector.Normalize();
        }
        else
        {
            //Visit all 8 children of this node next
            NodeStack.PushChildren();
        }
    }

    return TotalGravity;
}

Not too bad, right? The value “Precision” is basically the angular area of a node below which, that node will not be subdivided. We tuned Precision so that gravity feels great, but computations are minimal and each particle visits only ~50 nodes to compute its gravity.
One last thing to note: Even though we only visit ~50 nodes to compute the local gravity, each particle visits a different set of nodes. Also, each and every voxel down to the tiniest spec across the world will still contribute to the local gravity calculation. This is because that tiny voxel has had an effect on both the Density and CenterOfMass of its parent nodes going back up the tree.
I hope you found this interesting!

Once again, you can vote for Gravity Architect on the the LDJam site here:
https://ldjam.com/events/ludum-dare/38/gravity-architect

Download and play it (Vive, Oculus, and Daydream only) here:
https://nathansculptrvr.itch.io/gravity-architect

Thanks for reading!

Share This