Limiting the terrain that an actor can walk on

Hi.

I’m trying to find a simple way to limit the terrain that an actor can walk on, like a track that you can’t get off.

I’ve loaded my terrain and then i’ve opened the .png texture in photoshop, as a guide.

Then, based on it, i’ve created a new image, with the same resolution, and painted the path in red.

So, i’ve loaded it like this:

self.redLine = PNMImage()
self.redLine.read("assets/terrain/red_line.png")

Knowing that the Green component (from RGB) inside the red line is 0 and outside is 1, i’m trying to obtain the Green color from the redLine, in the actor’s coordinates:

str(int(cm.getGreen(int(self.mainActor.getX()), int(self.mainActor.getY()))*255))

The problem is, when i launch the game, i start following the track, but the values i’m getting are inconsistent. The Green value sometimes is 0 and sometimes is 255, doesn’t matter if i stay inside the track or if i’m outside of it.
I’ve even loaded the redLine as a texture and it matches the track perfectly, but the Green value doesn’t.

Do you know what i’m doing wrong?
Or do you have a better (yet as simple) approach to what i’m trying to achieve?

Thank you in advance.

Ok, second picture got deleted for some reason, so i’ve reuploaded it.

Anyway, can anybody help me? I’m stuck here…

Just create an invisible wall along both sides of your path. And apply physics to it.

How can i create an invisible wall?

Well if your using Blender, use the edge select tool and select all the edges along the side of your path, then extrude it. That will create your wall. You then select all the faces of your wall and press P in edit mode. That will separate your wall from your terrain mesh. If you dont apply a material or texture to the wall it should be invisible in panda. If your using Bullet for physics I think they have a “ghost” mode that will set invisible objects so that you cant pass threw then.

Another option is to create a duplicate of your terrains path and set it so that anything in collision with the path moves normally, other wise all objects move at a much decreased speed. So when your not on the path your character moves as though it is walking threw wet cement.

But i’m using GeoMip Terrain with a heightfield image. Is there a way to open it on blender, modify it that way and generate a model?

Hmmm I am not sure of how you would go about doing what you want to using Geomip. Me personally I use L3DT bundysoft.com/L3DT/ it has a free version. It has a terrain sculptor and painter. I export my terrain from there as a collada file, I load that into blender,
use the decimate modifier to lower the poly count then convert the triangles into quads “Edit Mode Alt + J” and smooth it with the “Smooth Vertex” button. And apply the Texture, Normap Map, Light Map, Spec Map, Shadow Map" in the materials panel and export it to egg.

This is in panda

Here is a scene with the same terrain but its fall

Wow! That is an amazing piece of information!
Thank you very much, i may try it tomorrow.

Sure not a problem sorry I couldn’t be more helpful with your particular issue. But if you need any help making terrains let me know. Ill try to make a tutorial on how I do terrains tonight or tomorrow.

I’m not sure creating collision walls around the path is your best solution here. That looks like it would take quite a lot of collision geometry.

I actually really like your idea of checking the color of an image at a given coordinate to determine if it’s on the path or off of it.

Looking at the little bit of code you provided, I’m seeing two things that make me curious.

First, you’re loading the image into self.redline. Then you’re getting the green value from something called cm, right? What is cm? Don’t you want the green value from self.redline?

Secondly, I see that you’re inputting the actor’s coordinates directly. Do the coordinates of the space your actor is in perfectly match the coordinates for the image? That seems unlikely, you probably need to convert the coordinates with a formula, even if it’s just up or downscaling. I haven’t messed with PNMImage much, so I don’t know how it describes the coordinates of the image, but I’m guessing it’s probably between 0 and 1, or maybe between 0 and [image dimension in pixels].

In fact, I liked the idea you present so much that I went ahead and made a working example of it.

You can use any 1024x1024 image for the texture, just save it in the same folder with the name “Terrain.png”. Black areas in the image are traversable, white areas are not. Make sure the center of the image is black, since you start in the middle of the image.

Note that in this example I’m using the Z coordinate in place of Y for the camera position because the texture card is vertical, like a billboard, instead of horizontal, like a terrain would be.

main.py:

import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from pandac.PandaModules import *


class Game(DirectObject):
  def __init__(self):
    base.disableMouse()
    
    self.card = loader.loadModel("Terrain.egg")
    self.card.reparentTo(render)
    self.card.setY(1000)
    
    self.dude = loader.loadModel("panda.egg")
    self.dude.reparentTo(base.camera)
    self.dude.setColor(1,0,0,1)
    self.dude.setY(100)
    
    self.image = PNMImage()
    self.image.read("Terrain.png")
    
    self.keyMap = {  "w" : False,
            "s" : False,
            "a" : False,
            "d" : False }
    
    self.accept("w", self.setKey, ["w", True])
    self.accept("s", self.setKey, ["s", True])
    self.accept("a", self.setKey, ["a", True])
    self.accept("d", self.setKey, ["d", True])
    
    self.accept("w-up", self.setKey, ["w", False])
    self.accept("s-up", self.setKey, ["s", False])
    self.accept("a-up", self.setKey, ["a", False])
    self.accept("d-up", self.setKey, ["d", False])
    
    taskMgr.add(self.moveTask, "Move Task")
    
  def setKey(self, key, value):
    self.keyMap[key] = value
    
  def moveTask(self, task):
    dt = globalClock.getDt()
    if (dt > .05):
      return task.cont
      
    newCamPos = base.camera.getPos()
      
    if(self.keyMap["w"] == True):
      newCamPos = newCamPos + Point3(0,0, dt * 100)
    elif(self.keyMap["s"] == True):
      newCamPos = newCamPos + Point3(0,0, dt * -100)
      
    if(self.keyMap["a"] == True):
      newCamPos = newCamPos + Point3(dt * -100, 0,0)
    elif(self.keyMap["d"] == True):
      newCamPos = newCamPos + Point3(dt * 100, 0,0)
      
    imageX = int(newCamPos.getX()) + 512
    imageY = 1024 - (int(newCamPos.getZ()) + 512)
    if(0 < imageX and imageX < 1024):
      if(0 < imageY and imageY < 1024):
        bright = self.image.getBright(imageX, imageY)
        if(bright < 0.5):
          base.camera.setPos(newCamPos)
      
    return task.cont
      
g = Game()
run()

Terrain.egg:

<Comment> {
  "egg-texture-cards -g -512,512,-512,512 -p 1024,1024 -o Terrain.egg Terrain.png"
}
<Texture> Terrain {
  Terrain.png
}
<Group> {
  <VertexPool> vpool {
    <Vertex> 0 {
      -512 512 0
      <UV> { 0 1 }
    }
    <Vertex> 1 {
      -512 -512 0
      <UV> { 0 0 }
    }
    <Vertex> 2 {
      512 -512 0
      <UV> { 1 0 }
    }
    <Vertex> 3 {
      512 512 0
      <UV> { 1 1 }
    }
  }
  <Group> Terrain {
    <Polygon> {
      <RGBA> { 1 1 1 1 }
      <TRef> { Terrain }
      <VertexRef> { 0 1 2 3 <Ref> { vpool } }
    }
  }
}

Some notes on things I learned: PNMImage uses 0 to [pixel dimension size] scheme for it’s coordinates, and the top left corner of the image is 0,0. So you need to map the terrain coordinates to the image coordinates.

Here’s an example:

I have a terrain image that is 1024x1024, and a terrain model that is 100 panda units square, with it’s origin in the middle. That means my terrain stretches from -50 to 50. To translate this, I use the following steps:

pandaX = pandaX + 50 #Converts from -50 to 50 range to 0 to 100 range
pandaX = pandaX / 100 #Converts from 0 to 100 range to 0-1 range (percentage)
imageX = int(pandaX * 1024) #Converts from percentage to number of pixels

Those steps would be duplicated for Y. You may also need to invert one of the coordinates, like I needed to invert my imageY coordinate. An easy way to figure that out is to texture your terrain with an image that has a different colored dot covering each corner, then load up the same image as a PNMImage and use print(image.getPixel(x,y)) to print the color of each corner of the image. That will tell you the image coordinates for each corner of your terrain.

Cool, but how does this work in combination with a terrain?
How does the characters location on the terrain translate into is position on the path map?

I wouldn’t mind using your code my self just have a few questions on how it works :slight_smile:

The concept is the same for a character on terrain as for the camera. You get the character’s location relative to the terrain and use a formula to map that location onto the image.

For another example, we can say that I have a piece of terrain that is 1000 units square with it’s origin in the center in a file called “Terrain.egg”. I have an image that is 2048x2048 with the walkable path drawn on it called “Path.png”. My character is in “Character.egg”.

terrain = loader.loadModel("Terrain.egg")
terrain.reparentTo(render)

character = loader.loadModel("Character.egg")
character.reparentTo(render)

path = PNMImage()
path.read("Path.png")

def getPathPos(character, terrain, path):
  terrainPos = character.getPos(terrain) #Get character's pos relative to terrain.

#Map the 3D coords to the image's coordinates
  pathX = int( ( ( terrainPos.getX() + 500 )/1000 ) * 2048 )
  pathY = int( ( ( terrainPos.getY() + 500 )/1000 ) * 2048 )

return(pathX, pathY)

That code would give you the coordinates of the character’s position on the image if the positive-Y direction in 3D space is the same direction as moving down (from top to bottom) on the image. If the positive-Y direction in 3D space was the same as moving up (from bottom to top) on the image, you would need to use the mapping equations shown below:

  pathX = int( ( ( terrainPos.getX() + 500 )/1000 ) * 2048 )
  pathY = int( 2048 - ( ( ( terrainPos.getY() + 500 )/1000 ) * 2048 ) )

Basically, you get the character’s coordinates relative to the terrain using character.getPos(terrain). Then you remap them to a a new value range to fit the image.

The steps I use for remapping a range of values to a new range are as follows. If you already know how to remap a range of values to a new range, then feel free to ignore this part.

  1. If the minimum value for the starting range is not 0, adjust the current value so that the minimum is 0.
    Example: If the minimum value is -500, add +500 to the current value. If the minimum value is +250, add -250 to the current value.

  2. Convert the current value into a percentage of the maximum value. Note: If you adjusted the current value in step 1, you must adjust the maximum value by the same amount.
    Example: If the maximum value is 500, and I DID NOT change the current value in step 1 because the minimum value was already 0, then I just divide the current value by 500, to get the percentage.
    Example2: If the maximum value is 1500, and I added +1500 to the current value in step 1 to get a minimum value of 0, then I add +1500 to the maximum value to get the new maximum: 3000. Then I divide by the adjusted maximum of 3000 to get the percentage.

  3. Multiply the percentage from step 2 by the maximum value of the new range to get the new value.
    Example: If the maximum value of the new range is 1024, and my percentage from step 2 was 0.75, I multiply 1024 by 0.75 and get 768 as my current value in the new range.

For this method to work, you need to know the coordinate range for the terrain and for the image, of course. Both of those should be known to you anyway, though.

To simplify the calculation, I prefer using P3D’s nodepath relative transform mechanism.
Here it is, without using Terrain.egg :

import direct.directbase.DirectStart 
from direct.showbase.DirectObject import DirectObject 
from pandac.PandaModules import *
import os


class Game(DirectObject): 
  def __init__(self): 
    base.disableMouse() 
    
    terrainSize = 512.
    cm = CardMaker('')
    cm.setFrame(-terrainSize,0, -terrainSize,0)
    self.card = render.attachNewNode(cm.generate())
    self.card.setTexture(loader.loadTexture('Terrain.png'))
    base.camera.setPos(self.card.getBounds().getCenter())
    self.card.setY(1000)
    
    self.dude = loader.loadModel("panda.egg")
    self.dude.reparentTo(base.camera)
    self.dude.setColor(1,0,0,1)
    self.dude.setY(100)
    
    self.image = PNMImage()
    self.image.read("Terrain.png")
    self.imgSize = self.image.getXSize()
    
    self.keyMap = {  "w" : False,
            "s" : False,
            "a" : False,
            "d" : False }
    
    self.accept("escape", os._exit,[0])
    for key in self.keyMap.keys():
        self.accept(key, self.keyMap.__setitem__, [key, True])
        self.accept(key+"-up", self.keyMap.__setitem__, [key, False])
    
    taskMgr.add(self.moveTask, "Move Task")
    
    self.conversionNP = render.attachNewNode('')
    minb, maxb = self.card.getTightBounds()
    self.conversionNP.setPos(minb.x,0,maxb.z)
    self.conversionNP.setScale(terrainSize/self.imgSize,1,-terrainSize/self.imgSize)
    
  def moveTask(self, task):
    dt = globalClock.getDt()
    if (dt > .05):
      return task.cont

    newCamPos = base.camera.getPos()

    if self.keyMap["w"]:
      newCamPos += Point3(0,0, dt * 100)
    elif self.keyMap["s"]:
      newCamPos += Point3(0,0, dt * -100)

    if self.keyMap["a"]:
      newCamPos += Point3(dt * -100, 0,0)
    elif self.keyMap["d"]:
      newCamPos += Point3(dt * 100, 0,0)

    imageX,imageY = self.conversionNP.getRelativePoint(render,newCamPos).xz
    if 0<imageX<self.imgSize and 0<imageY<self.imgSize:
        bright = self.image.getBright(int(imageX), int(imageY))
        if(bright < .5):
          base.camera.setPos(newCamPos)

    return task.cont

g = Game()
run()

Change the terrain size and origin as you like, the result remains correct.

I like the code you used to simplify the accepting of keystrokes there, ynjh_jo. I never thought of doing it like that.

The way you set up the conversion NodePath is pretty elegant too. A good improvement to my code, I’d say.