#Author: Kwasi Mensah
#Date: 7/11/2005
#
# This is a tutorial to show some of the more advanced things
# you can do with Cg. Specifically, with Non Photo Realistic
# effects like Toon Shading. It also shows how to implement
# multiple buffers in Panda.


import direct.directbase.DirectStart
from direct.task import Task
from direct.actor import Actor
from pandac.PandaModules import *
from direct.gui.DirectGui import *
from direct.showbase.DirectObject import *
from direct.showbase.BufferViewer import *
import sys,os

# Figure out what directory this program is in.
MYDIR=os.path.abspath(sys.path[0])
MYDIR=Filename.fromOsSpecific(MYDIR).getFullpath()

# Function to put instructions on the screen.
def addInstructions(pos, msg):
    return OnscreenText(text=msg, style=1, fg=(1,1,1,1),
			pos=(-1.3, pos), align=TextNode.ALeft, scale = .05)

# Function to put title on the screen.
def addTitle(text):
    return OnscreenText(text=text, style=1, fg=(1,1,1,1),
	                pos=(1.3,-0.95), align=TextNode.ARight, scale = .07)


class ToonMaker(DirectObject):
    def __init__(self):
	base.disableMouse()
	camera.setPos(0, -50, 0)

        # Preliminary capabilities check.
        if (base.win.getGsg().getSupportsBasicShaders()==0):
            self.t=addTitle("Toon Shader: Video driver reports that shaders are not supported.")
            return
        
        # We want to create an offscreen buffer containing an auxiliary
        # color bitplane.  This is not a common request, so we have to
        # use the full-fledged most-complicated version of the
        # buffer-creation API.  This involves creating a FrameBufferProperties
        # structure that describes the features we need, and then
        # calling GraphicsEngine.makeOutput

        props = FrameBufferProperties()
        props.setAuxRgba(1)
        props.setFrameBufferMode(FrameBufferProperties.FMRgba | FrameBufferProperties.FMDepth)

        self.buffer=base.graphicsEngine.makeOutput(
             base.pipe, "offscreen", -1,
             props, 0, 0, GraphicsPipe.BFSizeTrackHost | GraphicsPipe.BFRefuseWindow,
             base.win.getGsg(), base.win)

        # Creation of a high-powered buffer can fail, if the graphics card
        # doesn't support the necessary OpenGL extensions.

        if (self.buffer == None):
            self.t=addTitle("Toon Shader: Video driver does not support multiple render targets")
            return

        # Set up the render-textures.
        
        self.normalsTexture = Texture("normals")
        self.diffuseTexture = Texture("diffuse")
        self.buffer.addRenderTexture(self.normalsTexture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPAuxRgba0)
        self.buffer.addRenderTexture(self.diffuseTexture, GraphicsOutput.RTMBindOrCopy, GraphicsOutput.RTPColor)
        self.buffer.setClearColor(VBase4(0.5,0.5,0.5,1))

        # Create a camera to render the model into the offscreen buffer.

        modelCamera=base.makeCamera(self.buffer, aspectRatio=base.getAspectRatio())
        modelCamera.node().setScene(render)
        tempnode = NodePath(PandaNode("temp node"))
        tempnode.setShader(Shader.load(MYDIR+"/modelGen.sha"))
        modelCamera.node().setInitialState(tempnode.getState())
        
        # By default, panda usually clears the screen before every
        # camera and before every window.  Tell it not to do that.
        # Then, tell it specifically when to clear and what to clear.

        modelCamera.node().getDisplayRegion(0).disableClears()
        base.cam.node().getDisplayRegion(0).disableClears()
        base.cam2d.node().getDisplayRegion(0).disableClears()
        self.buffer.disableClears()
        base.win.disableClears()

        self.buffer.setClearColorActive(1)
        self.buffer.setClearDepthActive(1)
        self.buffer.setClearColor(Vec4(0.5,0.5,0.5,1))

        # Disable the main panda camera.  Our model will only be rendered by modelCamera

        base.cam.node().setActive(0)
        
        # This is the object that represents the single "light", as far
        # the shader is concerned.  It's not a real Panda3D LightNode, but
        # the shader doesn't care about that.

        light = render.attachNewNode("light")
        light.setPos(30,-50,0)

        # this call puts the light's nodepath into the render state.
        # this enables the shader to access this light by name.

        render.setShaderInput("light", light)
        
        # Create a full-screen quad for the postprocessing stage.

        fsQuad=self.buffer.getTextureCard()
        fsQuad.setColor(1,1,1,0)
        fsQuad.reparentTo(render2d)
        self.fsQuad = fsQuad

        # the postprocessing shader accepts, as input, the two textures from 
        # the offscreen buffer.  The normals texture is analyzed, comparing
        # each adjacent pixel, looking for discontinuities.
        # wherever a discontinuity exists, postprocessing emits black ink.
        
        self.separation = 0.001
        self.cutoff = 0.3
        postGen=Shader.load(MYDIR+"/postGen.sha")
        self.fsQuad.setShader(postGen)
        self.fsQuad.setShaderInput("normalsTexture", self.normalsTexture)
        self.fsQuad.setShaderInput("diffuseTexture", self.diffuseTexture)
        self.fsQuad.setShaderInput("separation", Vec4(self.separation,0,self.separation,0));
        self.fsQuad.setShaderInput("cutoff", Vec4(self.cutoff,self.cutoff,self.cutoff,self.cutoff));
        
	# Post the instructions.

	self.title = addTitle("Panda3D: Tutorial - Toon Shading with Normals-Based Inking")
	self.inst1 = addInstructions(0.95,"ESC: Quit")
	self.inst2 = addInstructions(0.90,"Up/Down: Increase/Decrease Line Thickness")
	self.inst3 = addInstructions(0.85,"Left/Right: Decrease/Increase Line Darkness")
	self.inst4 = addInstructions(0.80,"V: View the render-to-texture results")

        # Panda contains a built-in viewer that lets you view the results of
        # your render-to-texture operations.  This code configures the viewer.

        self.accept("v", base.bufferViewer.toggleEnable)
        self.accept("V", base.bufferViewer.toggleEnable)
        base.bufferViewer.setPosition("llcorner")

	# Load a dragon model and animate it.

	self.character=Actor.Actor()
	self.character.loadModel(MYDIR+'/nik-dragon')
	self.character.reparentTo(render)
	self.character.loadAnims({'win': MYDIR+'/nik-dragon'})
	self.character.loop('win')
	self.character.hprInterval(15, Point3(360, 0,0)).loop()

        # these allow you to change cartooning parameters in realtime

        self.accept("escape", sys.exit, [0])
        self.accept("arrow_up", self.increaseSeparation)
        self.accept("arrow_down", self.decreaseSeparation)
        self.accept("arrow_left", self.increaseCutoff)
        self.accept("arrow_right", self.decreaseCutoff)
        
    def increaseSeparation(self):
        self.separation = self.separation * 1.11111111;
        print self.separation
        self.fsQuad.setShaderInput("separation", Vec4(self.separation,0,self.separation,0));
        
    def decreaseSeparation(self):
        self.separation = self.separation * 0.90000000;
        print self.separation
        self.fsQuad.setShaderInput("separation", Vec4(self.separation,0,self.separation,0));
        
    def increaseCutoff(self):
        self.cutoff = self.cutoff * 1.11111111;
        print self.cutoff
        self.fsQuad.setShaderInput("cutoff", Vec4(self.cutoff,self.cutoff,self.cutoff,self.cutoff));
        
    def decreaseCutoff(self):
        self.cutoff = self.cutoff * 0.90000000;
        print self.cutoff
        self.fsQuad.setShaderInput("cutoff", Vec4(self.cutoff,self.cutoff,self.cutoff,self.cutoff));
        

t=ToonMaker()

run()
