Game Development Community

Rule based layer distribution for Torque3D terrains

by Konrad Kiss · 05/05/2009 (10:01 am) · 58 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):

www.xenocell.com/dev/t3dald1.jpg

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

www.xenocell.com/dev/t3dald2.jpg

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

www.xenocell.com/dev/t3dald3.jpg

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

Lead Developer at Bitgap Games (www.bitgap.com) currently working on Xenocell (www.xenocell.com) a massively multiplayer action strategy game based on Torque 3D technology.

#22
05/25/2009 (6:26 pm)
I am having some issue implementing this when i try and compile the terraineditor.cpp i recieve an 26 errors.

Edit: Nevermind Figured it out.
#23
06/03/2009 (5:23 pm)
Minor problem:
Cannot cancel the dialog once it's launched. The X does nothing. While Undo covers going back, it'd be nice to cancel if you changed your mind.
#24
06/03/2009 (5:57 pm)
Oops, you're right.

Add the following parameter to the GuiWindowCtrl in GuiTerrainPainterProceduralGui.gui:
closeCommand = "Canvas.popDialog(TerrainPainterProceduralGui);";

I've updated the resource to reflect this.
#25
06/03/2009 (6:24 pm)
Doesnt seem to work in beta 2.
#26
06/03/2009 (6:25 pm)
It works for me in Beta 2. Can you elaborate?
#27
06/03/2009 (6:29 pm)
I have a pretty large terrain and when I apply the layer, it will basically freeze T3D. I am not sure if its just calculating or what, but ive waited for 15 minutes before and nothing. Worked fine for the same terrain in Beta 1 with no freeze.
#28
06/03/2009 (6:30 pm)
You probably have undo compiled in. Just comment it out, and it will work a lot faster - except you won't be able to undo. But I think it's worth the speed, undo for this much data is very slow.
#29
07/04/2009 (2:52 am)
Awesome, I'll be sure to use this in my project.
#30
07/09/2009 (7:11 am)
Konrad, this is really, really cool. A couple questions from this non-programmer:

Is it built into Beta 3?
IS there a GUI for it?
How do I access it?

Thanks!
#31
07/09/2009 (7:15 am)
Hello Robert, glad you like it! :)

It's not in Torque3D Beta 3, it's an optional addition to it.
The GUI is in this thread, you can add that if you want. Accessing it is also described in the resource and the follow-up posts.
#32
07/09/2009 (2:17 pm)
Great addition Konrad!

You will be definitely becoming one of the community heroes ;P
#33
07/09/2009 (2:18 pm)
:) Thanks JoZ, I hope you will find it useful!
#34
07/11/2009 (3:21 pm)
I must be missing something. Can't get it to compile, I get 32 errors. Hmm, got it to work. Here are some notes for noobs (like me).

1. Mission editor no longer exists as of beta 3. Everywhere you see the words mission editor, change it to World Editor.

2. You need to add to TerrainEditor.H the following line:
void TerrainEditor::autoMaterialLayer( F32, F32, F32, F32 )

I put it in public section.
#35
07/27/2009 (12:49 am)
@Konrad,
regarding the addition of the autoLayers() toggle button:
the button, in beta 4, takes up the full space of the column available, pushing the image boxes off to the right, and if I drag the box bigger, the button grows correspondingly, thus always the image boxes remain "hidden"
any ideas?
#36
07/27/2009 (3:39 am)
@deepscratch

Here's my button that works with Beta 4.. Insert it as a last child of EPainterPreview. See if this works for you. Other than that, you could put it anywhere you wish. You could just create a new button with the Command = "autoLayers();"; param.

// >>>
      new GuiButtonCtrl() {  
         canSaveDynamicFields = "0";  
         isContainer = "0";  
         Profile = "GuiButtonProfile";  
         HorizSizing = "left";  
         VertSizing = "bottom";  
         Position = "106 229";
         Extent = "45 18";
         MinExtent = "8 2";  
         canSave = "1";  
         Visible = "1";  
         Command = "autoLayers();";  
         tooltipprofile = "GuiToolTipProfile";  
         hovertime = "1000";  
         text = "fill";  
         groupNum = "-1";  
         buttonType = "PushButton";  
         useMouseEvents = "0";  
      };
      // <<<
#37
07/27/2009 (3:53 am)
awesome.
thanks Konrad, works beautifully.
#38
07/30/2009 (10:10 am)
Konrad lately every time I read a your resource post I have to hit CTRL+D and choose "TORQUE !!! Changes to add" as bookmark folder... :-)
#39
07/30/2009 (10:11 am)
Haha, :) I'm glad they are useful, JoZ!
#40
10/09/2009 (10:50 pm)
Deleted :0