A tutorial introducing how to implement custom materials from scratch using textures.
Contents
Introduction
In the previous tutorial, we saw some of the important basics on building materials from scratch by building a flat coloured material. Basic indeed, and not very interesting. To improve on that, we’ll make a small step in the right direction by introducing textures, which in turn exposes a bit more of the material framework’s requirements.
Setting the texture
We can start out exactly the same as last time’s TrivialColorMaterial
and TrivialColorPass
, naming them TrivialTextureMaterial
and TrivialTexturePass
, respectively. We’ll highlight the differences necessary to use textures.
Instead of a colour value, we’ll be using a texture as the source for the surface colour. Away3D’s 2D textures are represented by Texture2DBase
(and more concretely its subclasses, which can be BitmapData
based, ATF-file based or completely custom), so we need a way to assign the texture to the pass. Simply provide a getter and setter:
public function get texture() : Texture2DBase { return _texture; } public function set texture(value : Texture2DBase) : void { _texture = value; }
No color getter/setter is needed this time, and since we don’t have any more constant data to send to the fragment shader, there’s no need to create a Vector.<Number>
for that either.
Providing the AGAL code
As far as the vertex shader is concerned, only one thing changes. Since we’ll need to map a texture on our objects, we need to provide some UV coordinates per vertex and pass them on to the fragment shader where the texture will be sampled.
We’re still using vertex attribute stream 0 for the position, so we’ll use stream 1 for the UV coordinates. We simply pass them on to varying register v0, so the fragment shader can access it, which simply needs to sample the texture we set at texture stream 0.
/** * Get the vertex shader code for this shader */ override arcane function getVertexCode() : String { // transform to view space and pass on uv coords to the fragment shader return "m44 op, va0, vc0\n" + "mov v0, va1"; } /** * Get the fragment shader code for this shader * @param fragmentAnimatorCode Any additional fragment animation code imposed by the framework, used by some animators. Ignore this for now, since we're not using them. */ override arcane function getFragmentCode(fragmentAnimatorCode : String) : String { // simply set sampled colour from interpolated uv value as output value return "tex oc, v0, fs0 <2d, clamp, linear, miplinear>"; }
Activating the pass
The only render state constant for the entire pass is the texture, which we already decided to place in stream 0. We’ll need to get the actual Stage3D Texture
object from the Texture2DBase
, which is done as follows:
/** * Sets the render state which is constant for this pass * @param stage3DProxy The stage3DProxy used for the current render pass * @param camera The camera currently used for rendering */ override arcane function activate(stage3DProxy : Stage3DProxy, camera : Camera3D) : void { super.activate(stage3DProxy, camera); stage3DProxy._context3D.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, _fragmentData, 1); }
Rendering
The render
method is practically the same as the TrivialColorPass.render
method, with one obvious addition: assigning the UV coordinate stream to attribute stream 1 along with the position we already set at stream 0:
renderable.activateUVBuffer(1, stage3DProxy);
Deactivating the pass
While last time we could just skip implementing this step, when using textures this is necessary. Stage3D won’t let us keep textures bound to texture stream slots if they’re not used by the fragment shader. This means that if we’d render an object with our current TrivialTextureMethod
followed by a render of an object with TrivialColorMethod
, the texture would still be bound in slot 0 resulting in an error. The same is the case for vertex attributes streams, so the UV coordinates assigned to stream 1 need to be unassigned.
/** * Clear render state for the next pass. * @param stage3DProxy The stage3DProxy used for the current render pass. */ override arcane function deactivate(stage3DProxy : Stage3DProxy) : void { super.deactivate(stage3DProxy); var context : Context3D = stage3DProxy._context3D; // clear the texture and stream we set before context.setTextureAt(0, null); context.setVertexBufferAt(1, null); }
“Hold on!”, I hear you say, “We’ve been using vertex stream 0 last time and didn’t need to disable that!” True enough, but since we’re always using the vertex position, we’ll always have something assigned to stream 0 anyway, so disabling it is unnecessary (and it made the previous example a bit simpler).
NOT deactivating the pass
An alternative approach to clear the unused texture and vertex streams is to tell the material framework how many of them are used. In this case, we can simply set the correct values in the constructor:
_numUsedStreams = 2; // vertex position and uv coords _numUsedTextures = 1; // a single texture
This will allow the material framework to disable streams automatically as needed, and you won’t have to overrride the deactivate
method.
Conclusion
The accompanying example code shows how the material can be used, in conjunction with the old TrivialColorMaterial
in order to make sure the render state is cleared correctly.
These first two tutorials already provided most of the info you need to start tinkering around with writing your own custom shaders. However, there’s still some upcoming tutorials to show how to access lights.