I need to create a real time painting game and the other methods for model painting on the forum are a bit too slow for my purpose so I was forced to figure out how to solve this in shaders. Much of this code is built on the offscreen buffer samples and the texture painting samples that are scattered around in the forums. This snippet is public domain.
Main python test file.
from direct.showbase.DirectObject import *
from pandac.PandaModules import Texture, PNMImage, PNMImageHeader, GraphicsOutput, NodePath, Filename,\
TextureStage, CardMaker, Vec3, PandaNode, Point3, WindowProperties,\
FrameBufferProperties, GraphicsPipe
from direct.interval.IntervalGlobal import *
class EditTexture:
def __init__(self, node_path=None, image_path=None):
self.texture = Texture()
self.image = PNMImage()
if node_path:
node_textures = node_path.findAllTextures()
node_textures.removeDuplicateTextures()
self.texture = node_textures.getTexture(node_textures.getNumTextures()-1)
self.texture.store(self.image)
self.relatedNode = node_path
self.relatedNode.setPythonTag("EditTexture", self)
if image_path:
# Make a filepath
self.originalFile = Filename(image_path)
if self.originalFile.empty():
raise IOError, "File not found"
# Instead of loading it outright, check with the PNMImageHeader if we can open
# the file.
imgHead = PNMImageHeader()
if not imgHead.readHeader(self.imgFile):
raise IOError, "PNMImageHeader could not read file. Try using absolute filepaths"
self.image.read(self.originalFile)
self.reload()
def reload(self):
self.texture.load(self.image)
class ShaderPaintTest(DirectObject):
DRAW_CYCLE_TIME = 0.5
def __init__(self):
base.disableMouse()
self.smiley = loader.loadModel('models/smiley')
self.smileTex = EditTexture(self.smiley)
self.smiley.reparentTo(render)
self.smiley.setZ(10)
base.camera.setPos(0, -10, 15)
base.camera.lookAt(0,0,8)
smileTurn = LerpHprInterval(self.smiley, 5, Vec3(360,0,0))
smileTurn.loop()
taskMgr.add(self.paintTask, "Paint Task")
self.isPainting = False
self.accept('mouse1', setattr, [self, 'isPainting', True])
self.accept('mouse1-up', setattr, [self, 'isPainting', False])
self.pickLayer = PNMImage()
self.windowX = base.win.getXSize()
self.windowY = base.win.getYSize()
self.accept('v', base.bufferViewer.toggleEnable)
self.accept('V',base.bufferViewer.toggleEnable)
self.lastPaintTime = 0
self.makeBuffers()
def makeBuffers(self):
paintBuffer = self.createOffscreenBuffer(-3, 1024, 1024)
self.paintMap = Texture()
paintBuffer.addRenderTexture(self.paintMap, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor)
self.paintCam = base.makeCamera(paintBuffer, lens=base.cam.node().getLens())
loadPaintNode = NodePath(PandaNode('paintnode'))
loadPaintNode.setShader(loader.loadShader('painter.sha'))
# Divide the texture size by 1000 cause shader inputs must be in float
self.smiley.setShaderInput('texsize', 512, 256, 0, 0)
self.paintCam.node().setInitialState(loadPaintNode.getState())
# Create a small buffer for the shader program that will fetch the point from the texture made
# by the paintBuffer
#self.getPointBuffer = self.createOffscreenBuffer(-2, 2, 2)
self.pointTex = Texture()
#self.getPointBuffer.addRenderTexture(self.pointTex, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor)
getPointBuffer = base.win.makeTextureBuffer("pick buffer", 2, 2, self.pointTex, True)
self.pointScene = NodePath('point scene')
self.pointCam = base.makeCamera(getPointBuffer, lens=base.cam.node().getLens())
self.pointCam.reparentTo(self.pointScene)
self.pointCam.setY(-2)
cm = CardMaker('pointer scene card')
cm.setFrameFullscreenQuad()
pointCard = self.pointScene.attachNewNode(cm.generate())
loadPoint = NodePath(PandaNode('pointnode'))
loadPoint.setShader(loader.loadShader('pointer.sha'))
# Feed the paintmap from the paintBuffer to the shader and initial mouse positions
self.pointScene.setShaderInput('paintmap', self.paintMap)
self.pointScene.setShaderInput('mousepos', 0, 0, 0, 1)
self.pointCam.node().setInitialState(loadPoint.getState())
self.pickLayer = PNMImage()
def createOffscreenBuffer(self, sort, xsize, ysize):
winprops = WindowProperties.size(xsize,ysize)
props = FrameBufferProperties()
props.setRgbColor(1)
props.setAlphaBits(1)
props.setDepthBits(1)
return base.graphicsEngine.makeOutput(
base.pipe, "offscreenBuffer",
sort, props, winprops,
GraphicsPipe.BFRefuseWindow,
base.win.getGsg(), base.win)
def paintTask(self, task):
if not base.mouseWatcherNode.hasMouse():
return task.cont
if self.isPainting is False:
return task.cont
mpos = base.mouseWatcherNode.getMouse()
x_ratio = min( max( ((mpos.getX()+1)/2), 0), 1)
y_ratio = min( max( ((mpos.getY()+1)/2), 0), 1)
mx = int(x_ratio*self.windowX)
my = self.windowY - int(y_ratio*self.windowY)
print mx, my
print "Mouse", x_ratio, y_ratio
self.pointScene.setShaderInput('mousepos', x_ratio, y_ratio, 0, 1)
self.pointTex.store(self.pickLayer)
# get the color below the mousepick from the rendered frame
r = self.pickLayer.getRedVal(0,0)
g = self.pickLayer.getGreenVal(0,0)
b = self.pickLayer.getBlueVal(0,0)
# calculate uv-texture position from the color
x = r + ((b%16)*256)
y = g + ((b//16)*256)
for i in range(6):
ny = i + y
if ny < self.smileTex.texture.getYSize():
for j in range(6):
nx = j + x
if nx < self.smileTex.texture.getXSize():
self.smileTex.image.setXel(nx,ny,255,0,0)
self.smileTex.reload()
print "UV Coordinates:", x, y
return task.cont
if __name__ == "__main__":
import direct.directbase.DirectStart
s = ShaderPaintTest()
run()
sys.exit()
painter.sha - this generates the UV picking texture. The maths can probably be optimized but I’m confused on this.
//Cg
void vshader(
uniform float4x4 mat_modelproj,
in float4 vtx_position : POSITION,
in float2 vtx_texcoord0 : TEXCOORD0,
out float2 l_my : TEXCOORD0,
out float4 l_position : POSITION)
{
l_position = mul(mat_modelproj, vtx_position);
l_my = vtx_texcoord0;
}
void fshader(
uniform float4 k_texsize,
in float2 l_my : TEXCOORD0,
out float4 o_color : COLOR)
{
int x = int((k_texsize.r) * l_my.x);
int y = int(k_texsize.g)-int((k_texsize.g) * l_my.y);
float r = float((x%256)/float(255));
float g = float((y%256)/float(255));
float b = float( (int(x/256) + int(y/256) * 16) / float(255) );
o_color = float4(r, g, b, 1);
}
pointer.sha - Extracts the colour from the output of painter.sha so the X/Y values in the texture can be figured out
//Cg
void vshader(
uniform float4x4 mat_modelproj,
in float4 vtx_position : POSITION,
in float2 vtx_texcoord0 : TEXCOORD0,
out float2 l_my : TEXCOORD0,
out float4 l_position : POSITION)
{
l_position = mul(mat_modelproj, vtx_position);
l_my = vtx_texcoord0;
}
void fshader(
uniform sampler2D k_paintmap,
uniform float4 k_mousepos,
out float4 o_color : COLOR)
{
o_color = tex2D(k_paintmap, float2(k_mousepos.r, k_mousepos.g));
}