I like shiny balls

So if you liked my previous balls physics app… here’s a new version using shaders to make them shiny.

Don’t let youtubes preview still put you off, it’s much shinier than that…

The most interesting thing in here for me, is I got it working with fake bump mapping. Ie, the mesh is actually a single rectangle, but I use a texture which encodes the normals to drive lighting which means I can fake true 3d. In real modeling terms, it’s a way to increase the “resolution” without having too many triangles in your mesh.

The next thing to try and learn from here is whether I can bind multiple textures to a mesh so I can use one as a texture and the other for bump mapping…


function setup()
    displayMode(FULLSCREEN)
    --myFPSReporter = FPSReporter(4)
    parameter.number("ambient", 0, 1, 0.1)
    parameter.number("diffuse", 0, 1, 1.0)
    parameter.number("specular", 0, 1, 1)
    parameter.color("lightColor", color(255,255,255,255))

    --defaults are 10,8 so not sure why the following is necessary
    --physics.iterations(40,40)
    
  --  physics.gravity(0,16)
    noSmooth()
    
    balls = {}
    touches = {}
    
    base = physics.body(EDGE, vec2(100,0), vec2(WIDTH-100,0))
    
    nextball = 0
    textureSize = 150
    ballTexture = genCircularNormals(textureSize)
    ballMesh = mesh()
    ballMesh:addRect(0,0,1,1,0)
    ballMesh.texture = ballTexture
    ballMesh:setRectTex(1,0,0,1,1) 
    ballMesh.shader = shader(bumpLighting.vertexShader, bumpLighting.fragmentShader)
end

function genCircularNormals(w)
    local t = image(w, w)
    r = w/2
    for x=1,w do
        for y=1,w do
            d = math.sqrt((r-x)^2+((r-y)^2))
            if d <= r then
                angle = math.acos(d/r)
                z = r*math.sin(angle)
                v = (vec3(x-r,y-r,z):normalize()+vec3(1,1,1))/2*255
                
                t:set(x, y, v.x, v.y, v.z, 255)
            end             
        end
    end
    
    return t
end


function touched(touch)
    if touch.state == ENDED then
        touches[touch.id] = nil
    else
        --if touches[touch.id] == nil then
        touches[touch.id] = touch
        --end
    end    
end    

function touchActions()
    for k,v in pairs(touches) do 
        if CurrentTouch.state == ENDED then
            --if there are no current touches then we kill all current touches to avoid bugged ball producers
            touches[k] = nil
        else
                
            --add a new ball at the touch location
            size = math.random(1,20)
            tspot = physics.body(CIRCLE, size)
            tspot.position = vec2(v.x+math.random(-1,1), v.y+math.random(-1,1))
            tspot.restitution = 0.95
        
            balls[nextball] = { tspot = tspot, size = size * 2, r = math.random(30,255), g = math.random(30,255), b = math.random(30,255) }
            
            nextball = nextball + 1
        end    
    end
end

function draw()
    background(0, 0, 0, 255)
    --myFPSReporter:draw(3)
    strokeWidth(0)
    ballMesh.shader.vAmbientMaterial = ambient  
    ballMesh.shader.vDiffuseMaterial = diffuse
    ballMesh.shader.vSpecularMaterial = specular
    ballMesh.shader.lightColor = lightColor
    ballMesh.shader.mView = viewMatrix()
    ballMesh.shader.mProjection = projectionMatrix()
    

    ballMesh.shader.mModel = modelMatrix()
    ballMesh.shader.vEyePosition = vec4(0,0,500,0)
    ballMesh.shader.vLightPosition = vec4(500,-500,500,0)

    for k,v in pairs(balls) do
        if v.tspot.x < -20 or v.tspot.x > WIDTH + 20 or v.tspot.y < -20 then
            balls[k].tspot:destroy()
            balls[k] = nil  
        else
            resetMatrix()
            translate(v.tspot.x,v.tspot.y)
            scale(v.size)
            
            --[[
            ballMesh.shader.mModel = modelMatrix()
            ballMesh.shader.vEyePosition = vec4(v.tspot.x,v.tspot.y,250,0)
            ballMesh.shader.vLightPosition = vec4(v.tspot.x,v.tspot.y,250,0)
            ]]
            ballMesh:setColors(color(v.r,v.g,v.b,255))
    
            ballMesh:draw()
            --fill(v.r, v.g, v.b, 255)
            --ellipse(v.tspot.x, v.tspot.y, v.size)
        end
    end
    
    touchActions()
end

bumpLighting = {
vertexShader = [[
//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;

//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

varying lowp vec4 vColor;
varying highp vec2 vTexCoord;
varying highp vec4 vPosition;  

void main()
{
    //Pass the mesh color to the fragment shader
    vColor = color;
    vTexCoord = vec2(texCoord.x, 1.0 - texCoord.y);
    vPosition = position;             
    //Multiply the vertex position by our combined transform
    gl_Position = modelViewProjection * position;
}
]],
fragmentShader = [[
//
// A basic fragment shader
//

//Default precision qualifier
precision highp float;

//This represents the current texture on the mesh
uniform lowp sampler2D texture;

//The interpolated vertex color for this fragment
varying lowp vec4 vColor;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;

varying highp vec4 vPosition; 

//****new attributes
// Our Model, View and Projection matrices
// we need them to transform the incoming vertices and for lighting
// calculation
uniform mat4 mModel;
uniform mat4 mView;
uniform mat4 mProjection;
 
// Position of the "Camera" and the Light
// in Model space
uniform vec4 vEyePosition;
uniform vec4 vLightPosition;
 
// The Colors of the material
uniform float vAmbientMaterial;
uniform float vDiffuseMaterial;
uniform float vSpecularMaterial;
uniform vec4 lightColor;


// Returns the specular component of the color
vec4 GetSpecularColor(vec3 vVertexNormal, vec4 vVertexPosition)
{
    // Transform the Vertex and corresponding Normal into Model space
    vec4 vTransformedNormal = mModel * vec4( vVertexNormal, 1 );
    vec4 vTransformedVertex = mModel * vVertexPosition;
 
    // Get the directional vector to the light and to the camera
    // originating from the vertex position
    vec4 vLightDirection = normalize( vLightPosition - vTransformedVertex );
    vec4 vCameraDirection = normalize( vEyePosition - vTransformedVertex );
 
    // Calculate the reflection vector between the incoming light and the
    // normal (incoming angle = outgoing angle)
    // We have to use the invert of the light direction because "reflect"
    // expects the incident vector as its first parameter
    vec4 vReflection = reflect( -vLightDirection, vTransformedNormal );
 
    // Calculate specular component
    // Based on the dot product between the reflection vector and the camera
    // direction.
    //
    // hint: The Dot Product corresponds to the angle between the two vectors
    // hint: if the angle is out of range (0 ... 180 degrees) we use 0.0
    float spec = pow( max( 0.0, dot( vCameraDirection, vReflection )), 32.0 );
 
    return vec4( lightColor.r * spec, lightColor.g * spec, lightColor.b * spec, 1.0 ) * vSpecularMaterial;
}
 
// Ambient color component of vertex
vec4 GetAmbientColor()
{
    vec4 vAmbientColor;
    vAmbientColor.xyz = vColor.rgb * lightColor.rgb * vAmbientMaterial;
    vAmbientColor.a = 1.0;
    
    return vAmbientColor;
}
 
// Diffuse Color component of vertex
vec4 GetDiffuseColor(vec3 vVertexNormal)
{
    // Transform the normal from Object to Model space
    // we also normalize the vector just to be sure ...
    vec4 vTransformedNormal = normalize( mModel * vec4( vVertexNormal, 1 ));
 
    // Get direction of light in Model space
    vec4 vLightDirection = normalize( vLightPosition - vTransformedNormal );
 
    // Calculate Diffuse intensity
    float fDiffuseIntensity = max( 0.0, dot( vTransformedNormal, vLightDirection ));
 
    // Calculate resulting Color
    vec4 vDiffuseColor;
    vDiffuseColor.xyz = vColor.rgb * lightColor.rgb * fDiffuseIntensity * vDiffuseMaterial;
    vDiffuseColor.a = 1.0;
    
    return vDiffuseColor;
}

void main()
{
    //Sample the texture at the interpolated coordinate
    lowp vec4 col = texture2D( texture, vTexCoord );
    if (col.x == 0.0 && col.y == 0.0 && col.z == 0.0) discard;
    vec3 vNormal = normalize(vec3(col.x, col.y, col.z)*2.0-vec3(1.0,1.0,1.0));
    
    vec4 ambientColor = GetAmbientColor();
    vec4 diffuseColor = GetDiffuseColor(vNormal);
    vec4 specularColor = GetSpecularColor(vNormal, vPosition);
    
    // Combine into final color
    vec4 outColor;
    outColor = ambientColor + diffuseColor + specularColor;

    //Set the output color to the texture color
    gl_FragColor = outColor;
}
]]}

Sweet!

Here is another tweaked version, now the shader takes 2 texture images (assumed same layout against vertexes) one for the bumpMap and one for the texture.

It then does the ADS lighting based on the normals from the bumpMap and colours points based on the texture. Not very interesting in the context of balls, but it proves what you need to head towards things like this:
http://fabiensanglard.net/bumpMapping/index.php


function setup()
    displayMode(FULLSCREEN)
    --myFPSReporter = FPSReporter(4)
    parameter.number("ambient", 0, 1, 0.2)
    parameter.number("diffuse", 0, 1, 1)
    parameter.number("specular", 0, 1, 1)
    parameter.color("lightColor", color(255,255,255,255))

    --defaults are 10,8 so not sure why the following is necessary
    --physics.iterations(40,40)
    
  --  physics.gravity(0,16)
    noSmooth()
    
    balls = {}
    touches = {}
    
    base = physics.body(EDGE, vec2(100,0), vec2(WIDTH-100,0))
    
    nextball = 0
    textureSize = 150
    ballTexture = genCircularNormals(textureSize)
    ballMesh = mesh()
    ballMesh:addRect(0,0,1,1,0)
    ballMesh.texture = "SpaceCute:Planet"
    ballMesh:setRectTex(1,0,0,1,1) 
    ballMesh.shader = shader(bumpLighting.vertexShader, bumpLighting.fragmentShader)
    --ballMesh.shader = shader("Documents:Bump Lighting w Texture")
    ballMesh.shader.bumpMap = ballTexture
    
    lightPosition = vec4(-500,-500,500,0)
    
    tween( 2.0, lightPosition, { x = 500 }, { 
        easing = tween.easing.quadInOut,
        loop = tween.loop.pingpong } )
    tween( 3.0, lightPosition, { y = 500 }, { 
        easing = tween.easing.quadInOut,
        loop = tween.loop.pingpong } )
end

function genCircularNormals(w)
    local t = image(w, w)
    r = w/2
    for x=1,w do
        for y=1,w do
            d = math.sqrt((r-x)^2+((r-y)^2))
            if d <= r then
                angle = math.acos(d/r)
                z = r*math.sin(angle)
                v = (vec3(x-r,y-r,z):normalize()+vec3(1,1,1))/2*255
                
                t:set(x, y, v.x, v.y, v.z, 255)
            end             
        end
    end
    
    return t
end


function touched(touch)
    if touch.state == ENDED then
        touches[touch.id] = nil
    else
        --if touches[touch.id] == nil then
        touches[touch.id] = touch
        --end
    end    
end    

function touchActions()
    for k,v in pairs(touches) do 
        if CurrentTouch.state == ENDED then
            --if there are no current touches then we kill all current touches to avoid bugged ball producers
            touches[k] = nil
        else
                
            --add a new ball at the touch location
            size = math.random(1,20)
            tspot = physics.body(CIRCLE, size)
            tspot.position = vec2(v.x+math.random(-1,1), v.y+math.random(-1,1))
            tspot.restitution = 0.95
        
            balls[nextball] = { tspot = tspot, size = size * 2, r = math.random(30,255), g = math.random(30,255), b = math.random(30,255) }
            
            nextball = nextball + 1
        end    
    end
end

function draw()
    background(0, 0, 0, 255)
    --myFPSReporter:draw(3)
    strokeWidth(0)
    ballMesh.shader.vAmbientMaterial = ambient  
    ballMesh.shader.vDiffuseMaterial = diffuse
    ballMesh.shader.vSpecularMaterial = specular
    ballMesh.shader.lightColor = lightColor
    ballMesh.shader.mView = viewMatrix()
    ballMesh.shader.mProjection = projectionMatrix()
    

    ballMesh.shader.mModel = modelMatrix()
    ballMesh.shader.vEyePosition = vec4(0,0,500,0)
    ballMesh.shader.vLightPosition = lightPosition

    for k,v in pairs(balls) do
        if v.tspot.x < -20 or v.tspot.x > WIDTH + 20 or v.tspot.y < -20 then
            balls[k].tspot:destroy()
            balls[k] = nil  
        else
            resetMatrix()
            translate(v.tspot.x,v.tspot.y)
            scale(v.size)
            
            --ballMesh:setColors(color(v.r,v.g,v.b,255))
    
            ballMesh:draw()
            --fill(v.r, v.g, v.b, 255)
            --ellipse(v.tspot.x, v.tspot.y, v.size)
        end
    end
    
    touchActions()
end

bumpLighting = {
vertexShader = [[
//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;

//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec2 texCoord;

varying highp vec2 vTexCoord;
varying highp vec4 vPosition;  

void main()
{
    vTexCoord = vec2(texCoord.x, 1.0 - texCoord.y);
    vPosition = position;             
    //Multiply the vertex position by our combined transform
    gl_Position = modelViewProjection * position;
}
]],
fragmentShader = [[
//
// A basic fragment shader
//

//Default precision qualifier
precision highp float;

//This represents the current texture on the mesh
uniform lowp sampler2D texture;

uniform lowp sampler2D bumpMap;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;

varying highp vec4 vPosition; 

//****new attributes
// Our Model, View and Projection matrices
// we need them to transform the incoming vertices and for lighting
// calculation
uniform mat4 mModel;
uniform mat4 mView;
uniform mat4 mProjection;
 
// Position of the "Camera" and the Light
// in Model space
uniform vec4 vEyePosition;
uniform vec4 vLightPosition;
 
// The Colors of the material
uniform float vAmbientMaterial;
uniform float vDiffuseMaterial;
uniform float vSpecularMaterial;
uniform vec4 lightColor;


// Returns the specular component of the color
vec4 GetSpecularColor(vec3 vVertexNormal, vec4 vVertexPosition)
{
    // Transform the Vertex and corresponding Normal into Model space
    vec4 vTransformedNormal = mModel * vec4( vVertexNormal, 1 );
    vec4 vTransformedVertex = mModel * vVertexPosition;
 
    // Get the directional vector to the light and to the camera
    // originating from the vertex position
    vec4 vLightDirection = normalize( vLightPosition - vTransformedVertex );
    vec4 vCameraDirection = normalize( vEyePosition - vTransformedVertex );
 
    // Calculate the reflection vector between the incoming light and the
    // normal (incoming angle = outgoing angle)
    // We have to use the invert of the light direction because "reflect"
    // expects the incident vector as its first parameter
    vec4 vReflection = reflect( -vLightDirection, vTransformedNormal );
 
    // Calculate specular component
    // Based on the dot product between the reflection vector and the camera
    // direction.
    //
    // hint: The Dot Product corresponds to the angle between the two vectors
    // hint: if the angle is out of range (0 ... 180 degrees) we use 0.0
    float spec = pow( max( 0.0, dot( vCameraDirection, vReflection )), 32.0 );
 
    return vec4( lightColor.r * spec, lightColor.g * spec, lightColor.b * spec, 1.0 ) * vSpecularMaterial;
}
 
// Ambient color component of vertex
vec4 GetAmbientColor(vec4 texCol)
{
    vec4 vAmbientColor;
    vAmbientColor.xyz = texCol.rgb * lightColor.rgb * vAmbientMaterial;
    vAmbientColor.a = 1.0;
    
    return vAmbientColor;
}
 
// Diffuse Color component of vertex
vec4 GetDiffuseColor(vec3 vVertexNormal, vec4 texCol)
{
    // Transform the normal from Object to Model space
    // we also normalize the vector just to be sure ...
    vec4 vTransformedNormal = normalize( mModel * vec4( vVertexNormal, 1 ));
 
    // Get direction of light in Model space
    vec4 vLightDirection = normalize( vLightPosition - vTransformedNormal );
 
    // Calculate Diffuse intensity
    float fDiffuseIntensity = max( 0.0, dot( vTransformedNormal, vLightDirection ));
 
    // Calculate resulting Color
    vec4 vDiffuseColor;
    vDiffuseColor.xyz = texCol.rgb * lightColor.rgb * fDiffuseIntensity * vDiffuseMaterial;
    vDiffuseColor.a = 1.0;
    
    return vDiffuseColor;
}

void main()
{
    //Sample the texture at the interpolated coordinate
    lowp vec4 bump = texture2D( bumpMap, vTexCoord );
    if (bump.x == 0.0 && bump.y == 0.0 && bump.z == 0.0) discard;
    vec3 vNormal = normalize(vec3(bump.x, bump.y, bump.z)*2.0-vec3(1.0,1.0,1.0));
    
    lowp vec4 texCol = texture2D( texture, vTexCoord);
    
    vec4 ambientColor = GetAmbientColor(texCol);
    vec4 diffuseColor = GetDiffuseColor(vNormal, texCol);
    vec4 specularColor = GetSpecularColor(vNormal, vPosition);
    
    // Combine into final color
    vec4 outColor;
    outColor = ambientColor + diffuseColor + specularColor;

    //Set the output color to the texture color
    gl_FragColor = outColor;
}
]]}

Nice work monkey!

That’s really great!
Would it be possible to achieve something like this:
http://www.youtube.com/watch?v=ggZBZqIoWcg