Color Palettes using shaders

  • How to setup the project?
  • How to transfer the shader?
  • How to select a palette for each object?

This is a small tutorial in which we learn how to transfer an existing shader from the Shadertoy library to PlayCanvas. Mastering this process opens up many possibilities for using open source shaders and effects in your projects.

This tutorial is for advanced users and assumes that you are already aware of how the PlayCanvas shader chunks system works. You can study this tutorial to learn more on how to begin with PlayCanvas and shaders.

Shadertoy color paletes


How to setup the project?

The shader that we will transfer today can be found here. It creates some nice cosine based color palettes, which are animated and can provide a nice background in 3D scenes.

It was authored by Inigo Quilez, owner of the website, and is available to use under the MIT license.

What is Shadertoy? is a cross-browser online community and tool for creating and sharing shaders through WebGL, used both for learning and teaching 3D computer graphics in a web browser. It is a great resource for getting inspiration for your own shaders and effects.

What is the MIT license?

The MIT License is a permissive free software license originating at the Massachusetts Institute of Technology (MIT). As a permissive license, it puts only very limited restriction on reuse and has, therefore, an excellent license compatibility. The MIT license permits reuse within proprietary software provided that all copies of the licensed software include a copy of the MIT License terms and the copyright notice.

For this project I used the default Model Viewer template which provides a nice and smooth camera to orbit around.

PlayCanvas new project

Create the project and setup the following:

  • Upload a simple 4x4 or 8x8 black texture.
  • Grab the dithering image used on the iChannel0 on the shadertoy page and upload it.
  • Remove the skybox, light and cube model.
  • Set the ambient light to complete white (255,255,255).
  • Create a simple Box model with scale: 2.5, 0.25, 0.1 and rotation 0, 90, 0
  • Create an effect-palette.js script and attach it to the Box model.

We won't create a shader asset as we will be passing the shader to the material directly as a string the script.

PlayCanvas blank project


How to transfer the shader?

We will create an effect script that will use a script attribute to select the desired palette to render.

Open the effect-palette.js script and add the following attributes on top:

EffectPalette.attributes.add('palette', {
    type: 'string',
    enum: [
        { 'Palette 1': 'pal( p.x, vec3(0.5,0.5,0.5),vec3(0.5,0.5,0.5),vec3(1.0,1.0,1.0),vec3(0.0,0.33,0.67) );' },
        { 'Palette 2': 'pal( p.x, vec3(0.5,0.5,0.5),vec3(0.5,0.5,0.5),vec3(1.0,1.0,1.0),vec3(0.0,0.10,0.20) );' },
        { 'Palette 3': 'pal( p.x, vec3(0.5,0.5,0.5),vec3(0.5,0.5,0.5),vec3(1.0,1.0,1.0),vec3(0.3,0.20,0.20) );' },
        { 'Palette 4': 'pal( p.x, vec3(0.5,0.5,0.5),vec3(0.5,0.5,0.5),vec3(1.0,1.0,0.5),vec3(0.8,0.90,0.30) );' },
        { 'Palette 5': 'pal( p.x, vec3(0.5,0.5,0.5),vec3(0.5,0.5,0.5),vec3(1.0,0.7,0.4),vec3(0.0,0.15,0.20) );' },
        { 'Palette 6': 'pal( p.x, vec3(0.5,0.5,0.5),vec3(0.5,0.5,0.5),vec3(2.0,1.0,0.0),vec3(0.5,0.20,0.25) );' },
        { 'Palette 7': 'pal( p.x, vec3(0.8,0.5,0.4),vec3(0.2,0.4,0.2),vec3(2.0,1.0,1.0),vec3(0.0,0.25,0.25) );' },

EffectPalette.attributes.add('dithering', { type: 'asset', assetType: 'texture' });

We are creating two attributes:

  • A string enumeration property to select the desired palette. Notice how we set directly the value to the relevant shader line. We will use this property to inject the palette in the shader code.
  • An asset texture property to insert to the shader the dithering texture used.

Before we move on with the script, we need to create a simple material to use for injecting our custom shader. We will use the PlayCanvas shader chunks system. 

PlayCanvas script propertiesCreate a material and attach the black texture you uploaded on the diffuseMap slot. The black texture won't be rendered but we need it so the PlayCanvas shader engine attaches the diffuseTexPS chunk in the default shader.

Also set the U tiling of the material to 10.

Now apply the material to the Box model. And now that you are here, Parse once more the effectsPalette script, select the Palette 1 and put the dithering.png texture in the asset slot created.

Let's get back to the script and add the following code in the initialize() method:

EffectPalette.prototype.initialize = function() {

    // --- variables
    this.timer = 0.0;
    this.material = this.entity.model.meshInstances[0].material.clone();
    this.entity.model.meshInstances[0].material = this.material;

    // --- shader code
    this.material.chunks.diffuseTexPS =         
        "uniform sampler2D texture_diffuseMap;\n" +
        "uniform sampler2D texture_dithering;\n" +
        "uniform float uTime;\n" +
        "vec3 pal( in float t, in vec3 a, in vec3 b, in vec3 c, in vec3 d )\n" +
        "{\n" +
        "    return a + b*cos( 6.28318*(c*t+d) );\n" +
        "}\n" +
        "\n" +
        "void getAlbedo() {\n" +
        "\n" +    
        "    vec2 p = $UV / 7.2;\n" +
        "\n" +    
        "    p.x += 0.01*uTime;\n" +
        "\n" +    
        "    vec3 col = "+this.palette+"\n" +
        "\n" +    
        "    float f = fract(p.y*7.0);\n" +
        "    col *= smoothstep( 0.49, 0.47, abs(f-0.5) );\n" +
        "    col *= 0.5 + 0.5*sqrt(4.0*f*(1.0-f));\n" +
        "    col += (1.0/255.0)*texture( texture_dithering, p ).xyz;\n" +
        "\n" +    
        "    dAlbedo = col;\n" +

    // push dithering texture to shader
    this.material.setParameter("texture_dithering", this.dithering.resource);

As you see, we inject the shader in the chunks system as a string, which we can create on the fly, and take into account the user selected palette.

There isn't anything exceptionally special in converting the shader code to work in PlayCanvas. The $UV variable is already available from the chunks system and provides the fragment coordinates.

We removed the iResolution variable from the original shader as we won't be sizing the effect based on the screen resolution.

And instead of returning the final pixel color to fragColor, we return it to the dAlbedo variable. The variable used to hold the diffuse color in the shader chunks system.

Lastly, add the following code in the update() method:

EffectPalette.prototype.update = function(dt) {
    this.timer += dt * 10;
    this.material.setParameter("uTime", this.timer);

We push every frame a simple timer to the shader, to use it for animating the colors.

We are ready! Press launch and enjoy your first animated color palette.

PlayCanvas color palette.


How to select a palette for each object?

As you have guessed, the script is already set and ready to provide selections of all available color palettes.

You can clone the original Box Model, offset it, and select one by one all palettes for a nice effect.

I duplicated 6 times the original Box Model, and positioned each copy 0.25 above the other. You can use the palette property on the effectPalette script to select a palette.

So, I ended up with a nice disco-like animated panel:

PlayCanvas color palettes.


Where is the juice?

As usual the PlayCanvas project is public and can be accessed here.

The shader code and dithering texture license terms can be found on the original site here.