Storing Raytraced Geometry in Textures

2010, Dr. Lawlor, CS 481/681, CS, UAF

It's pretty easy to raytrace several objects at once.  For example, I can check for shadows from two quadric objects by just uploading two separate uniform variables:
	uniform mat4 A0; // quadric surface matrix: 0 = x^T A x, for first object
uniform mat4 A1; // ditto, second object
Then we're in shadow if either object hits our shadow ray:
	// Check if shadow ray hits any of our geometry
if (quadric_intersection(start,dir,A0).t<miss_t
||quadric_intersection(start,dir,A1).t<miss_t)
{ /* we're in shadow */
...
}
This approach is easy, but hardcoding both the names of the geometry and the number of objects clearly doesn't scale well.  You gain a lot of flexibility by storing object information in a texture.

A texture?  To store geometry?

Surprisingly, this works quite well.  The basic problem here is that from a pixel shader, you can:
Basically, the only variable-sized data structure a GLSL shader can read at the moment is textures, so that's what we use.

Basic Texture Access

If you've only got one texture at a time, the syntax nowadays is actually really quite simple.  From C++, you upload the texture pixel data with:
	float texpixels[2*4]={ 1,0,0,1, 0,1,1,1 }; /* red, and cyan pixels */
gluBuild2DMipmaps(GL_TEXTURE_2D,GL_RGBA8, // <- 8-bit texture
2,1, /* width and height */
GL_RGBA,GL_FLOAT,texpixels);
glFastUniform1i(prog,"texsamp0",0); /* active texture unit for this sampler */
From GLSL, you declare a uniform sampler2D:
	uniform sampler2D texsamp0; // texture unit 0
And then sample the texture with a "texture2D" call.  The texture coordinates can be anything you like:
	vec4 pixel=texture2D(texsamp0,vec2(worldHitLocation));
This works fine for simple color texturing.  Mipmaps provide an efficient way to filter these too.

Storing Data in Textures

But if you're storing arbitrary data structures inside textures, like geometry data, your needs are different:
The C++ code for this case is:
	float texpixels[2*4]={ 1,0,0,1, 0,1,1,1 }; /* red, and cyan pixels */
glTexImage2D(GL_TEXTURE_RECTANGLE_ARB,0,GL_RGBA32F_ARB, // <- float texture
2,1, /* width and height */
0,GL_RGBA,GL_FLOAT,texpixels);
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_RECTANGLE_ARB,GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFastUniform1i(prog,"texsamp0",0); /* active texture unit for this sampler */
And from GLSL:
	uniform sampler2DRect texsamp0; // texture unit 0
...
vec4 data=texture2DRect(texsamp0,vec2(1.0,0.0));
It's a little weird that we have to pack our data structures into 2D arrays, but 2D is actually somewhat handy.  For example, to make a list of our quadric objects, I can give each object one row in the texture, and allocate the pixels along that row however I like.  I'm currently using pixels as follows:
Note that adjacent pixels can contain totally different semantic information--they're only stored in the texture together, just like arbitrary data coexists happily in memory.  From C++, in fact, the easy way to set up a structure like this is to pad it out to match the 4-float vec4's we'll be uploading into the texture:
struct quadric_object {
// four vec4's, x==0.0 through 3.0:
osl::mat4 A; // quadric surface matrix: 0 = x^T A x

// one vec4, x==4.0:
vec3 center; // trim center
float radius; // trim radius

// another vec4, x==5.0:
vec3 reflectance;
float opacity;
};
Defining a class like this allows you to upload a vector of quadric_objects with a single call to glTexImage2D:
	glTexImage2D(GL_TEXTURE_RECTANGLE_ARB,0,GL_RGBA32F_ARB, // <- float texture
sizeof(quadric_object)/sizeof(vec4),qvec.size(), /* width and height */
0,GL_RGBA,GL_FLOAT,&qvec[0]);
From the GLSL side, I prefer to write an "unpack" function that extracts all the info about one object from a row of the texture, and puts it into a GLSL struct modeled after the C++ version:
uniform sampler2DRect quadriclist; // texture unit 0: texture rectangle of geometry

// Load up this quadric object from the geometry texture
quadric_object load_quadric(float objectNo) {
quadric_object q;
q.A[0]=texture2DRect(quadriclist,vec2(0.0,objectNo));
q.A[1]=texture2DRect(quadriclist,vec2(1.0,objectNo));
q.A[2]=texture2DRect(quadriclist,vec2(2.0,objectNo));
q.A[3]=texture2DRect(quadriclist,vec2(3.0,objectNo));

vec4 p=texture2DRect(quadriclist,vec2(4.0,objectNo));
q.center=vec3(p); q.radius=p.w;

p=texture2DRect(quadriclist,vec2(5.0,objectNo));
q.reflectance=vec3(p); q.opacity=p.w;

return q;
}
You can then loop over all the rows of the texture, and perform your intersection testing on each object:
	float shadow=0.0;
for (float q=0.0;q<quadriccount;q++) {
ray_intersection iq=quadric_intersection(i.P,L,load_quadric(q));
if (iq.t<miss_t) shadow=max(shadow,iq.opacity);
}
This "geometry in a texture" approach is both efficient and flexible, and it's the standard way to write a GPU raytracer.