Craft sphere texture

@LoopSpace Here’s a picture of what I was seeing. Commenting out the code above eliminated it.

This appears to have fixed the poles (note that it still looks a bit off, but that’s because the texture is designed for cubes).

displayMode(FULLSCREEN)
displayMode(OVERLAY)

function setup()
    assert(craft, "Please include Craft as a dependency")
    assert(OrbitViewer, "Please include Cameras (not Camera) as a dependency")        
    scene = craft.scene()
    skyMaterial=scene.sky.material
    skyMaterial.sky=color(158, 202, 223, 255)
    skyMaterial.horizon=color(98, 166, 114, 255)
    scene.sun.rotation=quat.eulerAngles(-90,0,0)
    v=scene.camera:add(OrbitViewer, vec3(0,0,0), 100, 0, 300)

    texture=readImage("Surfaces:Stone Brick Height")

    spheres = {}
    local b=true
    table.insert(spheres, createSphere(vec3(12,30,0),0,b))
    table.insert(spheres, createSphere(vec3(12,15,0),1,b))
    table.insert(spheres, createSphere(vec3(12,0,0),2,b))
    table.insert(spheres, createSphere(vec3(12,-15,0),3,b))
    table.insert(spheres, createSphere(vec3(12,-30,0),4,b))
    b=nil
    table.insert(spheres, createSphere1(vec3(-12,30,0),0,b))
    table.insert(spheres, createSphere1(vec3(-12,15,0),1,b))
    table.insert(spheres, createSphere1(vec3(-12,0,0),2,b))
    table.insert(spheres, createSphere1(vec3(-12,-15,0),3,b))
    table.insert(spheres, createSphere1(vec3(-12,-30,0),4,b))

    parameter.integer("angle",0,360,300)
end

function draw()
    update(DeltaTime)
    scene:draw()
    sprite(texture,100,HEIGHT/2,100)
    fill(255)
    text("No texture",WIDTH/2-120,HEIGHT-30)
    text("Use texture",WIDTH/2+120,HEIGHT-30)
    text("Texture used",100,HEIGHT/2+70)
end

function update(dt)
    for k,v in ipairs(spheres) do
        v.rotation = quat.eulerAngles(0,angle,0)
    end
    scene:update(dt)
end

function createSphere(p,s,b)
    local pt=scene:entity()
    pt.position=vec3(p.x,p.y,p.z)
    pt.rotation=quat.eulerAngles(0,90,0)
    pt.model = craft.model.icosphere(5,s,b)
    pt.material = craft.material("Materials:Standard")
    return pt
end

function createSphere1(p,s,b)
    local pt=scene:entity()
    pt.position=vec3(p.x,p.y,p.z)
    pt.model = craft.model.icosphere(5,s,b)
    if isouvs and isouvs[s] and false then
        print("using saved uvs")
        pt.model.uvs = isouvs[s]
    else
        local uv={}   -- need to change the uvs for texture
        local pts={}
        local pa,pb,pc
        local ta,tb,tc,tm,tx
        local pha,phb,phc
        local u,v = vec3(0,1,0),vec3((math.sqrt(5)-1)/2,0,-1):normalize()
        local za,zb,zc
        local sa,sb,sc
        local da,db,dc
        local nv = pt.model.vertexCount
        local inv = pt.model.vertexCount
        for z=1,pt.model.indexCount,3 do

            za,zb,zc = pt.model.indices[z],pt.model.indices[z+1],pt.model.indices[z+2]
            pa = pt.model.positions[za]
            pb = pt.model.positions[zb]
            pc = pt.model.positions[zc]

            ta, pha = longlat(u,v,pa)
            tb, phb = longlat(u,v,pb)
            tc, phc = longlat(u,v,pc)

            -- set defaults
            uv[za] = vec2(ta,pha)
            uv[zb] = vec2(tb,phb)
            uv[zc] = vec2(tc,phc)
            
            tm = {ta,tb,tc}
            table.sort(tm)
            -- See if the triangle wraps around the seam
            if tm[3] - tm[1] > tm[1] - tm[3] + 1 then

                sa = math.floor(ta+.5) -- 0 or 1, depending on which seam is closest
                sb = math.floor(tb+.5)
                sc = math.floor(tc+.5)
                da = math.abs(sa - ta)
                db = math.abs(sb - tb)
                dc = math.abs(sc - tc)
                if ta == .5 then
                    da = 0
                end
                if tb == .5 then
                    db = 0
                end
                if tc == .5 then
                    dc = 0
                end
                if (ta ~= .5 and tb ~= .5 and sa ~= sb) or (tc ~= .5 and tb ~= .5 and sb ~= sc) or (ta ~= .5 and tc ~= .5 and sc ~= sa) then
                    if da > math.max(db, dc) then
                        -- a is furthest away from seam so use as basepoint
                        if tb ~= .5 and sb ~= sa then
                            -- shift point b
                            if not b then
                                table.insert(pts,zb)
                                nv = nv + 1
                                zb = nv
                            end
                            tb = sa
                        end
                        if tc ~= .5 and sc ~= sa then
                            -- shift point c
                            if not b then
                                table.insert(pts,zc)
                                nv = nv + 1
                                zc = nv
                            end
                            tc = sa
                        end
                    elseif db > dc then
                        -- b is furthest away
                        if ta ~= .5 and sa ~= sb then
                            -- shift point a
                            if not b then
                                table.insert(pts,za)
                                nv = nv + 1
                                za = nv
                            end
                            ta = sb
                        end
                        if tc ~= .5 and sc ~= sb then
                            -- shift point c
                            if not b then
                                table.insert(pts,zc)
                                nv = nv + 1
                                zc = nv
                            end
                            tc = sb
                        end
                    else
                        -- c is furthest away
                        if tb ~= .5 and sb ~= sc then
                            -- shift point b
                            if not b then
                                table.insert(pts,zb)
                                nv = nv + 1
                                zb = nv
                            end
                            tb = sc
                        end
                        if ta ~= .5 and sa ~= sc then
                            -- shift point a
                            if not b then
                                table.insert(pts,za)
                                nv = nv + 1
                                za = nv
                            end
                            ta = sc
                        end
                    end
                end
            end

            if pha == 1 or pha == 0 then
                ta = (tb+tc)/2
                if not b then
                    table.insert(pts,za)
                    nv = nv + 1
                    za = nv
                end
            end
            if phb == 1 or phb == 0 then
                tb = (ta+tc)/2
                if not b then
                    table.insert(pts,zb)
                    nv = nv + 1
                    zb = nv
                end
            end
            if phc == 1 or phc == 0 then
                tc = (tb+ta)/2
                if not b then
                    table.insert(pts,zc)
                    nv = nv + 1
                    zc = nv
                end
            end
            pt.model:index(z, za)
            pt.model:index(z+1, zb)
            pt.model:index(z+2, zc)
            uv[za] = vec2(ta,pha)
            uv[zb] = vec2(tb,phb)
            uv[zc] = vec2(tc,phc)


        end
        --[[
        local tuv = {}
        for k,v in ipairs(uv) do
            table.insert(tuv, string.format("vec2(%f,%f)",v.x,v.y))
        end
        saveText("Project:Isosphere Textures " .. s,"uv[" .. s .. "] = {\
" .. table.concat(tuv,",\
") .. "\
}")
        --]]
        if not b then
            pt.model:resizeVertices(nv)
            -- print(nv,inv,#pts,#uv)
            for k,v in ipairs(pts) do
                -- print(k,v)
                pt.model:position(inv+k,pt.model.positions[v])
                pt.model:normal(inv+k, pt.model.normals[v])
                pt.model:color(inv+k, pt.model.colors[v])

            end
        end
        for k=1,nv do
            if not uv[k] then
                print(k)
            end
        end
        pt.model.uvs = uv
        --[[
        for k=1,pt.model.vertexCount do
            ta = pt.model.uvs[k].x
            pt.model:color(k,color(ta*127+127))
        end
          ]]
    end

    pt.material = craft.material("Materials:Standard")
    pt.material.map=texture
    return pt
end

function longlat(u,v,p)
    local p = p:normalize()
    local ph = math.asin(p:dot(u))/math.pi + .5
    local x,y = p:dot(v), p:dot(u:cross(v))
    local th = math.atan(y,x)/(2*math.pi)+.5
    return th, ph
end

@LoopSpace Where’s local uvtab = readProjectTab(“Library Craft:IsosphereTexture”)

@dave1707 Sorry! That stuff should be commented out for the moment – I’ll edit the original post. That’s the first steps toward serialising the uvs.

There, that should work.

@LoopSpace - looks great, couldn’t see a defect. Tried to save uvs to tab but would only print the header. I’ll wait until you finalise the package.
Thanks for the hard work - have you alerted @John to this?

Here’s my version of texturing an icosphere. I know I’m not doing it the correct way, but it’s fast. I can texture a level 5 icosphere in less than a second. Setting flat to true (shows small triangle on icosphere) seems to do the texture correct. Setting flat to false (shows a more rounded icosphere) still has a few problems. At level 5, it’s not too noticeable. I set up my own texture to make checking the texture wrap easier. Slide the flip slider to change between my texture and the Stone Brick texture. Also, change the level value (0 to 5) to change the level of the icosphere and rerun the code. I’m still working on fixing the rounded icosphere problem. There seems to be some triangles that aren’t set the way I think they should be. I don’t know how all the icosphere arrays work together, but that’s something to figure out. Use the angle slider to rotate the sphere, or slide your finger on the screen.

function setup()
    parameter.integer("angle",-180,180,-90)
    parameter.boolean("flip",false)
    -- create test texture
    myImg=image(1024,512)
    setContext(myImg)
    background(170, 198, 223, 255)
    fill(255,0,0)
    ellipse(250,256,200,100)
    fill(0,255,0)
    ellipse(900,256,100,200)
    fill(255,255,0)
    rect(512,320,256,128)
    fill(0, 19, 255, 255)
    fontSize(25)
    text("This       is       my       texture       example,       move       image       slider       for       more.",512,490)
    text("This       is       my       texture       example,       move       image       slider       for      more.",512,22)
    fontSize(40)
    text("This is my texture example, move image slider for more.",512,512*.8)
    text("This is my texture example, move image slider for more.",512,512*.65)
    text("This is my texture example, move image slider for more.",512,512*.5)
    text("This is my texture example, move image slider for more.",512,512*.35)
    text("This is my texture example, move image slider for more.",512,512*.2)    
    stroke(0)
    strokeWidth(3)
    for z=0,1024,64 do
        line(z,0,z,512)
    end
    for z=0,512,32 do
        line(0,z,1024,z)
    end
    setContext()    
        
    texture1=myImg
    texture2=readImage("Surfaces:Stone Brick Height")
    texture=texture1
    
    level=1     --  0 thru 5
    flat=true  -- true (triangles) or false (rounded)
    
    assert(craft, "Please include Craft as a dependency")
    assert(OrbitViewer, "Please include Cameras (not Camera) as a dependency")        
    scene = craft.scene()
    v=scene.camera:add(OrbitViewer, vec3(0,0,0), 250, 0, 600)
    createSphere()
end

function draw()
    update(DeltaTime)
    scene:draw()
    if flip then
        texture=texture2
    else
        texture=texture1
    end
    sph.material.map=texture
    sprite(texture,WIDTH/2,140,texture.height/2)

end

function update(dt)
    scene:update(dt)
    sph.rotation = quat.eulerAngles(angle,0,0)
end

function createSphere()
    sph=scene:entity()
    sph.position=vec3(0,33,0)
    sph.model = craft.model.icosphere(60,level,flat)
    sph.material = craft.material("Materials:Standard")    
    sph.material.map=texture
    print("indexs",sph.model.indexCount)
    local temp={}  -- temp table for latitude and longitude
    local seam=0
    -- convert to latitude and longitude
    for a,b in pairs(sph.model.positions) do
        local c=b:normalize()
        local lat=math.deg(math.asin(c.z))+90
        local lon=math.deg(math.atan2(c.y,c.x))
        table.insert(temp,vec2(lon,lat))     -- save lat, lat in temp table
        -- get exact value of seam
        if lon//1==-149 then
            seam=lon
        end
    end  
    -- shift points on the left side of the seam to the right side  
    for a,b in pairs(temp) do        
        b.x=b.x+seam*-1
        if b.x<-.01 then
            b.x=b.x+360
        end
    end  
    -- shift individual points if needed   
    for z=1,#temp,3 do
        ax,bx,cx=temp[z].x,temp[z+1].x,temp[z+2].x        
        if ax>250 or bx>250 or cx>250 then
            if ax<.01 then 
                temp[z].x=360
            end
            if bx<.01 then
                temp[z+1].x=360
            end
            if cx<.01 then
                temp[z+2].x=360
            end  
        end        
        ax,ay=temp[z].x,temp[z].y
        bx,by=temp[z+1].x,temp[z+1].y
        cx,cy=temp[z+2].x,temp[z+2].y
        if ay==0 or ay==180 then
            temp[z].x=(bx+cx)/2
        elseif by==0 or by==180 then
            temp[z+1].x=(ax+cx)/2
        elseif cy==0 or cy==180 then
            temp[z+2].x=(ax+bx)/2
        end
    end
    -- create uv table
    local uvTab={}    
    for a,b in pairs(temp) do
        table.insert(uvTab,vec2(b.x/360,b.y/180))
    end    
    sph.model.uvs=uvTab
end

@dave1707 - excellent it is very fast rendering, level 3 seems to be the best compromise for my needs. It still suffers from the triangular banding effect, tested with EarthDay.png an most obvious on Sahara desert. Also, as with a few of the projects displayed one reflective triangle near the apex.

I think Ignatz made a point on the triangular lighting effect on his WOT demo,
He said the lighting was set for each triangle and he used a shader to normalise each pixel, see the following:

https://coolcodea.wordpress.com/2015/10/16/239-wot-building-a-tank/

That may resolve the problems with the icosphere, worth a look.

@Bri_G Was that level 3 flat=true, or flat=false. I haven’t seen any problems yet with flat=true for levels 1 thru 5.

@dave1707 - that was flat = true, switched to flat = false made the triangle/banding disappear and the single mirrored triangle at the apex became a small whit circle.

@dave1707 - the routine is certainly fast even at level 5. Just noticed when flat = false at level 3 there is something funny happening on the poles, one or two unrelated textured triangles. It’s not as noticeable when flat = true but the the triangles are pronounced with some banding.

@Bri_G How icospheres are drawn is kind of a mystery to me. There’s several arrays that are used, but they seem to be a little different when doing flat=true or flat=false. When flat=true, then you can see all the triangles that make up the icosphere. When flat=false, the icosphere has a rounded shape to it and the triangles are less noticeable at the higher levels. I’m still playing around with coloring individual triangles of the icospheres which might help with understanding how to get the texture to work in all instances.

Fixing those “small” issues is what makes my code slower. There is probably some optimisation to be had in my code, but the intention is to serialise the uvs rather than have to calculate them afresh every time.

The smooth isosphere cannot be done just by assigning texture coordinates to positions. You will always end up with defects on the seam. Again, that’s what all the extras in my code fix.

@Loopspace @dave1707 - looking at the plotting of points used for texture allocation, in the project written by @dave1707, you can see a series of longitudinal lines covering almost the height of the texture. There are only two points at the poles. If you triangulate from the longest longitude you are left with 4 triangles in each corner of the texture - it may be these allocations that are causing the problem.

@LoopSpace Let me ask you a question, does the indices array contain the index into the positions table to get the position for each corner to create the triangles.

@dave1707

Let me ask you a question, does the indices array hold an index into the positions array used to form the 3 vertices of each triangle.

Yes. The indices array should be thought of as consisting of triples, each triple being the indices of three positions making up a triangle.

In the non-smooth case, each position is contained in a unique triangle. This makes it possible to simply assign the texture coordinates based on the triangle. In the smooth case, positions are re-used and for positions near the seam (and poles) this means that they can have more than one texture coordinate. My code deals with this by duplicating those positions. This is the only way to fix this.

@LoopSpace That’s what I thought. I’m only using the position array in groups of 3 to create the triangles in my code. For the rough icosphere, only using the position array works OK. For the smooth icosphere, the majority of triplets are correct, but there are a few triplets that span across several other triangles which is what causes a problem in my code. I don’t see why the position array can’t have the correct info in both cases.

@dave1707 The way the icosphere is set up, in the non-smooth case then every position is in a unique triangle. This leads to vertices being duplicated quite a lot (roughly five or six times) but this is needed because although the positions are the same for each re-use of the vertex, other data (specifically the normals) is not.

However, in the smooth case, the normals depend only on the vertex and not on the triangle and therefore the vertices can be re-used in different triangles. For the vast majority of the vertices, this is okay with the texture as the texture coordinates only depend on the position on the sphere.

Where this fails is at the edges of the texture, which with the usual sphere wrapping are at the poles and at the seam. At these places, more than one point on the texture goes to the same point on the sphere. This is perhaps most obvious at the poles where the entire top of the texture maps to the north pole (and bottom to the south pole). Therefore, the texture coordinate at the north pole depends on which triangle is under consideration. So the pole needs duplicating, once for each triangle it is in. A similar thing happens down the seam.

@LoopSpace That explains why in my smooth icosphere the bad textures are at the poles and at certain places along the seam. I’ll have to keep looking at the way I do my code to see if there might be an easy way to fix the bad spots. The fun is in trying to get it to work, but I don’t want to spend a lot of time trying to fix it because I probably won’t do anything with it anyways.

@LoopSpace Here’s the problem I was running into using the positions array. The first image shows a grid of my longitude and longitude points. This is for the smooth icosphere. I draw the triangles in white and you can see where the green triangles and the green line across the middle are the problem areas. They cross over several other triangles. The second image is for the rough icosphere and you can see all the triangles are OK. Ignore the long triangles that run from the left edge to the right edge. Those would actually connect in the opposite direction.

It seems like my icosphere generator is causing a few issues for people. Honestly the best solution would probably be for me to include a UV sphere generator as I think they are much easier to properly texture automatically.

@dave1707
The reason why you see different mesh data between flat and smooth icospheres is due to the way it shares normals in smooth meshes to save on vertices.