In today’s article we will speak about an experiment I wanted to lead to make plateformer more interesting in VR. It started from a simple interrogation “2D Plateformer are cool, but can it be cooler to play them in an immersive world ?”.
Is there a way to get full profit of the 360 environment ? We could just perfom some plane scrolling, it could be interesting but in VR it doesn’t feel like the experience would be improved that much over a flat screen. So how could we still keep the 2D idea of the game yet use the 3D aspect of VR, what if we could … wrap the world around us !

This is what we will explore in this article. Let’s get right into it !

Methods

Let’s talk about the different ways we could approach this, right now I can think of 3.

1. Make curved assets

So the first option would be to make your assets already curved, it can be done in blender when you create your asset, however this idea doesn’t seem promising, the obvious drawbacks are :

  • It take a long time to make assets.
  • It will be hard to maintain, let’s suppose you want to change the curve of the world it will require too much effort to change every assets.
  • Now you need to create a special physics system for your curved world.
ewwww

2. Transform geometry when building

Another option would be to curve the asset with some script when it’s added to the project or to the scene, honestly it can be a good idea however you now have to implement your own physics system, editor system etc … It can be really hard to manage, so maybe let’s keep it for another experiment.

We probably can do simpler …

3. I summon you “Shader”

What about faking it ?!

I mean we’re developer, we fake a ton of things but can we in this case ? Yes we can ! And the solution is the ultimate tool at faking visual effects SHADER. This time thanks to shaders we’ll just fake the drawing of objects meaning that the object will exist in our 2D map but we will show it to the player as some circular world enveloping him.

This solution seems promising, firstly because it’s should not be that hard to make, but also because since we fake the drawing of our world we can still work with the basics physics of unity, additionally changing the wrapping parameters should be as easy as moving a slider.

Principle

So how can we bend our object using shader, my idea here is to wrap our 2D map onto a circle.

Let’s first make some assumption for our system:

  • Our world will be build on a plane defined by \(z\) = some constant, so it spans the \(xy\) axis.

  • The \(y\) coordinate will be left untouched, so we can write our coords as \((x, z)\).

  • We will wrap our map around a circle with center \((0, 0)\) and radius \(z\)

  • Our map \(x\) axis will be bound between \([-w, w]\), \(w\) being our map width. The map points with \(x = 0\) won’t be moved.

Theory

So from our assumptions let’s speak about the math behind it !

The idea is to look at our map from the top, let’s call it \(W\), doing so we can see that our world is a line along the \(x\) axis. It is away on the \(z\)-axis by a distance that we will denote \(z\). Here we wish to wrap our map around a circle centered at \((0, 0)\) with a radius \(z\).

ProblemConstruction

In our case our map is tangent to our circle. The goal is to wrap the world around the cirle so let’s first put some reference points. There is no math behind this, this is a design decision I made here so if you want to change it feel free to, you only will need to adapt your formulas to fit your design.

ExplainPointProjection

Here we need to find a function to perform such a transformation. We could find many, but let’s use the simplest one possible, a linear function. So in practice that means that if you take a point in between the start and the end of the world and measure its position as percentage of the line crossed, to create the matching point on the circle you would take the start and end of the perimeter and put the point at the previously computed percentage in between both ends.

ShowMapCircleRelationship

Since we decided to use a linear function it takes the following form

$$f(x) = ax$$

With \(f\) the function which transform the object position on the \(x\) axis to the angle theta of the point in the circular map.

$$f(x) = \theta$$

$$ax = \theta$$

Let’s determine \(a\), knowing we want to convert our initial space \([-w, w]\) to \([-\pi, \pi]\). From our design decision we know the following

$$f(0) = 0$$

$$f(w) = \pi$$

For 0 this is obvious \(a \cdot 0 = 0\) it will work for any \(a\), but let’s resolve for \(w\)

$$f(w) = \pi$$

$$a \cdot w = \pi$$

$$a = \frac{\pi}{w}$$

If we plug \(-w\) we notice that it lead to the same value of \(a\).

Now we can rewrite \(f\)

$$f(x) = \frac{\pi}{w} \cdot x \tag{1} = \theta(x)$$

So once we have the angle we can determine the position in the circle perimeter of our point using some trigonometry, first let’s start from the function to express the coordinates \((xc, yc)\) for a circle of radius \(r\) based on \(\theta\)

$$(x_c, y_c) = (sin(\theta), cos(\theta)) \cdot r \tag{2}$$

Injecting \((1)\) into \((2)\) we got:

$$(x_c, y_c) = (sin(\frac{\pi}{w} \cdot x), cos(\frac{\pi}{w} \cdot x)) \cdot r$$

Now we are able to map the original position \(x\) to the final one \((x_c, y_c)\), let’s write the code !

Application

So here let’s do the simplest map possible

2Dworld

Yeah this is bare !

Let’s write our shader, first the properties.

Properties
{
    _MainTex ("Texture", 2D) = "white" {}
    _Tint ("Tint", Color) = (1,1,1,1)
    _GameWidth ("Game width", Float) = 1
}

Now that we have the properties let’s setup the input data and output data of our pass and define the variable we might need.

// We define the constant pi
#define PI 3.14159265359

// The input of our vertex shader
struct appdata
{
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
};

// The data passed from our vertex shader to our fragment shader
struct v2f
{
    float2 uv : TEXCOORD0;
    float4 vertex : SV_POSITION;
};

// We fetch our properties
sampler2D _MainTex;
float4 _MainTex_ST;
float _GameWidth;
float4 _Tint;

It was quite simple, we just get the vertex and uv as inputs, we can modify them however we want and we pass them to the fragment shader.

Let’s first write the fragment shader since it will be simple

fixed4 frag (v2f i) : SV_Target
{
    // sample the texture
    fixed4 col = tex2D(_MainTex, i.uv) * _Tint;
    return col;
}

So basically we just get the color from our texture and mix it with our tint and apply it to the pixel.

So for our most interesting part, we can write our vertex pass ! We just need to write down the formula that we built previously.

v2f vert (appdata v)
{
    v2f o;

    // Compute the position of the vertex in the world space
    float4 worldSpaceVertex = mul(unity_ObjectToWorld, v.vertex);

    // Compute the relative position of the vertex in the game window
    float percent = worldSpaceVertex.x / (_GameWidth / 2);

    // Compute the matching angle associated to the vertex position
    float theta = percent * PI ;

    // Store the radius
    float radius = worldSpaceVertex.z;

    // Compute the circular coordinate
    float2 circularCoord = float2(sin(theta), cos(theta)) * radius;

    // Set the new vertex coordinate
    float4 newWorldVertex = float4(circularCoord.x, worldSpaceVertex.y, circularCoord.y, 1);

    // Project the vertex to the clip space
    o.vertex = mul(UNITY_MATRIX_VP, newWorldVertex);
    
    // Set the uv of the vertex
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);

    // Pass the data to the fragment shader
    return o;
}

And that should be it !

Wtf

Arf we could not call that bending, let’s look at the objects wireframes

WireframeWorld

It make sense we only change the positions of the vertex so if you only have that little vertex you cannot really bend it. Let’s add more details to our cube.

Ok now have our cube, let’s bend it !

Yes we’re g… good, are we ? Something strange is happening when I’m looking on the side

The objects disappear, that’s because Unity will cull objects when their bounding boxes don’t appear in screens, we did change the vertex positions but not the bounding boxes. Luckily it’s quite easy to do it, but this time we need to write some C# scripts.

public class BoundingBoxUpdater : MonoBehaviour
{
    // Start is called before the first frame update
    private Material mat;

    void Start()
    {
        // Check if the object got a renderer and a material, if not we exit
        var renderer = GetComponent<Renderer>();
        if (renderer == null)
            return;

        var mat = renderer.sharedMaterial;
        if (mat == null)
            return;

        // Check that the material is our wrap shader
        if (mat.shader.name != "Unlit/WrapShader")
            return;

        // Update the render box by making it huge
        renderer.bounds = new Bounds(newCenter, Vector3.one * 1000);
    }
}

Ok, Does it work now ?

Yes, this time we got it ! Just be careful about this solution to change the bounding box. Here we just make it huge so it can be drawn from anywhere but it have performance implications, my advice would be to properly compute bounding boxes based on the new edges positions of the target shape.

Ok last things let’s update this shader so it works in VR !

struct appdata
{
    ...
    UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct v2f
{
    ...
    UNITY_VERTEX_OUTPUT_STEREO
};

v2f vert (appdata v)
{
    v2f o;

    UNITY_SETUP_INSTANCE_ID(v); //Insert
    UNITY_INITIALIZE_OUTPUT(v2f, o); //Insert
    UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); //Insert

    ...
}

So for a little VR test, I setup a stupidly simple scene with a character than can move and jump

As we can see we can start from our 2D plateformer to a circular plateformer ! And the physics work as intended because in reality we’re still playing a 2D game.

Just let’s be careful since our objects can go out of screen we can still see them since our formula follow a sinusoidal pattern. However the objects will physically be out of the scene, to go further we could either implement some scrolling in our world, or reset the position of the character when we reach the limit, all this is up to your game !

Conclusion

The solution is quite satisfying however there are some concerns to take into account

  • First the solution have a computation cost since you will remap each vertex each frame, even though it should be bearable it still need to be taken into account and it should be properly benchmarked.

  • Objects can get deformed based on the circle / world map ratio so you need to careful set it up and thoroughly think about the way you design your assets, it might also be possible to pick a different projection formula to mitigate these effects.

  • You might need to write some tooling to help your designer work in this new environment.

However I think there are a ton of possibilites and I believe it can make VR game damn fun, I’d love to see some game using this principle in VR ! I might try to experiment more with it and maybe make something playable, tell me what you think about it.

Also here the source for anyone who desire to play with it here