shithub: libgraphics

Download patch

ref: b429311ed087ee7cf7fc7771a8e1834ea074f8da
parent: cf3e0cb65c03955ef18624a50b9e33c3f6e8c6e5
author: rodri <rgl@antares-labs.eu>
date: Fri May 3 09:32:48 EDT 2024

add a general primitive with support for points, lines and triangles.

also got rid of the dependency on OBJ for the entire renderer,
instead letting the user load a Model from any given OBJ. this
modularity will allow for other formats to be used in the same
way, relying on a single, internal representation for the
entire pipeline.

--- /dev/null
+++ b/clip.c
@@ -1,0 +1,220 @@
+#include <u.h>
+#include <libc.h>
+#include <thread.h>
+#include <draw.h>
+#include <memdraw.h>
+#include <geometry.h>
+#include "libobj/obj.h"
+#include "graphics.h"
+#include "internal.h"
+
+enum {
+	CLIPL = 1,
+	CLIPR = 2,
+	CLIPT = 4,
+	CLIPB = 8,
+};
+
+static void
+mulsdm(double r[6], double m[6][4], Point3 p)
+{
+	int i;
+
+	for(i = 0; i < 6; i++)
+		r[i] = m[i][0]*p.x + m[i][1]*p.y + m[i][2]*p.z + m[i][3]*p.w;
+}
+
+static int
+addvert(Polygon *p, Vertex v)
+{
+	if(++p->n > p->cap)
+		p->v = erealloc(p->v, (p->cap = p->n)*sizeof(*p->v));
+	p->v[p->n-1] = v;
+	return p->n;
+}
+
+static void
+swappoly(Polygon *a, Polygon *b)
+{
+	Polygon tmp;
+
+	tmp = *a;
+	*a = *b;
+	*b = tmp;
+}
+
+static void
+cleanpoly(Polygon *p)
+{
+	int i;
+
+	for(i = 0; i < p->n; i++)
+		delvattrs(&p->v[i]);
+	p->n = 0;
+}
+
+/*
+ * references:
+ * 	- James F. Blinn, Martin E. Newell, “Clipping Using Homogeneous Coordinates”,
+ * 	  SIGGRAPH '78, pp. 245-251
+ * 	- https://cs418.cs.illinois.edu/website/text/clipping.html
+ * 	- https://github.com/aap/librw/blob/14dab85dcae6f3762fb2b1eda4d58d8e67541330/tools/playground/tl_tests.cpp#L522
+ */
+int
+clipprimitive(Primitive *p)
+{
+	/* signed distance from each clipping plane */
+	static double sdm[6][4] = {
+		 1,  0,  0, 1,	/* l */
+		-1,  0,  0, 1,	/* r */
+		 0,  1,  0, 1,	/* b */
+		 0, -1,  0, 1,	/* t */
+		 0,  0,  1, 1,	/* f */
+		 0,  0, -1, 1,	/* n */
+	};
+	double sd0[6], sd1[6];
+	double d0, d1, perc;
+	Polygon Vin, Vout;
+	Vertex *v0, *v1, v;	/* edge verts and new vertex (line-plane intersection) */
+	int i, j, nt;
+
+	nt = 0;
+	memset(&Vin, 0, sizeof Vin);
+	memset(&Vout, 0, sizeof Vout);
+	for(i = 0; i < p[0].type+1; i++)
+		addvert(&Vin, p[0].v[i]);
+
+	for(j = 0; j < 6 && Vin.n > 0; j++){
+		for(i = 0; i < Vin.n; i++){
+			v0 = &Vin.v[i];
+			v1 = &Vin.v[(i+1) % Vin.n];
+
+			mulsdm(sd0, sdm, v0->p);
+			mulsdm(sd1, sdm, v1->p);
+
+			if(sd0[j] < 0 && sd1[j] < 0)
+				continue;
+
+			if(sd0[j] >= 0 && sd1[j] >= 0)
+				goto allin;
+
+			d0 = (j&1) == 0? sd0[j]: -sd0[j];
+			d1 = (j&1) == 0? sd1[j]: -sd1[j];
+			perc = d0/(d0 - d1);
+
+			lerpvertex(&v, v0, v1, perc);
+			addvert(&Vout, v);
+
+			if(sd1[j] >= 0){
+allin:
+				addvert(&Vout, dupvertex(v1));
+			}
+		}
+		cleanpoly(&Vin);
+		if(j < 6-1)
+			swappoly(&Vin, &Vout);
+	}
+
+	if(Vout.n < 2)
+		cleanpoly(&Vout);
+	else switch(p[0].type){
+	case PLine:
+		/* TODO fix line clipping (they disappear instead, why?) */
+		p[0].v[0] = dupvertex(&Vout.v[0]);
+		p[0].v[1] = dupvertex(&Vout.v[1]);
+		cleanpoly(&Vout);
+		break;
+	case PTriangle:
+		/* triangulate */
+		for(i = 0; i < Vout.n-2; i++, nt++){
+			/*
+			 * when performing fan triangulation, indices 0 and 2
+			 * are referenced on every triangle, so duplicate them
+			 * to avoid complications during rasterization.
+			 */
+			memmove(&p[nt], &p[0], sizeof *p);
+			p[nt].v[0] = i < Vout.n-2-1? dupvertex(&Vout.v[0]): Vout.v[0];
+			p[nt].v[1] = Vout.v[i+1];
+			p[nt].v[2] = i < Vout.n-2-1? dupvertex(&Vout.v[i+2]): Vout.v[i+2];
+		}
+		break;
+	}
+	free(Vout.v);
+	free(Vin.v);
+
+	return nt;
+}
+
+static int
+ptisinside(int code)
+{
+	return !code;
+}
+
+static int
+lineisinside(int code0, int code1)
+{
+	return !(code0|code1);
+}
+
+static int
+lineisoutside(int code0, int code1)
+{
+	return code0 & code1;
+}
+
+static int
+outcode(Point p, Rectangle r)
+{
+	int code;
+
+	code = 0;
+	if(p.x < r.min.x) code |= CLIPL;
+	if(p.x > r.max.x) code |= CLIPR;
+	if(p.y < r.min.y) code |= CLIPT;
+	if(p.y > r.max.y) code |= CLIPB;
+	return code;
+}
+
+/*
+ * Cohen-Sutherland rectangle-line clipping
+ */
+void
+rectclipline(Rectangle r, Point *p0, Point *p1)
+{
+	int code0, code1;
+	int Δx;
+	double m;
+
+	Δx = p1->x - p0->x;
+	m = Δx == 0? 0: (p1->y - p0->y)/Δx;
+
+	for(;;){
+		code0 = outcode(*p0, r);
+		code1 = outcode(*p1, r);
+
+		if(lineisinside(code0, code1) || lineisoutside(code0, code1))
+			break;
+
+		if(ptisinside(code0)){
+			swappt(p0, p1);
+			swapi(&code0, &code1);
+		}
+
+		if(code0 & CLIPL){
+			p0->y += (r.min.x - p0->x)*m;
+			p0->x = r.min.x;
+		}else if(code0 & CLIPR){
+			p0->y += (r.max.x - p0->x)*m;
+			p0->x = r.max.x;
+		}else if(code0 & CLIPB){
+			if(p0->x != p1->x)
+				p0->x += (r.min.y - p0->y)/m;
+			p0->y = r.min.y;
+		}else if(code0 & CLIPT){
+			if(p0->x != p1->x)
+				p0->x += (r.max.y - p0->y)/m;
+			p0->y = r.max.y;
+		}
+	}
+}
--- a/doc/libgraphics.ms
+++ b/doc/libgraphics.ms
@@ -13,6 +13,7 @@
 .ps 7
 circlerad = 0.3
 moveht = 0.1
+arrowhead = 9
 box "Renderjob"
 arrow
 R: circle "renderer"
binary files a/doc/libgraphics.pdf b/doc/libgraphics.pdf differ
--- a/doc/libgraphics.ps
+++ b/doc/libgraphics.ps
@@ -636,11 +636,25 @@
 (Renderjob) 1123 3834 w
 1566 3820 1926 3820 Dl
 1854 3838 1926 3820 Dl
+1853 3833 1925 3820 Dl
+1852 3829 1925 3820 Dl
+1851 3824 1925 3820 Dl
+1851 3820 1925 3820 Dl
+1851 3815 1925 3819 Dl
+1852 3810 1925 3819 Dl
+1853 3806 1925 3819 Dl
 1854 3802 1926 3820 Dl
 1926 3820 432 432 De
 (renderer) 1997 3834 w
 2358 3820 2718 3820 Dl
 2646 3838 2718 3820 Dl
+2645 3833 2717 3820 Dl
+2644 3829 2717 3820 Dl
+2643 3824 2717 3820 Dl
+2643 3820 2717 3820 Dl
+2643 3815 2717 3819 Dl
+2644 3810 2717 3819 Dl
+2645 3806 2717 3819 Dl
 2646 3802 2718 3820 Dl
 2718 3820 432 432 De
 (entityproc) 2763 3834 w
@@ -672,39 +686,123 @@
 (n) 4671 4590 w
 3090 3670 3569 3213 Dl
 3530 3275 3569 3213 Dl
+3526 3273 3569 3213 Dl
+3522 3270 3568 3213 Dl
+3519 3267 3569 3213 Dl
+3515 3264 3569 3213 Dl
+3512 3261 3568 3214 Dl
+3510 3257 3569 3213 Dl
+3507 3253 3568 3213 Dl
 3504 3249 3568 3213 Dl
 3139 3754 3519 3634 Dl
 3456 3672 3519 3634 Dl
+3454 3668 3519 3633 Dl
+3452 3664 3519 3634 Dl
+3450 3660 3519 3634 Dl
+3449 3655 3519 3633 Dl
+3448 3651 3520 3633 Dl
+3447 3647 3519 3634 Dl
+3446 3642 3519 3633 Dl
 3445 3637 3519 3633 Dl
 3139 3885 3519 4005 Dl
 3445 4002 3519 4006 Dl
+3446 3997 3519 4006 Dl
+3447 3992 3519 4005 Dl
+3448 3988 3520 4006 Dl
+3449 3984 3519 4006 Dl
+3450 3979 3519 4005 Dl
+3452 3975 3519 4005 Dl
+3454 3971 3519 4006 Dl
 3456 3967 3519 4005 Dl
 3090 3969 3569 4426 Dl
 3504 4390 3568 4426 Dl
+3507 4386 3568 4426 Dl
+3510 4382 3569 4426 Dl
+3512 4378 3568 4425 Dl
+3515 4375 3569 4426 Dl
+3519 4372 3569 4426 Dl
+3522 4369 3568 4426 Dl
+3526 4366 3569 4426 Dl
 3530 4364 3569 4426 Dl
 3942 3064 4302 3064 Dl
 4230 3082 4302 3064 Dl
+4229 3077 4301 3064 Dl
+4228 3073 4301 3064 Dl
+4227 3068 4301 3064 Dl
+4227 3064 4301 3064 Dl
+4227 3059 4301 3063 Dl
+4228 3054 4301 3063 Dl
+4229 3050 4301 3063 Dl
 4230 3046 4302 3064 Dl
 3908 3179 4335 3451 Dl
 4265 3428 4335 3451 Dl
+4266 3424 4335 3452 Dl
+4268 3420 4334 3451 Dl
+4271 3416 4335 3452 Dl
+4273 3412 4335 3451 Dl
+4275 3408 4334 3451 Dl
+4278 3404 4335 3451 Dl
+4281 3401 4335 3451 Dl
 4284 3398 4335 3452 Dl
 3859 3233 4383 3901 Dl
 4325 3856 4383 3901 Dl
+4328 3853 4383 3901 Dl
+4332 3849 4384 3901 Dl
+4335 3846 4384 3901 Dl
+4338 3843 4384 3901 Dl
+4342 3840 4384 3901 Dl
+4345 3838 4383 3901 Dl
+4350 3836 4384 3901 Dl
 4354 3834 4384 3901 Dl
 3826 3255 4417 4384 Dl
 4368 4329 4417 4384 Dl
+4371 4326 4417 4384 Dl
+4375 4323 4417 4384 Dl
+4379 4321 4417 4384 Dl
+4383 4318 4417 4383 Dl
+4387 4316 4417 4383 Dl
+4391 4315 4416 4384 Dl
+4395 4313 4416 4384 Dl
 4400 4312 4417 4384 Dl
 3908 3452 4335 3180 Dl
 4284 3233 4335 3179 Dl
+4281 3230 4335 3180 Dl
+4278 3227 4335 3180 Dl
+4275 3223 4334 3180 Dl
+4273 3219 4335 3180 Dl
+4271 3215 4335 3179 Dl
+4268 3211 4334 3180 Dl
+4266 3208 4335 3180 Dl
 4265 3203 4335 3180 Dl
 3942 3568 4302 3568 Dl
 4230 3586 4302 3568 Dl
+4229 3581 4301 3568 Dl
+4228 3577 4301 3568 Dl
+4227 3572 4301 3568 Dl
+4227 3568 4301 3568 Dl
+4227 3563 4301 3567 Dl
+4228 3558 4301 3567 Dl
+4229 3554 4301 3567 Dl
 4230 3550 4302 3568 Dl
 3908 3683 4335 3955 Dl
 4265 3932 4335 3955 Dl
+4266 3928 4335 3956 Dl
+4268 3924 4334 3955 Dl
+4271 3920 4335 3956 Dl
+4273 3916 4335 3955 Dl
+4275 3912 4334 3955 Dl
+4278 3908 4335 3955 Dl
+4281 3905 4335 3955 Dl
 4284 3902 4335 3956 Dl
 3859 3737 4383 4405 Dl
 4325 4360 4383 4405 Dl
+4328 4357 4383 4405 Dl
+4332 4353 4384 4405 Dl
+4335 4350 4384 4405 Dl
+4338 4347 4384 4405 Dl
+4342 4344 4384 4405 Dl
+4345 4342 4383 4405 Dl
+4350 4340 4384 4405 Dl
 4354 4338 4384 4405 Dl
 10 /LucidaSans-Demi f
 (Figure 1) 720 4990 w
@@ -1098,15 +1196,43 @@
 3272 3644 3292 3614 Dl
 3309 3588 3329 3558 Dl
 3304 3627 3329 3558 Dl
+3300 3626 3329 3558 Dl
+3296 3624 3329 3558 Dl
+3292 3622 3329 3558 Dl
+3288 3619 3329 3558 Dl
+3285 3617 3329 3558 Dl
+3281 3614 3329 3558 Dl
+3278 3611 3329 3558 Dl
 3275 3607 3329 3558 Dl
 2970 4242 3330 4062 Dl
 3273 4110 3329 4062 Dl
+3270 4106 3329 4062 Dl
+3268 4103 3329 4062 Dl
+3265 4099 3329 4062 Dl
+3263 4095 3329 4062 Dl
+3261 4090 3329 4062 Dl
+3260 4086 3329 4062 Dl
+3258 4082 3329 4062 Dl
 3257 4077 3329 4062 Dl
 2970 4386 3330 4566 Dl
 3257 4550 3329 4565 Dl
+3258 4545 3329 4565 Dl
+3260 4541 3329 4565 Dl
+3261 4537 3329 4565 Dl
+3263 4532 3329 4565 Dl
+3265 4528 3329 4565 Dl
+3268 4524 3329 4565 Dl
+3270 4521 3329 4565 Dl
 3273 4517 3329 4565 Dl
 2970 4530 3330 5070 Dl
 3275 5020 3329 5069 Dl
+3278 5016 3329 5069 Dl
+3281 5013 3329 5069 Dl
+3285 5010 3329 5069 Dl
+3288 5008 3329 5069 Dl
+3292 5005 3329 5069 Dl
+3296 5003 3329 5069 Dl
+3300 5001 3329 5069 Dl
 3304 5000 3329 5069 Dl
 10 /LucidaSans-Demi f
 (Figure 3) 720 5484 w
--- a/graphics.h
+++ b/graphics.h
@@ -86,7 +86,7 @@
 	Point3 n;		/* surface normal */
 	Color c;		/* shading color */
 	Point2 uv;		/* texture coordinate */
-	OBJMaterial *mtl;
+	Material *mtl;
 
 	/* TODO it'd be neat to use a dynamic hash table instead */
 	Vertexattr *attrs;	/* attributes (aka varyings) */
@@ -106,6 +106,7 @@
 	Color diffuse;
 	Color specular;
 	double shininess;
+	Memimage *diffusemap;
 };
 
 struct Primitive
@@ -112,19 +113,17 @@
 {
 	int type;
 	Vertex v[3];
+	Material *mtl;
 };
 
 struct Model
 {
-	Primitive *mesh;
+	Primitive *prims;
+	ulong nprims;
 	Memimage *tex;		/* texture map */
 	Memimage *nor;		/* normals map */
 	Material *materials;
 	ulong nmaterials;
-
-	OBJ *obj;
-	OBJElem **elems;	/* cache of renderable elems */
-	ulong nelems;
 };
 
 struct Entity
@@ -167,7 +166,7 @@
 	Memimage *frag;
 	Renderjob *job;
 	Entity *entity;
-	OBJElem **eb, **ee;
+	Primitive *eb, *ee;
 
 	uvlong uni_time;
 
@@ -283,7 +282,7 @@
 void orthographic(Matrix3, double, double, double, double, double, double);
 
 /* scene */
-int refreshmodel(Model*);
+int loadobjmodel(Model*, OBJ*);
 Model *newmodel(void);
 void delmodel(Model*);
 Entity *newentity(Model*);
--- a/internal.h
+++ b/internal.h
@@ -1,7 +1,15 @@
+typedef struct Polygon Polygon;
 typedef struct Tilerparam Tilerparam;
 typedef struct Rasterparam Rasterparam;
 typedef struct Rastertask Rastertask;
 
+struct Polygon
+{
+	Vertex *v;
+	ulong n;
+	ulong cap;
+};
+
 struct Tilerparam
 {
 	int id;
@@ -42,9 +50,15 @@
 void delvattrs(Vertex*);
 void fprintvattrs(int, Vertex*);
 
+/* clip */
+int clipprimitive(Primitive*);
+void rectclipline(Rectangle, Point*, Point*);
+
 /* util */
 int min(int, int);
 int max(int, int);
+void swapi(int*, int*);
+void swappt(Point*, Point*);
 void memsetd(double*, double, usize);
 
 /* nanosec */
--- a/mkfile
+++ b/mkfile
@@ -5,6 +5,7 @@
 	camera.$O\
 	viewport.$O\
 	render.$O\
+	clip.$O\
 	scene.$O\
 	vertex.$O\
 	texture.$O\
--- a/render.c
+++ b/render.c
@@ -52,134 +52,7 @@
 	return sa <= 0;
 }
 
-static void
-mulsdm(double r[6], double m[6][4], Point3 p)
-{
-	int i;
-
-	for(i = 0; i < 6; i++)
-		r[i] = m[i][0]*p.x + m[i][1]*p.y + m[i][2]*p.z + m[i][3]*p.w;
-}
-
-typedef struct
-{
-	Vertex *v;
-	ulong n;
-	ulong cap;
-} Polygon;
-
-static int
-addvert(Polygon *p, Vertex v)
-{
-	if(++p->n > p->cap)
-		p->v = erealloc(p->v, (p->cap = p->n)*sizeof(*p->v));
-	p->v[p->n-1] = v;
-	return p->n;
-}
-
-static void
-swappoly(Polygon *a, Polygon *b)
-{
-	Polygon tmp;
-
-	tmp = *a;
-	*a = *b;
-	*b = tmp;
-}
-
-static void
-cleanpoly(Polygon *p)
-{
-	int i;
-
-	for(i = 0; i < p->n; i++)
-		delvattrs(&p->v[i]);
-	p->n = 0;
-}
-
 /*
- * references:
- * 	- James F. Blinn, Martin E. Newell, “Clipping Using Homogeneous Coordinates”,
- * 	  SIGGRAPH '78, pp. 245-251
- * 	- https://cs418.cs.illinois.edu/website/text/clipping.html
- * 	- https://github.com/aap/librw/blob/14dab85dcae6f3762fb2b1eda4d58d8e67541330/tools/playground/tl_tests.cpp#L522
- */
-static int
-cliptriangle(Primitive *p)
-{
-	/* signed distance from each clipping plane */
-	static double sdm[6][4] = {
-		 1,  0,  0, 1,	/* l */
-		-1,  0,  0, 1,	/* r */
-		 0,  1,  0, 1,	/* b */
-		 0, -1,  0, 1,	/* t */
-		 0,  0,  1, 1,	/* f */
-		 0,  0, -1, 1,	/* n */
-	};
-	double sd0[6], sd1[6];
-	double d0, d1, perc;
-	Polygon Vin, Vout;
-	Vertex *v0, *v1, v;	/* edge verts and new vertex (line-plane intersection) */
-	int i, j, nt;
-
-	nt = 0;
-	memset(&Vin, 0, sizeof Vin);
-	memset(&Vout, 0, sizeof Vout);
-	for(i = 0; i < 3; i++)
-		addvert(&Vin, p[0].v[i]);
-
-	for(j = 0; j < 6 && Vin.n > 0; j++){
-		for(i = 0; i < Vin.n; i++){
-			v0 = &Vin.v[i];
-			v1 = &Vin.v[(i+1) % Vin.n];
-
-			mulsdm(sd0, sdm, v0->p);
-			mulsdm(sd1, sdm, v1->p);
-
-			if(sd0[j] < 0 && sd1[j] < 0)
-				continue;
-
-			if(sd0[j] >= 0 && sd1[j] >= 0)
-				goto allin;
-
-			d0 = (j&1) == 0? sd0[j]: -sd0[j];
-			d1 = (j&1) == 0? sd1[j]: -sd1[j];
-			perc = d0/(d0 - d1);
-
-			lerpvertex(&v, v0, v1, perc);
-			addvert(&Vout, v);
-
-			if(sd1[j] >= 0){
-allin:
-				addvert(&Vout, dupvertex(v1));
-			}
-		}
-		cleanpoly(&Vin);
-		if(j < 6-1)
-			swappoly(&Vin, &Vout);
-	}
-
-	/* triangulate */
-	if(Vout.n < 3)
-		cleanpoly(&Vout);
-	else
-		for(i = 0; i < Vout.n-2; i++, nt++){
-			/*
-			 * when performing fan triangulation, indices 0 and 2
-			 * are referenced on every triangle, so duplicate them
-			 * to avoid complications during rasterization.
-			 */
-			p[nt].v[0] = i < Vout.n-2-1? dupvertex(&Vout.v[0]): Vout.v[0];
-			p[nt].v[1] = Vout.v[i+1];
-			p[nt].v[2] = i < Vout.n-2-1? dupvertex(&Vout.v[i+2]): Vout.v[i+2];
-		}
-	free(Vout.v);
-	free(Vin.v);
-
-	return nt;
-}
-
-/*
  * transforms p from e's reference frame into
  * the world.
  */
@@ -297,54 +170,83 @@
 	SUparams *params;
 	Primitive prim;
 	FSparams fsp;
-	Triangle2 t₂;
+	Triangle2 t;
 	Rectangle bbox;
-	Point p;
+	Point p, dp, Δp, p0, p1;
 	Point3 bc;
 	Color c;
-	double z, depth;
+	double z, depth, dplen, perc;
+	int steep = 0, Δe, e, Δy;
 
 	params = task->params;
 	prim = task->p;
 	memmove(prim.v, task->p.v, sizeof prim.v);
-
-	t₂.p0 = Pt2(prim.v[0].p.x, prim.v[0].p.y, 1);
-	t₂.p1 = Pt2(prim.v[1].p.x, prim.v[1].p.y, 1);
-	t₂.p2 = Pt2(prim.v[2].p.x, prim.v[2].p.y, 1);
-	/* find the triangle's bbox and clip it against our wr */
-	bbox.min.x = min(min(t₂.p0.x, t₂.p1.x), t₂.p2.x);
-	bbox.min.y = min(min(t₂.p0.y, t₂.p1.y), t₂.p2.y);
-	bbox.max.x = max(max(t₂.p0.x, t₂.p1.x), t₂.p2.x)+1;
-	bbox.max.y = max(max(t₂.p0.y, t₂.p1.y), t₂.p2.y)+1;
-	bbox.min.x = max(bbox.min.x, task->wr.min.x);
-	bbox.min.y = max(bbox.min.y, task->wr.min.y);
-	bbox.max.x = min(bbox.max.x, task->wr.max.x);
-	bbox.max.y = min(bbox.max.y, task->wr.max.y);
 	fsp.su = params;
 	memset(&fsp.v, 0, sizeof fsp.v);
 
-	for(p.y = bbox.min.y; p.y < bbox.max.y; p.y++)
-		for(p.x = bbox.min.x; p.x < bbox.max.x; p.x++){
-			bc = barycoords(t₂, Pt2(p.x,p.y,1));
-			if(bc.x < 0 || bc.y < 0 || bc.z < 0)
-				continue;
+	switch(prim.type){
+	case PPoint:
+		p = Pt(prim.v[0].p.x, prim.v[0].p.y);
 
-			z = fberp(prim.v[0].p.z, prim.v[1].p.z, prim.v[2].p.z, bc);
+		depth = fclamp(prim.v[0].p.z, 0, 1);
+		if(depth <= params->fb->zb[p.x + p.y*Dx(params->fb->r)])
+			break;
+		params->fb->zb[p.x + p.y*Dx(params->fb->r)] = depth;
+
+		fsp.v = dupvertex(&prim.v[0]);
+		fsp.p = p;
+		c = params->fshader(&fsp);
+		memfillcolor(params->frag, col2ul(c));
+
+		pixel(params->fb->cb, p, params->frag);
+		delvattrs(&fsp.v);
+		break;
+	case PLine:
+		p0 = Pt(prim.v[0].p.x, prim.v[0].p.y);
+		p1 = Pt(prim.v[1].p.x, prim.v[1].p.y);
+		/* clip it against our wr */
+		rectclipline(task->wr, &p0, &p1);
+
+		/* transpose the points */
+		if(abs(p0.x-p1.x) < abs(p0.y-p1.y)){
+			steep = 1;
+			swapi(&p0.x, &p0.y);
+			swapi(&p1.x, &p1.y);
+		}
+
+		/* make them left-to-right */
+		if(p0.x > p1.x){
+			swapi(&p0.x, &p1.x);
+			swapi(&p0.y, &p1.y);
+		}
+
+		dp = subpt(p1, p0);
+		Δe = 2*abs(dp.y);
+		e = 0;
+		Δy = p1.y > p0.y? 1: -1;
+
+		/* TODO find out why sometimes lines go invisible depending on their location */
+
+		for(p = p0; p.x <= p1.x; p.x++){
+			Δp = subpt(p, p0);
+			dplen = hypot(dp.x, dp.y);
+			perc = dplen == 0? 0: hypot(Δp.x, Δp.y)/dplen;
+
+			if(steep) swapi(&p.x, &p.y);
+
+			z = flerp(prim.v[0].p.z, prim.v[1].p.z, perc);
 			depth = fclamp(z, 0, 1);
 			if(depth <= params->fb->zb[p.x + p.y*Dx(params->fb->r)])
-				continue;
+				break;
 			params->fb->zb[p.x + p.y*Dx(params->fb->r)] = depth;
 
 			/* interpolate z⁻¹ and get actual z */
-			z = fberp(prim.v[0].p.w, prim.v[1].p.w, prim.v[2].p.w, bc);
+			z = flerp(prim.v[0].p.w, prim.v[1].p.w, perc);
 			z = 1.0/(z < 1e-5? 1e-5: z);
 
 			/* perspective-correct attribute interpolation  */
-			bc.x *= prim.v[0].p.w;
-			bc.y *= prim.v[1].p.w;
-			bc.z *= prim.v[2].p.w;
-			bc = mulpt3(bc, z);
-			berpvertex(&fsp.v, &prim.v[0], &prim.v[1], &prim.v[2], bc);
+			perc *= prim.v[0].p.w * z;
+			lerpvertex(&fsp.v, &prim.v[0], &prim.v[1], perc);
 
 			fsp.p = p;
 			c = params->fshader(&fsp);
@@ -352,7 +254,62 @@
 
 			pixel(params->fb->cb, p, params->frag);
 			delvattrs(&fsp.v);
+
+			if(steep) swapi(&p.x, &p.y);
+
+			e += Δe;
+			if(e > dp.x){
+				p.y += Δy;
+				e -= 2*dp.x;
+			}
 		}
+		break;
+	case PTriangle:
+		t.p0 = Pt2(prim.v[0].p.x, prim.v[0].p.y, 1);
+		t.p1 = Pt2(prim.v[1].p.x, prim.v[1].p.y, 1);
+		t.p2 = Pt2(prim.v[2].p.x, prim.v[2].p.y, 1);
+		/* find the triangle's bbox and clip it against our wr */
+		bbox.min.x = min(min(t.p0.x, t.p1.x), t.p2.x);
+		bbox.min.y = min(min(t.p0.y, t.p1.y), t.p2.y);
+		bbox.max.x = max(max(t.p0.x, t.p1.x), t.p2.x)+1;
+		bbox.max.y = max(max(t.p0.y, t.p1.y), t.p2.y)+1;
+		bbox.min.x = max(bbox.min.x, task->wr.min.x);
+		bbox.min.y = max(bbox.min.y, task->wr.min.y);
+		bbox.max.x = min(bbox.max.x, task->wr.max.x);
+		bbox.max.y = min(bbox.max.y, task->wr.max.y);
+
+		for(p.y = bbox.min.y; p.y < bbox.max.y; p.y++)
+			for(p.x = bbox.min.x; p.x < bbox.max.x; p.x++){
+				bc = barycoords(t, Pt2(p.x,p.y,1));
+				if(bc.x < 0 || bc.y < 0 || bc.z < 0)
+					continue;
+
+				z = fberp(prim.v[0].p.z, prim.v[1].p.z, prim.v[2].p.z, bc);
+				depth = fclamp(z, 0, 1);
+				if(depth <= params->fb->zb[p.x + p.y*Dx(params->fb->r)])
+					continue;
+				params->fb->zb[p.x + p.y*Dx(params->fb->r)] = depth;
+
+				/* interpolate z⁻¹ and get actual z */
+				z = fberp(prim.v[0].p.w, prim.v[1].p.w, prim.v[2].p.w, bc);
+				z = 1.0/(z < 1e-5? 1e-5: z);
+
+				/* perspective-correct attribute interpolation  */
+				bc.x *= prim.v[0].p.w;
+				bc.y *= prim.v[1].p.w;
+				bc.z *= prim.v[2].p.w;
+				bc = mulpt3(bc, z);
+				berpvertex(&fsp.v, &prim.v[0], &prim.v[1], &prim.v[2], bc);
+
+				fsp.p = p;
+				c = params->fshader(&fsp);
+				memfillcolor(params->frag, col2ul(c));
+
+				pixel(params->fb->cb, p, params->frag);
+				delvattrs(&fsp.v);
+			}
+		break;
+	}
 }
 
 static void
@@ -363,6 +320,7 @@
 	SUparams *params;
 	Memimage *frag;
 	uvlong t0;
+	int i;
 
 	rp = arg;
 	frag = rgb(DBlack);
@@ -389,9 +347,8 @@
 		params->frag = frag;
 		rasterize(task);
 
-		delvattrs(&task->p.v[0]);
-		delvattrs(&task->p.v[1]);
-		delvattrs(&task->p.v[2]);
+		for(i = 0; i < task->p.type+1; i++)
+			delvattrs(&task->p.v[i]);
 		params->job->times.Rn.t1 = nanosec();
 		free(params);
 		free(task);
@@ -405,11 +362,7 @@
 	SUparams *params, *newparams;
 	Rastertask *task;
 	VSparams vsp;
-	OBJVertex *verts, *tverts, *nverts;	/* geometric, texture and normals vertices */
-	OBJIndexArray *idxtab;
-	OBJElem **ep;
-	Point3 n;				/* surface normal */
-	Primitive *p;				/* primitives to raster */
+	Primitive *ep, *p;			/* primitives to raster */
 	Rectangle *wr, bbox;
 	Channel **taskchans;
 	ulong Δy, nproc;
@@ -452,99 +405,32 @@
 		if(wr[nproc-1].max.y < params->fb->r.max.y)
 			wr[nproc-1].max.y = params->fb->r.max.y;
 
-		verts = params->entity->mdl->obj->vertdata[OBJVGeometric].verts;
-		tverts = params->entity->mdl->obj->vertdata[OBJVTexture].verts;
-		nverts = params->entity->mdl->obj->vertdata[OBJVNormal].verts;
-
 		for(ep = params->eb; ep != params->ee; ep++){
 			np = 1;	/* start with one. after clipping it might change */
 
-			/* TODO handle all the primitive types */
+			memmove(p, ep, sizeof *p);
+			switch(ep->type){
+			case PPoint:
+				p[0].v[0].c = Pt3(1,1,1,1);
+				p[0].v[0].mtl = ep->mtl;
+				p[0].v[0].attrs = nil;
+				p[0].v[0].nattrs = 0;
 
-			idxtab = &(*ep)->indextab[OBJVGeometric];
-			p[0].v[0].p = Pt3(verts[idxtab->indices[0]].x,
-					  verts[idxtab->indices[0]].y,
-					  verts[idxtab->indices[0]].z,
-					  verts[idxtab->indices[0]].w);
-			p[0].v[1].p = Pt3(verts[idxtab->indices[1]].x,
-					  verts[idxtab->indices[1]].y,
-					  verts[idxtab->indices[1]].z,
-					  verts[idxtab->indices[1]].w);
-			p[0].v[2].p = Pt3(verts[idxtab->indices[2]].x,
-					  verts[idxtab->indices[2]].y,
-					  verts[idxtab->indices[2]].z,
-					  verts[idxtab->indices[2]].w);
+				vsp.v = &p[0].v[0];
+				vsp.idx = 0;
+				p[0].v[0].p = params->vshader(&vsp);
 
-			idxtab = &(*ep)->indextab[OBJVNormal];
-			if(idxtab->nindex == 3){
-				p[0].v[0].n = Vec3(nverts[idxtab->indices[0]].i,
-						   nverts[idxtab->indices[0]].j,
-						   nverts[idxtab->indices[0]].k);
-				p[0].v[0].n = normvec3(p[0].v[0].n);
-				p[0].v[1].n = Vec3(nverts[idxtab->indices[1]].i,
-						   nverts[idxtab->indices[1]].j,
-						   nverts[idxtab->indices[1]].k);
-				p[0].v[1].n = normvec3(p[0].v[1].n);
-				p[0].v[2].n = Vec3(nverts[idxtab->indices[2]].i,
-						   nverts[idxtab->indices[2]].j,
-						   nverts[idxtab->indices[2]].k);
-				p[0].v[2].n = normvec3(p[0].v[2].n);
-			}else{
-				/* TODO build a list of per-vertex normals earlier */
-				n = normvec3(crossvec3(subpt3(p[0].v[1].p, p[0].v[0].p), subpt3(p[0].v[2].p, p[0].v[0].p)));
-				p[0].v[0].n = p[0].v[1].n = p[0].v[2].n = n;
-			}
+				if(!isvisible(p[0].v[0].p))
+					break;
 
-			idxtab = &(*ep)->indextab[OBJVTexture];
-			if(idxtab->nindex == 3){
-				p[0].v[0].uv = Pt2(tverts[idxtab->indices[0]].u,
-						   tverts[idxtab->indices[0]].v, 1);
-				p[0].v[1].uv = Pt2(tverts[idxtab->indices[1]].u,
-						   tverts[idxtab->indices[1]].v, 1);
-				p[0].v[2].uv = Pt2(tverts[idxtab->indices[2]].u,
-						   tverts[idxtab->indices[2]].v, 1);
-			}else{
-				p[0].v[0].uv = p[0].v[1].uv = p[0].v[2].uv = Vec2(0,0);
-			}
+				p[0].v[0].p = clip2ndc(p[0].v[0].p);
+				p[0].v[0].p = ndc2viewport(params->fb, p[0].v[0].p);
 
-			for(i = 0; i < 3; i++){
-				p[0].v[i].c = Pt3(1,1,1,1);
-				p[0].v[i].mtl = (*ep)->mtl;
-				p[0].v[i].attrs = nil;
-				p[0].v[i].nattrs = 0;
-			}
+				bbox.min.x = p[0].v[0].p.x;
+				bbox.min.y = p[0].v[0].p.y;
+				bbox.max.x = p[0].v[0].p.x+1;
+				bbox.max.y = p[0].v[0].p.y+1;
 
-			vsp.v = &p[0].v[0];
-			vsp.idx = 0;
-			p[0].v[0].p = params->vshader(&vsp);
-			vsp.v = &p[0].v[1];
-			vsp.idx = 1;
-			p[0].v[1].p = params->vshader(&vsp);
-			vsp.v = &p[0].v[2];
-			vsp.idx = 2;
-			p[0].v[2].p = params->vshader(&vsp);
-
-			if(!isvisible(p[0].v[0].p) || !isvisible(p[0].v[1].p) || !isvisible(p[0].v[2].p))
-				np = cliptriangle(p);
-
-			while(np--){
-				p[np].v[0].p = clip2ndc(p[np].v[0].p);
-				p[np].v[1].p = clip2ndc(p[np].v[1].p);
-				p[np].v[2].p = clip2ndc(p[np].v[2].p);
-
-				/* culling */
-//				if(isfacingback(p[np]))
-//					goto skiptri;
-
-				p[np].v[0].p = ndc2viewport(params->fb, p[np].v[0].p);
-				p[np].v[1].p = ndc2viewport(params->fb, p[np].v[1].p);
-				p[np].v[2].p = ndc2viewport(params->fb, p[np].v[2].p);
-
-				bbox.min.x = min(min(p[np].v[0].p.x, p[np].v[1].p.x), p[np].v[2].p.x);
-				bbox.min.y = min(min(p[np].v[0].p.y, p[np].v[1].p.y), p[np].v[2].p.y);
-				bbox.max.x = max(max(p[np].v[0].p.x, p[np].v[1].p.x), p[np].v[2].p.x)+1;
-				bbox.max.y = max(max(p[np].v[0].p.y, p[np].v[1].p.y), p[np].v[2].p.y)+1;
-
 				for(i = 0; i < nproc; i++)
 					if(rectXrect(bbox,wr[i])){
 						newparams = emalloc(sizeof *newparams);
@@ -552,15 +438,112 @@
 						task = emalloc(sizeof *task);
 						task->params = newparams;
 						task->wr = wr[i];
-						task->p.v[0] = dupvertex(&p[np].v[0]);
-						task->p.v[1] = dupvertex(&p[np].v[1]);
-						task->p.v[2] = dupvertex(&p[np].v[2]);
+						memmove(&task->p, &p[0], sizeof task->p);
+						task->p.v[0] = dupvertex(&p[0].v[0]);
 						sendp(taskchans[i], task);
 					}
+				delvattrs(&p[0].v[0]);
+				break;
+			case PLine:
+				for(i = 0; i < 2; i++){
+					p[0].v[i].c = Pt3(1,1,1,1);
+					p[0].v[i].mtl = ep->mtl;
+					p[0].v[i].attrs = nil;
+					p[0].v[i].nattrs = 0;
+
+					vsp.v = &p[0].v[i];
+					vsp.idx = i;
+					p[0].v[i].p = params->vshader(&vsp);
+				}
+
+				if(!isvisible(p[0].v[0].p) || !isvisible(p[0].v[1].p))
+					np = clipprimitive(p);
+
+				while(np--){
+					p[np].v[0].p = clip2ndc(p[np].v[0].p);
+					p[np].v[1].p = clip2ndc(p[np].v[1].p);
+
+					/* culling */
+//					if(isfacingback(p[np]))
+//						goto skiptri2;
+
+					p[np].v[0].p = ndc2viewport(params->fb, p[np].v[0].p);
+					p[np].v[1].p = ndc2viewport(params->fb, p[np].v[1].p);
+
+					bbox.min.x = min(p[np].v[0].p.x, p[np].v[1].p.x);
+					bbox.min.y = min(p[np].v[0].p.y, p[np].v[1].p.y);
+					bbox.max.x = max(p[np].v[0].p.x, p[np].v[1].p.x)+1;
+					bbox.max.y = max(p[np].v[0].p.y, p[np].v[1].p.y)+1;
+
+					for(i = 0; i < nproc; i++)
+						if(rectXrect(bbox,wr[i])){
+							newparams = emalloc(sizeof *newparams);
+							*newparams = *params;
+							task = emalloc(sizeof *task);
+							task->params = newparams;
+							task->wr = wr[i];
+							memmove(&task->p, &p[np], sizeof task->p);
+							task->p.v[0] = dupvertex(&p[np].v[0]);
+							task->p.v[1] = dupvertex(&p[np].v[1]);
+							sendp(taskchans[i], task);
+						}
+//skiptri2:
+					delvattrs(&p[np].v[0]);
+					delvattrs(&p[np].v[1]);
+				}
+				break;
+			case PTriangle:
+				for(i = 0; i < 3; i++){
+					p[0].v[i].c = Pt3(1,1,1,1);
+					p[0].v[i].mtl = p->mtl;
+					p[0].v[i].attrs = nil;
+					p[0].v[i].nattrs = 0;
+
+					vsp.v = &p[0].v[i];
+					vsp.idx = i;
+					p[0].v[i].p = params->vshader(&vsp);
+				}
+
+				if(!isvisible(p[0].v[0].p) || !isvisible(p[0].v[1].p) || !isvisible(p[0].v[2].p))
+					np = clipprimitive(p);
+
+				while(np--){
+					p[np].v[0].p = clip2ndc(p[np].v[0].p);
+					p[np].v[1].p = clip2ndc(p[np].v[1].p);
+					p[np].v[2].p = clip2ndc(p[np].v[2].p);
+
+					/* culling */
+//					if(isfacingback(p[np]))
+//						goto skiptri;
+
+					p[np].v[0].p = ndc2viewport(params->fb, p[np].v[0].p);
+					p[np].v[1].p = ndc2viewport(params->fb, p[np].v[1].p);
+					p[np].v[2].p = ndc2viewport(params->fb, p[np].v[2].p);
+
+					bbox.min.x = min(min(p[np].v[0].p.x, p[np].v[1].p.x), p[np].v[2].p.x);
+					bbox.min.y = min(min(p[np].v[0].p.y, p[np].v[1].p.y), p[np].v[2].p.y);
+					bbox.max.x = max(max(p[np].v[0].p.x, p[np].v[1].p.x), p[np].v[2].p.x)+1;
+					bbox.max.y = max(max(p[np].v[0].p.y, p[np].v[1].p.y), p[np].v[2].p.y)+1;
+
+					for(i = 0; i < nproc; i++)
+						if(rectXrect(bbox,wr[i])){
+							newparams = emalloc(sizeof *newparams);
+							*newparams = *params;
+							task = emalloc(sizeof *task);
+							task->params = newparams;
+							task->wr = wr[i];
+							memmove(&task->p, &p[np], sizeof task->p);
+							task->p.v[0] = dupvertex(&p[np].v[0]);
+							task->p.v[1] = dupvertex(&p[np].v[1]);
+							task->p.v[2] = dupvertex(&p[np].v[2]);
+							sendp(taskchans[i], task);
+						}
 //skiptri:
-				delvattrs(&p[np].v[0]);
-				delvattrs(&p[np].v[1]);
-				delvattrs(&p[np].v[2]);
+					delvattrs(&p[np].v[0]);
+					delvattrs(&p[np].v[1]);
+					delvattrs(&p[np].v[2]);
+				}
+				break;
 			}
 		}
 		params->job->times.Tn.t1 = nanosec();
@@ -575,9 +558,9 @@
 	Tilerparam *tp;
 	Rasterparam *rp;
 	SUparams *params, *newparams;
-	OBJElem **eb, **ee;
+	Primitive *eb, *ee;
 	char *nprocs;
-	ulong stride, nelems, nproc, nworkers;
+	ulong stride, nprims, nproc, nworkers;
 	int i;
 	uvlong t0;
 
@@ -622,16 +605,16 @@
 			continue;
 		}
 
-		eb = params->entity->mdl->elems;
-		nelems = params->entity->mdl->nelems;
-		ee = eb + nelems;
+		eb = params->entity->mdl->prims;
+		nprims = params->entity->mdl->nprims;
+		ee = eb + nprims;
 
-		if(nelems <= nproc){
-			nworkers = nelems;
+		if(nprims <= nproc){
+			nworkers = nprims;
 			stride = 1;
 		}else{
 			nworkers = nproc;
-			stride = nelems/nproc;
+			stride = nprims/nproc;
 		}
 
 		for(i = 0; i < nworkers; i++){
--- a/scene.c
+++ b/scene.c
@@ -20,16 +20,16 @@
 {
 	OBJIndexArray *newidxtab;
 	OBJIndexArray *idxtab;
-	int i, nt;
+	int i;
 
-	nt = 0;
 	idxtab = &e->indextab[OBJVGeometric];
 	for(i = 0; i < idxtab->nindex-2; i++){
 		idxtab = &e->indextab[OBJVGeometric];
-		newe[nt++] = emalloc(sizeof **newe);
-		newe[nt-1]->type = OBJEFace;
-		newe[nt-1]->mtl = e->mtl;
-		newidxtab = &newe[nt-1]->indextab[OBJVGeometric];
+		newe[i] = emalloc(sizeof **newe);
+		memset(newe[i], 0, sizeof **newe);
+		newe[i]->type = OBJEFace;
+		newe[i]->mtl = e->mtl;
+		newidxtab = &newe[i]->indextab[OBJVGeometric];
 		newidxtab->nindex = 3;
 		newidxtab->indices = emalloc(newidxtab->nindex*sizeof(*newidxtab->indices));
 		newidxtab->indices[0] = idxtab->indices[0];
@@ -37,7 +37,7 @@
 		newidxtab->indices[2] = idxtab->indices[i+2];
 		idxtab = &e->indextab[OBJVTexture];
 		if(idxtab->nindex > 0){
-			newidxtab = &newe[nt-1]->indextab[OBJVTexture];
+			newidxtab = &newe[i]->indextab[OBJVTexture];
 			newidxtab->nindex = 3;
 			newidxtab->indices = emalloc(newidxtab->nindex*sizeof(*newidxtab->indices));
 			newidxtab->indices[0] = idxtab->indices[0];
@@ -46,7 +46,7 @@
 		}
 		idxtab = &e->indextab[OBJVNormal];
 		if(idxtab->nindex > 0){
-			newidxtab = &newe[nt-1]->indextab[OBJVNormal];
+			newidxtab = &newe[i]->indextab[OBJVNormal];
 			newidxtab->nindex = 3;
 			newidxtab->indices = emalloc(newidxtab->nindex*sizeof(*newidxtab->indices));
 			newidxtab->indices[0] = idxtab->indices[0];
@@ -55,53 +55,236 @@
 		}
 	}
 
-	return nt;
+	return i;
 }
 
-/*
- * rebuild the cache of renderable OBJElems.
- *
- * run it every time the Model's geometry changes.
- */
+typedef struct OBJ2MtlEntry OBJ2MtlEntry;
+typedef struct OBJ2MtlMap OBJ2MtlMap;
+
+struct OBJ2MtlEntry
+{
+	OBJMaterial *objmtl;
+	ulong idx;
+	OBJ2MtlEntry *next;
+};
+
+struct OBJ2MtlMap
+{
+	OBJ2MtlEntry *head;
+	Material *mtls;
+};
+
+static void
+addmtlmap(OBJ2MtlMap *map, OBJMaterial *om, ulong idx)
+{
+	OBJ2MtlEntry *e;
+
+	if(om == nil)
+		return;
+
+	e = emalloc(sizeof *e);
+	memset(e, 0, sizeof *e);
+	e->objmtl = om;
+	e->idx = idx;
+
+	if(map->head == nil){
+		map->head = e;
+		return;
+	}
+
+	e->next = map->head;
+	map->head = e;
+}
+
+static Material *
+getmtlmap(OBJ2MtlMap *map, OBJMaterial *om)
+{
+	OBJ2MtlEntry *e;
+
+	for(e = map->head; e != nil; e = e->next)
+		if(e->objmtl == om)
+			return &map->mtls[e->idx];
+	return nil;
+}
+
+static void
+clrmtlmap(OBJ2MtlMap *map)
+{
+	OBJ2MtlEntry *e, *ne;
+
+	for(e = map->head; e != nil; e = ne){
+		ne = e->next;
+		free(e);
+	}
+}
+
 int
-refreshmodel(Model *m)
+loadobjmodel(Model *m, OBJ *obj)
 {
-	OBJElem **trielems;
+	Primitive *p;
+	OBJVertex *pverts, *tverts, *nverts, *v;	/* geometric, texture and normals vertices */
+	OBJElem **trielems, *e, *ne;
 	OBJObject *o;
-	OBJElem *e;
 	OBJIndexArray *idxtab;
-	int i, nt;
+	OBJ2MtlMap mtlmap;
+	OBJMaterial *objmtl;
+	Material *mtl;
+	Point3 n;					/* surface normal */
+	int i, idx, nt, maxnt, neednormal, gottaclean;
 
-	if(m->obj == nil)
+	if(obj == nil)
 		return 0;
 
-	if(m->elems != nil){
-		free(m->elems);
-		m->elems = nil;
+	pverts = obj->vertdata[OBJVGeometric].verts;
+	tverts = obj->vertdata[OBJVTexture].verts;
+	nverts = obj->vertdata[OBJVNormal].verts;
+	trielems = nil;
+	maxnt = 0;
+
+	if(m->prims != nil){
+		free(m->prims);
+		m->prims = nil;
 	}
-	m->nelems = 0;
-	for(i = 0; i < nelem(m->obj->objtab); i++)
-		for(o = m->obj->objtab[i]; o != nil; o = o->next)
-			for(e = o->child; e != nil; e = e->next){
-				idxtab = &e->indextab[OBJVGeometric];
-				/* discard non-surfaces */
-				if(e->type != OBJEFace || idxtab->nindex < 3)
-					continue;
-				if(idxtab->nindex > 3){
+	m->nprims = 0;
+
+	mtlmap.head = nil;
+	for(i = 0; obj->materials != nil && i < nelem(obj->materials->mattab); i++)
+		for(objmtl = obj->materials->mattab[i]; objmtl != nil; objmtl = objmtl->next){
+			mtlmap.mtls = m->materials = erealloc(m->materials, ++m->nmaterials*sizeof(*m->materials));
+			mtl = &m->materials[m->nmaterials-1];
+			memset(mtl, 0, sizeof *mtl);
+
+			mtl->ambient.r = objmtl->Ka.r;
+			mtl->ambient.g = objmtl->Ka.g;
+			mtl->ambient.b = objmtl->Ka.b;
+			mtl->ambient.a = 1;
+			mtl->diffuse.r = objmtl->Kd.r;
+			mtl->diffuse.g = objmtl->Kd.g;
+			mtl->diffuse.b = objmtl->Kd.b;
+			mtl->diffuse.a = 1;
+			mtl->specular.r = objmtl->Ks.r;
+			mtl->specular.g = objmtl->Ks.g;
+			mtl->specular.b = objmtl->Ks.b;
+			mtl->specular.a = 1;
+			mtl->shininess = objmtl->Ns;
+
+			if(objmtl->map_Kd != nil){
+				mtl->diffusemap = allocmemimaged(objmtl->map_Kd->r, objmtl->map_Kd->chan, objmtl->map_Kd->data);
+				if(mtl->diffusemap == nil)
+					sysfatal("allocmemimaged: %r");
+				mtl->diffusemap->data->ref++;
+			}
+
+			addmtlmap(&mtlmap, objmtl, m->nmaterials-1);
+		}
+
+	for(i = 0; i < nelem(obj->objtab); i++)
+		for(o = obj->objtab[i]; o != nil; o = o->next)
+			for(e = o->child; e != nil; e = ne){
+				ne = e->next;
+
+				switch(e->type){
+				case OBJEPoint:
+					m->prims = erealloc(m->prims, ++m->nprims*sizeof(*m->prims));
+					p = &m->prims[m->nprims-1];
+					memset(p, 0, sizeof *p);
+					p->type = PPoint;
+					p->mtl = getmtlmap(&mtlmap, e->mtl);
+
+					idxtab = &e->indextab[OBJVGeometric];
+					v = &pverts[idxtab->indices[0]];
+					p->v[0].p = Pt3(v->x, v->y, v->z, v->w);
+
+					idxtab = &e->indextab[OBJVTexture];
+					if(idxtab->nindex == 1){
+						v = &tverts[idxtab->indices[0]];
+						p->v[0].uv = Pt2(v->u, v->v, 1);
+					}
+					break;
+				case OBJELine:
+					m->prims = erealloc(m->prims, ++m->nprims*sizeof(*m->prims));
+					p = &m->prims[m->nprims-1];
+					memset(p, 0, sizeof *p);
+					p->type = PLine;
+					p->mtl = getmtlmap(&mtlmap, e->mtl);
+
+					for(idx = 0; idx < 2; idx++){
+						idxtab = &e->indextab[OBJVGeometric];
+						v = &pverts[idxtab->indices[idx]];
+						p->v[idx].p = Pt3(v->x, v->y, v->z, v->w);
+
+						idxtab = &e->indextab[OBJVTexture];
+						if(idxtab->nindex == 2){
+							v = &tverts[idxtab->indices[idx]];
+							p->v[idx].uv = Pt2(v->u, v->v, 1);
+						}
+					}
+					break;
+				case OBJEFace:
+					idxtab = &e->indextab[OBJVGeometric];
+					assert(idxtab->nindex >= 3);
+					gottaclean = 0;
+
 					/* it takes n-2 triangles to fill any given n-gon */
-					trielems = emalloc((idxtab->nindex-2)*sizeof(*trielems));
-					nt = triangulate(trielems, e);
-					m->nelems += nt;
-					m->elems = erealloc(m->elems, m->nelems*sizeof(*m->elems));
-					while(nt-- > 0)
-						m->elems[m->nelems-nt-1] = trielems[nt];
-					free(trielems);
-				}else{
-					m->elems = erealloc(m->elems, ++m->nelems*sizeof(*m->elems));
-					m->elems[m->nelems-1] = e;
+					nt = idxtab->nindex-2;
+					if(nt > maxnt){
+						maxnt = nt;
+						trielems = erealloc(trielems, maxnt*sizeof(*trielems));
+					}
+					if(nt > 1){
+						assert(triangulate(trielems, e) == nt);
+						gottaclean = 1;
+					}else
+						trielems[0] = e;
+
+					while(nt-- > 0){
+						e = trielems[nt];
+						neednormal = 0;
+
+						m->prims = erealloc(m->prims, ++m->nprims*sizeof(*m->prims));
+						p = &m->prims[m->nprims-1];
+						memset(p, 0, sizeof *p);
+						p->type = PTriangle;
+						p->mtl = getmtlmap(&mtlmap, e->mtl);
+
+						for(idx = 0; idx < 3; idx++){
+							idxtab = &e->indextab[OBJVGeometric];
+							v = &pverts[idxtab->indices[idx]];
+							p->v[idx].p = Pt3(v->x, v->y, v->z, v->w);
+
+							idxtab = &e->indextab[OBJVNormal];
+							if(idxtab->nindex == 3){
+								v = &nverts[idxtab->indices[idx]];
+								p->v[idx].n = normvec3(Vec3(v->i, v->j, v->k));
+							}else
+								neednormal = 1;
+
+							idxtab = &e->indextab[OBJVTexture];
+							if(idxtab->nindex == 3){
+								v = &tverts[idxtab->indices[idx]];
+								p->v[idx].uv = Pt2(v->u, v->v, 1);
+							}
+						}
+						if(neednormal){
+							/* TODO build a list of per-vertex normals earlier */
+							n = normvec3(crossvec3(subpt3(p->v[1].p, p->v[0].p), subpt3(p->v[2].p, p->v[0].p)));
+							p->v[0].n = p->v[1].n = p->v[2].n = n;
+						}
+						if(gottaclean){
+							free(e->indextab[OBJVGeometric].indices);
+							free(e->indextab[OBJVNormal].indices);
+							free(e->indextab[OBJVTexture].indices);
+							free(e);
+						}
+					}
+					break;
+				default: continue;
 				}
 			}
-	return m->nelems;
+
+	free(trielems);
+	clrmtlmap(&mtlmap);
+	return m->nprims;
 }
 
 Model *
@@ -119,14 +302,12 @@
 {
 	if(m == nil)
 		return;
-	if(m->obj != nil)
-		objfree(m->obj);
 	if(m->tex != nil)
 		freememimage(m->tex);
 	if(m->nor != nil)
 		freememimage(m->nor);
-	if(m->nelems > 0)
-		free(m->elems);
+	if(m->nprims > 0)
+		free(m->prims);
 	free(m);
 }
 
--- a/util.c
+++ b/util.c
@@ -33,6 +33,26 @@
 }
 
 void
+swapi(int *a, int *b)
+{
+	int t;
+
+	t = *a;
+	*a = *b;
+	*b = t;
+}
+
+void
+swappt(Point *a, Point *b)
+{
+	Point t;
+
+	t = *a;
+	*a = *b;
+	*b = t;
+}
+
+void
 memsetd(double *p, double v, usize len)
 {
 	double *dp;