ShaderTerrainMesh collision with ode

Hello i have question, how can i add collision between my lot of objects and terrain ?

i see tutorial
https://www.panda3d.org/manual/index.php/Collision_Detection_with_ODE

and adapte my code, i have terrain, gravity, and load egg object but no collision

from direct.showbase.ShowBase import ShowBase
from panda3d.core import ShaderTerrainMesh, Shader, load_prc_file_data
from panda3d.core import SamplerState

from panda3d.ode import OdeWorld, OdeSimpleSpace, OdeJointGroup
from panda3d.ode import OdeBody, OdeMass, OdeBoxGeom, OdePlaneGeom
from panda3d.core import BitMask32, CardMaker, Vec4, Quat
from random import randint, random
from panda3d.core import Vec3, load_prc_file_data, ShaderTerrainMesh
from panda3d.core import ShaderTerrainMesh, Shader, load_prc_file_data

from direct.actor.Actor import Actor
from panda3d.core import NodePath, CardMaker, SamplerState


class ShaderTerrainDemo(ShowBase):
    def __init__(self):

        # Load some configuration variables, its important for this to happen
        # before the ShowBase is initialized
        load_prc_file_data("", """
            textures-power-2 none
            gl-coordinate-system default
            window-title Panda3D ShaderTerrainMesh Demo
        """)

        # Initialize the showbase
        ShowBase.__init__(self)

        # Increase camera FOV as well as the far plane
        self.camLens.set_fov(90)
        self.camLens.set_near_far(0.1, 50000)

        # Construct the terrain
        self.terrain_node = ShaderTerrainMesh()

        # Set a heightfield, the heightfield should be a 16-bit png and
        # have a quadratic size of a power of two.
        self.terrain_node.heightfield = self.loader.loadTexture("heightfield.png")

        # Set the target triangle width. For a value of 10.0 for example,
        # the terrain will attempt to make every triangle 10 pixels wide on screen.
        self.terrain_node.target_triangle_width = 10.0

        # Generate the terrain
        self.terrain_node.generate()

        # Attach the terrain to the main scene and set its scale. With no scale
        # set, the terrain ranges from (0, 0, 0) to (1, 1, 1)
        self.terrain = self.render.attach_new_node(self.terrain_node)
        
        
        self.terrain.set_scale(1024, 1024, 100)
        self.terrain.set_pos(-512, -512, -70.0)

        # Set a shader on the terrain. The ShaderTerrainMesh only works with
        # an applied shader. You can use the shaders used here in your own application
        terrain_shader = Shader.load(Shader.SL_GLSL, "terrain.vert.glsl", "terrain.frag.glsl")
        self.terrain.set_shader(terrain_shader)
        self.terrain.set_shader_input("camera", self.camera)

        # Shortcut to view the wireframe mesh
        self.accept("f3", self.toggleWireframe)

        # Set some texture on the terrain
        grass_tex = self.loader.loadTexture("textures/grass.png")
        grass_tex.set_minfilter(SamplerState.FT_linear_mipmap_linear)
        grass_tex.set_anisotropic_degree(16)
        self.terrain.set_texture(grass_tex)

        # Load a skybox - you can safely ignore this code
        skybox = self.loader.loadModel("models/skybox.bam")
        skybox.reparent_to(self.render)
        skybox.set_scale(20000)

        skybox_texture = self.loader.loadTexture("textures/skybox.jpg")
        skybox_texture.set_minfilter(SamplerState.FT_linear)
        skybox_texture.set_magfilter(SamplerState.FT_linear)
        skybox_texture.set_wrap_u(SamplerState.WM_repeat)
        skybox_texture.set_wrap_v(SamplerState.WM_mirror)
        skybox_texture.set_anisotropic_degree(16)
        skybox.set_texture(skybox_texture)

        skybox_shader = Shader.load(Shader.SL_GLSL, "skybox.vert.glsl", "skybox.frag.glsl")
        skybox.set_shader(skybox_shader)
        
        # Setup our physics world
        world = OdeWorld()
        world.setGravity(0, 0, -9.81)
         
        # The surface table is needed for autoCollide
        world.initSurfaceTable(1)
        world.setSurfaceEntry(0, 0, 150, 0.0, 9.1, 0.9, 0.00001, 0.0, 0.002)
         
        # Create a space and add a contactgroup to it to add the contact joints
        space = OdeSimpleSpace()
        space.setAutoCollideWorld(world)
        contactgroup = OdeJointGroup()
        space.setAutoCollideJointGroup(contactgroup)
         
        # Load the box
        box = loader.loadModel("box")
        # Make sure its center is at 0, 0, 0 like OdeBoxGeom
        box.setPos(-.5, -.5, -.5)
        box.flattenLight() # Apply transform
        box.setTextureOff()

        dancer = Actor("dancer.egg.pz")
        geomnode = dancer.find('**/-GeomNode').node()
        geomnode.setIntoCollideMask(BitMask32.bit(1))
        
        dancer.setPos(0,0,0)
        chorusline = NodePath('chorusline')
        
        boxes = []
        for i in range(5):
          placeholder = chorusline.attachNewNode("Dancer-Placeholder")
          placeholder.setPos(i*5,-.5, -.5)
          dancer.instanceTo(placeholder)
          
          boxBody = OdeBody(world)
          M = OdeMass()
          M.setBox(50, 1, 1, 1)
          boxBody.setMass(M)
          boxBody.setPosition(dancer.getPos(render))
          boxBody.setQuaternion(dancer.getQuat(render))
          # Create a BoxGeom
          boxGeom = OdeBoxGeom(space, 1, 1, 1)
          boxGeom.setCollideBits(BitMask32(0x00000002))
          boxGeom.setCategoryBits(BitMask32(0x00000001))
          boxGeom.setBody(boxBody)
          boxes.append((dancer, boxBody))
          
        for i in range(3):
          placeholder = render.attachNewNode("Line-Placeholder")
          placeholder.setPos(-.5,i*10,-.5)
          chorusline.instanceTo(placeholder)

         
        # Add a random amount of boxes
        
        for i in range(randint(15, 30)):
          # Setup the geometry
          boxNP = box.copyTo(render)
          boxNP.setPos(randint(-10, 10), randint(-10, 10), 10 + random())
          boxNP.setColor(random(), random(), random(), 1)
          boxNP.setHpr(randint(-45, 45), randint(-45, 45), randint(-45, 45))
          # Create the body and set the mass
          boxBody = OdeBody(world)
          M = OdeMass()
          M.setBox(50, 1, 1, 1)
          boxBody.setMass(M)
          boxBody.setPosition(boxNP.getPos(render))
          boxBody.setQuaternion(boxNP.getQuat(render))
          # Create a BoxGeom
          boxGeom = OdeBoxGeom(space, 1, 1, 1)
          boxGeom.setCollideBits(BitMask32(0x00000002))
          boxGeom.setCategoryBits(BitMask32(0x00000001))
          boxGeom.setBody(boxBody)
          boxes.append((boxNP, boxBody))

        #self.terrain.setPos(0, 0, 0); self.terrain.lookAt(0, 0, -1)
        
        self.terrain.setCollideMask(BitMask32.bit(0))
         
        # Set the camera position
        base.camera.setPos(40, 40, 20)
        base.camera.lookAt(0, 0, 0)
         
        # The task for our simulation
        def simulationTask(task):
          space.autoCollide() # Setup the contact joints
          # Step the simulation and set the new positions
          world.quickStep(globalClock.getDt())
          for np, body in boxes:
            np.setPosQuat(render, body.getPosition(), Quat(body.getQuaternion()))
          contactgroup.empty() # Clear the contact joints
          return task.cont
         
        # Wait a split second, then start the simulation  
        taskMgr.doMethodLater(0.5, simulationTask, "Physics Simulation")

ShaderTerrainDemo().run()

i want collision with my ShaderTerrainMesh and all objects load (box and dancer egg file)

is not possible in panda3d ?

I am not seeing where you actually setup a physics shape for your terrain_node. Also, if you are using a vertex shader to handle terrain, which I believe ShaderTerrainMesh is doing, then the geometry data available on the CPU (i.e., what Panda has in its geom nodes) will not line up with the visuals. I know that Panda’s Bullet code has a Heightfield shape to deal with this. I do not know if the ODE code has something similar.

i try to use Bullet, i view your link and i prudct this code :

from direct.showbase.ShowBase import ShowBase
from panda3d.core import ShaderTerrainMesh, Shader, load_prc_file_data
from panda3d.core import SamplerState
from panda3d.core import GeoMipTerrain

from panda3d.ode import OdeWorld, OdeSimpleSpace, OdeJointGroup
from panda3d.ode import OdeBody, OdeMass, OdeBoxGeom, OdePlaneGeom
from panda3d.core import BitMask32, CardMaker, Vec4, Quat
from random import randint, random
from panda3d.core import Vec3, load_prc_file_data, ShaderTerrainMesh
from panda3d.core import ShaderTerrainMesh, Shader, load_prc_file_data

from direct.actor.Actor import Actor
from panda3d.core import NodePath, CardMaker, SamplerState


from panda3d.core import Vec3
from panda3d.bullet import BulletWorld
from panda3d.bullet import BulletPlaneShape
from panda3d.bullet import BulletRigidBodyNode
from panda3d.bullet import BulletBoxShape

from panda3d.core import Filename
from panda3d.core import PNMImage
from panda3d.bullet import BulletHeightfieldShape
from panda3d.bullet import ZUp


class ShaderTerrainDemo(ShowBase):
    def __init__(self):

        # Initialize the showbase
        ShowBase.__init__(self)
        
        # Load some configuration variables, its important for this to happen
        # before the ShowBase is initialized
        load_prc_file_data("", """
            textures-power-2 none
            gl-coordinate-system default
            window-title Panda3D ShaderTerrainMesh Demo
        """)

        # Set up the GeoMipTerrain
        terrain = GeoMipTerrain("myDynamicTerrain")
        terrain.setHeightfield("heightfield.png")
         
        # Set terrain properties
        terrain.setBlockSize(32)
        terrain.setNear(40)
        terrain.setFar(100)
        terrain.setFocalPoint(base.camera)
         
        # Store the root NodePath for convenience
        root = terrain.getRoot()
        root.reparentTo(render)
        root.setSz(100)
        
        # World
        world2 = BulletWorld()
        world2.setGravity(Vec3(0, 0, -9.81))
        
        
        
        

        height = 10.0
        img = PNMImage(Filename('heightfield.png'))
        shape = BulletHeightfieldShape(img, height, ZUp)
        
        node = BulletRigidBodyNode('Ground')
        node.addShape(shape)
        
        np = render.attachNewNode(node)
        np.setPos(0, 0, -2)
        
        world2.attachRigidBody(node)
        
        # Boxes
        model = loader.loadModel('models/box.egg')

        model.setPos(30, 50, 20)
        root.setPos(-0.5, -0.5, -10)
        model.flattenLight()
        shape = BulletBoxShape(Vec3(0.5, 0.5, 0.5))


        for i in range(10):
            node = BulletRigidBodyNode('Box')
            node.setMass(1.0)
            node.addShape(shape)
            np = render.attachNewNode(node)
            np.setPos(0, 0, 2+i*2)
            world2.attachRigidBody(node)
            model.copyTo(np)
         
        # Update
        def update(task):
          dt = globalClock.getDt()
          world2.doPhysics(dt)
          return task.cont
         
        taskMgr.add(update, 'update')
         
        # Generate it.
        terrain.generate()
        # Add a task to keep updating the terrain
        def updateTask(task):
          terrain.update()
          return task.cont
        taskMgr.add(updateTask, "update")

ShaderTerrainDemo().run()

i don’t understand how have collision with my terrain, my box have no collision with terrain

I took a quick skim through your code, and it seems fine. I see that you are adding the terrain and the cubes to the Bullet world, and that the terrain is using the heightfield shape. Perhaps someone who has actually used this terrain stuff can chime in and mention any gotchas/quircks.

I’m confused, why is the code using both GeoMipTerrain and ShaderTerrainMesh, two separate systems? Which one is being used for rendering?

I’d rather use ShaderTerrainMesh and ODE physic engine

https://www.panda3d.org/manual/index.php/Physics
ODE seems to be the best physique for panda3d

but on the panda3d doc I found, they use geomipterrain with bullet

no idea ?
can panda3d handle collisions on terrain?

I try to search help in forum all discussion, but i have only message that is not possible with this engine.

well, it’s badly supported by the engine
I don’t understand, it’s still something basic for a game engine I think right ?

How do you not cross a ShaderTerrainMesh ?

We don’t support the ODE heightfield shape specifically. In general, I think you will find that Bullet is better supported and enjoys broader use in Panda. You can use the Bullet heightfield shape which works well in conjunction with ShaderTerrainMesh.

ok, but i have not collision with bullet and geomip terrain, why ?

import direct.directbase.DirectStart
from panda3d.core import Vec3
from panda3d.bullet import BulletWorld
from panda3d.bullet import BulletRigidBodyNode
from panda3d.bullet import BulletBoxShape
from panda3d.core import Filename
from panda3d.core import GeoMipTerrain


from panda3d.core import Filename
from panda3d.core import PNMImage
from panda3d.bullet import BulletHeightfieldShape
from panda3d.bullet import ZUp


base.cam.setPos(10, -30, 20)
base.cam.lookAt(0, 0, 5)


 
# World
world = BulletWorld()
world.setGravity(Vec3(0, 0, -9.81))
 
# Boxes
model = loader.loadModel('models/box.egg')
model.setPos(-0.5, -0.5, -0.5)
model.flattenLight()
shape = BulletBoxShape(Vec3(0.5, 0.5, 0.5))
for i in range(10):
    node = BulletRigidBodyNode('Box')
    node.setMass(1.0)
    node.addShape(shape)
    np = render.attachNewNode(node)
    np.setPos(0, 0, 2+i*2)
    world.attachRigidBody(node)
    model.copyTo(np)
 
# Update
def update(task):
  dt = globalClock.getDt()
  world.doPhysics(dt)
  return task.cont
  
# Set up the GeoMipTerrain
terrain = GeoMipTerrain("myDynamicTerrain")

img = PNMImage(Filename('heightfield.png'))
terrain.setHeightfield('heightfield.png')
 
# Set terrain properties
terrain.setBlockSize(32)
terrain.setNear(40)
terrain.setFar(100)
terrain.setFocalPoint(base.camera)
 
# Store the root NodePath for convenience
root = terrain.getRoot()
root.reparentTo(render)
root.setPos(0, -50, -100)
height=100
root.setSz(height)
shape = BulletHeightfieldShape(img, height, ZUp)

node = BulletRigidBodyNode('Ground')
node.addShape(shape)
world.attachRigidBody(node)

# Generate it.
terrain.generate()
# Add a task to keep updating the terrain
def updateTask(task):
  terrain.update()
  return task.cont
taskMgr.add(updateTask, "update")
 
taskMgr.add(update, 'update')
run()

What’s wrong with my code?

panda3d can not detect colision in the terrain
you can use only primitive like plane, or create terrain in blancer and convert to egg/bam

geomipterrain and shadderTerrain is very bad supported in this engine
for example shadderTerrain not support multiple texture

i suggest create your world in blender

@sazearte I don’t know what you’re talking about. GeoMipTerrain and ShaderTerrainMesh support texturing just as well as a Blender-created mesh, in the sense that they only deal with geometry and that the same texturing mechanism applies to all of them. Texturing is not a feature of terrain, but a feature of the shader you apply to it.

ShaderTerrainMesh and BulletHeightfieldShape have been tuned to work well together. I have not yet had time to try and run @Uther’s code, however.

For ShaderTerrainMesh texturing, i think only support color like vec3 basecolor, not real multiple texture like .png or .jpg file

the code of uther seems to me in any case correct, Maybe it’s an engine bug?

ShaderTerrainMesh as the name suggests makes a terrain mesh using a shader. You need to write your own shader to get multiple textures on the mesh

I didn’t know it was possible with ShaderTerrainMesh, would you have an example?

maybe I can create another topic, because it has nothing to do with the author’s topic

[edit]
Ah, actually strike the text below. Looking again, I see that the scale is being applied to the visual geometry, not the height-field shape. My mistake, and my apologies! ^^;
[/edit]

This is a bit of a stab in the dark, but should you be setting a scale on the NodePath of the height-field shape? According to the manual, scaling Bullet physics objects isn’t ideal, and furthermore you’re passing the maximum height into the shape’s constructor anyway. As a result, it may be that your height is being applied twice–once in the height-field shape itself, and then again when it’s scaled by that factor.

All right, I did a bit of experimenting, and seem to have managed to get it to work. It seems that the problem is that GeoMipTerrain and Bullet’s height-field terrain use different origins: the former has its origin at one of its corners, while the latter has its origin at its centre.

The solution, then, is to offset the GeoMipTerrain to match the Bullet terrain. Note that the GeoMipTerrain’s horizontal size seems to be dependant on the size of the height-field image: if the image is 128 x 128, then the height-field will have x- and y- sizes of 128 and 128. In the code below, I’m presuming that the height-field image is square, and that its size is stored in the variable “size”. The terrain’s height is simply the z-scale applied to the node.

The relevant line would then be this:

root.setPos(-size*0.5, -size*0.5, -height*0.5)

Note that you can see whether the physics and visual terrains are aligned by using the Bullet Debug Node (see here) to visualise the physics terrain. Fair warning, however: it seems to be rather slow for a large terrain, so if you want to do this I recommend using a small test-terrain! (I use a 128 x 128 terrain, I believe.)