Actually I may have something even better
.
The following attempt involves the use of the depth buffer (*). It solves both problems encountered with the previous code, although it’s not perfect either:
- accuracy depends on the precision of the depth buffer (it’s possible to use the average of the image values, but that may not always be desirable);
- since it doesn’t seem possible to get the correct image from the depth buffer every single frame, there will be a lag when checking the terrain continuously.
These shortcomings may prove more acceptable than the previous ones, but even if not, you could try to combine both methods, e.g.:
use the previous code to get the difference between the point guaranteed to be under the mouse and the point guaranteed to be on the terrain; if that difference is greater than a certain threshold value, use the depth buffer instead.
from panda3d.core import *
loadPrcFileData("", "sync-video 1")
##loadPrcFileData("", "prefer-texture-buffer 1")
##loadPrcFileData("", "prefer-parasite-buffer 0")
##loadPrcFileData("", "force-parasite-buffer 0")
from direct.showbase.ShowBase import ShowBase
class TerrainCamera(object):
def __init__(self, app):
self._app = app
self._average_depth = False # whether or not the depth buffer image should be averaged
props = FrameBufferProperties()
props.setDepthBits(1)
winprops = WindowProperties.size(*((4, 4) if self._average_depth else (1, 1)))
self._buffer = app.graphicsEngine.makeOutput(app.pipe, "depth_buffer", -2,
props, winprops,
GraphicsPipe.BFRefuseWindow,
app.win.getGsg(), app.win)
assert self._buffer is not None
self._depth_tex = Texture("depth_texture")
self._buffer.addRenderTexture(self._depth_tex, GraphicsOutput.RTMBindOrCopy,
GraphicsOutput.RTPDepth)
self._np = app.makeCamera(self._buffer)
node = self._np.node()
self._mask = BitMask32.bit(21)
node.setCameraMask(self._mask)
lens = OrthographicLens() # for a linear depth buffer
lens.setFilmSize(*((5., 5.) if self._average_depth else (.01, .01)))
lens.setNearFar(0., 100000.)
node.setLens(lens)
self._mouse_watcher = app.mouseWatcherNode
self._main_cam_lens = app.camLens
self._depth = 0.
self._point = Point3()
self._app.taskMgr.add(self.__update, "get_pixel_under_mouse", priority=0)
# calling GraphicsEngine.extractTextureData() every frame seems problematic
self._app.taskMgr.doMethodLater(.02, self.__getDepth, "get_depth", priority=1)
def __getDepth(self, task):
if self._app.graphicsEngine.extractTextureData(self._depth_tex, self._app.win.getGsg()):
if self._average_depth:
img = PNMImage(4, 4)
self._depth_tex.store(img)
self._depth = img.getAverageGray() * 100000.
else:
img = PNMImage(1, 1)
self._depth_tex.store(img)
pixel = img.getXelA(0, 0)
self._depth = pixel[2] * 100000.
else:
print "Could not retrieve texture data!"
return task.again
def __update(self, task):
if not self._mouse_watcher.hasMouse():
return task.cont
screen_pos = self._mouse_watcher.getMouse()
far_point = Point3()
self._main_cam_lens.extrude(screen_pos, Point3(), far_point)
self._np.lookAt(far_point)
dir_vec = self._app.render.getRelativeVector(self._np, Vec3(0., 1., 0.))
self._point = self._np.getPos(self._app.render) + dir_vec * self._depth
return task.cont
def getMask(self):
return self._mask
def getPoint(self):
return self._point
class Terrain(object):
def __init__(self, app, parent_np, height_map, height, focal_point):
self._terrain = GeoMipTerrain("terrain")
self._terrain.setHeightfield(height_map)
self._terrain.setBlockSize(128)
self._terrain.setMinLevel(2)
self._terrain.setBruteforce(True)
self._terrain.setNear(4)
self._terrain.setFar(100)
self._terrain.setFocalPoint(focal_point)
## self._terrain.setAutoFlatten(GeoMipTerrain.AFMStrong)
root = self._terrain.getRoot()
root.reparentTo(parent_np)
self._height = height
root.setSz(height)
## root.setTexture(app.loader.loadTexture("terrain_tex.jpg"))
self._terrain.generate()
app.taskMgr.add(self.__update, "update_terrain")
def __update(self, task):
self._terrain.update()
return task.cont
def getElevation(self, x, y):
return self._terrain.getElevation(x, y) * self._height
class Cube(object):
def __init__(self, parent_np):
vertex_format = GeomVertexFormat.getV3n3cpt2()
vertex_data = GeomVertexData("cube_data", vertex_format, Geom.UHStatic)
tri = GeomTriangles(Geom.UHStatic)
pos_writer = GeomVertexWriter(vertex_data, "vertex")
normal_writer = GeomVertexWriter(vertex_data, "normal")
vertex_count = 0
for direction in (-1, 1):
for i in range(3):
normal = VBase3()
normal[i] = direction
for a, b in ( (-1., -1.), (-1., 1.), (1., 1.), (1., -1.) ):
pos = VBase3()
pos[i] = direction
pos[(i + direction) % 3] = a
pos[(i + direction * 2) % 3] = b
pos_writer.addData3f(pos)
normal_writer.addData3f(normal)
vertex_count += 4
tri.addVertices(vertex_count - 2, vertex_count - 3, vertex_count - 4)
tri.closePrimitive()
tri.addVertices(vertex_count - 4, vertex_count - 1, vertex_count - 2)
tri.closePrimitive()
cube_geom = Geom(vertex_data)
cube_geom.addPrimitive(tri)
cube_node = GeomNode("cube_geom_node")
cube_node.addGeom(cube_geom)
self._origin = parent_np.attachNewNode(cube_node)
def getOrigin(self):
return self._origin
class Panda3DApp(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setFrameRateMeter(True)
p_light = PointLight("point_light")
p_light.setColor(VBase4(1., 1., 1., 1.))
self._light = self.camera.attachNewNode(p_light)
self._light.setPos(5., -10., 7.)
self.render.setLight(self._light)
self._terrain_cam = TerrainCamera(self)
self._marker = Cube(self.render).getOrigin()
self._marker.setColor(1., 0., 0.)
self._marker.hide(self._terrain_cam.getMask())
self._terrain = Terrain(self, self.render, "height_map.png",
100., self.camera)
self._get_point_on_terrain = False # whether the point should be on the terrain,
# rather than under the mouse cursor
self.accept("enter", self.__copyMarker)
self.taskMgr.add(self.__setMarkerPos, "set_marker_pos", priority=1)
self.run()
def __copyMarker(self):
self._marker.copyTo(self.render)
def __setMarkerPos(self, task):
if not self.mouseWatcherNode.hasMouse():
return task.cont
if self._get_point_on_terrain:
x, y, z = self._terrain_cam.getPoint()
z = self._terrain.getElevation(x, y)
# point might not be under mouse, but is guaranteed to be on terrain
self._marker.setPos(x, y, z)
else:
# point might not be on terrain, but is guaranteed to be under mouse
self._marker.setPos(self._terrain_cam.getPoint())
return task.cont
if __name__ == "__main__":
Panda3DApp()
(*) It was actually kinda hard to access the depth buffer image. For those interested, here are my misadventures:
- using DisplayRegion.getScreenshot() or GraphicsOutput.getScreenshot() gave me the contents of the main window instead of the depth buffer, presumably because a parasite buffer was created, but “prefer-texture-buffer 1”, “prefer-parasite-buffer 0” and/or “force-parasite-buffer 0” did not help;
- creating a TexturePeeker failed, probably because it’s incompatible with the format of the depth texture;
- calling GraphicsEngine.extractTextureData() every single frame gave incorrect results (first, the image stayed blank, but after moving the mouse out of the window and back, it started changing seemingly randomly).