Posts
Search
Contact
Cookies
About
RSS

raylib parallax effect using 3D

Added 4 Oct 2020, 1:30 a.m. edited 18 Jun 2023, 1:12 a.m.

The simple way to do some kind of parallax scrolling background, is simply to render in 2D a number of textures (scrolling at different speeds) usually with transparent (alpha) areas.

However mix in a more complex scenario with particles and its time to leverage the power of the GPU to sort things by depth.

This means that that you can just throw things at the screen in any old order. The trick here is to render the layers as 3D objects at different distances, a shader is required to discard any transparent areas, and as a shader is in use that might as well do the scrolling as well...

Lets look at the shader:

#version 330

// Input vertex attributes (from vertex shader)
in vec2 fragTexCoord;
in vec4 fragColor;

// Input uniform values
uniform sampler2D texture0;
uniform vec4 colDiffuse;
uniform float xoffset;

// Output fragment color
out vec4 finalColor;

void main()
{
    vec4 texelColor = texture(texture0, vec2(fragTexCoord.x + xoffset, fragTexCoord.y));
    if (texelColor.a == 0.0) discard;
    finalColor = texelColor * fragColor * colDiffuse;
}

Handily raylib allows us to specify just a fragment shader and it automagically adds in a default vertex shader, obviously this does mean we need to use specific inputs and uniform names to fit in with the default vertex shader.

In the main body of the shader we first take the texture coordinate supplied by the vertex shader, and add an offset to it (this handles the scrolling).

The alpha (transparency) of the selected texture fragment is checked, if the pixel is fully see through then we discard the fragment, crucially because nothing is rendered when a discard is executed, it means the Z buffer is not written to.

Finally we mix in the different colour values (one from the tint given by DrawModel and the other from the vertex colour buffer)

We do need a simple flat 3D plane to hang our textures on and give something to use the discard/scrolling shader. This is created programmatically.

    Mesh plane = GenMeshPlane(2, 1,1,1);
    Model quad = LoadModelFromMesh(plane);
    quad.transform = MatrixRotateX(-90*DEG2RAD);

As the plane mesh is created as a horizontal plane we need to rotate it, both so its upright but also facing the camera (or it will be back face culled). In orthographic mode this mesh exactly fills the entire screen, a smaller square quad (rectangle) is also in use for the sprite that's moving through the layers.

While talking about orthographic projection its worth noting that you have a new 2D coordinate system, the centre of the screen is at coordinates 0, 0 with the Y axis decreasing as you go down the screen, with the maximum dimensions from negative to positive being 2 units across and 1 down.

Lets look finally at actually drawing our layers and sprites.

            BeginMode3D(camera);

                DrawModel(smallquad, (Vector3){-.25,-0.25,cos(frame/10.0f)*2.5-2.25},1.0,WHITE);
                
                for (int i=0; i<5; i++) {
                    offsets[i] += 0.001/((i+1)*(i+1));
                    SetShaderValue(alphaDiscard, offsetLoc, &offsets[i], UNIFORM_FLOAT);
                    quad.materials[0].maps[MAP_DIFFUSE].texture = l[i];
                    DrawModel(quad,  (Vector3){0,0,-i},1.0,WHITE);
                }
                
                DrawModel(smallquad, (Vector3){0.25,-0.25,sin(frame/10.0f)*2.5-2.25},1.0,WHITE);
                
            EndMode3D();

Just to show it doesn't matter what order things are drawn in I have drawn one sprite before all the layers are drawn and one after. In the layers loop I update the offset for each layer, the increment decreasing depending how far back the layer is. Notice we don't even need a model per layer, the same model has its texture changed on the fly and the layers texture offset is set in the shader immediately before the layer is drawn.

Finally the quad is rendered at the centre of the screen but crucially the Z coordinate is set to the layer number.

And that's all there is to it, you basically get the sorting for free in terms of performance, as don't forget rendering 2D is really rendering 3D with a fixed Z coordinate, and in any case 3D is really just 2D with perspective - we're basically doing a very similar render operation as far as the GPU is concerned.

here's the entire code

/*
 * Copyright (c) 2019 Chris Camacho (codifies -  http://bedroomcoders.co.uk/)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */

#include <stddef.h>
#include <stdio.h>

#include "raylib.h"
#include "raymath.h"

#define screenWidth 1280
#define screenHeight 720


int main(void)
{
    // Initialization
    //--------------------------------------------------------------------------------------
    InitWindow(screenWidth, screenHeight, "raylib - test");

    // Define the camera to look into our 2/3d world
    Camera camera = { 0 };
    camera.position = (Vector3){ 0.0f, 0.0f, 1.0f };
    camera.target = (Vector3){ 0.0f, 0.0f, 0.0f };
    camera.up = (Vector3){ 0.0f, 1.0f, 0.0f };
    
    // load 5 layers of parallax textures
    Texture l[5];
    l[0] = LoadTexture("data/parallax-mountain-foreground-trees.png");
    l[1] = LoadTexture("data/parallax-mountain-trees.png");
    l[2] = LoadTexture("data/parallax-mountain-mountains.png");
    l[3] = LoadTexture("data/parallax-mountain-montain-far.png");
    l[4] = LoadTexture("data/parallax-mountain-bg.png");
    
    for (int i=0; i<5; i++) {
        SetTextureWrap(l[i], WRAP_REPEAT);
    }
    
    Mesh plane = GenMeshPlane(2, 1,1,1);
    Model quad = LoadModelFromMesh(plane);
    quad.transform = MatrixRotateX(-90*DEG2RAD);

    
    Texture tex = LoadTexture("data/test.png");

    Mesh plane2 = GenMeshPlane(.25, .25,1,1);
    Model smallquad = LoadModelFromMesh(plane2);
    smallquad.transform = MatrixRotateX(-90*DEG2RAD);
    smallquad.materials[0].maps[MAP_DIFFUSE].texture = tex;
    
    Shader alphaDiscard = LoadShader(NULL, "data/alphaDiscard.fs");
    quad.materials[0].shader = alphaDiscard;
    int offsetLoc = GetShaderLocation(alphaDiscard, "xoffset");
    float offsets[5]={0};

    // frame counter
    int frame = 0;

    SetTargetFPS(60);               // Set  to run at 60 frames-per-second
    //--------------------------------------------------------------------------------------

    // Main game loop
    while (!WindowShouldClose())    // Detect window close button or ESC key
    {
        // Update
        //----------------------------------------------------------------------------------

        frame ++;

        UpdateCamera(&camera);
        
        if (IsKeyDown(KEY_SPACE)) {
            camera.fovy = 45;
            camera.type = CAMERA_PERSPECTIVE;
        } else {
            camera.fovy = (screenWidth / screenHeight);
            camera.type = CAMERA_ORTHOGRAPHIC;            
        }

        // Draw
        //----------------------------------------------------------------------------------
        BeginDrawing();

            ClearBackground(WHITE);

            BeginMode3D(camera);

                DrawModel(smallquad, (Vector3){-.25,-0.25,cos(frame/10.0f)*2.5-2.25},1.0,WHITE);
                
                for (int i=0; i<5; i++) {
                    offsets[i] += 0.001/((i+1)*(i+1));
                    SetShaderValue(alphaDiscard, offsetLoc, &offsets[i], UNIFORM_FLOAT);
                    quad.materials[0].maps[MAP_DIFFUSE].texture = l[i];
                    DrawModel(quad,  (Vector3){0,0,-i},1.0,WHITE);
                }
                
                DrawModel(smallquad, (Vector3){0.25,-0.25,sin(frame/10.0f)*2.5-2.25},1.0,WHITE);
                
            EndMode3D();

            DrawFPS(10, 10);

            int l = MeasureText(FormatText("Frame %i", frame), 20);
            DrawRectangle(16, 698, l+8, 42, BLUE);
            DrawText(FormatText("Frame %i", frame), 20, 700, 20, WHITE);

        EndDrawing();
        //----------------------------------------------------------------------------------
    }

    // De-Initialization
    //--------------------------------------------------------------------------------------

    for (int i=0; i<5; i++) {
        UnloadTexture(l[i]);
    }

    UnloadModel(quad);
    UnloadModel(smallquad);

    CloseWindow();        // Close window and OpenGL context
    //--------------------------------------------------------------------------------------

    return 0;
}