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