RestAttributes

From Odwiki

Jump to: navigation, search


Contents

Rest Position Attributes

Many times when writing shaders, we come across situations where we would like certain spatial attributes to remain fixed, even though the object that we are shading may be transforming and/or deforming. The need for this typically arises when doing procedural textures that depend on attributes such as surface position (P) or normal (N). In these cases, failing to somehow "freeze" these attributes, will result in the object appearing to "swim" through the texture as it transforms/deforms. This is due to the fact that as the object moves, the positions on its surface change; and as it deforms, both the positions and the normals on its surface change.

The solution to this problem is quite simple: express the needed attributes in a space where their values are not transforming, and sample the texture in that space instead. Unfortunately, finding such a space is only possible under one very specific set of conditions; for the rest of the cases, we need to resort to some trickery.

Before we begin exploring these issues, we'll need a shader that depends on something like P or N to do its magic. Perhaps the quintessential procedural texture is Perlin noise, so let's start with that. The variant we'll use here depends on position, and is available through the VEX function noise() (or the Turbulent Noise VOP). Here's a surface shader which uses noise to color the surface, and is dependent on position (P).

surface Noisy1 (
      vector Frequency = 5;
      vector Offset    = 0;
   )
{
   Cf = float( noise( P * Frequency + Offset ));
}

This version of the noise function takes one vector as an argument (here we are feeding it P), and returns a float that is somewhere in the range [-0.5,+0.5]. The Frequency and Offset parameters scale and translate P, but don't themselves define it. For now, we will only assign our shader to polygonal or parametric surfaces; we'll deal with primitives later, as they will need special treatment.


World-Space Transformations

As a first experiment, we assign Noisy1 to an object that contains a single default NURBs sphere SOP. Then we animate the object so it translates in X. Note that there are no transform SOPs inside our object; the translation is being applied to the object's "Translate" parameter, not to a transform sop inside the object. In other words, it is a world space transformation. Here's what happens to Noisy1:

Image:Mgm_space_rest.0001.jpg

As you can see, our noise texture is not "sticking" to the object; and if you saw it in motion, it would look as though the object were moving through the texture, which, incidentally, is exactly what's happening: P is moving through the space that we are shading, but the space itself is not moving along with it, so P is picking up different colors as it moves through the static space that defines the noise -- the space where P lives; i.e: the space of the rendering camera, or world space.

As I mentioned earlier, one way to fix this is to sample our texture in a space where P doesn't move. In this particular case we're in luck: P is moving through world space, but it is static in object space (there are no transform SOPs moving our sphere). This means that instead of using P (which is in world space) to sample our noise, we could use the object space version of P instead. To get the object space version of P we simply transform P to object space using the VEX function wo_space() or the equivalent "Space Change" VOP. Here's our improved shader:

#pragma label  t_ospace    "Sample Noise in Object Space"
#pragma hint   t_ospace    toggle

surface Noisy2 (
      int    t_ospace  = 1;
      vector Frequency = 5;
      vector Offset    = 0;
   )
{
   vector Pnoise = t_ospace ? wo_space(P) : P;
   Cf = float( noise( Pnoise * Frequency + Offset ));
}

Now we can toggle between sampling our noise in world space or object space. Here's what happens when we sample in object space using "Noisy2".

Image:Mgm_space_rest.0002.jpg

This time the noise sticks to the surface as we had hoped. Note however that the noise pattern itself is different than when we rendered using "Noisy1". This is because in "Noisy1" the camera is at the origin, whereas in "Noisy2" the object is at the origin, so the noise gets sampled at different locations in each case. If we had scaled our object as well as translating it, then you'd also notice a change in the frequency of the noise, as the two spaces would have had different "sizes" -- we'll explore ways to combat that small but important side effect later.


Object-Space Transformations

Now let's move our translation into object space and see what happens. To do this, we remove the translation that we had in the object's "Translate" parameter, and add it instead to a transform SOP inside the object. If we now render using "Noisy2", we get this:

Image:Mgm_space_rest.0003.jpg

It's ba-aaaaack... our wo_space() fix doesn't work anymore. Now we get the same noise pattern as in our last test (meaning it is still being sampled in object space), but the object is again swimming through the texture as it translates. The reason should hopefully be apparent by now: P is now moving in object space (and, by extension, in world space as well), so transforming to object space no longer gives us a static P.

What about this "Texture" or "Shader" space that is available to surface and displacement shaders? If you recall, this is an arbitrary space given in the form of an object reference. We could create a Null object that doesn't transform at all, and use it as our Shader Space. Since we know our Null won't move, then maybe if we sample our noise in the Null's space, the noise won't move either... In order to test this, we need to change our shader to enable it to work with Texture Space. Here's our new version:

#pragma label  nspace   "Sample Noise in"
#pragma choice nspace   "w"   "World Space"
#pragma choice nspace   "o"   "Object Space"
#pragma choice nspace   "t"   "Texture Space" 

surface Noisy3 (
      string nspace    = "t";
      vector Frequency = 5;
      vector Offset    = 0;
   )
{
   vector Pn = P;
   if(nspace!="w") Pn = nspace=="t" ? wt_space(Pn) : wo_space(Pn);
   Cf = float( noise( Pn * Frequency + Offset ));
}

There; now we can choose to sample our noise in World Space (the space of our render camera), Object Space (the space of our sphere object), or Shader Space (a.k.a: Texture Space; which defaults to be identical to Object Space, but which we'll set to reference a specific object for this test). All that's left to do then, is to set our sphere object's Texture Space to a freshly-created Null object with no transformations. And here's what we get with Noisy3 set to sample in that Null object's space (given as Texture Space):

Image:Mgm_space_rest.0004.jpg

...and we can transform the Null object to convince ourselves that that is indeed the space in which the noise is being sampled. It is also painfully clear that this technique is not a solution to our problem: the noise doesn't stick to the sphere. We'll get to why that is in a second; but before any of that, take a moment to note the pattern of the noise in this last test: it is different than both of the other spaces we've tried so far: World Space (or Camera Space) with "Noisy1", and Object Space with "Noisy2" .

This last space (the one that belongs to the reference Null object that we used as our designated Texture Space) is what we (not VEX) would normally refer to as World Space; the place where most of our objects live, or /obj. Also note that using objects as space references is precicely how we would go about defining a Named Space -- meaning that declaring a Named Space is really no different than declaring a Texture Space; the tools are different, but underneath, they are both just references to objects. Why am I going on about this? Because being able to access that space that most of us would call World Space is a very useful thing, and we just accidentally stumbled upon one way to do it!

Now back to why this is still not working. To answer this, we need to understand what it means to transform something in Object Space (or in SOPs). Since Object Space is the space in which we define our objects, animating our objects in this space means we're actually re-defining our object on every frame: at this frame, our object is a collection of points and primitives "over here", and in the next frame, our object is a collection of points and primitives "over there". Our definition of what our object is is always changing. And by extension, this means that there is no space in which our object is static because we're defining our object as a collection of ever-changing points and primitives. What to do?


Rest Attributes

The solution is to take a snapshot of our object in any one of its many configurations, and declare that configuration as the one we will use to sample our procedural texture with. This frozen version of our object is comonly referred to as its "Rest Position" -- a version of our object "at rest".

For the specific case of surface position (P) and normal (N), there is a RestPosition SOP available that will take the incoming geometry's point positions and/or normals, and stash the information in two separate point attributes that it creates: rest and rnml. After the RestPosition SOP is done, these attributes will contain a "frozen" version of each point's position and/or normal respectively.

Whether we use the Rest Position SOP or a combination of the various other attribute SOPs to create these rest attributes, is a matter of convenience and personal taste; the method used to bring these attributes into existence has no influence on the overall technique -- i.e: there is nothing "magical" about the Rest Position SOP; it just adds attributes with predefined names, and whose "rest" values are aquired from a secondary source. It is also important to realize that the act of assigning these rest attributes to our geometry is only half the story. The attributes by themselves won't fix our sliding procedural texture; they are no different than any other attribute, and as such, will only influence the outcome if and when used explicitly by the shader.

Since we can safely assume that people will use the Rest Position SOP to create both rest-position and rest-normal attributes, we can adopt the convention that, if they exist, rest positions and normals will come in the form of point attributes with the names rest and rnml respectively. When an attribute exists for a single geometric element, it is guaranteed to exist for all such elements, and is said to be "bound" to the geometry. If a bound attribute's name matches the name of one of the parameters to our shader, and if their data types are the same (e.g: they are both vectors), then the bound value will override any user set values for that parameter (and the parameter's own default value, by extension). Therefore, one way to "bring attributes into our shader", is simply to add a parameter with the same name and type as the attribute.

With all of this in mind, we can add support for the use of rest positions in our shader. We will fix the name of the rest position attribute to the one given by the Rest Position SOP: rest. Note that while this choice may be "reasonable", it is nevertheless hard-wired in the shader and invisible to the user, so it should be well documented as a general naming convension.

#pragma label  nspace   "Sample Noise in"
#pragma choice nspace   "none" "Its Original Space"
#pragma choice nspace   "w"    "World Space"
#pragma choice nspace   "o"    "Object Space"
#pragma choice nspace   "t"    "Texture Space" 

#pragma hint   rest     hidden

surface Noisy4 (
      string nspace    = "none";
      vector Frequency = 5;
      vector Offset    = 0;
      vector rest      = 0;
   )
{ 

   vector Pn;
 
   if(isbound("rest"))
   {
      Pn = rest;
      if(nspace!="none" && nspace!="o")
         Pn = nspace=="t" ? wt_space(ow_space(Pn)) : ow_space(Pn);
   }
   else
   {
      Pn = P;
      if(nspace!="none" && nspace!="w")
         Pn = nspace=="t" ? wt_space(Pn) : wo_space(Pn);
   } 
  
   Cf = float( noise( Pn * Frequency + Offset )); 
  
 }

The shader will check that the attribute rest is bound; if it is, then that is the value (after optional transformation) that will be used to sample our noise; otherwise, it uses the global P as before. And here is the result with Noisy4 at default and used on a sphere translating in object space (as in our previous test), but this time with the "rest" attribute defined using a Rest Position SOP inserted just before the Transform SOP that translates it:

Image:Mgm_space_rest.0006.jpg

We have a winner. Note that the noise pattern matches that of our earlier object space test. This is exactly what we would expect since the "rest" attribute is being defined in object space (i.e: in SOPs) Using Primitive Geometry


See Also

© 2009 od[force].net | advertise