1 /// 2 module nwnlibd.wavefrontobj; 3 4 import std.typecons; 5 import std.string; 6 import std.algorithm; 7 import std.format; 8 import std.conv; 9 import std.array; 10 import std.exception; 11 12 import gfm.math.vector; 13 14 /// 15 class WavefrontObj { 16 17 string[] mtllibs; 18 19 /// 20 static struct WFVertex{ 21 this(in vec3f position, Nullable!vec3f color = Nullable!vec3f()){ 22 this.position.xyz = position; 23 this.position.w = 1.0; 24 this.color = color; 25 } 26 /// 27 vec4f position; 28 /// 29 Nullable!vec3f color; 30 } 31 /// 32 WFVertex[] vertices; 33 /// 34 vec2f[] textCoords; 35 /// 36 vec3f[] normals; 37 38 /// 39 static struct WFFace { 40 /// 41 size_t[] vertices; 42 /// 43 Nullable!(size_t[]) textCoords; 44 /// 45 Nullable!(size_t[]) normals; 46 /// 47 string material; 48 } 49 /// 50 static struct WFLine { 51 /// 52 size_t[] vertices; 53 } 54 /// 55 static struct WFGroup { 56 /// 57 WFFace[] faces; 58 /// 59 WFLine[] lines; 60 } 61 /// 62 static struct WFObject { 63 /// 64 WFGroup[string] groups; 65 /// 66 alias groups this; 67 } 68 /// 69 WFObject[string] objects; 70 71 /// 72 this(){} 73 74 /// 75 this(in string data){ 76 import std.uni : isWhite; 77 78 string currentMtl; 79 80 string object, group; 81 foreach(ref line ; data.lineSplitter.map!strip.filter!(a => a[0] != '#')){ 82 auto ws = line.countUntil!isWhite; 83 string type = line[0 .. ws]; 84 line = line[ws .. $].strip; 85 86 switch(type){ 87 case "mtllib": 88 mtllibs ~= line; 89 break; 90 case "o": 91 object = line; 92 group = null; 93 objects[object] = WFObject(); 94 break; 95 case "g": 96 group = line; 97 objects[object][group] = WFGroup(); 98 break; 99 case "v": 100 auto values = line 101 .split!isWhite 102 .filter!(a => a.length > 0) 103 .array; 104 105 WFVertex vtx; 106 if(values.length >= 3) 107 vtx.position.v[0 .. 3] = values[0 .. 3].map!(a => a.to!float).array; 108 109 if(values.length >= 4) 110 vtx.position.v[3] = values[3].to!float; 111 else 112 vtx.position[3] = 1.0; 113 114 if(values.length >= 7) 115 vtx.color = vec3f(values[4 .. 7].map!(a => a.to!float).array); 116 117 vertices ~= vtx; 118 break; 119 case "vt": 120 textCoords ~= vec2f(line 121 .split!isWhite 122 .filter!(a => a.length > 0) 123 .map!(a => a.to!float) 124 .array[0 .. 2]); 125 break; 126 case "vn": 127 normals ~= vec3f(line 128 .split!isWhite 129 .filter!(a => a.length > 0) 130 .map!(a => a.to!float) 131 .array[0 .. 3]); 132 break; 133 case "f": 134 if(group !in objects[object]) 135 objects[object][group] = WFGroup(); 136 137 auto indices = line 138 .split!isWhite 139 .filter!(a => a.length > 0) 140 .map!(a => a.split("/")) 141 .array; 142 143 WFFace face; 144 face.vertices = indices.map!(a => a[0].to!size_t).array; 145 if(indices[0].length >= 2 && indices[0][1].length > 0) 146 face.textCoords = indices.map!(a => a[1].to!size_t).array; 147 if(indices[0].length >= 3 && indices[0][2].length > 0) 148 face.normals = indices.map!(a => a[2].to!size_t).array; 149 face.material = currentMtl; 150 151 objects[object][group].faces ~= face; 152 break; 153 154 case "l": 155 if(group !in objects[object]) 156 objects[object][group] = WFGroup(); 157 158 objects[object][group].lines ~= WFLine( 159 line 160 .split!isWhite 161 .filter!(a => a.length > 0) 162 .map!(a => a.to!size_t) 163 .array 164 ); 165 break; 166 167 case "usemtl": 168 currentMtl = line; 169 break; 170 171 default: break; 172 } 173 } 174 } 175 /// 176 string serialize() const { 177 string objData; 178 179 foreach(ref lib ; mtllibs){ 180 objData ~= format!"mtllib %s\n"(lib); 181 } 182 183 foreach(ref v ; vertices){ 184 if(v.color.isNull) 185 objData ~= format!"v %(%f %)\n"(v.position.v); 186 else 187 objData ~= format!"v %(%f %) %(%f %)\n"(v.position.v, v.color.get.v); 188 } 189 190 foreach(ref vt ; textCoords) 191 objData ~= format!"vt %(%f %)\n"(vt.v); 192 193 foreach(ref vn ; normals) 194 objData ~= format!"vn %(%f %)\n"(vn.v); 195 196 string currentMtl; 197 foreach(ref objName ; objects.keys().sort()){ 198 objData ~= format!"o %s\n"(objName); 199 foreach(ref groupName ; objects[objName].groups.keys().sort()){ 200 if(groupName != null) 201 objData ~= format!"g %s\n"(objName); 202 203 foreach(ref f ; objects[objName].groups[groupName].faces){ 204 205 if(f.material != currentMtl){ 206 objData ~= format!"usemtl %s\n"(f.material); 207 currentMtl = f.material; 208 } 209 210 string[] values; 211 values.length = f.vertices.length; 212 213 foreach(i ; 0 .. f.vertices.length){ 214 values[i] ~= f.vertices[i].to!string; 215 if(!f.textCoords.isNull || !f.normals.isNull) 216 values[i] ~= "/"; 217 values[i] ~= f.textCoords.isNull? null : f.textCoords.get[i].to!string; 218 if(!f.normals.isNull) 219 values[i] ~= "/"; 220 values[i] ~= f.normals.isNull? null : f.normals.get[i].to!string; 221 } 222 223 objData ~= format!"f %-(%s %)\n"(values); 224 } 225 226 foreach(ref l ; objects[objName].groups[groupName].lines){ 227 objData ~= format!"l %(%d %)\n"(l.vertices); 228 } 229 } 230 } 231 232 return objData; 233 } 234 235 void validate() const { 236 foreach(vi, ref v ; vertices){ 237 if(!v.color.isNull){ 238 foreach(ci, c ; v.color.get.v) 239 enforce(0 <= c && c <= 1.0, 240 format!"vertices[%d].color[%d]: value %f is invalid"(vi, ci, c)); 241 } 242 } 243 244 foreach(oname, ref o ; objects){ 245 foreach(gname, ref g ; o.groups){ 246 foreach(fi, ref f ; g.faces){ 247 foreach(vi, ref v ; f.vertices) 248 enforce(0 < v && v <= vertices.length, 249 format!"objects[%s][%s].faces[%d].vertices[%d] %d is out of bounds"(oname, gname, fi, vi, v)); 250 if(!f.textCoords.isNull) 251 foreach(vi, ref v ; f.textCoords.get()) 252 enforce(0 < v && v <= textCoords.length, 253 format!"objects[%s][%s].faces[%d].textCoords[%d] %d is out of bounds"(oname, gname, fi, vi, v)); 254 if(!f.normals.isNull) 255 foreach(vi, ref v ; f.normals.get()) 256 enforce(0 < v && v <= normals.length, 257 format!"objects[%s][%s].faces[%d].normals[%d] %d is out of bounds"(oname, gname, fi, vi, v)); 258 } 259 foreach(li, ref l ; g.lines){ 260 foreach(vi, ref v ; l.vertices) 261 enforce(0 < v && v <= vertices.length, 262 format!"objects[%s][%s].lines[%d].vertices[%d] %d is out of bounds"(oname, gname, li, vi, v)); 263 } 264 } 265 } 266 } 267 }