Adding light sources to a 3D scene

Is it possible to add light sources in a 3D scene in Codea? In the following example, I’ve drawn a cube. Without a light source, it’s quite unpleasant to look at! Also, is there a way to stroke the outlines of each square on the cube? I intend to add the ability to rotate the cube with single-touch in the future.

-- Cube
function setup()
    displayMode(FULLSCREEN)
    
    cube = mesh()

    cube.vertices =      {vec3(WIDTH/2 - 100, HEIGHT/2 - 100, 0),
                          vec3(WIDTH/2 + 100, HEIGHT/2 - 100, 0),
                          vec3(WIDTH/2 - 100, HEIGHT/2 + 100, 0),
                        
                          vec3(WIDTH/2 + 100, HEIGHT/2 - 100, 0),
                          vec3(WIDTH/2 + 100, HEIGHT/2 + 100, 0),
                          vec3(WIDTH/2 - 100, HEIGHT/2 + 100, 0),
                        
                          vec3(WIDTH/2 - 100, HEIGHT/2 - 100, 0),
                          vec3(WIDTH/2 - 150, HEIGHT/2, 1),
                          vec3(WIDTH/2 - 150, HEIGHT/2 + 150, 1),
                        
                          vec3(WIDTH/2 - 150, HEIGHT/2 + 150, 1),
                          vec3(WIDTH/2 - 100, HEIGHT/2 + 100, 0),
                          vec3(WIDTH/2 - 100, HEIGHT/2 - 100, 0),
                        
                          vec3(WIDTH/2 - 150, HEIGHT/2 + 150, 1),
                          vec3(WIDTH/2 - 100, HEIGHT/2 + 100, 0),
                          vec3(WIDTH/2 + 100, HEIGHT/2 + 100 , 0),
                        
                          vec3(WIDTH/2 + 100, HEIGHT/2 + 100 , 0),
                          vec3(WIDTH/2 - 150, HEIGHT/2 + 150, 1),
                          vec3(WIDTH/2 + 50, HEIGHT/2 + 150, 1)}
end                    

function draw()
    background(40, 40, 50)

    cube:draw()
end

Hello @the_dude. It is possible to write shaders that give the effect of lighting from light sources. If you look at some of the recent discussions on the forum about shaders, you’ll come across examples of that.

Thank you, @mpilgrem

You’ve piqued my interest… broadly I have now implemented an ADS lighting model (off some interweb page) and can use it vertex normalled, or bump mapped normalled.

I will try and work this up as an example against your cube… but first, your cube needs to be a cube in 3d space, right now, it’s not for 2 reasons. It contains only 6 triangles, and a cube has 6 faces of 2 triangles each. You are still drawing in “2d” mode.

Below is a new version with the cube from the 3d lab, drawn in perspective with a texture on it just to make it clearer to see.

Next steps: shaders for lighting

Edit:I put in rotation… it’s not correct explicitly, but you can spin the cube with your finger which is good enough for this task.

-- Cube
function setup()
    displayMode(FULLSCREEN)
    
    cube = mesh()
    
    --vertices for the corners of the cube (stolen from 3d lab)
    local vertices = {
      vec3(-0.5, -0.5,  0.5), -- Left  bottom front
      vec3( 0.5, -0.5,  0.5), -- Right bottom front
      vec3( 0.5,  0.5,  0.5), -- Right top    front
      vec3(-0.5,  0.5,  0.5), -- Left  top    front
      vec3(-0.5, -0.5, -0.5), -- Left  bottom back
      vec3( 0.5, -0.5, -0.5), -- Right bottom back
      vec3( 0.5,  0.5, -0.5), -- Right top    back
      vec3(-0.5,  0.5, -0.5), -- Left  top    back
    }

    -- now construct a cube out of the vertices above
    cube.vertices = {
      -- Front
      vertices[1], vertices[2], vertices[3],
      vertices[1], vertices[3], vertices[4],
      -- Right
      vertices[2], vertices[6], vertices[7],
      vertices[2], vertices[7], vertices[3],
      -- Back
      vertices[6], vertices[5], vertices[8],
      vertices[6], vertices[8], vertices[7],
      -- Left
      vertices[5], vertices[1], vertices[4],
      vertices[5], vertices[4], vertices[8],
      -- Top
      vertices[4], vertices[3], vertices[7],
      vertices[4], vertices[7], vertices[8],
      -- Bottom
      vertices[5], vertices[6], vertices[2],
      vertices[5], vertices[2], vertices[1],
    }
    
    --now texture it
    -- all the unique texture positions needed
    local texvertices = { vec2(0,0),
                          vec2(1,0),
                          vec2(0,1),
                          vec2(1,1) }
                
    -- apply the texture coordinates to each triangle
    cube.texCoords = {
      -- Front
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Right
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Back
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Left
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Top
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Bottom
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
    }
    cube.texture = "Cargo Bot:Codea Icon"
    currentModelMatrix = modelMatrix()
end                    

function draw()
    background(40, 40, 50)
    camera(-1,-1,-5,0,0,0)
    perspective()
    modelMatrix(currentModelMatrix)
    --do rotation for touch
    if CurrentTouch.state == MOVING then
        rotate(CurrentTouch.deltaX,0,1,0)
        rotate(CurrentTouch.deltaY,1,0,0)
        currentModelMatrix = modelMatrix()
    end
    cube:draw()
end

Wow! Thanks a bunch @spacemonkey! I can’t wait to see the next version of your cube.

OK, here is a new version.
Make sure you check out all the parameters especially the texture ones.


-- Cube
function setup()
    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))
    parameter.color("surfaceColor", color(191,41,85,255))
    allTextures = {
                    CAMERA,
                    "Cargo Bot:Codea Icon",
                    "Small World:Store Extra Large",
                    "Small World:Windmill",
                    "Tyrian Remastered:Boss D",
                  }

    cameraSource(CAMERA_FRONT)    
        
    cube = mesh()
    
    --vertices for the corners of the cube (stolen from 3d lab)
    local vertices = {
      vec3(-0.5, -0.5,  0.5), -- Left  bottom front
      vec3( 0.5, -0.5,  0.5), -- Right bottom front
      vec3( 0.5,  0.5,  0.5), -- Right top    front
      vec3(-0.5,  0.5,  0.5), -- Left  top    front
      vec3(-0.5, -0.5, -0.5), -- Left  bottom back
      vec3( 0.5, -0.5, -0.5), -- Right bottom back
      vec3( 0.5,  0.5, -0.5), -- Right top    back
      vec3(-0.5,  0.5, -0.5), -- Left  top    back
    }

    -- now construct a cube out of the vertices above
    cube.vertices = {
      -- Front
      vertices[1], vertices[2], vertices[3],
      vertices[1], vertices[3], vertices[4],
      -- Right
      vertices[2], vertices[6], vertices[7],
      vertices[2], vertices[7], vertices[3],
      -- Back
      vertices[6], vertices[5], vertices[8],
      vertices[6], vertices[8], vertices[7],
      -- Left
      vertices[5], vertices[1], vertices[4],
      vertices[5], vertices[4], vertices[8],
      -- Top
      vertices[4], vertices[3], vertices[7],
      vertices[4], vertices[7], vertices[8],
      -- Bottom
      vertices[5], vertices[6], vertices[2],
      vertices[5], vertices[2], vertices[1],
    }
    
    --now texture it
    -- all the unique texture positions needed
    local texvertices = { vec2(0,0),
                          vec2(1,0),
                          vec2(0,1),
                          vec2(1,1) }
                
    -- apply the texture coordinates to each triangle
    cube.texCoords = {
      -- Front
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Right
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Back
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Left
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Top
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
      -- Bottom
      texvertices[1], texvertices[2], texvertices[4],
      texvertices[1], texvertices[4], texvertices[3],
    }
    --setup vertex normals
    createNormalsFromSurfaceNormals()
    
    cube.texture = "Cargo Bot:Codea Icon"
    cube.shader = shader(ADSLighting.vertexShader, ADSLighting.fragmentShader)
    cube.shader.vLightPosition = vec4(-2,2,-3,1)
    cube.shader.vEyePosition = vec4(0,0,-3,1)
    cube.shader.useTexture = false
    cube.shader.useBumpMap = false 

    currentModelMatrix = matrix(0.97, 0.15, -0.19, 0.00, -0.09, 0.96, 0.28, 0.00, 0.22, -0.26, 0.94, 0.00, 0.00, 0.00, 0.00, 1.00)

    
    parameter.integer("Texture",0,5,0,changeTexture)
end 

function changeTexture()
    if Texture == 0 then
        cube.shader.useTexture = false
    else
        cube.shader.useTexture = true
        cube.texture = allTextures[Texture]
    end
end                   

function createNormalsFromSurfaceNormals()
    --this assumes flat surfaces, and hard edges between triangles (which is good for cubes)
    normalBuffer = cube:buffer("normal")
    normalBuffer:resize(cube.size)
    for i=1, cube.size/3 do
        --determine the surfacenormal for the triangle
        normal = (cube:vertex(i*3-1) - cube:vertex(i*3-2)):cross(cube:vertex(i*3) - cube:vertex(i*3-2))
        normalBuffer[i*3-2] = normal
        normalBuffer[i*3-1] = normal
        normalBuffer[i*3] = normal
    end  
end

function draw()
    background(40, 40, 50)
    camera(0,0,-3,0,0,0)
    perspective()
    modelMatrix(currentModelMatrix)
    --do rotation for touch
    if CurrentTouch.state == MOVING then
        rotate(CurrentTouch.deltaX,0,1,0)
        rotate(CurrentTouch.deltaY,1,0,0)
        currentModelMatrix = modelMatrix()
    end
    cube.shader.mModel = modelMatrix()
    cube.shader.mView = viewMatrix()
    cube.shader.mProjection = projectionMatrix()
    cube.shader.vAmbientMaterial = ambient  
    cube.shader.vDiffuseMaterial = diffuse
    cube.shader.vSpecularMaterial = specular
    cube.shader.lightColor = lightColor
        
    cube:setColors(surfaceColor)
    
    cube:draw()
end

ADSLighting = {

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;
attribute vec3 normal;

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

void main()
{
    //Pass the mesh color to the fragment shader
    vNormal = normal;
    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;
uniform lowp sampler2D bumpMap;
uniform bool useTexture;
uniform bool useBumpMap;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;
varying highp vec3 vNormal;
varying lowp vec4 vColor;
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;
 
// 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
    vec3 curNormal;
    if (useBumpMap) {
        lowp vec4 bump = texture2D( bumpMap, vTexCoord );
        if (bump.x == 0.0 && bump.y == 0.0 && bump.z == 0.0) discard;
        curNormal = normalize(vec3(bump.x, bump.y, bump.z)*2.0-vec3(1.0,1.0,1.0));
    }
    else {
        curNormal = vNormal;
    }
    lowp vec4 curCol;
    if (useTexture) {
        curCol = texture2D( texture, vTexCoord);
        if (curCol.a == 0.0) discard;
    }
    else {
        curCol = vColor;
    }
    
    vec4 ambientColor = GetAmbientColor(curCol);
    vec4 diffuseColor = GetDiffuseColor(curNormal, curCol);
    vec4 specularColor = GetSpecularColor(curNormal, vPosition);
    
    // Combine into final color
    vec4 outColor;
    outColor = ambientColor + diffuseColor + specularColor;

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

The shader in the previous post should be complete for textured/non textured, bumpMap/non bumpMap.

Currently though the code is only feeding texture/non texture and non bumpMapped into it. I will try and figure out a couple of nice demo bumpMaps tomorrow.

You can adjust light settings: Ambient, the brightness if the surface is away from the light. Diffuse, the brightness when the surface faces the light directly. Specular the brightness of the light when the surface reflects it straight into your eye.
Light Color will be the color when it’s specular, and also the light color will dampen the underlying surface/texture color when it’s not full white.

You can adjust the texture: 0 is the surface color, 1 is camera, 2-5 are other textures. (texture code from Codea demos.)

Shader code nicked off an internet tutorial, although I had to rejig it a fair bit, so it’s mainly the lighting math I copied.

Thanks @spacemonkey! This is wonderful! I wish I could understand half of what you just explained about graphics. Can you point me to any materials, tutorials to help me learn more?

Great example! something like this should be included as a Codea sample.

Just a big " …!!!"
Thanks for sharing, a great starting base.

@spacemonkey its pretty :D! iv been trying all night to get it to work with @Xavier 3D tile code … :stuck_out_tongue:

No new version today… I started looking into bumpmapping and learnt a whole new raft of things i didn’t know before :wink:

I’ve now added NTB (normal, tangent, binormal) mapping for all vertexes, and almost got it back to where I started the evening. This is a prereq for bump mapping, my previous approach was flawed when you went out of 2d space.

Hopefully get towards a final version in the next day or 2.

.@warox theoretically it should work with other meshes, but I’m sure I have bad code that would stuff it up, I’ll have a nosey at that one after I finish getting the cube working.

After that I’m thinking maybe refactor it to make it easier to plug any mesh onto, and then try and write up a bit of a step by step tutorial of how it works.

@spacemonkey the 3Dtile based rpg uses same type of mesh as illustrated in your code. it uses same system like the mesh from 3D lab

Right, heres a nearly working correctly version. It has bumpmaps, 0 is off, 1 is generated from texture (slow when you first switch to it) and 2 is “spherical”.

There is a bug I haven’t tracked down, sometimes if you switch bump mapping to 1 and then switch the textures around, sometimes you get the bump map but with a black color…
This wouldn’t be an issue in a real scenario, just in this one where we switch textures all the time.

Code moved to github because it’s got a little long:
https://gist.github.com/sp4cemonkey/5033682

Real nice @spacemonkey !

This is some good looking work - both the graphics and the code. We could use a little primer on that crazy math making the bumpmaps, though!

Also, what’s with the magic matrix?

Now that I finally got it working I’m going to extract all the necessary for lighting code into a class for ease of use elsewhere. Once I’ve done that I’ll try plugging it into .@Xavier’s 3d tile based example which I’m sure will draw out some bugs in my code.

After that I’ll go through and do a heavily commented up version to try and explain what it’s doing in as simple terms as I can.

Some of the maths is honestly beyond me, I understand the concepts, but got the actual maths from google/books.

Another thing I’m thinking, is the generated normal map (bumpmap) from texture is very slow, but I could write another shader to do this task and make it quick.

Updated Gist:
https://gist.github.com/sp4cemonkey/5033682

I’ve extracted all the lit mesh stuff into it’s own class which I’ve attempted to document regarding usage. I also fixed some bugs which meant it wouldn’t give the right results for translated meshes.

Modified the main (the cube example using my lit mesh class) it now rotates from parameters and translates based on touch, this was mainly to make it easier to test/confirm my bug fixes.

Hopefully it’s clean and usable…

I’ll let you know; I’ve been using your code for a sample lighting thing; you’ve merged it nicely (I was trying to do the same).

One problem I was having - I hae some meshes without any texCoords, and one version of the code crashed with that scenario; it required a texCoord set. Is that still the same?

The earliest version you made didn’t require texCoords, so I am wondering if there’s some sort of swtich to put in the shader.

.@aciolino you are right, when I added bump mapping I needed to derive tangent space (tangent, normal and binormal for each vertex) and that made it dependant on texCoords…

I’ve updated the gist with a new version, the change is the deriveVertexNTB() function which now detects a lack of texCoords and fakes up some NTB which is fine since you aren’t texturing or bump mapping.