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:
- Read hardcoded values, or hardcoded uniforms, like above.
- Read varying variables, but these need to be written by the vertex shader, and the total count is hardcoded.
- Read register values, like temporary variables. You can
declare and initialize an array of temporaries, but these seem to run
incredibly slowly--I think the GLSL compiler replaces every array
access like "return A[i]" with something like "if (i==0) return
A_0; if (i==1) return A_1; ...".
- Read textures.
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:
- You need full floating point precision in the texture pixels. The texture's internalFormat will be something like GL_RGBA32F_ARB (one vec4 per pixel) or GL_LUMINANCE32F_ARB (greyscale, one float per pixel).
- You usually don't want texture filtering, you want GL_NEAREST.
- You usually don't want wraparound GL_REPEAT mode, you want GL_CLAMP mode.
- You probably don't even want 0.0 ... 1.0 texture coordinates, you want 0 ... n-1 array-index texture coordinates. You can get these with "texture rectangles", which are supported on all modern cards.
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:
- pixel x==0 through x==3 will contain the quadric matrix A.
- pixel x==4 will contain the object's center point, used for trimming. This pixel's alpha will determine the trim radius.
- pixel x==5 will contain the object's diffuse reflectance. The alpha channel will contain the object's opacity.
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.