Rule based layer distribution for Torque3D terrains
by Konrad Kiss · 05/05/2009 (5:01 pm) · 41 comments
Although Torque3D's terrain is the fastest one can create terrains with in Torque until now, the tools within allow for even more robust, automated solutions.
I've been experimenting with the terrain system from the inside this time, and came up with a rule based layer mask generator for Torque3D.
How does it help? The following terrain was painted by hand and took about 1.5 hours (the entire terrain, not this part only):

The second image was created from scratch (untextured heightmap) in about 3 minutes (!) with an extra layer added:

The difference is not only speed, but a more realistic distribution of terrain materials using this simple tool:

What it does is it takes the currently selected material, and paints it everywhere where the height is between the minimum and maximum height (z coordinate) and where the slope angle (in degrees) is within the given bounds.
The second image was generated by creating a snow layer everywhere (default settings). Then I checked the waterplane's z, and used that value +1 as max height for the pebbles on every slope.. That pretty much took care of everything below sea level. I added other layers with constraints, and finally made everything with a slope between 40-90 degrees a cliff. It's pretty easy to paint this way.
If you need to do custom painting - ie a riverbed - do that after you're done with the entire automatic generation, otherwise you might overwrite your layers.
Before implementing this resource, please note that:
1. Update: Using Ctrl-Z or Edit/Undo you can now undo your actions. For changes that affect the entire terrain - and also depending on your terrain's size, recording the action can take a significant time. But at least there's a way to revert to the previous state. If you'd rather keep it fast without undo, comment everything between the // undo >> and // undo << comments.
2. This feature is currently not directly wired into the editor. To use this feature, select a terrain layer in the Terrain Painter, open the console, type "autoLayers();" at the console and close the console. This is slow, but I am hoping that someone who can create a gui for this will do that eventually. I didn't feel like breaking up the beautiful new gui with some programmer art.. so until then - type away! Hey, maybe GG will like it and add it to the next Beta :) *hint*
Ok, now, the required changes:
source/gui/missionEditor/terrainEditor.cpp
tools/missionEditor/main.cs - add the following to the begining of initializeMissionEditor
create tools/missionEditor/gui/guiTerrainPainterProceduralGui.gui:
And you're done. Again, usage: This is not wired into the editor yet. To use this feature, select a terrain layer in the Terrain Painter, open the console, type "autoLayers();" at the console and close the console to see the panel where you can set the distribution parameters.
Enjoy!
Update: Added undo. If you'd rather keep it fast without undo, comment everything between the // undo >> and // undo << comments.
Update: Fixed a bug that was caused by using world space coords instead of object space coords when querying for normals of terrain squares.
Update: The dialog can now be closed with the close button if you change your mind.
I've been experimenting with the terrain system from the inside this time, and came up with a rule based layer mask generator for Torque3D.
How does it help? The following terrain was painted by hand and took about 1.5 hours (the entire terrain, not this part only):

The second image was created from scratch (untextured heightmap) in about 3 minutes (!) with an extra layer added:

The difference is not only speed, but a more realistic distribution of terrain materials using this simple tool:

What it does is it takes the currently selected material, and paints it everywhere where the height is between the minimum and maximum height (z coordinate) and where the slope angle (in degrees) is within the given bounds.
The second image was generated by creating a snow layer everywhere (default settings). Then I checked the waterplane's z, and used that value +1 as max height for the pebbles on every slope.. That pretty much took care of everything below sea level. I added other layers with constraints, and finally made everything with a slope between 40-90 degrees a cliff. It's pretty easy to paint this way.
If you need to do custom painting - ie a riverbed - do that after you're done with the entire automatic generation, otherwise you might overwrite your layers.
Before implementing this resource, please note that:
1. Update: Using Ctrl-Z or Edit/Undo you can now undo your actions. For changes that affect the entire terrain - and also depending on your terrain's size, recording the action can take a significant time. But at least there's a way to revert to the previous state. If you'd rather keep it fast without undo, comment everything between the // undo >> and // undo << comments.
2. This feature is currently not directly wired into the editor. To use this feature, select a terrain layer in the Terrain Painter, open the console, type "autoLayers();" at the console and close the console. This is slow, but I am hoping that someone who can create a gui for this will do that eventually. I didn't feel like breaking up the beautiful new gui with some programmer art.. so until then - type away! Hey, maybe GG will like it and add it to the next Beta :) *hint*
Ok, now, the required changes:
source/gui/missionEditor/terrainEditor.cpp
// >>>
//------------------------------------------------------------------------------
void TerrainEditor::autoMaterialLayer( F32 mMinHeight, F32 mMaxHeight, F32 mMinSlope, F32 mMaxSlope )
{
if (!mActiveTerrain)
return;
S32 mat = getPaintMaterialIndex();
if (mat == -1)
return;
// undo >>
mUndoSel = new Selection;
// undo <<
U32 terrBlocks = mActiveTerrain->getBlockSize();
for (U32 y=0;y<terrBlocks;y++) {
for (U32 x=0;x<terrBlocks;x++) {
// get info
GridPoint gp;
gp.terrainBlock = mActiveTerrain;
gp.gridPos.set(x, y);
GridInfo gi;
getGridInfo(gp, gi);
if (gi.mMaterial==mat)
continue;
Point3F wp;
gridToWorld(gp, wp);
if (!(wp.z>=mMinHeight && wp.z<=mMaxHeight))
continue;
// objspace >>
// transform wp to object space
Point3F op;
mActiveTerrain->getWorldTransform().mulP(wp, &op);
Point3F norm;
mActiveTerrain->getNormal(Point2F(op.x, op.y), &norm, true);
// objspace <<
if (mMinSlope>0)
if (norm.z > mSin(mDegToRad(90.0f-mMinSlope)))
continue;
if (mMaxSlope<90)
if (norm.z < mSin(mDegToRad(90.0f-mMaxSlope)))
continue;
// ok, looks like we can change the material here
gi.mMaterialChanged = true;
// undo >>
mUndoSel->add(gi);
// undo <<
gi.mMaterial = mat;
setGridInfo(gi);
}
}
// undo >>
if(mUndoSel->size())
submitUndo( mUndoSel );
else
delete mUndoSel;
mUndoSel = 0;
// undo <<
scheduleMaterialUpdate();
}
ConsoleMethod( TerrainEditor, autoMaterialLayer, void, 6, 6, "(float minHeight, float maxHeight, float minSlope, float maxSlope)")
{
object->autoMaterialLayer( dAtof( argv[2] ), dAtof( argv[3] ), dAtof( argv[4] ), dAtof( argv[5] ) );
}
// <<<tools/missionEditor/main.cs - add the following to the begining of initializeMissionEditor
// >>>
exec("./gui/guiTerrainPainterProceduralGui.gui" );
// <<<create tools/missionEditor/gui/guiTerrainPainterProceduralGui.gui:
//--- OBJECT WRITE BEGIN ---
%guiContent = new GuiControl(TerrainPainterProceduralGui) {
canSaveDynamicFields = "0";
isContainer = "1";
Profile = "GuiDefaultProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "0 0";
Extent = "1024 768";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
new GuiWindowCtrl() {
canSaveDynamicFields = "0";
isContainer = "1";
Profile = "GuiWindowProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "285 83";
Extent = "175 209";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
Margin = "0 0 0 0";
Padding = "0 0 0 0";
AnchorTop = "1";
AnchorBottom = "0";
AnchorLeft = "1";
AnchorRight = "0";
resizeWidth = "0";
resizeHeight = "0";
canMove = "1";
canClose = "1";
canMinimize = "0";
canMaximize = "0";
minSize = "50 50";
EdgeSnap = "1";
canCollapse = "0";
CollapseGroup = "-1";
CollapseGroupNum = "-1";
closeCommand = "Canvas.popDialog(TerrainPainterProceduralGui);";
text = "Generate layer mask";
new GuiButtonCtrl() {
canSaveDynamicFields = "0";
isContainer = "0";
Profile = "GuiButtonProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "19 164";
Extent = "140 30";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
Command = "generateProceduralTerrainMask();";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
text = "Generate";
groupNum = "-1";
buttonType = "PushButton";
useMouseEvents = "0";
};
new GuiTextCtrl() {
canSaveDynamicFields = "0";
isContainer = "0";
Profile = "GuiTextProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "15 37";
Extent = "33 13";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
Margin = "0 0 0 0";
Padding = "0 0 0 0";
AnchorTop = "1";
AnchorBottom = "0";
AnchorLeft = "1";
AnchorRight = "0";
text = "HEIGHT";
maxLength = "1024";
};
new GuiTextCtrl() {
canSaveDynamicFields = "0";
isContainer = "0";
Profile = "GuiTextProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "59 37";
Extent = "23 14";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
Margin = "0 0 0 0";
Padding = "0 0 0 0";
AnchorTop = "1";
AnchorBottom = "0";
AnchorLeft = "1";
AnchorRight = "0";
text = "Min.";
maxLength = "1024";
};
new GuiTextCtrl() {
canSaveDynamicFields = "0";
isContainer = "0";
Profile = "GuiTextProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "59 62";
Extent = "23 14";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
Margin = "0 0 0 0";
Padding = "0 0 0 0";
AnchorTop = "1";
AnchorBottom = "0";
AnchorLeft = "1";
AnchorRight = "0";
text = "Max.";
maxLength = "1024";
};
new GuiTextEditCtrl() {
canSaveDynamicFields = "0";
isContainer = "0";
Profile = "GuiTextEditProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "97 35";
Extent = "66 18";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
Variable = "$TPPHeightMin";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
Margin = "0 0 0 0";
Padding = "0 0 0 0";
AnchorTop = "1";
AnchorBottom = "0";
AnchorLeft = "1";
AnchorRight = "0";
maxLength = "1024";
historySize = "0";
password = "0";
tabComplete = "0";
sinkAllKeyEvents = "0";
passwordMask = "*";
};
new GuiTextEditCtrl() {
canSaveDynamicFields = "0";
isContainer = "0";
Profile = "GuiTextEditProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "97 60";
Extent = "66 18";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
Variable = "$TPPHeightMax";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
Margin = "0 0 0 0";
Padding = "0 0 0 0";
AnchorTop = "1";
AnchorBottom = "0";
AnchorLeft = "1";
AnchorRight = "0";
maxLength = "1024";
historySize = "0";
password = "0";
tabComplete = "0";
sinkAllKeyEvents = "0";
passwordMask = "*";
};
new GuiTextCtrl() {
canSaveDynamicFields = "0";
isContainer = "0";
Profile = "GuiTextProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "15 101";
Extent = "33 13";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
Margin = "0 0 0 0";
Padding = "0 0 0 0";
AnchorTop = "1";
AnchorBottom = "0";
AnchorLeft = "1";
AnchorRight = "0";
text = "SLOPE";
maxLength = "1024";
};
new GuiTextCtrl() {
canSaveDynamicFields = "0";
isContainer = "0";
Profile = "GuiTextProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "59 101";
Extent = "23 14";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
Margin = "0 0 0 0";
Padding = "0 0 0 0";
AnchorTop = "1";
AnchorBottom = "0";
AnchorLeft = "1";
AnchorRight = "0";
text = "Min.";
maxLength = "1024";
};
new GuiTextCtrl() {
canSaveDynamicFields = "0";
isContainer = "0";
Profile = "GuiTextProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "59 126";
Extent = "23 14";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
Margin = "0 0 0 0";
Padding = "0 0 0 0";
AnchorTop = "1";
AnchorBottom = "0";
AnchorLeft = "1";
AnchorRight = "0";
text = "Max.";
maxLength = "1024";
};
new GuiTextEditCtrl() {
canSaveDynamicFields = "0";
isContainer = "0";
Profile = "GuiTextEditProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "97 99";
Extent = "66 18";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
Variable = "$TPPSlopeMin";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
Margin = "0 0 0 0";
Padding = "0 0 0 0";
AnchorTop = "1";
AnchorBottom = "0";
AnchorLeft = "1";
AnchorRight = "0";
maxLength = "1024";
historySize = "0";
password = "0";
tabComplete = "0";
sinkAllKeyEvents = "0";
passwordMask = "*";
};
new GuiTextEditCtrl() {
canSaveDynamicFields = "0";
isContainer = "0";
Profile = "GuiTextEditProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "97 124";
Extent = "66 18";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
Variable = "$TPPSlopeMax";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
Margin = "0 0 0 0";
Padding = "0 0 0 0";
AnchorTop = "1";
AnchorBottom = "0";
AnchorLeft = "1";
AnchorRight = "0";
maxLength = "1024";
historySize = "0";
password = "0";
tabComplete = "0";
sinkAllKeyEvents = "0";
passwordMask = "*";
};
};
};
//--- OBJECT WRITE END ---
$TPPHeightMin = -10000;
$TPPHeightMax = 10000;
$TPPSlopeMin = 0;
$TPPSlopeMax = 90;
function autoLayers() {
Canvas.pushDialog(TerrainPainterProceduralGui);
}
function generateProceduralTerrainMask() {
Canvas.popDialog(TerrainPainterProceduralGui);
ETerrainEditor.autoMaterialLayer($TPPHeightMin, $TPPHeightMax, $TPPSlopeMin, $TPPSlopeMax);
}And you're done. Again, usage: This is not wired into the editor yet. To use this feature, select a terrain layer in the Terrain Painter, open the console, type "autoLayers();" at the console and close the console to see the panel where you can set the distribution parameters.
Enjoy!
Update: Added undo. If you'd rather keep it fast without undo, comment everything between the // undo >> and // undo << comments.
Update: Fixed a bug that was caused by using world space coords instead of object space coords when querying for normals of terrain squares.
Update: The dialog can now be closed with the close button if you change your mind.
About the author
See www.bitgap.com.
#2
05/05/2009 (5:31 pm)
That's extremely useful.
#3
05/05/2009 (6:26 pm)
Very nice. You get a cookie! Heck you get 2 cookie!
#4
05/05/2009 (6:31 pm)
holy cow, nifty work :)
#5
05/05/2009 (7:10 pm)
Very usefull resource
#7
05/06/2009 (12:53 am)
Ohhh... nice! Thanks! This reminds me a bit of L3DTs layer tools. I think the community will make great use of this!
#8
Thanks guys for the awesome comments. The resource has been updated to use the undo feature - note that some of the fills that affect most or all of the terrain will take some time to be added to the undo buffer.
Also, if you wish to wire it into the Terrain Painter gui, use the following code to add a small "fill" button over the thumbnail of the currently selected layer.
Add this to tools/missionEditor/gui/TerrainPainterWindow.ed.gui as a child of the EPainter control after other children:
05/06/2009 (10:15 am)
Thanks guys for the awesome comments. The resource has been updated to use the undo feature - note that some of the fills that affect most or all of the terrain will take some time to be added to the undo buffer.Also, if you wish to wire it into the Terrain Painter gui, use the following code to add a small "fill" button over the thumbnail of the currently selected layer.
Add this to tools/missionEditor/gui/TerrainPainterWindow.ed.gui as a child of the EPainter control after other children:
new GuiButtonCtrl() {
canSaveDynamicFields = "0";
isContainer = "0";
Profile = "GuiButtonProfile";
HorizSizing = "right";
VertSizing = "bottom";
Position = "116 25";
Extent = "31 16";
MinExtent = "8 2";
canSave = "1";
Visible = "1";
Command = "autoLayers();";
tooltipprofile = "GuiToolTipProfile";
hovertime = "1000";
text = "fill";
groupNum = "-1";
buttonType = "PushButton";
useMouseEvents = "0";
};
#9
05/06/2009 (1:45 pm)
This is awesome Konrad, I miss those procedural rulesets from the Legacy terrains, this more than makes up for it!
#10
The problem is that TerrainBlock::getNormal always returns "false" on first "if".
Haven't tracked down how to fix it yet, no time :(
05/06/2009 (4:17 pm)
cool addition, though it does not work with terrain which placed at negative coordinates (like in stock FPS Starter Kit the terrain is on "-1024 -1024 0"). If you move terrain to "0 0 0" - it works great.The problem is that TerrainBlock::getNormal always returns "false" on first "if".
Haven't tracked down how to fix it yet, no time :(
#11
05/06/2009 (5:18 pm)
Hmm! I never tested it on negative coords. I'll take the FPS Genre Kit for a spin, and let you know what I came up with..
#12
I think the reasoning for it was so that "0 0" would be the center of the terrain (that concept goes all the way back to Tribes). Since newly created terrains are now started at "0 0", that means "0 0" is at the corner of the terrain -- never actually noticed that until just now.
05/06/2009 (6:24 pm)
All existing terrains in the beta have that negative offset, Template & FPS Kit, maybe because every one of those terrains were ported from TGEa. Probably just a holdover from the legacy terrain.I think the reasoning for it was so that "0 0" would be the center of the terrain (that concept goes all the way back to Tribes). Since newly created terrains are now started at "0 0", that means "0 0" is at the corner of the terrain -- never actually noticed that until just now.
#13
05/06/2009 (6:38 pm)
I checked the source and to me it seems that the terrain was not designed to allow for negative coords, which is really strange. Not only getNormal, and all normals related functions seem to not expect negative coords, but the coord parameters of the function for finding a terrain square are defined as U32s. I'll report this bug on the forums. Until its fixed, you're better off keeping all your coords above 0.
#14
Just wanted to submit bug-report, but looks like you got on it first. Ty Konrad!
05/06/2009 (6:40 pm)
yeah, I ended up with the same conclusion.Just wanted to submit bug-report, but looks like you got on it first. Ty Konrad!
#15
05/06/2009 (6:44 pm)
Thank _you_, Bank. Alright, I'll report it, and I think I'll add something else closely related - an issue with terrain collision on coords below 0.
#16
05/06/2009 (7:10 pm)
@Michael: Thanks for the info & check, I included that info in the report as well.
#17
05/07/2009 (2:23 am)
Fantastic Resource!
#18
Normals queries must get object space coord params, and I was passing coordinates from world space.
I have updated the resource (// objspace >> - // objspace <<), and now it should work flawlessly with any terrain.
@Glenn: Thanks! :)
05/07/2009 (9:08 am)
Thanks to Tom Spilman, I was shed some light on this. Normals queries must get object space coord params, and I was passing coordinates from world space.
I have updated the resource (// objspace >> - // objspace <<), and now it should work flawlessly with any terrain.
@Glenn: Thanks! :)
#19
05/07/2009 (11:28 am)
Ah, of course, object space vs world space! At least it was something simple.
#20
05/12/2009 (5:53 am)
Beautiful! I'd been experimenting lately with some external scripts to do something similar, but this is much more interactive, much more direct.
Torque Owner William Gooding