The frustum culling is multithreaded in
BetaCell so the
BCThreadPool must be
initialized:
BCThreadPool.init(4);
In this example, 4 threads are set. This parameter is only valid on the
PC, on the XBOX is allways 4.
Since we started the thread pool we need to stop it at the end of the
execution, this is done by overriding the
OnExiting
method:
protected override void OnExiting(object sender,
EventArgs args)
{
BCThreadPool.terminate();
base.OnExiting(sender, args);
}
To draw the scene in a
front to back order [3.4.4], we will delegate the drawing to a class
that knows how to do that instead of performing the ordering the
ourselves; the
Initialize
method starts by creating an instance of this class:
drawer = new
BCDepthSorterDrawer(device, 4, 2000);
The depth sorted drawer receives three parameters; first the device,
second the log2(n) (2^n, in this example 2^4 = 16) of the number of
sublist to order the scene (see the documentation for more information)
and finally the maximum depth of an object to be drawn.
Now
we must load the scene. If a user starts an application and a window
doesn't appear immediately, he/she will think that the application
didn't start at all, also if the application starts but doesn't keep
feeding the user with data, he/she will think that the application
hanged.
The user will be confident (doesn't mean that he/she is
happy with an application that takes forever to load) with the
application as long as it refreshes the data that the user
sees
constantly.
In the left, we have a
single
threaded application (only the main thread), it prints that the
application started and starts to load the scene, since in XNA we can't
print data and load the scene at the same time, during the portion of
time devoted to loading the scene, the application will appear as it
has hanged, and if the process takes long, then the user will probably
end the task.
In the right we have two threads, the main one keeps printing data
(showing the progress of the load procedure) and the second one loads
the data. Notice the uninterrupted line of the print data task.
To
achieve the scenario shown in the right side of the figure; we must
create a thread, pass it the method that loads the scene and start it.
loader = new Thread(new
ParameterizedThreadStart(this.AsyncLoader));
loader.Start();
After the thread is started the
AsyncLoader
method begins to load the scene in it.
This method is divided in tasks [3.4.5], the main task is
"Loading the data" with a weight
of 1.0.
Inside it we have the tasks that load the scene. The first one is
"Creating Common Data";
the new to this part is the creation of the ambient light and the
fog
effect parts:
BCAmbientLight ambient = new BCAmbientLight();
ambient.amb = Color.Gray;
BCFunction ambientPart = ambient.getFunction("Shader.Light.Ambient");
BCDynamicEffectVisitor ambientSetter =
new BCDynamicEffectVisitor(ambientPart, ambient, 1);
This creates the ambient light, which is the light that has
bounced in the scene so much that for practical purposes is applied to
all the pixels in the scene, regardless of the normal of the surfaces.
fog = new BCFog();
fog.fogColor = Color.White;
fog.fogStart = 0.9f;
fog.fogRange = 0.1f;
BCFunction fogPart = fog.getFunction("Environment.Fog");
BCDynamicEffectVisitor fogSetter = new BCDynamicEffectVisitor(
fog.getFunction(BCFog.FOG), fog,
BCDynamicEffect.MAX_FUNCTIONS
);
This
creates the fog effect feeder, sets the fog color, the distance at
which the fog starts and the distance at which the fog ends (fog
range) relative to the camera's far clip plane. Also, a visitor for the
fog effect is created.
The next two tasks are
"Creating
Building Mesh" and
"Creating
Terrain", there's nothing new in them.
The following task is
"Preparing
terrain for culling",
here; we divide the terrain in square parts. When the terrain was
built, a 1024 X 1024 map was passed as a parameter; meaning that there
are 1024 rows and 1024 columns right? wrong. Lets downscale the problem
to a map of 16 X 16.

If we load a 16 X 16
map, it means
that there will be 16 vertices in each of the axises of the terrain as
shown in the figure. The problem is that vertices is different than
rows/columns; if we have 16 vertices we have 15 rows/columns
(count the vertices and the rows/columns).
So
intuitively say that we are going to load a 16 X 16 map and divide it
in 4 squares of 8 vertices X 8 vertices is wrong; in the figure the
blue and green squares have 8 vertices per axis each, and you can see
that we have a full column left with the 8 X 8 division.
The
solution is to divide based in the number of rows/columns, if we have
16 vertices, we have 15 rows/columns, so we can divide the terrain in
divisors of 15 (15, 5, 3, 1) in the figure, the red squares are
divisions of 5 rows/columns (6 vertices).
The tutorial divides the terrain with the following call:
floor.prepareForCulling(device, 94, 94);
Assuming
that the resulting plane is in the XZ axis, it receives the device, the
number of VERTICES not columns in the x axis and the number of VERTICES
not rows in the z axis. In the example of the figure the divisions in
red/white would be
prepareForCulling(device,
6, 6);.
The 94 X 94 division was done because we have a 1024 X 1024 map; that
means
that we have 1023 rows/columns. The 94 X 94 division means that each
division will have 93 rows/columns. So 1023 should be divisible by 93,
and 1023 / 93 = 11 so it is.
The values by which the 1024 map could be divided are 1024, 342, 94,
34, 32, 12 and 4.
The next task is
"Finishing the
terrain",
in it, we set the textures and the effect of the terrain. We have done
this before but here, we do it in a different way to save processing
and memory, since the terrain has a lot of mesh parts.
The first
difference is the way the textures are applied to the terrain. Since
the terrain mesh has many mesh parts, it would be better to have the
same textures shared by all parts than to have the textures repeated in
each part; so what we do is use a
BCPooledTextureStrategy
instead of the
BCMemoryTextureStrategy.
To
do this, we delegate the load of the textures to the texture pool of
the mesh by issuing the following call for each of the 4 textures
needed:
int referenceIndex =
floor.terrainMesh.textures.addTextureAbsolute(
"Content/gameAssets/channelsR", content);
The
call returns an integer number that represents the texture inside
the mesh. After that we create the texture visitors but with a
pooled texture strategy instead of a memory texture strategy:
BCPooledTextureStrategy
referenceStrategy =
new BCPooledTextureStrategy(referenceIndex,
floor.terrainMesh.textures);
The
pooled texture strategy takes the number that represents the texture
and the texture pool that holds the texture (in this case the texture
pool of the terrain mesh).
BCTextureVisitor blendMapTex =
new BCTextureVisitor("gBlendMap",
referenceStrategy);
The visitor isn't new, it takes the name of the texture and the
strategy.
We repeat the process for the other textures.
The
second difference is the way in which the effect is dealt with.
Until now, we just visited the mesh to set the effect; this
technique is very useful if we have meshes with only a few mesh parts
with different effects each but here we have a lot of parts, all of
which have the same effect, so instead of creating an effect
dynamically for each part, we create a common effect and set that
effect in all the parts.
To do that, we create a dynamic effect
with the functions that we have created in previous tasks; note that
these functions are the ones used to create the visitors:
BCDynamicEffect commonEffect = new
BCDynamicEffect(device, new BCFunction[] {
baseColor,
ambientPart, lightPart, channelBlendPart, fogPart
});
Then we set the feeders for the effect:
commonEffect.addFeeder(colorSet);
commonEffect.addFeeder(ambient);
commonEffect.addFeeder(light);
commonEffect.addFeeder(texFeeder);
commonEffect.addFeeder(fog);
And
we create a visitor that instead of plugging an effect function into
each part of the mesh, sets the entire effect of all the parts of the
mesh, so all the parts end up using the very same effect.
BCEffectSetterVisitor
commonEffectSetter =
new BCEffectSetterVisitor(commonEffect);
The
last operation of the initialize method is to attach the finished
terrain to the camera and notify that the loading process has been
completed:
camera.attachWalkable(floor, 2);
ready = true;
We don't attach the terrain to the camera in the onActivated method
because at the time the method is called, the terrain has not been
finished; making it too early.