Trying to make a dynamic 3D line renderer.

I’ve been working on implementing the particle system from unity into Codea craft, however in order to get trails working on the particles. I first must create a line rendering component. One that does not draw pixels, but rather creates a polygon with (n) points and connects them dynamically. Of course, you’d have to make the line face the camera, but that parts pretty easy. Here is where I’m getting confused… I’ve been working on getting 2 points on the line dynamically generated, but I’m having trouble with the indices, i think? Here is my code.

function createLine(pos,p1width,p2width)
    -- Create a blank model
    local m = craft.model()
    pos=pos or {}
    p1=table.remove(pos,1) or vec3(1,-1,0)
    p1width=p1width or 2
    p2=table.remove(pos,#pos) or vec3(-1,1,0)
    p2width=p2width or 2
    
    local positions = computeLineMesh(p1,p2)
    local indices = {3,2,1,4,3,1}
    
    -- Update model vertices using tables
    m.positions = positions
    m.indices = indices
    newEntity("lineT")
    add_model(lineT,m)
end

function computeLineMesh(start,fin,lineWidth)
normal = start:cross(fin)
side=normal:cross(fin-start)
side=side:normalize()
lineWidth=lineWidth or 10
a = start + side * (lineWidth / 2)
b = start + side * (lineWidth / -2)
c = fin + side * (lineWidth / 2)
d = fin + side * (lineWidth / -2)
return {a,b,c,d}
end

This code actually works! However, it almost always produces just a single triangle, and not a proper rectangle/polygon. I’m not extremely mathematical, so some help in that department is always appreciated. Though I suspect it’s my use of indices that causes issues.

I’m getting a bit closer to the result. I set the indices to 3,2,1,4,2,1, and now we have two distinct triangles. Still a bit confused as to why they’re thinner in the center, but it’s progress.

After rendering it with a wireframe shader, it’s pretty obvious why! I wonder how I can fix that? Maybe a 3rd triangle.
Edit** actually, you don’t have to make a new triangle Here are the proper indices: {3,2,1,3,4,2}

@arismoko Im not following what you’re after. If you just want lines in 3D, try this.
The format is scene.debug:line(vec3(x1,y1,z1),vec3(x2,y2,z2),color(r,g,b))

viewer.mode=FULLSCREEN

function setup()
    assert(OrbitViewer, "Please include Cameras (not Camera) as a dependency")        
    scene = craft.scene()
    v=scene.camera:add(OrbitViewer,vec3(0,0,0), 700, 0, 2000)
    v.camera.farPlane=3000
    tab1,tab2={},{}
    for z=1,150 do
        x1,y1,z1=math.random(-150,150),math.random(-150,150),math.random(-150,150)
        x2,y2,z2=math.random(-150,150),math.random(-150,150),math.random(-150,150)
        table.insert(tab1,vec3(x1,y1,z1))
        table.insert(tab2,vec3(x2,y2,z2))
    end
end

function draw()
    update(DeltaTime)
    scene:draw() 
    for a=1,#tab1 do
        scene.debug:line(vec3(tab1[a].x,tab1[a].y,tab1[a].z),
            vec3(tab2[a].x,tab2[a].y,tab2[a].z),color(28, 255, 0)  )
    end
end

function update(dt)
    scene:update(dt)
end

@dave1707 I’m trying to dynamically generate a mesh line model, given a list of points that describes the line. Doing this so I can create 3D trails, etc. it’s way harder than it looks though.
function computeLineMesh(pos,lineWidth) local offset=0 local positions={} local indices={} for k,v in pairs(pos) do if start==nil then start=pos[k] print(start) end fin=pos[k+1] offset=0 if fin~= nil then normal = start:cross(fin) side=normal:cross(fin-start) side=side:normalize() lineWidth=lineWidth or 2 a = start + side * (lineWidth / 2) b = start + side * (lineWidth / -2) c = fin + side * (lineWidth / 2) d = fin + side * (lineWidth / -2) table.insert(positions,a) table.insert(positions,b) table.insert(positions,c) table.insert(positions,d) if k<2 then table.insert(indices,2*k+1) table.insert(indices,2*k) table.insert(indices,2*k-1) table.insert(indices,2*k+1) table.insert(indices,2*k+2) table.insert(indices,2*k) else table.insert(indices,2*k+4+offset) table.insert(indices,2*k+3+offset) table.insert(indices,2*k+2+offset) table.insert(indices,2*k+1+offset) table.insert(indices,2*k+2+offset) table.insert(indices,2*k+3+offset) --tiny tri table.insert(indices,2*k+2) table.insert(indices,2*k+3) table.insert(indices,2*k-1) end start=fin end end return positions,indices end

That’s where I’m at now, forgive me for the horrendous code, still trying to wrap my head around how this works. it can draw a line about 50 percent of the time to up to any 3 points you want. The order you list the points is really important.

Here you go @dave1707 just add it to your setup
function createTestLine(name,pos,lw) local m = craft.model() local positions,indices= computeLineMesh(pos,lw) m.positions = positions m.indices = indices newEntity("linef") --makes an entity in the active scene add_model(linef,m) linef.update=function(entity) lookAt(linef,game.camera.position) end end

function computeLineMesh(pos,lineWidth)
    local offset=0
    local positions={}
    local indices={}
    for k,v in pairs(pos) do
        start=pos[k]
        fin=pos[k+1]
        offset=offset+1
        if fin~= nil then
            normal = start:cross(fin)
            side=normal:cross(fin-start)
            side=side:normalize()
            lineWidth=lineWidth or 2
            a = start + side * (lineWidth / 2)
            b = start + side * (lineWidth / -2)
            c = fin + side * (lineWidth / 2)
            d = fin + side * (lineWidth / -2)
            table.insert(positions,a)
            table.insert(positions,b)
            table.insert(positions,c)
            table.insert(positions,d)
            table.insert(indices,2*k-1)
            table.insert(indices,2*k)
            table.insert(indices,2*k+1)
            table.insert(indices,2*k+1)
            table.insert(indices,2*k)
            table.insert(indices,2*k+2)
            start=fin
        end
    end
    return positions,indices
end

@arismoko Can you create a small working example showing what your doing instead of just chunks of code. Trying to take what you’re showing and creating something that works is next to impossible.

@Bri_G thanks, but I don’t need much help with particles themselves. I’m trying to create a line renderer so I can use that to implement trails into the particle system. I took a look through the gist, however, I didn’t find much that applies to this particular case.

The line has to be rendered as a mesh/polygon dynamically.
I found an article that’s been helping me so far:
http://www.code-spot.co.za/2020/11/10/procedural-meshes-for-lines-in-unity/

@Briarfox - posted an early particle generator which used a package called Particular. Searched for it on the forum and found the thread but a link to Particular ran into problems. I do have the front end with all the parameters used to set up the engine but it’s useless without the Particular engine.

Particular Link

Edit: found a gist. The Link

Note the copyright in the main Lua file.

So I managed to fix it. I drew dots over all the positions being generated by the cross products, it became clear the reason why the triangles were disappearing due to the positions being too parallel.

I solved this by inserting all the positions and triangles separately. That way you can create two triangles starting from every index%4==0, and then draw triangles that connect them.

Now there is still one major issue, if you input a point that isn’t within say 90 degrees from the prior point, then the triangles will have to be flipped, I believe. I just don’t know how to implement such a feature, yet.

Once I’m able to move the points around at runtime, as well as refactor/clean the code. I’ll share it here.

@arismoko None of your example code was runnable unless other functions were written to call them. Without knowing what you’re actually doing, trying to write that other code is often hard. I always include example code that can be copied and run without trying to figure out what else to write.

Here’s something that will draw 3D lines between 2 sets of (x,y,z) points. You can vary the diameter of the lines and the color. You can also specify the number of sides the line has. A value of 2 creates a flat line, 3 a triangular line, 4 a square line, while higher numbers create rounder lines.

viewer.mode=FULLSCREEN

function setup()
    assert(OrbitViewer, "Please include Cameras as a dependency")        
    scene = craft.scene()
    v=scene.camera:add(OrbitViewer,vec3(0,0,0), 700, 0, 2000)
    v.camera.farPlane=3000

    diameter=3
    sides=5
    sx=math.random(-250,250)
    sy=math.random(-250,250)
    sz=math.random(-250,250)
    for a=1,40 do
        ex=math.random(-250,250)
        ey=math.random(-250,250)
        ez=math.random(-250,250)
        tube(vec3(sx,sy,sz),vec3(ex,ey,ez),diameter,sides)
        sx,sy,sz=ex,ey,ez
    end
end

function draw()
    update(DeltaTime)
    scene:draw() 
end

function update(dt)
    scene:update(dt)
end

function tube(p1,p2,dia,sides)
    local pos,ind,nor,col={},{},{},{}
    local rp=vec3(100,100,100)
    local v1=rp-p1
    local r1=v1:cross(p2-p1)
    local s1=r1:cross(p2-p1)
    local n
    r1,s1=r1:normalize(),s1:normalize() 
    for a=0,719,360/sides do
        n = r1 * math.cos(math.rad(a)) + s1 * math.sin(math.rad(a))
        n=n*dia
        if a>359 then
            table.insert(pos,n + p2)    -- add p2 to last loop
        else
            table.insert(pos,n + p1)    -- add p1 to loop
        end
        table.insert(nor,n)
    end
    
    local o,p={1,2,3,4,5,6,6,5,4,3,2,1},{}
    for z=1,#pos-sides do
        p[1],p[2],p[3],p[4],p[5],p[6]=z,z+1,z+sides+1,z,z+sides+1,z+sides
        if z%sides==0 then
            p[2]=z-sides+1
            p[3]=z+1
            p[5]=z+1
        end
        for t=1,12 do
            table.insert(ind,p[o[t]])
        end
    end 
    colr=color(math.random(255),math.random(255),math.random(255))
    for z=1,#pos do
        table.insert(col,colr)
    end
    
    local pt=scene:entity()
    pt.model = craft.model()
    pt.model.positions=pos
    pt.model.indices=ind
    pt.model.colors=col
    pt.model.normals=nor
    pt.material = craft.material(asset.builtin.Materials.Specular)   
end

@dave1707, yeah, that’s my bad! You’re exactly right about that. I have a system that automatically creates and changes the scene as well as tracking entities created per scene, and I left in those functions

I’ll post an update soon though that will definitely work lmao (hopefully)

@arismoko I guess I need an example to see what you’re trying to do. Your title “3D line renderer” suggests to me you just want a way to draw lines in 3D between points. Maybe once I see a working example I’ll see what you’re after. It takes me awhile to figure things out.

@dave1707 this should work to get the point across! can’t quite get the triangles to link 100 percent correctly but it’s getting there. (:

function gameSetup() parameter.number("tx",-360,360,-180) parameter.number("ty",-1000,1000,0) parameter.number("tz",-360,360) createTestLine("testy",{vec3(-5,20,0),vec3(4,10,0),vec3(3,-5),vec3(-5,-15),vec3(-5,5),vec3(tx,ty)}) scene.camera.z=-100 end

function gameUpdate()
    testy.points={vec3(0,0,0),vec3(4,10,0),vec3(3,-5),vec3(tx,ty),vec3(-5,5),vec3(1,-10)}
end

function createTestLine(name,posits,lw)
    local m = craft.model()
    m.positions = computeLinePoints(posits,lw)
    m.indices=computeLineTriangles(m.positions)
    
    _G[name]=scene:entity()
    _G[name]:add(craft.renderer,m)
    _G[name].material=craft.material(asset.builtin.Materials.Standard)
    _G[name].points=posits
    
    _G[name].fixedUpdate=function(entity)
        lookAt(entity,scene.camera.position)
    end
    _G[name].update=function(entity)
        entity.model.positions=computeLinePoints(_G[name].points)
    end
end

function computeLinePoints(pos,lineWidth)
    local positions={}
    for k,v in pairs(pos) do
        start=pos[k]
        fin=pos[k+1]
        if fin~=nil then
            normal = start:cross(fin)
            side=normal:cross(fin-start)
            side=side:normalize()
            lineWidth=lineWidth or 1
            a = start + side * (lineWidth / 2)
            b = start + side * (lineWidth / -2)
            c = fin + side * (lineWidth / 2)
            d = fin + side * (lineWidth / -2)
            local q=k
            table.insert(positions,a)
            table.insert(positions,b)
            table.insert(positions,c)
            table.insert(positions,d)
        end
    end
    return positions
end

function computeLineTriangles(tab)
    local indices={}
    local centers={}
    for k,v in pairs(tab) do
        if k%4==0 then
            table.insert(centers,k-2)
            table.insert(indices,k-3)
            table.insert(indices,k-2)
            table.insert(indices,k-1)
            table.insert(indices,k)
            table.insert(indices,k-1)
            table.insert(indices,k-2)
            --
            table.insert(indices,k-3)
            table.insert(indices,k-1)
            table.insert(indices,k-2)
            table.insert(indices,k)
            table.insert(indices,k-2)
            table.insert(indices,k-1)
        end
    end
    for k,v in pairs(centers) do
        if k%2==0 then
            table.insert(indices,v-3)
            table.insert(indices,v-2)
            table.insert(indices,v-1)
            table.insert(indices,v)
            table.insert(indices,v-1)
            table.insert(indices,v-2)

            table.insert(indices,v-3)
            table.insert(indices,v-1)
            table.insert(indices,v-2)
            table.insert(indices,v)
            table.insert(indices,v-2)
            table.insert(indices,v-1)
        end
    end
    return indices
end

function lookAt(entity, target, up, t)
    local dir = (entity.position - target):normalize()
    local rot = quat.lookRotation(-dir, up or vec3(0, 1, 0))
    if t and t > 0 then 
        entity.rotation = entity.rotation:slerp(rot, t)
    else 
        entity.rotation = rot
    end
end

Whoops posted twice.

@arismoko The above code doesn’t run the way it is. I added enough code to get it to run and it looks like you have the lines rotating in a complete circle based on the sliders.

@dave1707 oh, yeah the rotation is fine now. I’m just trying to figure out how to connect the vertices on every single line segment and where I went wrong. For right now, this is totally serviceable though.

i just gotta wrap my head around how those triangles work :stuck_out_tongue:

@arismoko Heres some code I hacked up from something else I had. It does something similar to your above code, but in 3D. Use the sliders to move the connecting point around. You can also rotate the whole screen to get a better view of the lines. At the tip of the connection, I create a sphere to close in the tubes to make them look more connected. All the line connections could be filled with a sphere but I’m only doing the one. In function aa(), I just have things hard coded because this is just hacked up code.

viewer.mode=STANDARD

function setup()
    parameter.integer("x",-250,250,0,aa)
    parameter.integer("y",-250,250,0,aa)
    parameter.integer("z",-250,250,0,aa)
    assert(OrbitViewer, "Please include Cameras as a dependency")        
    scene = craft.scene()
    scene.ambientColor=color(255)
    v=scene.camera:add(OrbitViewer,vec3(0,0,0), 700, 0, 2000)
    diameter,sides=1,15
    tab={}
    table.insert(tab,tube(vec3(-30,-60,0),vec3(-30,0,0),diameter,sides))
    table.insert(tab,tube(vec3(30,-60,0),vec3(30,0,0),diameter,sides))
    table.insert(tab,tube(vec3(30,0,0),vec3(50,50,0),diameter,sides))
    table.insert(tab,tube(vec3(-30,0,0),vec3(50,50,0),diameter,sides))
    
    createSphere(vec3(50,50,0),diameter)
end

function draw()
    update(DeltaTime)
    scene:draw() 
end

function aa()
    tab[3]:destroy()
    tab[4]:destroy()
    tab[3]=nil
    tab[4]=nil
    table.insert(tab,tube(vec3(30,0,0),vec3(50+x,50+y,z),diameter,sides))
    table.insert(tab,tube(vec3(-30,0,0),vec3(50+x,50+y,z),diameter,sides))
    
    pt:destroy()
    createSphere(vec3(50+x,50+y,z),diameter)
end

function update(dt)
    scene:update(dt)
end

function tube(p1,p2,dia,sides)
    local pos,ind,nor,col={},{},{},{}
    local rp=vec3(100,100,100)
    local v1=rp-p1
    local r1=v1:cross(p2-p1)
    local s1=r1:cross(p2-p1)
    r1,s1=r1:normalize(),s1:normalize() 
    for a=0,719,360/sides do
        local n = r1 * math.cos(math.rad(a)) + s1 * math.sin(math.rad(a))
        n=n*dia
        if a>359 then
            table.insert(pos,n + p2)    -- add p2 to last loop
        else
            table.insert(pos,n + p1)    -- add p1 to loop
        end
        table.insert(nor,n)
    end
    
    local o,p={1,2,3,4,5,6,6,5,4,3,2,1},{}
    for z=1,#pos-sides do
        p[1],p[2],p[3],p[4],p[5],p[6]=z,z+1,z+sides+1,z,z+sides+1,z+sides
        if z%sides==0 then
            p[2]=z-sides+1
            p[3]=z+1
            p[5]=z+1
        end
        for t=1,12 do
            table.insert(ind,p[o[t]])
        end
    end 
    
    for z=1,#pos do
        table.insert(col,color(255, 0, 0))
    end
    
    local pt=scene:entity()
    pt.model = craft.model()
    pt.model.positions=pos
    pt.model.indices=ind
    pt.model.colors=col
    pt.model.normals=nor
    pt.material = craft.material(asset.builtin.Materials.Specular)  
    return(pt) 
end

function createSphere(p,s)
    pt=scene:entity()
    pt.position=p
    pt.model = craft.model.icosphere(s,2)
    pt.material = craft.material(asset.builtin.Materials.Specular)
    pt.material.diffuse=color(255,0,0)
end