Oxygen Engine
Modern C++ 3D Engine using OpenGL
Loading...
Searching...
No Matches
Surface Shaders

Introduction

Readers are assumed to have at least read the chapter about shaders (to grasp general informations about how to use shaders in Oxygen Engine)

A minimal knowledge about the GLSL language is recommended

While you can use oe::render::Shader to write and generate shaders in a more standard way (Vertex + Fragment shaders), it can become complex as you need to duplicate the shaders for each new pass type

In this chapter, we will use a simpler tool available in the engine 🙂

Here, we will explain what are surface shaders and then how they are used to customize a material

How it works

The surface shaders can be seen as simple fragment shaders where it is only required to implement one function to "describe" how the surface should react to light (by setting physical properties) depending on the input properties

Then, vertex and fragment shaders will be generated from this function

The entry point prototype will be this one:

void surface(in SurfaceInput surface_input, inout SurfaceOutput result);

It can be the only function in the content of the shader and might be empty.
Of course, you are free to declare/call any additional functions if you want

Note
In surface shaders, the main() function should not be implemented (you will get an error if it is present)

Inputs

You can use uniforms like the standard shaders, see the shaders chapter about those available

The SurfaceInput provides the following inputs:

Texture coordinates

The coordinates are already rotated and tiled based on material data

Type Name Description
vec2 tex_coords_0 First set of coordinates of the surface
vec2 tex_coords_1 Second set of texture coordinates of the surface
vec3 color Vertex color

More inputs to come

  • Screen space position
  • ... ?

Outputs

Colors

Type Name Default Description
vec4 albedo white Base color + alpha value
vec3 emissive dark Light emitted by the surface

Physical properties

Type Name Default Description
float roughness 1.0f Surface roughness between 0.0f (reflective) and 1.0f (fully rough)
float metalness 0.0f Surface metalness between 0.0f (dielectric) and 1.0f (metallic)
float ao 1.0f Ambient occlusion of the surface between 0.0f and 1.0f

Additional properties for transparent surfaces (Forward rendering)

Type Name Default Description
float ior 1.5f Index of refraction of the surface
float transmission 1.0f Percentage of light transmitted through the surface between 0.0f (opaque) and 1.0f (transparent)
float thickness 0.0f Thickness of tghe surface between 0.0f (thin) and infinity (thick glass)

Miscellaneous

Type Name Default Description
vec3 normal {0.0f, 0.0f, 1.0f} Perturbated normal direction of the surface
bool use_pbr true Physically Based Rendering mode

About use_pbr :

  • by setting to false, PBR will be disabled.
  • If disabled, the final color will be taken from the albedo output
  • Switching this value based on fragment data will let you to mix realistic and non-realistic rendering (like Anime, Sketch, etc...)

Helpers

  • OE_UNPACK_NORMAL_MAP(sampler2D normal_map, vec2 textures_coords)
    • Read a perturbed normal from a normal_map at textures_coords coordinates
  • OE_CONVERT_SRGB_TO_LINEAR(vec3 color)
    • Convert an sRGB color into linear
    • Must be used when reading color from textures acting as color maps (albedo / emissive)

How to apply on a material

To apply the shader on a material it is exactly the same as a standard shader, the only change is to use the oe::render::SurfaceShader class

oe::scene::Material& material = some_node.getMaterial();
auto shader = std::make_shared<oe::render::SurfaceShader>("Shader content here");
material.shader = shader;
Render agnostic material.
Definition material.h:90
oe::render::ShaderBase * shader
Shader to use to render this material.
Definition material.h:114

You may have noticed that the surface shaders does not not need compilation

This is due to the fact that many shaders might be getting generated depending of the pass and vertex type

At runtime, only the needed ones are compiled at the first required bind

Examples

Default values

This is the minimal content of a working (but not interesting) surface shader using all defaults values

void surface(in SurfaceInput surface_input, inout SurfaceOutput result)
{}

Usage in application:

oe::scene::Material& material = some_node.getMaterial();
auto shader = std::make_shared<oe::render::SurfaceShader>("Shader code here");
material.shader = shader;

This default shader will generate the following material:

  • White
  • Flat (no perturbed normals)
  • Without emitting lights
  • Dielectric
  • Rough

Textured example

This sample will demonstrate how to fill surface data using input data and textures

Shader code:

uniform sampler2D uTexture1;
uniform sampler2D uTexture2;
uniform sampler2D uNormalMap;
void surface(in SurfaceInput surface_input, inout SurfaceOutput result)
{
result.albedo = texture(uTexture1, surface_input.tex_coords_0);
result.roughness = 0.1f;
result.metalness = texture(uTexture2, surface_input.tex_coords_0).r;
result.normal = OE_UNPACK_NORMAL_MAP(uNormalMap, surface_input.tex_coords_0);
}

Usage in application:

oe::scene::Material& material = some_node.getMaterial();
auto shader = std::make_shared<oe::render::SurfaceShader>("Shader code here");
material.shader = shader;
material.setTexture("uTexture1", ...);
material.setTexture("uTexture2", ...);
material.setTexture("uNormalMap", ...);

This sample shader will take 3 uniforms textures and will generate the following material:

  • The first texture will be used as base surface (albedo) color
  • The second one will be used to drive the surface's metalness
  • The last one will be used as a normal map to give a realistic bumpy look
  • An hardcoded value will be set to the roughness