Undulate: an experiment with vertex shaders and buffers (Codea 1.5)

Buffers are a new feature in Codea version 1.5 that allow per-vertex data to be associated with a mesh and passed to a shader as an attribute. Shaders can be used to manipulate the vertices of a mesh, as well as manipulate how pixels are coloured. The code below shows a simple application.

The code uses the ‘–#’ convention that allows it to be pasted into a new project with the Codea tab structure preserved (by a long touch on ‘Add New Project’ and then ‘Paste Into Project’).

I believe that, in Codea version 1.5, it is not yet possible to pass a Lua number directly to a GLSL ES float attribute using a buffer. It is, however, possible to pass a Codea vec2 userdata value to a GLSL ES vec2 attribute and this code uses the x coordinate of two-dimensional vectors to achieve its objective in passing data.

The vertex and fragment shaders were developed in the new Shader Lab. Here, they are stored as strings in table Undulate to make the code easier to share on the Forum.


--# Main
--
-- Undulate
--

supportedOrientations(LANDSCAPE_ANY)
function setup()
    local default = 40
    local ti = table.insert -- Cache the function for speed
    function reset()
        local halfN = n/2 - 0.5
        m = mesh()
        m.shader = shader(Undulate.vertexShader,
            Undulate.fragmentShader)
        local dBuffer = m:buffer("d")
        dBuffer:resize(n * n * 6)
        local ver = {}
        local col = {}
        local len = math.min(WIDTH, HEIGHT) / n
        p = n / 2 * len
        for j = 0, n - 1 do
            local y1 = j - halfN
            local ySqr = y1 * y1
            for i = 0, n - 1 do
                local x1 = i - halfN
                local d = math.sqrt(x1 * x1 + ySqr) * default / n
                local x = i * len
                local y = j * len
                local c = rCol()
                ti(ver, vec3(x, y, 0))
                ti(ver, vec3(x + len, y, 0))
                ti(ver, vec3(x, y + len, 0))
                ti(ver, vec3(x, y + len, 0))
                ti(ver, vec3(x + len, y, 0))
                ti(ver, vec3(x + len, y + len, 0))
                for k = 1, 6 do
                    ti(col, c)
                    -- Needs to be a vec2 to work in Codea beta 1.5(16)
                    dBuffer[(i + j * n) * 6 + k] = vec2(d, 0)
                end
            end
        end
        m.vertices = ver
        m.colors = col
    end      
    parameter.integer("n", 1, 100, default, reset)
end

function draw()
    background(0)
    perspective() 
    camera(p, -200, 1000, p, p, 0, 0, 1, 0)
    m.shader.t = ElapsedTime * 3
    m:draw()
end

function rCol()
    return color(
    math.random(255),
    math.random(255),
    math.random(255))
end

--# ShaderUndulate
Undulate = {

vertexShader = [[
//
// Vertex shader: Undulate
//

uniform mat4 modelViewProjection;
uniform float t;

attribute vec4 position;
attribute vec4 color;
attribute vec2 d; // Needs to be vec2 to work in Codea 1.5(16)

varying lowp vec4 vColor;

void main() {
    float z = 50.0 * sin(d.x + t);
    vec4 p = vec4(position.x, position.y, z, position.w);
    vColor = color;
    gl_Position = modelViewProjection * p;
}
]],

fragmentShader = [[
//
// Fragment shader: Undulate
//

varying lowp vec4 vColor;

void main() {
    gl_FragColor = vColor;
}
]]
}

This needed a little tweaking to run in Codea 2021. Here’s a zip if anyone wants to run it. It’s pretty cool tbh!

In trying to understand this code, I went through it and renamed the variables to be descriptive of their purpose and not just single letters.

I think I got it right, though it’s possible some names are inaccurate due to me misunderstanding the code.

In case anyone else who wants to understand this shader could learn better from descriptive variable names, I’m pasting a zip of that version (it also includes the original version for reference).

Here’s something similar that uses Craft spheres. Alter the values of the sliders.

viewer.mode=STANDARD

function setup()
    parameter.watch("fps")
    parameter.number("amplitude",1,6,6) 
    parameter.number("speed",1,3,1.5) 
    parameter.number("wave",1,6,3) 
    assert(OrbitViewer, "Please include Cameras as a dependency")        
    scene = craft.scene()
    scene.ambientColor=color(255)
    v=scene.camera:add(OrbitViewer, vec3(0,0,0),130,0,3000)
    v.rx,v.ry=-45,-20
    grid,s,f,col,sph=40,.1,90,{},{}
    for x=-grid,grid do
        sph[x],col[x]={},{}
        for y=-grid,grid do
            sph[x][y]=createSphere(vec3(x,y,0))
            col[x][y]=color(math.random(255),math.random(255),math.random(255))
        end
    end
end

function draw()
    update(DeltaTime)
    scene:draw()
    fps=1//DeltaTime
end

function update(dt)
    scene:update(dt)
    f=f+(s*speed)
    if f>180 then
        f=f-180
    end
    for x=-grid,grid do
        for y=-grid,grid  do
            sph[x][y].material.diffuse=col[x][y]
            sph[x][y].position=vec3(sph[x][y].x,sph[x][y].y,math.sin(math.sqrt((x/wave)^2+(y/wave)^2)-f*speed)*amplitude)
        end
    end
end

function createSphere(p)
    local pt=scene:entity()
    pt.position=p
    pt.model = craft.model.icosphere(.7,1)
    pt.material = craft.material(asset.builtin.Materials.Specular)
    return pt
end

nice! why is it necessary to set the color each time thru update?

@RonJeffries It’s not. I was thinking of having colors ripple out with the waves but never got around to it. Here’s the updated code setting the colors once.

viewer.mode=STANDARD

function setup()
    parameter.watch("fps")
    parameter.number("amplitude",1,6,6) 
    parameter.number("speed",1,3,1.5) 
    parameter.number("wave",1,6,3) 
    assert(OrbitViewer, "Please include Cameras as a dependency")        
    scene = craft.scene()
    scene.ambientColor=color(255)
    v=scene.camera:add(OrbitViewer, vec3(0,0,0),130,0,3000)
    v.rx,v.ry=-45,-20
    grid,s,f,sph=40,.1,90,{}
    for x=-grid,grid do
        sph[x]={}
        for y=-grid,grid do
            sph[x][y]=createSphere(vec3(x,y,0))
        end
    end
end

function draw()
    update(DeltaTime)
    scene:draw()
    fps=1//DeltaTime
end

function update(dt)
    scene:update(dt)
    f=f+(s*speed)
    if f>180 then
        f=f-180
    end
    for x=-grid,grid do
        for y=-grid,grid  do
            sph[x][y].position=vec3(sph[x][y].x,sph[x][y].y,math.sin(math.sqrt((x/wave)^2+(y/wave)^2)-f*speed)*amplitude)
        end
    end
end

function createSphere(p)
    local pt=scene:entity()
    pt.position=p
    pt.model = craft.model.icosphere(.7,1)
    pt.material = craft.material(asset.builtin.Materials.Specular)
    pt.material.diffuse=color(math.random(255),math.random(255),math.random(255))
    return pt
end

@RonJeffries Here’s what I was originally going for when I was updating the color each time thru the draw function. I added the ripple and colorize sliders.

viewer.mode=STANDARD

function setup()
    fill(255)
    parameter.number("amplitude",1,6,6) 
    parameter.number("speed",1,3,1.5) 
    parameter.number("wave",1,6,3) 
    parameter.boolean("ripple",false) 
    parameter.color("colorize",color(0,0,255)) 
    assert(OrbitViewer, "Please include Cameras as a dependency")        
    scene = craft.scene()
    scene.ambientColor=color(255)
    v=scene.camera:add(OrbitViewer, vec3(0,0,0),130,0,3000)
    v.rx,v.ry=-45,-20
    grid,s,f,col,sph=40,.1,90,{},{}
    for x=-grid,grid do
        sph[x],col[x]={},{}
        for y=-grid,grid do
            col[x][y]=color(math.random(255),math.random(255),math.random(255))
            sph[x][y]=createSphere(vec3(x,y,0))
        end
    end
end

function draw()
    update(DeltaTime)
    scene:draw()
    text("FPS  "..1//DeltaTime,WIDTH/2,HEIGHT-50)
end

function update(dt)
    scene:update(dt)
    f=f+(s*speed)
    if f>180 then
        f=f-180
    end
    for x=-grid,grid do
        for y=-grid,grid  do
            sph[x][y].material.diffuse=col[x][y]
            if ripple then
                if math.sin(math.sqrt((x/wave)^2+(y/wave)^2)-f*speed)<-.98 then
                    sph[x][y].material.diffuse=colorize
                end
            end
            sph[x][y].position=vec3(sph[x][y].x,sph[x][y].y,math.sin(math.sqrt((x/wave)^2+(y/wave)^2)-f*speed)*amplitude)
        end
    end
end

function createSphere(p)
    local pt=scene:entity()
    pt.position=p
    pt.model = craft.model.icosphere(.7,1)
    pt.material = craft.material(asset.builtin.Materials.Specular)
    return pt
end

yes, but with your previous i tried to init sph color just once, instead if each time thru and just got white. i thought one should be able to save the continual setting of material color, but no. maybe i did it wrong.

@RonJeffries Once you set sph[x][y].position or sph[x][y].material.diffuse, those should remain what you set until you change them. On my above code with the ripple color, I have to save my original color in a table to put it back after the ripple color passes. In setup, I’m not setting the color for each sphere, but instead I’m setting the color in a corresponding table so in update I’m either setting the color from the table or the ripple color. If I don’t set the diffuse color in update, all the spheres will be white or eventually the ripple color.

yes i must have done something wrong.

@dave1707 its really cool!

Would it be hard to have the selected color apply to all the balls, not just the crests, but also apply a gradient effect, so that the balls get darker and darker the closer to the bottom they are?

@UberGoober It should be possible to do a lot of modification to the colors based on the position of the sphere in the sine curve. Don’t have time right now to play with it.

@RonJeffries @dave1707 here’s my attempt to apply the OP shader version to a craft shader (includes the OP version for comparison).

It doesn’t fully work, and the way it fails illustrates the biggest frustration I’ve had with converting mesh shaders to craft shaders.

The functions called setMeshDistanceBufferFor in the mesh version, and setCraftDistanceBufferFor In the Craft version, isolate the crucial code for making the shaders move each square independently (I think).

It works in the mesh version but I haven’t found a way to get the craft shader to do it.

@john out of nowhere it popped into my head that this could pretty easily become a cool first-person game where you jump from platform to platform…

…if only Craft shaders could use buffers.

Please consider this my semi-regular plea for Craft shader buffers.