W4 Material Creation

From Ciliz|W4

Scope[edit]

Assuming that you have read the article W4 Physics for Dummies, and are willing to explore the W4 Engine further study the cases below.

This page describes how to create material and use code from external resources.

Basic Information[edit]

As an example, the material is created based on the shader from the www.shadertoy.com.

Take the following code as the basis:

#include "W4Framework.h"

W4_USE_UNSTRICT_INTERFACE

struct ShadertoyDemo : public IGame
{
public:
    void onStart() override
    {
        m_cube = Mesh::create::cube({2.f, 2.f, 2.f});
        Render::getRoot()->addChild(m_cube);
    }

    virtual void onTouch(const event::Touch::Begin&) override
    {
    }

    void onUpdate(float dt) override
    {
        m_cube->rotate(Rotator{dt,dt,dt});
    }

private:
    sptr<Mesh> m_cube;
};

W4_RUN(ShadertoyDemo)

Run the program. A spinning cube will appear, as in the figure below:

Material_01.png

The material will be added on a later stage.

Project Structure[edit]

The structure of the project:

.
├── resources
   └── materials
       ├── shaders
          ├── topologica.fs
          └── topologica.vs
       └── topologica.mat
└── sources
    └── sample.cpp

In this case, topologia.fs and topologia.vs are the fragment and vertex shaders, accordingly, while topologia.mat is the material file, and sample.cpp is the program code.

Shader Creation[edit]

To test the program, let's start with simple vertex and fragment shaders.

Vertex Shader[edit]

Add the following to topologica.vs:

attribute vec3 w4_a_normal;

varying vec3 v_toView;
varying vec3 v_normal;
varying vec3 v_toLight;

void w4_main()
{
    const vec3 lightPosition = vec3(0., 10., -10.);
    gl_Position = w4_u_projectionView * w4_getVertexPosition();
    v_normal  = w4_u_normalSpace * w4_a_normal;
    v_toView  = w4_u_eyePosition - gl_Position.xyz;
    v_toLight = lightPosition - gl_Position.xyz;
}

All parameters passed to the shader are listed in the W4_Material_System article. All uniforms described above are accessible by default in a shader. The only attribute that was originally added is vec3 w4_a_position. This attribute is needed to calculate the position in w4_getVertexPosition

Let’s analyze the values passed to the shader:

w4_a_normal attribute Vertex normal. Here it is indicated that in addition to the vertex position, the model also contains a normal.
projectionView uniform This parameter is the result of the multiplication of projection and view matrixes.

Note. The projection matrix is calculated from camera parameters such as Fov, Aspect, Near, Far. ‘View matrix’ is calculated from the rotation and position of the camera. Since both matrixes are rarely used separately, they reach the shader pre-multiplied

w4_u_normalSpace uniform Normal matrix is the matrix which is used to transform the normal to the view space (camera space)
w4_u_eyePosition uniform Camera position

At this point, the vertex shader defines a fixed point of lighting, calculates the position of a point, and passes the normal to the fragment shader (vector from the current point to the camera and vector from the current point to the light source).

The vertex position is obtained using the standard w4_getVertexPosition function. Its implementation depends on the type of surface (static / skeletal animation).

Note that it is the w4_main function that is described. Do not confuse it with the standard main, which is already included in the basic material template.

Fragment shader[edit]

Create a file named topologica.fs with the following lines:

uniform vec4 baseColor; //user uniform
uniform vec4 specColor; //user uniform

varying vec3 v_normal;
varying vec3 v_toView;
varying vec3 v_toLight;

void w4_main()
{
    const float specPower = 10.0;
    vec3 n2 = normalize ( v_normal );
    vec3 v2 = normalize ( v_toView );
    vec3 r  = reflect   ( -v2, n2 );
    vec3 l2 = normalize ( v_toLight );
    vec4 diff = baseColor * max ( dot ( n2, l2 ), 0.0 );
    vec4 spec = specColor * pow ( max ( dot ( l2, r ), 0.0 ), specPower );
    gl_FragColor = vec4(diff.rgb, 1.);
    gl_FragColor.rgb += spec.rgb;
}

This shader paints the surface in a specified color, taking into account that there is only one light source of a defined color. Hereinafter, the baseColor and specColor parameters are set from the code.

Add material[edit]

Create a file with a description of the material in accordance with the Material File Format article. In our case, topologica.mat contains only the paths to the shader files:

{
    "vertexFile" : "resources/materials/shaders/topologica.vs",
    "fragmentFile" : "resources/materials/shaders/topologica.fs",
}

Program Code (sample.cpp):

#include "W4Framework.h"

W4_USE_UNSTRICT_INTERFACE

struct ShadertoyDemo : public IGame
{
public:
    void onStart() override
    {
        m_cube = Mesh::create::cube({2.f, 2.f, 2.f});
        auto matInst = Material::get("resources/materials/topologica.mat")->createInstance();
        matInst->setParam("baseColor", vec4(1.f, 0.f, 0.f, 1.f));
        matInst->setParam("specColor", vec4(0.f, 0.f, 1.f, 1.f));
        m_cube->setMaterialInst(matInst);
        Render::getRoot()->addChild(m_cube);
    }

    virtual void onTouch(const event::Touch::Begin&) override
    {
    }

    void onUpdate(float dt) override
    {
        m_cube->rotate(Rotator{dt,dt,dt});
    }

private:
    sptr<Mesh> m_cube;
};

W4_RUN(ShadertoyDemo)

This is enough to retrieve a red cube with blue lighting:

Material_02.png

Adding a complex shader[edit]

After you made sure that everything works smoothly, you can switch to the shader from www.shadertoy.com. Its contents are below:

/*--------------------------------------------------------------------------------------
License CC0 - http://creativecommons.org/publicdomain/zero/1.0/
To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty.
----------------------------------------------------------------------------------------
^ This means do ANYTHING YOU WANT with this code. Because we are programmers, not lawyers.
-Otavio Good
*/

// various noise functions
float Hash2d(vec2 uv)
{
    float f = uv.x + uv.y * 47.0;
    return fract(cos(f*3.333)*100003.9);
}
float Hash3d(vec3 uv)
{
    float f = uv.x + uv.y * 37.0 + uv.z * 521.0;
    return fract(cos(f*3.333)*100003.9);
}
float mixP(float f0, float f1, float a)
{
    return mix(f0, f1, a*a*(3.0-2.0*a));
}
const vec2 zeroOne = vec2(0.0, 1.0);
float noise2d(vec2 uv)
{
    vec2 fr = fract(uv.xy);
    vec2 fl = floor(uv.xy);
    float h00 = Hash2d(fl);
    float h10 = Hash2d(fl + zeroOne.yx);
    float h01 = Hash2d(fl + zeroOne);
    float h11 = Hash2d(fl + zeroOne.yy);
    return mixP(mixP(h00, h10, fr.x), mixP(h01, h11, fr.x), fr.y);
}
float noise2dT(vec2 uv)
{
    vec2 fr = fract(uv);
    vec2 smoothv = fr*fr*(3.0-2.0*fr);
    vec2 fl = floor(uv);
    uv = smoothv + fl;
    return textureLod(iChannel0, (uv + 0.5)/iChannelResolution[0].xy, 0.0).y;	// use constant here instead?
}
float noise(vec3 uv)
{
    vec3 fr = fract(uv.xyz);
    vec3 fl = floor(uv.xyz);
    float h000 = Hash3d(fl);
    float h100 = Hash3d(fl + zeroOne.yxx);
    float h010 = Hash3d(fl + zeroOne.xyx);
    float h110 = Hash3d(fl + zeroOne.yyx);
    float h001 = Hash3d(fl + zeroOne.xxy);
    float h101 = Hash3d(fl + zeroOne.yxy);
    float h011 = Hash3d(fl + zeroOne.xyy);
    float h111 = Hash3d(fl + zeroOne.yyy);
    return mixP(
        mixP(mixP(h000, h100, fr.x), mixP(h010, h110, fr.x), fr.y),
        mixP(mixP(h001, h101, fr.x), mixP(h011, h111, fr.x), fr.y)
        , fr.z);
}

float PI=3.14159265;

vec3 saturate(vec3 a)
{
	return clamp(a, 0.0, 1.0);
}
vec2 saturate(vec2 a)
{
	return clamp(a, 0.0, 1.0);
}
float saturate(float a)
{
	return clamp(a, 0.0, 1.0);
}

float Density(vec3 p)
{
    //float ws = 0.06125*0.125;
    //vec3 warp = vec3(noise(p*ws), noise(p*ws + 111.11), noise(p*ws + 7111.11));
    float final = noise(p*0.06125);// + sin(iTime)*0.5-1.95 + warp.x*4.0;
    float other = noise(p*0.06125 + 1234.567);
    other -= 0.5;
    final -= 0.5;
    final = 0.1/(abs(final*final*other));
    final += 0.5;
    return final*0.0001;
}

void mainImage( out vec4 fragColor, in vec2 fragCoord )
{
	// ---------------- First, set up the camera rays for ray marching ----------------
	vec2 uv = fragCoord.xy/iResolution.xy * 2.0 - 1.0;// - 0.5;

	// Camera up vector.
	vec3 camUp=vec3(0,1,0); // vuv

	// Camera lookat.
	vec3 camLookat=vec3(0,0.0,0);	// vrp

	float mx=iMouse.x/iResolution.x*PI*2.0 + iTime * 0.01;
	float my=-iMouse.y/iResolution.y*10.0 + sin(iTime * 0.03)*0.2+0.2;//*PI/2.01;
	vec3 camPos=vec3(cos(my)*cos(mx),sin(my),cos(my)*sin(mx))*(200.2); 	// prp

	// Camera setup.
	vec3 camVec=normalize(camLookat - camPos);//vpn
	vec3 sideNorm=normalize(cross(camUp, camVec));	// u
	vec3 upNorm=cross(camVec, sideNorm);//v
	vec3 worldFacing=(camPos + camVec);//vcv
	vec3 worldPix = worldFacing + uv.x * sideNorm * (iResolution.x/iResolution.y) + uv.y * upNorm;//scrCoord
	vec3 relVec = normalize(worldPix - camPos);//scp

	// --------------------------------------------------------------------------------
	float t = 0.0;
	float inc = 0.02;
	float maxDepth = 70.0;
	vec3 pos = vec3(0,0,0);
    float density = 0.0;
	// ray marching time
    for (int i = 0; i < 37; i++)	// This is the count of how many times the ray actually marches.
    {
        if ((t > maxDepth)) break;
        pos = camPos + relVec * t;
        float temp = Density(pos);
        //temp *= saturate(t-1.0);

        inc = 1.9 + temp*0.05;	// add temp because this makes it look extra crazy!
        density += temp * inc;
        t += inc;
    }

	// --------------------------------------------------------------------------------
	// Now that we have done our ray marching, let's put some color on this.
	vec3 finalColor = vec3(0.01,0.1,1.0)* density*0.2;

	// output the final color with sqrt for "gamma correction"
	fragColor = vec4(sqrt(clamp(finalColor, 0.0, 1.0)),1.0);
}

Fragment Shader Updating[edit]

Copy the shader from the site to the fragment shader of the project, adjusting some names of parameters. All functions are copied, except for the following:

  • mainImage as it will be copied on later stage;
  • noise2dT as it will is not used and leads to an error.

As a result, topologica.fs looks the following way:

uniform vec4 baseColor;
uniform vec4 specColor;

varying vec3 v_normal;
varying vec3 v_toView;
varying vec3 v_toLight;

// various noise functions
float Hash2d(vec2 uv)
{
    float f = uv.x + uv.y * 47.0;
    return fract(cos(f*3.333)*100003.9);
}
float Hash3d(vec3 uv)
{
    float f = uv.x + uv.y * 37.0 + uv.z * 521.0;
    return fract(cos(f*3.333)*100003.9);
}
float mixP(float f0, float f1, float a)
{
    return mix(f0, f1, a*a*(3.0-2.0*a));
}
const vec2 zeroOne = vec2(0.0, 1.0);
float noise2d(vec2 uv)
{
    vec2 fr = fract(uv.xy);
    vec2 fl = floor(uv.xy);
    float h00 = Hash2d(fl);
    float h10 = Hash2d(fl + zeroOne.yx);
    float h01 = Hash2d(fl + zeroOne);
    float h11 = Hash2d(fl + zeroOne.yy);
    return mixP(mixP(h00, h10, fr.x), mixP(h01, h11, fr.x), fr.y);
}
float noise(vec3 uv)
{
    vec3 fr = fract(uv.xyz);
    vec3 fl = floor(uv.xyz);
    float h000 = Hash3d(fl);
    float h100 = Hash3d(fl + zeroOne.yxx);
    float h010 = Hash3d(fl + zeroOne.xyx);
    float h110 = Hash3d(fl + zeroOne.yyx);
    float h001 = Hash3d(fl + zeroOne.xxy);
    float h101 = Hash3d(fl + zeroOne.yxy);
    float h011 = Hash3d(fl + zeroOne.xyy);
    float h111 = Hash3d(fl + zeroOne.yyy);
    return mixP(
        mixP(mixP(h000, h100, fr.x), mixP(h010, h110, fr.x), fr.y),
        mixP(mixP(h001, h101, fr.x), mixP(h011, h111, fr.x), fr.y)
        , fr.z);
}

float PI=3.14159265;

vec3 saturate(vec3 a)
{
	return clamp(a, 0.0, 1.0);
}
vec2 saturate(vec2 a)
{
	return clamp(a, 0.0, 1.0);
}
float saturate(float a)
{
	return clamp(a, 0.0, 1.0);
}

float Density(vec3 p)
{
    //float ws = 0.06125*0.125;
    //vec3 warp = vec3(noise(p*ws), noise(p*ws + 111.11), noise(p*ws + 7111.11));
    float final = noise(p*0.06125);// + sin(iTime)*0.5-1.95 + warp.x*4.0;
    float other = noise(p*0.06125 + 1234.567);
    other -= 0.5;
    final -= 0.5;
    final = 0.1/(abs(final*final*other));
    final += 0.5;
    return final*0.0001;
}

void w4_main()
{
    const float specPower = 10.0;
    vec3 n2 = normalize ( v_normal );
    vec3 v2 = normalize ( v_toView );
    vec3 r  = reflect   ( -v2, n2 );
    vec3 l2 = normalize ( v_toLight );
    vec4 diff = baseColor * max ( dot ( n2, l2 ), 0.0 );
    vec4 spec = specColor * pow ( max ( dot ( l2, r ), 0.0 ), specPower );
    gl_FragColor = vec4(diff.rgb, 1.);
    gl_FragColor.rgb += spec.rgb;
}

The program with the above shader runs without errors, but the cube has not changed, as the mainImage part is missing.

Copy the contents of mainImage to the main of fragment shader and you will get the following code:

uniform vec4 baseColor;
uniform vec4 specColor;

varying vec3 v_normal;
varying vec3 v_toView;
varying vec3 v_toLight;

// various noise functions
float Hash2d(vec2 uv)
{
    float f = uv.x + uv.y * 47.0;
    return fract(cos(f*3.333)*100003.9);
}
float Hash3d(vec3 uv)
{
    float f = uv.x + uv.y * 37.0 + uv.z * 521.0;
    return fract(cos(f*3.333)*100003.9);
}
float mixP(float f0, float f1, float a)
{
    return mix(f0, f1, a*a*(3.0-2.0*a));
}
const vec2 zeroOne = vec2(0.0, 1.0);
float noise2d(vec2 uv)
{
    vec2 fr = fract(uv.xy);
    vec2 fl = floor(uv.xy);
    float h00 = Hash2d(fl);
    float h10 = Hash2d(fl + zeroOne.yx);
    float h01 = Hash2d(fl + zeroOne);
    float h11 = Hash2d(fl + zeroOne.yy);
    return mixP(mixP(h00, h10, fr.x), mixP(h01, h11, fr.x), fr.y);
}

float noise(vec3 uv)
{
    vec3 fr = fract(uv.xyz);
    vec3 fl = floor(uv.xyz);
    float h000 = Hash3d(fl);
    float h100 = Hash3d(fl + zeroOne.yxx);
    float h010 = Hash3d(fl + zeroOne.xyx);
    float h110 = Hash3d(fl + zeroOne.yyx);
    float h001 = Hash3d(fl + zeroOne.xxy);
    float h101 = Hash3d(fl + zeroOne.yxy);
    float h011 = Hash3d(fl + zeroOne.xyy);
    float h111 = Hash3d(fl + zeroOne.yyy);
    return mixP(
        mixP(mixP(h000, h100, fr.x), mixP(h010, h110, fr.x), fr.y),
        mixP(mixP(h001, h101, fr.x), mixP(h011, h111, fr.x), fr.y)
        , fr.z);
}

float PI=3.14159265;

vec3 saturate(vec3 a)
{
	return clamp(a, 0.0, 1.0);
}
vec2 saturate(vec2 a)
{
	return clamp(a, 0.0, 1.0);
}
float saturate(float a)
{
	return clamp(a, 0.0, 1.0);
}

float Density(vec3 p)
{
    //float ws = 0.06125*0.125;
    //vec3 warp = vec3(noise(p*ws), noise(p*ws + 111.11), noise(p*ws + 7111.11));
    float final = noise(p*0.06125);// + sin(iTime)*0.5-1.95 + warp.x*4.0;
    float other = noise(p*0.06125 + 1234.567);
    other -= 0.5;
    final -= 0.5;
    final = 0.1/(abs(final*final*other));
    final += 0.5;
    return final*0.0001;
}

void w4_main()
{
    	// ---------------- First, set up the camera rays for ray marching ----------------
    	vec2 uv = fragCoord.xy/iResolution.xy * 2.0 - 1.0;// - 0.5;

    	// Camera up vector.
    	vec3 camUp=vec3(0,1,0); // vuv

    	// Camera lookat.
    	vec3 camLookat=vec3(0,0.0,0);	// vrp

    	float mx=iMouse.x/iResolution.x*PI*2.0 + iTime * 0.01;
    	float my=-iMouse.y/iResolution.y*10.0 + sin(iTime * 0.03)*0.2+0.2;//*PI/2.01;
    	vec3 camPos=vec3(cos(my)*cos(mx),sin(my),cos(my)*sin(mx))*(200.2); 	// prp

    	// Camera setup.
    	vec3 camVec=normalize(camLookat - camPos);//vpn
    	vec3 sideNorm=normalize(cross(camUp, camVec));	// u
    	vec3 upNorm=cross(camVec, sideNorm);//v
    	vec3 worldFacing=(camPos + camVec);//vcv
    	vec3 worldPix = worldFacing + uv.x * sideNorm * (iResolution.x/iResolution.y) + uv.y * upNorm;//scrCoord
    	vec3 relVec = normalize(worldPix - camPos);//scp

    	// --------------------------------------------------------------------------------
    	float t = 0.0;
    	float inc = 0.02;
    	float maxDepth = 70.0;
    	vec3 pos = vec3(0,0,0);
        float density = 0.0;
    	// ray marching time
        for (int i = 0; i < 37; i++)	// This is the count of how many times the ray actually marches.
        {
            if ((t > maxDepth)) break;
            pos = camPos + relVec * t;
            float temp = Density(pos);
            //temp *= saturate(t-1.0);

            inc = 1.9 + temp*0.05;	// add temp because this makes it look extra crazy!
            density += temp * inc;
            t += inc;
        }

    	// --------------------------------------------------------------------------------
    	// Now that we have done our ray marching, let's put some color on this.
    	vec3 finalColor = vec3(0.01,0.1,1.0)* density*0.2;

    	// output the final color with sqrt for "gamma correction"
    	fragColor = vec4(sqrt(clamp(finalColor, 0.0, 1.0)),1.0);

        const float specPower = 10.0;
        vec3 n2 = normalize ( v_normal );
        vec3 v2 = normalize ( v_toView );
        vec3 r  = reflect   ( -v2, n2 );
        vec3 l2 = normalize ( v_toLight );
        vec4 diff = fragColor * max ( dot ( n2, l2 ), 0.0 );
        vec4 spec = specColor * pow ( max ( dot ( l2, r ), 0.0 ), specPower );
        gl_FragColor = vec4(diff.rgb, 1.);
        gl_FragColor.rgb += spec.rgb;
}

Debugging[edit]

If you run the program now, errors will appear with the missing variables: fragCoord, iResolution, iTime and iMouse variables. Let's review them case by case:

1) fragCoord and iResolution are used to calculate uv. uv can be retrieved from the vertex shader (v_uv). Do not forget to complete the headers of both shaders with lines accordingly:

 varying vec2 v_uv;

and add the following to the vertex shader:

attribute vec2 w4_a_uv0;
...
v_uv = w4_a_uv0;

iTime is the time attribute. Add it to the fragment shader:

uniform float w4_u_time;

replace iTime with w4_u_time.

Hardcode iMouse (for later use).

vec2 iMouse = vec2(0, 0);

iResolution is also used to calculate the aspect ratio. To optimize the execution, let's calculate it in the vertex shader.

uniform vec2 w4_u_resolution;
...
varying float v_aspect;
...
v_aspect = w4_u_resolution.x / w4_u_resolution.y;

Note. Since baseColor is no longer used, don't forget to remove it from the shader and source code.

Result[edit]

The resulting shaders:

topologica.vs

attribute vec3 position;
attribute vec3 normal;
attribute vec2 uv0;

uniform mat4 projectionView;
uniform mat4 model;
uniform mat3 normalSpace;
uniform vec3 eyePosition;
uniform vec2 resolution;

varying vec3 v_toView;
varying vec3 v_normal;
varying vec3 v_toLight;
varying vec2 v_uv;
varying float v_aspect;

void main()
{
    v_uv = uv0;

    const vec3 lightPosition = vec3(0., 10., -10.);
    gl_Position = projectionView * model * vec4(position , 1.0);
    v_normal  = normalSpace * normal;
    v_toView  = eyePosition - gl_Position.xyz;
    v_toLight = lightPosition - gl_Position.xyz;
    v_aspect = resolution.x / resolution.y;
}

topologica.fs

uniform vec4 specColor;
uniform float w4_u_time;

varying vec3 v_normal;
varying vec3 v_toView;
varying vec3 v_toLight;
varying vec2 v_uv;
varying float v_aspect;

// various noise functions
float Hash2d(vec2 uv)
{
    float f = uv.x + uv.y * 47.0;
    return fract(cos(f*3.333)*100003.9);
}
float Hash3d(vec3 uv)
{
    float f = uv.x + uv.y * 37.0 + uv.z * 521.0;
    return fract(cos(f*3.333)*100003.9);
}
float mixP(float f0, float f1, float a)
{
    return mix(f0, f1, a*a*(3.0-2.0*a));
}
const vec2 zeroOne = vec2(0.0, 1.0);
float noise2d(vec2 uv)
{
    vec2 fr = fract(uv.xy);
    vec2 fl = floor(uv.xy);
    float h00 = Hash2d(fl);
    float h10 = Hash2d(fl + zeroOne.yx);
    float h01 = Hash2d(fl + zeroOne);
    float h11 = Hash2d(fl + zeroOne.yy);
    return mixP(mixP(h00, h10, fr.x), mixP(h01, h11, fr.x), fr.y);
}

float noise(vec3 uv)
{
    vec3 fr = fract(uv.xyz);
    vec3 fl = floor(uv.xyz);
    float h000 = Hash3d(fl);
    float h100 = Hash3d(fl + zeroOne.yxx);
    float h010 = Hash3d(fl + zeroOne.xyx);
    float h110 = Hash3d(fl + zeroOne.yyx);
    float h001 = Hash3d(fl + zeroOne.xxy);
    float h101 = Hash3d(fl + zeroOne.yxy);
    float h011 = Hash3d(fl + zeroOne.xyy);
    float h111 = Hash3d(fl + zeroOne.yyy);
    return mixP(
        mixP(mixP(h000, h100, fr.x), mixP(h010, h110, fr.x), fr.y),
        mixP(mixP(h001, h101, fr.x), mixP(h011, h111, fr.x), fr.y)
        , fr.z);
}

float PI=3.14159265;

vec3 saturate(vec3 a)
{
	return clamp(a, 0.0, 1.0);
}
vec2 saturate(vec2 a)
{
	return clamp(a, 0.0, 1.0);
}
float saturate(float a)
{
	return clamp(a, 0.0, 1.0);
}

float Density(vec3 p)
{
    //float ws = 0.06125*0.125;
    //vec3 warp = vec3(noise(p*ws), noise(p*ws + 111.11), noise(p*ws + 7111.11));
    float final = noise(p*0.06125);// + sin(iTime)*0.5-1.95 + warp.x*4.0;
    float other = noise(p*0.06125 + 1234.567);
    other -= 0.5;
    final -= 0.5;
    final = 0.1/(abs(final*final*other));
    final += 0.5;
    return final*0.0001;
}

void w4_main()
{
    	// ---------------- First, set up the camera rays for ray marching ----------------
    	// Camera up vector.
    	vec3 camUp=vec3(0,1,0); // vuv

    	// Camera lookat.
    	vec3 camLookat=vec3(0,0.0,0);	// vrp

        vec2 iMouse = vec2(0, 0);
        vec2 iResolution = vec2(1, 1);

    	float mx=iMouse.x/iResolution.x*PI*2.0 + w4_u_time * 0.01;
    	float my=-iMouse.y/iResolution.y*10.0 + sin(w4_u_time * 0.03)*0.2+0.2;//*PI/2.01;
    	vec3 camPos=vec3(cos(my)*cos(mx),sin(my),cos(my)*sin(mx))*(200.2); 	// prp

    	// Camera setup.
    	vec3 camVec=normalize(camLookat - camPos);//vpn
    	vec3 sideNorm=normalize(cross(camUp, camVec));	// u
    	vec3 upNorm=cross(camVec, sideNorm);//v
    	vec3 worldFacing=(camPos + camVec);//vcv
    	vec3 worldPix = worldFacing + v_uv.x * sideNorm * v_aspect + v_uv.y * upNorm;//scrCoord
    	vec3 relVec = normalize(worldPix - camPos);//scp

    	// --------------------------------------------------------------------------------
    	float t = 0.0;
    	float inc = 0.02;
    	float maxDepth = 70.0;
    	vec3 pos = vec3(0,0,0);
        float density = 0.0;
    	// ray marching time
        for (int i = 0; i < 37; i++)	// This is the count of how many times the ray actually marches.
        {
            if ((t > maxDepth)) break;
            pos = camPos + relVec * t;
            float temp = Density(pos);
            //temp *= saturate(t-1.0);

            inc = 1.9 + temp*0.05;	// add temp because this makes it look extra crazy!
            density += temp * inc;
            t += inc;
        }

    	// --------------------------------------------------------------------------------
    	// Now that we have done our ray marching, let's put some color on this.
    	vec3 finalColor = vec3(0.01,0.1,1.0)* density*0.2;

    	// output the final color with sqrt for "gamma correction"
    	vec4 fragColor = vec4(sqrt(clamp(finalColor, 0.0, 1.0)),1.0);

        const float specPower = 10.0;
        vec3 n2 = normalize ( v_normal );
        vec3 v2 = normalize ( v_toView );
        vec3 r  = reflect   ( -v2, n2 );
        vec3 l2 = normalize ( v_toLight );
        vec4 diff = fragColor * max ( dot ( n2, l2 ), 0.0 );
        vec4 spec = specColor * pow ( max ( dot ( l2, r ), 0.0 ), specPower );
        gl_FragColor = vec4(diff.rgb, 1.);
        gl_FragColor.rgb += spec.rgb;
}

sample.cpp has remained unchanged, except for the deleted line with baseColor.

Let's run the program:

Material_03.png

As a result of the previous steps the material is added.

Cursor handler[edit]

The mouse position can simply be removed from the shader, but in our case we keep it. This can be done by adding a mouse movement handler and passing the normalized cursor position to the shader.

The resulting code: sample.cpp

#include "W4Framework.h"

W4_USE_UNSTRICT_INTERFACE

struct ShadertoyDemo : public IGame
{
public:
    void onStart() override
    {
        m_cube = Mesh::create::cube({2.f, 2.f, 2.f});
        m_matInst = Material::get("resources/materials/topologica.mat")->createInstance();
        m_matInst->setParam("specColor", vec4(0.f, 0.f, 1.f, 1.f));
        m_cube->setMaterialInst(m_matInst);
        Render::getRoot()->addChild(m_cube);

        event::Touch::Move::subscribe(std::bind(&ShadertoyDemo::onMove, this, std::placeholders::_1));
    }

    void onMove(const event::Touch::Move& evt)
    {
        const auto& screenSize = Platform::instance().getSize();
        m_matInst->setParam("mousePos", vec2(static_cast<float>(evt.point.x) / screenSize.w, static_cast<float>(evt.point.y) / screenSize.h));
    }

    void onUpdate(float dt) override
    {
        m_cube->rotate(Rotator{dt,dt,dt});
    }

private:
    sptr<Mesh> m_cube;
    sptr<MaterialInst> m_matInst;
};

W4_RUN(ShadertoyDemo)

topologica.fs

uniform vec4 specColor;
uniform vec2 mousePos;

varying vec3 v_normal;
varying vec3 v_toView;
varying vec3 v_toLight;
varying vec2 v_uv;
varying float v_aspect;

// various noise functions
float Hash2d(vec2 uv)
{
    float f = uv.x + uv.y * 47.0;
    return fract(cos(f*3.333)*100003.9);
}
float Hash3d(vec3 uv)
{
    float f = uv.x + uv.y * 37.0 + uv.z * 521.0;
    return fract(cos(f*3.333)*100003.9);
}
float mixP(float f0, float f1, float a)
{
    return mix(f0, f1, a*a*(3.0-2.0*a));
}
const vec2 zeroOne = vec2(0.0, 1.0);
float noise2d(vec2 uv)
{
    vec2 fr = fract(uv.xy);
    vec2 fl = floor(uv.xy);
    float h00 = Hash2d(fl);
    float h10 = Hash2d(fl + zeroOne.yx);
    float h01 = Hash2d(fl + zeroOne);
    float h11 = Hash2d(fl + zeroOne.yy);
    return mixP(mixP(h00, h10, fr.x), mixP(h01, h11, fr.x), fr.y);
}

float noise(vec3 uv)
{
    vec3 fr = fract(uv.xyz);
    vec3 fl = floor(uv.xyz);
    float h000 = Hash3d(fl);
    float h100 = Hash3d(fl + zeroOne.yxx);
    float h010 = Hash3d(fl + zeroOne.xyx);
    float h110 = Hash3d(fl + zeroOne.yyx);
    float h001 = Hash3d(fl + zeroOne.xxy);
    float h101 = Hash3d(fl + zeroOne.yxy);
    float h011 = Hash3d(fl + zeroOne.xyy);
    float h111 = Hash3d(fl + zeroOne.yyy);
    return mixP(
        mixP(mixP(h000, h100, fr.x), mixP(h010, h110, fr.x), fr.y),
        mixP(mixP(h001, h101, fr.x), mixP(h011, h111, fr.x), fr.y)
        , fr.z);
}

float PI=3.14159265;

vec3 saturate(vec3 a)
{
	return clamp(a, 0.0, 1.0);
}
vec2 saturate(vec2 a)
{
	return clamp(a, 0.0, 1.0);
}
float saturate(float a)
{
	return clamp(a, 0.0, 1.0);
}

float Density(vec3 p)
{
    //float ws = 0.06125*0.125;
    //vec3 warp = vec3(noise(p*ws), noise(p*ws + 111.11), noise(p*ws + 7111.11));
    float final = noise(p*0.06125);// + sin(iTime)*0.5-1.95 + warp.x*4.0;
    float other = noise(p*0.06125 + 1234.567);
    other -= 0.5;
    final -= 0.5;
    final = 0.1/(abs(final*final*other));
    final += 0.5;
    return final*0.0001;
}

void w4_main()
{
    	// ---------------- First, set up the camera rays for ray marching ----------------
    	// Camera up vector.
    	vec3 camUp=vec3(0,1,0); // vuv

    	// Camera lookat.
    	vec3 camLookat=vec3(0,0.0,0);	// vrp

    	float mx=mousePos.x*PI*2.0 + w4_u_time * 0.01;
    	float my=-mousePos.y*10.0 + sin(w4_u_time * 0.03)*0.2+0.2;//*PI/2.01;
    	vec3 camPos=vec3(cos(my)*cos(mx),sin(my),cos(my)*sin(mx))*(200.2); 	// prp

    	// Camera setup.
    	vec3 camVec=normalize(camLookat - camPos);//vpn
    	vec3 sideNorm=normalize(cross(camUp, camVec));	// u
    	vec3 upNorm=cross(camVec, sideNorm);//v
    	vec3 worldFacing=(camPos + camVec);//vcv
    	vec3 worldPix = worldFacing + v_uv.x * sideNorm * v_aspect + v_uv.y * upNorm;//scrCoord
    	vec3 relVec = normalize(worldPix - camPos);//scp

    	// --------------------------------------------------------------------------------
    	float t = 0.0;
    	float inc = 0.02;
    	float maxDepth = 70.0;
    	vec3 pos = vec3(0,0,0);
        float density = 0.0;
    	// ray marching w4_u_time
        for (int i = 0; i < 37; i++)	// This is the count of how many times the ray actually marches.
        {
            if ((t > maxDepth)) break;
            pos = camPos + relVec * t;
            float temp = Density(pos);
            //temp *= saturate(t-1.0);

            inc = 1.9 + temp*0.05;	// add temp because this makes it look extra crazy!
            density += temp * inc;
            t += inc;
        }

    	// --------------------------------------------------------------------------------
    	// Now that we have done our ray marching, let's put some color on this.
    	vec3 finalColor = vec3(0.01,0.1,1.0)* density*0.2;

    	// output the final color with sqrt for "gamma correction"
    	vec4 fragColor = vec4(sqrt(clamp(finalColor, 0.0, 1.0)),1.0);

        const float specPower = 10.0;
        vec3 n2 = normalize ( v_normal );
        vec3 v2 = normalize ( v_toView );
        vec3 r  = reflect   ( -v2, n2 );
        vec3 l2 = normalize ( v_toLight );
        vec4 diff = fragColor * max ( dot ( n2, l2 ), 0.0 );
        vec4 spec = specColor * pow ( max ( dot ( l2, r ), 0.0 ), specPower );
        gl_FragColor = vec4(diff.rgb, 1.);
        gl_FragColor.rgb += spec.rgb;
}

The vertex shader has remained unchanged.

Run the program:

http://demo.w4-dev.ciliz.com/wiki-files/Material_final.mov

When you move the mouse with the pressed button, the shader reacts similarly to the shadertoy implementation. Therefore, the functionality is fully transferred!