I've added a .ply importer to @Ignatz 's object viewer (for vertex painting support)

I’ve hacked together a .ply importer as an extension to the .obj 3D model viewer/ importer that @Ignatz made.

.ply (aka Stanford) is, like .obj, a plaintext, easily parse-able 3D model format, supported by many 3D programs, including Blender.

So why bother adding support for it on top of .obj?

One advantage that .ply has over .obj, is that it can store colour information for each individual vertex, whereas .obj files only store colour values for each material that you use.[^1] This means that you can use Blender’s rather nice “vertex painting” mode. When you import the model into Codea, Open GL takes over and shades each face of the model by smoothly interpolating between the colours for each point on the face.

vertex painting

This is useful if you have a model that would just look better with very smooth, resolution-independent shading, or you can’t be bothered to add a texture, or you’re running into memory/processor constraints and want to cut down on textures. You could also combine the vertex painting with a texture. This again could be useful if you want to reuse the same texture image over and over, but make it look a little different each time (and add some resolution-independence to how the object is rendered). eg:

vertex painting combined with texture

I’m going to clean up the code a little before I post it. It works as a sub-class of @Ignatz 's OBJ class, so it uses all of the I/O code that @Ignatz wrote. The .ply parser is modified from the code posted a couple of years back by @aciolino . Thanks/ apologies to both of them, I hope neither of them mind me mashing their projects together like this!

[^1]: There is apparently a flavour of .obj that does support having colour information for each vertex, but I think that’s a somewhat non-standard version of .obj, and Blender cannot export those kind of .obj files. You could also “bake” the vertex painting into a raster image and export it as a texture, but that would defeeat the purpose somewhat.

No problem, this is what our community is all about, helping and sharing, thanks for your efforts

Here’s the code. This is to be added to @Ignatz 's 3D model viewer v5: code, blog post with documentation

-- PLY reader, with support for textures, normals, and vertex painting
-- CREDITS: adapted by Yojimbo2000 from Antonio Ciolino's parser
-- and appended to Ignatz 's .obj reader.
-- INSTALL: This tab must be to the right of OBJ tab. It inherits everything except ProcessData() from the OBJ class
-- USAGE: if using a texture image, you have to manually add it within the header of the .ply file, syntax:
-- comment texture imageName imageURL
-- nb this uses comment so that the syntax doesnt break.
-- If not using a texture the .ply file can be used as is.
-- NOTES: currently only supports triangular faces. If unsure, add a triangulate modifier to your object before export from blender.
-- currently only supports a single object with a single texture.

function ShowModel() --replaces the function in Main
    state=State.Loading
    if string.find(Models[Choose].url, ".ply", -4) then
        newModel=PLY(Models[Choose].name,Models[Choose].url)
    else
        newModel=OBJ(Models[Choose].name,Models[Choose].url)
    end
end

PLY = class(OBJ)

function PLY:ProcessData()
    --PROCESS HEADER
    local elements={}        -- elements of this file
    local endheader = string.find(self.data, "end_header")
    local header = string.sub(self.data, 1, endheader-1)
    local body = string.sub(self.data, endheader+11, 999999)
    local tex = {}
    local hasNormals, hasTexCoords, hasColours  = false, false, false
    local tbl,token,texture,entry,items,vtx
    -- make a table to interpret the header. this is the guide to read the file.
    for token in string.gmatch(header, "[^\\r\
]+") do --iterator, longest possible string of anything not a return
        -- pack each string and process
        -- special case find the texture file if it exists
        local i= string.find(token, "comment texture")
        if (i~=nil) then
            texture=string.sub(token, string.find(token, "comment texture")+16, 99)
            tex=split(texture,"%s")
        end
        
        i=string.find(token,"element")
        if (i~=nil) then
            --we have a new element
            tbl = split(token, " ")
            entry= { name=tbl[2], value=tbl[3], properties={}, lines={} }
            table.insert(elements, entry)
        end
        i=string.find(token, "property")
        if (i~=nil) then
            tbl = split(token, " ")
            --indexed = 0
            --[[
            if tbl[2] == "list" then
            -- this data is a lookup list. unused, we are hardcoding our lookups
            --against texcoords and faces.
            -- later we should deal with uchar, list data
            -- indexed = 1
        end
            ]]
            if tbl[3] == "nx" then hasNormals=true
            elseif tbl[3] == "s" then hasTexCoords=true
            elseif tbl[3] == "red" then hasColours=true
            end
            --prop = { property=tbl[table.maxn(tbl)], index=indexed} (me:not sure what this does)
            --table.insert(entry.properties, prop)
        end
    end
    
    --PROCESS BODY
    local newMesh=mesh()
    local points = {}        --each unique point, eventually used to build faces
    local pcolor={}        --colors, texCoords, normals, if defined, per vertex.
    local ptex={}
    local pnorm={}
    
    local vertices={}       --tables that get sent to the mesh
    local colors={}
    local texcoord={}
    local normals={}
    
    -- read through the body string using the header as a guide
    -- all elements have a value
    local lines=split(body, "[\\r\
]")
    
    -- lines=GetTable(body, "[^\\r\
]+")
    
    local offset=0
    for k,v in pairs(elements) do
        v.lines = {}
        -- get value number of lines and process
        for i=1, v.value do
            table.insert(v.lines, lines[i+offset])
        end
        offset=v.value
        --process verts these are points?
        if v.name=="vertex" then
            
            for lk, lv in pairs(v.lines) do
                local pt = vec3(0,0,0)
                -- items=GetTable(lv, "[^%s]+") --"([-+]?[0-9]*\\.?[0-9]+)")
                items=split(lv, "%s") --"([-+]?[0-9]*\\.?[0-9]+)")
                
                --for k,v in pairs(items) do  --needss to be aa case stmt later
                pt.x=items[1]
                pt.y=items[2]
                pt.z=items[3]
                table.insert(points, pt)
                local p=3 --place holder
                
                if hasNormals then
                    local no = vec3(0,0,0)
                    no.x = items[4]
                    no.y = items[5]
                    no.z = items[6]
                    table.insert(pnorm, no)
                    p=6
                end
                if hasTexCoords then
                    local tc=vec2(0,0)
                    tc.x = items[1+p]
                    tc.y = items[2+p]
                    table.insert(ptex,tc)
                    p = p + 2
                end
                if hasColours then
                    --colors of the verts go here
                    local r,g,b
                    r = items[1+p]
                    g = items[2+p]
                    b = items[3+p]
                    --  a = items[7]+0
                    table.insert(pcolor, color(r,g,b,255))
                end
                
            end
        end
        
        if v.name=="face" then
            --takes us from points to faces...we pack these into the mesh!
            for lk, lv in pairs(v.lines) do
                --items=GetTable(lv, "[^%s]+")
                items=split(lv, "%s")
                
                vtx =vec3(0,0,0)
                
                for a=2,4 do
                    vtx = items[a]+1
                    table.insert(vertices, points[vtx])
                    if hasNormals then table.insert(normals, pnorm[vtx]) end
                    if hasTexCoords then table.insert(texcoord, ptex[vtx]) end
                    if hasColours then table.insert(colors, pcolor[vtx]) end
                end
                
                --if we have teturecoords, read then here (me: is this a different version of .ply that places texCoords in the face section of the file?)
                --[[
                if (#items > 4) then
                uv=vec2(items[6], items[7])
                table.insert(texcoord, uv)
                uv=vec2(items[8], items[9])
                table.insert(texcoord, uv)
                uv=vec2(items[10], items[11])
                table.insert(texcoord, uv)
            end
                ]]
            end
        end
    end
    
    --print ("final points "..#points)
    -- print ("final verts "..#vertices)
    --  print ("final colors "..#colors)
    -- print ("final texcoord "..#texcoord)
    
    newMesh.vertices=vertices  -- a vec3 of points
    if hasColours then newMesh.colors=colors end
    if hasTexCoords then newMesh.texCoords=texcoord
        -- newMesh:setColors(255,255,255,255)  --IF WE HAVE A TEXTURE, IGNORE COLOR FOR NOW
        newMesh.settings={map=tex[1]} --add name of texture
    end
    if hasNormals then newMesh.normals=normals end
    
    self.m={}
    self.m[1]= newMesh              --
    --download images if not stored locally
    self.MissingImages={}
    -- for i,O in pairs(self.mtl) do
    if #tex>0 then
        local y=readImage(OBJ.imgPrefix..tex[1])
        if not y then self.MissingImages[#self.MissingImages+1]={tex[1],tex[2]} end
    end
    --  end
    if #self.MissingImages>0 then self:LoadImages()
    else self.ready=true return end
    
end

function split(str, pat) --could maybe use the split functions in the obj class
    local t = {}
    local fpat = "(.-)" .. pat
    local last_end = 1
    local s, e, cap = str:find(fpat, 1)
    while s do
        if s ~= 1 or cap ~= "" then
            table.insert(t,cap)
        end
        last_end = e+1
        s, e, cap = str:find(fpat, last_end)
    end
    if last_end <= #str then
        cap = str:sub(last_end)
        table.insert(t, cap)
    end
    return t
end

Compared to .obj:

Pros

  • supports colour-per-vertex and therefore vertex-painting (this was the main reason I added it)

  • .ply is held in a single file (.obj is split across two) and therefore less file wrangling needed. If there is no texture, you can just use the .ply file as is. Moreover, adding the URL to the .ply file doesn’t break the .ply syntax, as it uses a “comment” field. So the workflow between Codea and Blender is a little simpler.

Cons

  • .ply (or at least the .ply Blender exports) doesn’t seem to store data for various material properties such as reflectivity etc.

  • This only supports single objects with single textures. The .ply that Blender exports doesn’t seem to acknowledge multi-object or multi-texture scenes at all. For complex rigs, .obj is the way to go

  • The .ply exporter in Blender is a little less fully featured. It doesn’t have a triangulate option like the .obj exporter has, so you have to add a Triangulate modifier before you export (you don’t have to click “apply” on the modifier though).

OK, I’ve added a gist here that has everything you need to test it, including the “vertex painting” object used for the screen shot above. Just download the “3D Model Importer”, you don’t need to download the .ply file, as the Importer will automatically download it to your global data.

https://gist.github.com/Oliver-D/49460797dbc81b24e33d

@yojimbo2000 when i choose the object and press load, there is a brief change in the output panel (orange text) then the program seems to restart and nothing is showing on the screen.
I guess there is an error but i cant read it because the message is erased.

Did you get the code from the gist I posted?

https://gist.github.com/Oliver-D/49460797dbc81b24e33d

This one, not the links at the top of the post. I made a couple of changes to the gist right after I posted, which you might not have if you downloaded it right away.

Are you selecting the last model, “vertex paint”? (Choose slider all the way to the right)

It would be great if you could Comment out the two lines towards the end of configureModel() and let me know what the error is

   -- output.clear()
   -- for i=1,#Models do print(i,Models[i].name) end

i get a ‘shader compile error’ with last model

ERROR: 0:5: Attempt to redeclare 'reflect' as a variable
ERROR: 0:49: Attempt to use 'reflect' as a variable
ERROR: 0:50: Use of undeclared identifier 'totalColor'
ERROR: 0:51: Use of undeclared identifier 'totalColor'

I tried to download again, but still get bad result.
Thanks for helping me out!

That’s weird. I redownloaded the code from the gist into a separate project to check, I don’t get any of those errors. The “attempt to redeclare reflect as a variable” error is odd, reflect is only declared once in the shader. Is it possible that the code got messed up in the paste into project somehow? How did you get the code into Codea? (ie was it with the in-app browser “copy” button from the gist, or some other method?)

It loads ok for me. But for some reason no models show up.
It looks like this, no matter which model i choose:

http://s15.postimg.org/pedgnauh7/image.jpg

The more complex models (like the shuttle) will take a few seconds to download the first time you load them. 6, the vertex painting example, should be quick, because there’s no texture, so it only has to request a few kb from GitHub. @juce you don’t see anything when you load number 6?

@yojimbo2000, sorry, i spoke too soon. If i comment out lines 64 and 65, then i see the same error as @Jmv38 for model 6:

ERROR: 0:5: Attempt to redeclare 'reflect' as a variable
ERROR: 0:49: Attempt to use 'reflect' as a variable
ERROR: 0:50: Use of undeclared identifier 'totalColor'
ERROR: 0:51: Use of undeclared identifier 'totalColor'

It seems that reflect is a reserved word, and perhaps cannot be used as a variable. I did a global replace of it with reflectx and now it all works like a charm. Very cool! Awesome 3D models too.

@juce glad you got it working and thanks for the bug report. I wonder why I don’t have that issue? Are you a Codea beta tester? I’m on Codea 2.2(35) and iOS 8.1.3. Did I read somewhere that the shader specification changed at some point? @Jmv38 could you also try to do a search replace on reflect?

I made the vertex painting model very quickly in Blender, and it has some overlapping faces because the handwriting-style fonts overlap, so it does flicker a bit. Probably best to use a non-joined-up typeface for that kind of thing

@yojimbo2000 @Juce the trick worked great. I wonder how it could work on your side with the reserved word? Anyway, these models are AWSOME. Thanks for sharing.

@yojimbo2000 maybe you could update the gist with the new code?

I haven’t run it yet, but I saw the models include the space shuttle, ME262, spitfire etc that I put together a while back. As I recall, the Merc(edes) model was broken.

There is a bit of an art in selecting models that work well in Codea. I generally looked for models that weren’t too large (eg files size below 1 nb), didn’t have complex texturing, and didn’t require animation. And then not all models work, for one reason or another.

I’ve since added quite a few more models for use in my 3D dungeon, including weapons, a metal cage, a couple of nasty spiders and zombies etc.

OK, the gist has been updated. Does the totalColor error also disappear once you change the name of the reflect variable? I wonder why I don’t get that error… Do different iPads run slightly different versions of Open GL ES? I’m on an iPad Air

totalColor error disappears because it used reflect in its definition, so once that was sorted out, totalColor was good too. I am on iPad 2, btw. (The old one, not Retina). iOS 7.1.2