Animated isometric SIN function

This is my first attempt at some Codea goodness (so be gentle), based on some old basic stuff I once wrote.

It displays an animated SIN function in an isometric grid, with the centre of the function being touch. It supports multi touch so you can do multiple waves to look at interference patterns.

-- 3d sin

-- Use this function to perform your initial setup
function setup()
    parameter("speed", 0, 0.5,0.1)
    parameter("wavelength", -1.5,-0.25,-.7)
    gridSize = 30
    isoW = 20*math.cos(math.rad(30))
    isoH = 20*math.sin(math.rad(30))
    heights = {}
    coords = {}
    touches = {}
    cens = {}
    for x = 1,gridSize do
        heights[x] = {}
        coords[x] = {}
        for y = 1,gridSize do
            heights[x][y] = 0
            coords[x][y] = {x = WIDTH/2+(x-y)*isoW, y = 50+(x+y)*isoH}
        end
    end
    offset = 0
end

function touched(touch)
    if touch.state == ENDED then
    else
        cens[touch.id] = { x = ((touch.x - WIDTH/2)/isoW + (touch.y-50)/isoH)/2, y = ((touch.y-50)/isoH - (touch.x - WIDTH/2)/isoW)/2, t = os.clock() }
    end
    for k, touch in pairs(cens) do
        if os.clock() - touch.t > 1 then
            cens[k] = nil
        end    
    end
end    

-- This function gets called once every frame
function draw()
    for x = 1,gridSize do
        for y = 1,gridSize do
            heights[x][y] = 0
            for k, touch in pairs(cens) do
                heights[x][y] = heights[x][y] + math.sin((math.sqrt((touch.x-x)^2+(touch.y-y)^2)*wavelength)+offset)*20
            end
        end
    end
    offset = offset + speed
    -- This sets a dark background color 
    background(40, 40, 50)

    -- This sets the line thickness
    noSmooth()
    noStroke()
    
    -- Do your drawing here
    for x = 1,(gridSize-1) do
        for y = 1,(gridSize-1) do
            line(coords[x][y].x, coords[x][y].y+heights[x][y], coords[x+1][y].x, coords[x+1][y].y+heights[x+1][y])
            line(coords[x][y].x, coords[x][y].y+heights[x][y], coords[x][y+1].x, coords[x][y+1].y+heights[x][y+1])
        end
    end
end

Even though I have no clue how that works, it is by far the coolest thing I have ever seen in Codea. You are a god!

It is hypnotic, @spacemonkey! I put this up the top to see it better:

supportedOrientations(ANY)

Would it run faster in mesh? I note if you give it more cells it slows down quickly.

I don’t know anything about mesh… I’ll look it up and try it :wink:

So, presumably I’d want a mesh of vec3s and modify the wave form on the z axis… I can kinda follow that, but how do I project the mesh into isometric or some other 3d layout for rendering?

I think the 3d Lab example is the one to follow… better do some work, this is gonna take more thought than lunch time has to offer.

That’s hypnotic, @spacemonkey.

Wow! Way cool!
It didn’t look quite right to me, though, so I changed the wavelength to be

parameter("wavelength", -1,-.25,-.5)

Also tried

parameter("wavelength"' -10,10,0)

and got some very interesting effects at the extremes.

(Previous should have been -1,-.5,-.25)

Quite right on the wavelength. I’d been bashing my head against remembering maths from 20 years ago to do the reverse calculations for touch and ended up reversing the iso projection which meant the waves go the wrong way :wink:

So I did a mesh conversion. In a nutshell the drawing performance is AMAZING, however, on the flip side, I’m controlling the height field by modifying the y element of the vertices, and the performance of modifying the vertices in the mesh is awful. So the original at the top is better performing (about 25fps vs 15fps for the mesh version.

Note, the rest of this is pretty scrappy now as I have played with other optimisations like precalculating distances, but because the performance is poor I didn’t actually clean these up.

Mesh below


-- 3d sin

-- Use this function to perform your initial setup
function setup()
    --myFPSReporter = FPSReporter(4)
    parameter("speed", 0, 0.5,0.1)
    parameter("wavelength", 0.25,1.5,.7)
    gridSize = 30
    cens = {}
    vertcount=1
    surfaceMesh = mesh()
    verts = {}
    texverts = {}
    for x = 1,gridSize do
        for z = 1,gridSize do
            table.insert(verts, vertcount, vec3(x,0,z))
            table.insert(verts, vertcount+1, vec3(x+1,0,z))
            table.insert(verts, vertcount+2, vec3(x,0,z+1))
            table.insert(verts, vertcount+3, vec3(x+1,0,z))
            table.insert(verts, vertcount+4, vec3(x+1,0,z+1))
            table.insert(verts, vertcount+5, vec3(x,0,z+1))
            table.insert(texverts, vertcount, vec2(0,0))
            table.insert(texverts, vertcount+1, vec2(1,0))
            table.insert(texverts, vertcount+2, vec2(0,1))
            table.insert(texverts, vertcount+3, vec2(1,0))
            table.insert(texverts, vertcount+4, vec2(1,1))
            table.insert(texverts, vertcount+5, vec2(0,1))
            vertcount = vertcount + 6
        end
    end
    t = genWireframe(30)
    surfaceMesh.texture = t
    surfaceMesh.vertices = verts
    surfaceMesh.texCoords = texverts
    vertcount = vertcount -1
    --verts = nil
    texverts = nil
    offset = 0
    isoW = 20*math.cos(math.rad(30))
    isoH = 20*math.sin(math.rad(30))
end

function touched(touch)
    if touch.state == ENDED then
    else
        --cens[touch.id] = { x = touch.x, y = touch.y, t = os.clock() }
        
         
        x = ((touch.x - WIDTH/2)/isoW + (touch.y-50)/isoH)/2
        y = ((touch.y-50)/isoH - (touch.x - WIDTH/2)/isoW)/2
        dists = {}
        for j = 1, vertcount do
            vert = surfaceMesh:vertex(j)
            dists[j] = (math.sqrt((x-vert.x)^2+(y-vert.z)^2)*wavelength)
        end
        cens[touch.id] = {d = dists, t = os.clock()}
        dists = nil
    end
    for k, touch in pairs(cens) do
        if os.clock() - touch.t > 1 then
            cens[k] = nil
        end    
    end
end    

-- This function gets called once every frame
function draw()
    --verts = surfaceMesh.vertices
    for j = 1, vertcount do
        newy = 0
        for k, touch in pairs(cens) do
            newy = newy + math.sin(cens[k].d[j]+offset)
        end
        surfaceMesh:vertex(j, vec3(verts[j].x, newy, verts[j].z))
    end        
    
    offset = offset + speed
    -- This sets a dark background color 
    background(40, 40, 50)
    --myFPSReporter:draw(3)
    
    -- First arg is FOV, second is aspect
    perspective(45, WIDTH/HEIGHT)
 
    -- Position the camera up and back, look at origin
    camera(-20, 20,-20, gridSize/2,0,gridSize/2, 0,1,0)
    
    -- Do your drawing here
    surfaceMesh:draw()
end

-- Wireframe texture
function genWireframe(w)
    local t = image(w, w)
   
    for y=1, w do
        for x=1, 3 do
            t:set(x, y, 255, 255, 255, 255)
        end
    end
       
    for x=1, w do
        for y=1, 3 do    
            t:set(x, y, 255, 255, 255, 255)
        end
    end
       
    for x=2, w-1 do
        t:set(x+1, x, 255, 255, 255, 255)
        t:set(x, x, 255, 255, 255, 255)
        t:set(x-1, x, 255, 255, 255, 255)
    end
   
    t:set(w, w, 255, 255, 255, 255)
    t:set(w-1, w, 255, 255, 255, 255)
   
    return t
end

Hi @spacemonkey, based on your code I tried keeping all the vertex information in a table and recreate the mesh from scratch rather than adjusting vertices individually, but that didn’t help much…

Fred, I tried a number of similar things myself, but everything seemed to be similarly poor. I guess at draw time it reships the table (or in some other way processes changes) to the graphics chip or something and this is where the performance drop off comes from.

@spacemonkey Love it!

Here’s my take on using meshes - sticking an image on each grid intersection.

-- 3d sin

-- Use this function to perform your initial setup
function setup()
    parameter("speed", 0, 1,0.4)
    parameter("wavelength", -1,-.1,-.3)
    gridSize = 80
    
    g=mesh()
    g.texture="Tyrian Remastered:Energy Orb 2"
    isoW = 8*math.cos(math.rad(30))
    isoH = 8*math.sin(math.rad(30))
    heights = {}
    coords = {}
    touches = {}
    cens = {}
    for x = 1,gridSize do
        heights[x] = {}
        coords[x] = {}
        for y = 1,gridSize do
            heights[x][y] = 0
            coords[x][y] = {x = WIDTH/2+(x-y)*isoW, y = 50+(x+y)*isoH}
        end
    end
    offset = 0
end

function touched(touch)
    if touch.state == ENDED then
    else
        cens[touch.id] = { x = ((touch.x - WIDTH/2)/isoW + (touch.y-50)/isoH)/2, y = ((touch.y-50)/isoH - (touch.x - WIDTH/2)/isoW)/2, t = os.clock() }
    end
    for k, touch in pairs(cens) do
        if os.clock() - touch.t > 1 then
            cens[k] = nil
        end    
    end
end    

-- This function gets called once every frame
function draw()
    for x = 1,gridSize do
        for y = 1,gridSize do
            heights[x][y] = 0
            for k, touch in pairs(cens) do
                heights[x][y] = heights[x][y] + math.sin((math.sqrt((touch.x-x)^2+(touch.y-y)^2)*wavelength)+offset)*20
            end
        end
    end
    offset = offset + speed
    -- This sets a dark background color 
    background(40, 40, 50)

    -- This sets the line thickness
    noSmooth()
    noStroke()
g:clear()
    -- Do your drawing here
    for x = 1,(gridSize-1) do
        for y = 1,(gridSize-1) do
       --     line(coords[x][y].x, coords[x][y].y+heights[x][y], coords[x+1][y].x, coords[x+1][y].y+heights[x+1][y])
    --        line(coords[x][y].x, coords[x][y].y+heights[x][y], coords[x][y+1].x, coords[x][y+1].y+heights[x][y+1])
            
              local idx = g:addRect(coords[x][y].x, coords[x][y].y+heights[x][y],12,12) 
                g:setRectTex(idx, 0, 0, 1, 1)
        end
    end
    g:draw()
end