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.
Torque 3D Owner Travis Evans
EDIT:
Scratch that.. Forgot to select the Debug Build
... this really should be integrated in 1.1!
EDIT:
I've added this to my menu.ed.cs around line 139:
(tools/worldEditor/scripts/menu.ed.cs)