Skip to main content

Ready to market your stellar app?

Samsung Developer Program is your gateway to app monetization success.

Learn More

Samsung Developer Program

GVRShaderTemplate - Creating Custom Shaders

In addition to the built-in shaders, GearVRF also supports authoring of custom shaders. You can supply GLSL source for your own vertex and fragment shaders in a Java class that can be used to render a scene object. Using #ifdef declarations in the shader source, you can compile multiple variants from a single set of sources. This capability lets you generate different vertex and fragment shaders depending on what textures, uniforms and vertex attributes your scene object is using.

All custom shaders are derived from GVRShaderTemplate. Each custom shader is represented by a different Java class. GVRShaderTemplate controls generation of the real vertex and fragment shader sources from source code segments provided by the programmer. Source code snippets are presented as a set of named segments. All custom shaders must have at least a "VertexTemplate" and a "FragmentTemplate" segment. Additional segments can be provided for more complex shaders. Segments are set up in the constructor of the Java shader class.

In addition to the shader sources, the programmer must also supply a uniform descriptor designating which uniforms from the material are used by the shader. This is a string with the types and names of the uniforms. It is used internally to construct a block of uniform data to present to the shader.

Custom Shader Example

The "color shader" is the very simplest of shaders. It renders a mesh in a single color. No lighting or texturing is involved. The vertex shader multiplies the vertex position by the combined model, view and projection matrices to convert it from local mesh space to screen space. The fragment shader just returns a single color obtained from uniform value u_color.

Vertex Shader Segment Fragment Shader Segment
precision highp  float;
attribute vec3 a_position;
uniform mat4 u_mvp;
void main()
{
  gl_Position = u_mvp * vec4(a_position, 1.0);

}
precision mediump  float;
uniform vec4 u_color;
void main()
{
    gl_FragColor = u_color;
}

To implement the color shader as a Java class, we supply the shader sources and the uniform descriptor. In our case the shader uses a single uniform - a 4 float vector of colors. The uniform descriptor is "float4 u_color". All of the setup is done in the constructor.

public class ColorShader extends GVRShaderTemplate
{
    private static String fragTemplate = source for fragment shader ...
    private static String vtxTemplate = source for vertex shader ...
    public ColorShader(GVRContext gvrcontext)
    {
        super("float4 u_color", 300);
        setSegment("FragmentTemplate", fragTemplate);
        setSegment("VertexTemplate", vtxTemplate);
    }      
}

To use the custom shader in your application just call GVRRenderData.setShaderTemplate for each GVRRenderData to you want use the custom shader. If you do not specify a shader ID when creating a new GVRMaterial, the built-in texture shader is used (shader ID is GVRMaterial.GVRShaderType.Texture.ID). If you plan to use a custom shader, it is better to specify the shader ID explicitly as GVRMaterial.GVRShaderType.BeingGenerated.ID). This tells GearVRF that there is no shader associated with the material yet so it should not be rendered. Here is an example showing how to apply the color shader to a cube object.

GVRContext context = ...
GVRMaterial material = new GVRMaterial(context, GVRMaterial.GVRShaderType.BeingGenerated.ID);
GVRSceneObject sceneobj = new GVRCubeSceneObject(context, true, material);
material.setVec4("u_color", 1, 0, 0, 1);
sceneobj.getRenderData().setShaderTemplate(ColorShader.class);

Shader Variants

It is messy and inconvenient to provide a separate shader for every individual combination of mesh attributes, material uniforms and lights. GearVRF has shader templates to allow multiple similar shaders to be produced from the same source code. GearVRF looks for the pattern "#ifdef HAS_" in the shader source to indicate shader sections that are conditionally compiled. The name after "HAS_" is assumed to be the name of a vertex attribute, material uniform or texture sampler. The symbol will be defined if the name is actually used by a mesh or material, otherwise it will not.

To illustrate, consider the case of a textured scene object. It would be nice if we could make a shader which would take it's color from a texture if one is provided, otherwise it should take the color from a uniform in the material. Using GVRShaderTemplate we can produce both shaders from a single set of sources.

Vertex Shader Segment Fragment Shader Segment
precision highp  float;
attribute vec3 a_position;
attribute vec2 a_texcoord;
uniform mat4 u_mvp;
out vec2 diffuse_coord;
void main()
{
  gl_Position = u_mvp * vec4(a_position, 1);
  diffuse_coord = a_texcoord.xy;

}
precision mediump  float;
uniform vec4 diffuseColor;
uniform sampler2d diffuseTexture;
in vec2 diffuse_coord;
out vec4 fragColor;
void main()
{
#ifdef HAS_diffuseTexture
    fragColor = texture(diffuseTexture, diffuse_coord);
#else
    fragC olor = diffuseColor;
#endif
}

 public class TexturerShader extends GVRShaderTemplate
{
    private static String fragTemplate = source for fragment shader ...
    private static String vtxTemplate = source for vertex shader ...
    public TextureShader(GVRContext gvrcontext)
    {
        super("float4 diffuseColor", 300);
        setSegment("FragmentTemplate", fragTemplate);
        setSegment("VertexTemplate", vtxTemplate);
    }      
}

If the GVRMaterial being used with the TextureShader defined above has set a texture named "diffuseTexture", the symbol HAS_diffuseTexture will be defined when the shader template is compiled. This will produce a shader that takes its color from the texture sampler named diffuseTexture. Otherwise it will take the color from the material uniform named diffuseColor. Note that you can bind a null value as a texture and load the texture later. If the custom shader is expecting a texture, GearVRF will not render the scene object until the texture is available. Setting the texture to null will make GearVRF generate a shader that requires a texture. Not setting the texture at all in the material produces a shader which does not require a texture.

Light Shaders

GearVRF provides a way for your custom shader templates to use light sources defined externally. The symbol "@LIGHTSOURCES" in the fragment template will be replaced by the fragment shaders associated with the light sources in the scene. In this case HAS_LIGHTSOURCES is defined for both of the generated vertex and fragment shaders so they can compile conditionally for both the lit and non-lit scenarios.

Each type of light source is defined by a different Java class. Like a shader template, the light source class defines a uniform descriptor for the uniforms used by the light and a shader template for the code to compute the contribution of this light towards illumination. The shader template for a light defines a single function that computes a Radiance contribution for the light. It takes as input a Surface structure containing the material color information and a structure containing the light uniforms. The Radiance and Surface structures are defined by the shader author and their definitions must be the same for all light sources and custom shaders used together.

Below we show the definitions for the Surface and Radiance structures similar to those used by GVRPhongShader and the GearVRF light sources. The Radiance structure has the intensity of the light, it's direction and attenuation as calculated by the light shader. The Surface structure has the view space normal and the ambient, diffuse, specular and emissive colors calculated by the surface shader. The fragment shader combines these to produce the final color.

 

Let's look at the surface shader for the TextureShader defined previously. Instead of directly writing the output color, the shader must save it in the Surface structure to be used by the light sources. Since the TextureShader only uses the diffuseColor uniform, we will set the ambient, specular and emissive colors to black so they will not contribute to the final pixel color.

Surface Shader Directional Light Shader
Surface @ShaderName()
{
    Surface s = Surface(viewspaceNormal, vec4(0, 0, 0, 1),
                    diffuseColor, vec4(0, 0, 0, 1), vec4(0, 0, 0, 1));
#ifdef HAS_diffuseTexture
    s.diffuse = texture(diffuseTexture, diffuse_coord.xy);
#endif
    return s;
}
Radiance @LightType(Surface s, in Uniform@LightType data)
{
    vec4 L = u_view * vec4(data.world_direction.xyz, 0.0);
    return Radiance(data.ambient_intensity.xyz,
         data.diffuse_intensity.xyz,
         data.specular_intensity.xyz,
         normalize(-L.xyz), 1.0); 

The red symbols preceded by "@" are replaced by GearVRF with the shader class name and the light shader class name respectively. In this case @ShaderName becomes "TextureShader" and @LightType becomes "GVRDirectLight". GearVRF uses the uniform descriptor from the light to generate a structure definition for the light uniforms which is named after the light class - Uniform@LightType or UniformGVRDirectLight in this case).

GVRDirectLight Class UniformGVRDirectLight Structure

public class GVRDirectLight extends GVRLightBase
{
    private static String lightShader = light shader source ...
   
    public GVRDirectLight(GVRContext ctx)
   {
        super(ctx, null);
        uniformDescriptor = "float enabled, vec3 world_direction, "
            + "vec3 world_position, vec4 diffuse_intensity, "
            + "vec4 ambient_intensity, vec4 specular_intensity";         
         fragmentShaderSource = lightShader;
         setVec4("ambient_intensity",, 0.0f, 0.0f, 0.0f, 1.0f);
         setVec4("diffuse_intensity", 1.0f, 1.0f, 1.0f, 1.0f);
         setVec4("specular_intensity", 1.0f, 1.0f, 1.0f, 1.0f);
    }

struct UniformGVRDirectLight
{
     float enabled;
     vec3 world_direction;
     vec3 world_position;
     vec4 diffuse_intensity;
     vec4 ambient_intensity;
     vec4 specular_intensity;
};

Integrating Multiple Light Sources

To compute the final color resulting from illumination by an individual light source you must define a function called AddLight which takes the Surface and Radiance as input and produces an RGBA color as output. GearVRF will automatically generate a LightPixel function in your fragment shader which calls AddLight for each light source and computes the final pixel color. This results in a fragment shader template for the TextureShader that looks like:

precision highp float;
precision highp sampler2DArray;
out vec4 fragColor;
struct Radiance
{
   vec3 ambient_intensity;
   vec3 diffuse_intensity;
   vec3 specular_intensity;
   vec3 direction;
   float attenuation;
};
@FragmentSurface
@FragmentAddLight
@LIGHTSOURCES
void main()
{
 Surface s = @ShaderName();
#if defined(HAS_LIGHTSOURCES)
     vec4 color = LightPixel(s);
     color = clamp(color, vec4(0), vec4(1));
     fragColor = color;
#else
     fragColor = s.diffuse;
#end

The fragment shader first calls the surface shader to compute the Surface colors from the material uniforms and textures. If there are light sources in the scene, HAS_LIGHTSOURCES will be defined and the source for the LightPixel function will be included in the @LIGHTSOURCES section. Otherwise, the fragment color is set from the Surface diffuse color without involving light sources.

The template substitution in GVRShaderTemplate allows more than just two shader segments. You can have as many as you like to help organize your shader sources. Segments beginning with "Fragment" are substituted in the fragment shader and segments that start with "Vertex" go into the vertex shader. The TextureShader must define two additional segments to support the surface shader and the AddLight function. Segments names "FragmentTemplate" and "VertexTemplate" must be the first ones defined. Here is the new class definition for TextureShader:

public class TexturerShader extends GVRShaderTemplate
{
    private static String fragTemplate = source for fragment shader ...
    private static String vtxTemplate = source for vertex shader ...
    private static String addLight = source for AddLight function ...
    
private static String surfaceShader = source for surface shader ...

    public TextureShader(GVRContext gvrcontext)
    {
        super("float4 diffuseColor", 300);
        setSegment("FragmentTemplate", fragTemplate);
        setSegment("VertexTemplate", vtxTemplate);
        setSegment("FragmentAddLight", addLight);
        setSegment("FragmentSurface", surfaceShader);

    }      
}

 

  • Was this article helpful?