Selective cartoon shading

Hi! I was wondering how to apply the toon shader only to certain nodepaths of the scene-graph. Thanks

cla

Hi, this page may interest you, specifically this step:

Thanks dude! I already went ahead with an approach based on Tut-Cartoon-Advanced.py. I can render with cartoon shading selectively but I have a problem with cartoon inking.

Here is my code (just the part that matters):

        # Create a nodepath that will hold all the graphics with cartoon
        # shading.
        
        toon_shading = render.attachNewNode('Toon Shading')
        
        # Set up a point light for the light gen shader.
        
        light = toon_shading.attachNewNode("light")
        light.setPos(30, -50, 0)
        toon_shading.setShaderInput("light", light)
                
        # Create toon shading render state and set tags. 
        
        _tmp = NodePath(PandaNode("_tmp"))
        _tmp.setShader(loader.loadShader("lightingGen.sha"))
        base.cam.node().setTagStateKey("Toon Shading")
        base.cam.node().setTagState("True", _tmp.getState())
        toon_shading.setTag("Toon Shading", "True")
        
        # The "normals buffer" will contain a picture of the model colorized
        # so that the color of the model is a representation of the model's
        # normal at that point.
        
        normalsBuffer = base.win.makeTextureBuffer("normalsBuffer", 0, 0)
        normalsBuffer.setClearColor(Vec4(0.5, 0.5, 0.5, 1))
        self.normalsBuffer = normalsBuffer
        normalsCamera = base.makeCamera(normalsBuffer, lens=base.cam.node().getLens())
        normalsCamera.node().setScene(toon_shading)
        _tmp = NodePath(PandaNode("_tmp"))
        _tmp.setShader(loader.loadShader("normalGen.sha"))
        normalsCamera.node().setInitialState(_tmp.getState())
        
        #what we actually do to put edges on screen is apply them as a texture to 
        #a transparent screen-fitted card

        drawnScene = normalsBuffer.getTextureCard()
        drawnScene.setTransparency(1)
        drawnScene.setColor(1, 1, 1, 0)
        drawnScene.reparentTo(render2d)
        self.drawnScene = drawnScene

        # this shader accepts, as input, the picture from the normals buffer.
        # it compares each adjacent pixel, looking for discontinuities.
        # wherever a discontinuity exists, it emits black ink.
                
        self.separation = 0.001
        self.cutoff = 0.3
        inkGen = loader.loadShader("inkGen.sha")
        drawnScene.setShader(inkGen)
        drawnScene.setShaderInput("separation", Vec4(self.separation, 0, self.separation, 0));
        drawnScene.setShaderInput("cutoff", Vec4(self.cutoff, self.cutoff, self.cutoff, self.cutoff));
        
        # 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.nik1 = Actor()
        self.nik1.loadModel('models/nik-dragon')
        self.nik1.reparentTo(toon_shading)
        self.nik1.loadAnims({'win': 'models/nik-dragon'})
        self.nik1.loop('win')
        self.nik1.hprInterval(15, Point3(360, 0, 0)).loop()
        
        self.nik2 = Actor()
        self.nik2.loadModel('models/nik-dragon')
        self.nik2.reparentTo(render)
        self.nik2.setPos(10, 0, 1)
        self.nik2.setScale(0.3)
        self.nik2.loadAnims({'win': 'models/nik-dragon'})
        self.nik2.loop('win')
        self.nik2.hprInterval(15, Point3(360, 0, 0)).loop()        

        self.nik3 = Actor()
        self.nik3.loadModel('models/nik-dragon')
        self.nik3.reparentTo(render)
        self.nik3.setPos(0, -20, 1)
        self.nik3.setScale(0.3)
        self.nik3.loadAnims({'win': 'models/nik-dragon'})
        self.nik3.loop('win')
        self.nik3.hprInterval(15, Point3(360, 0, 0)).loop()        

Only nik1 has cartoon shading. nik3 is suppose to be in front of it. The problem is that inking is rendered on render2d and get drawn on top of nik3. Any idea on how to fix it?

Does anyone know how to solve this problem? Is it even possible to solve it in Panda3d?

I don’t have direct experience with the cartoon inker, but it seems like there must be several ways to solve this problem.

One approach would be to apply the inking to a different scene than render2d, and then allow your other 3-D objects to render on top of it, by setting up your DisplayRegions as appropriate.

Another approach would be to ensure that your objects that obscure the cartoon objects in the main scene also obscure them in the normalsBuffer scene, for instance by drawing them there as well (but with the color mask set so that they only fill the depth buffer, but don’t actually draw to the color buffer). This way the inking lines wouldn’t even be computed for the obscured parts of the cartoon objects.

A third approach would be to render (and ink) your cartoon objects entirely in an offscreen buffer, then reapply them to a card that is drawn in the main scene.

David

Thanks! I like the second approach, I’m going to try with it. By saying “…color mask set so that they only fill the depth buffer, but don’t actually draw to the color buffer” you mean to set a nodepath render attrib at ColorWriteAttrib.make(ColorWriteAttrib.COff)?

cla

Right, that’s what I mean.

David

Ok I made an experiment with the second approach but I wasn’t really successful. In short, I made an instance of nik2 and nik3 under the cartoon shading nodepath and I set the instances with a color mask all off. Everything under the cartoon shading nodepath is rendered into the normalsBuffer.

Here the result I got:

nik2 is rendered as expected and invisible in the normalsBuffer. nik3, that should be invisible as well, is rendered as a grey flickering shadow in the normalsBuffer and outlined on screen.

nik3 get inked because it’s a flickering grey shadow on the normalsBuffer. Any idea about why this is happening and how to fix it?

cla

After few more tests I decided to drop the approach nr 2. I realized that drawing objects on the normalsBuffer with color mask off produces ‘holes’ which trigger the inking effect anyway.

However, I still find hard to understand how to go about the first approach. Assuming I can do inking on a different scene (different display region?), how I can render 3d objects on top of it preserving depth information?

cla

As long as you don’t set your DisplayRegion to clear the depth buffer (and I believe the default behavior for a new DisplayRegion is not to clear the depth buffer), then the depth information is preserved from the previous DisplayRegion that was drawn in the same place. So if your camera and lens properties exactly match, the depth information will also exactly match, and you will get correct depth sorting.

Of course, there may be an issue with transparency, which doesn’t work 100% with the depth buffer. So you may have to be clever with sorting if your cartoon character involves semitransparent pieces that have to be blended smoothly with the background, while your character is also visible behind another semitransparent piece in the foreground. But this is a pretty contrived situation. :slight_smile:

David

Fantastic! So let’s say I have a display region where I render objects in cartoon shading and inking. How can I tell Panda that I want to draw on that display region all the nodes attached to render only after have drawn the cartoon shaded ones?

Claudio

Each DisplayRegion will have a different camera associated with it. You can send different items to the different DisplayRegions by associating them with the corresponding cameras.

This can be done either by setting each camera to a different scene (for instance, by reparenting each one into its own separate “render” node), or if you wish to use the same scene for all of them but only control nodes individually, you can set up a camera mask for each camera (choose one bit for each one), then use per-camera hide and show operations on the relevant nodes:

camera1.node().setCameraMask(BitMask32.bit(0))
camera2.node().setCameraMask(BitMask32.bit(1))
myNodePath.hide(BitMask32.bit(0))
myNodePath.show(BitMask32.bit(1))
# Now myNodePath will only be shown on camera2...

David

Ok this is clear. In fact in my demo I have:

  • the default display region with default camera.
  • a new display region for toon objects with a new camera attached.

Now I have two different scenes: render and another that I called toon_world (both nodepaths).

What I still find hard to get is where should I draw the ink black outlines. Those are drawn by the ink shader taking as input a texture card that comes from the normalsBuffer (a texture buffer). Where should I reparent the texture card?

Claudio

You can create a third DisplayRegion that is drawn between the two 3-D DisplayRegions, and make it a 2-D DisplayRegion by creating a OrthographicLens for its camera, similar to the way render2d is set up.

Or, you can simply repurpose the existing render2d, by changing the sort value of its DisplayRegion to fall between your two 3-D DisplayRegions. (This supposes you won’t need to use render2d for any gui displays.)

Or, instead of creating a new DisplayRegion for this purpose, you can parent your card directly to the 3-D camera for the second DisplayRegion, positioned carefully so that it exactly fills the frame, with a bin set on it so it draws first in the scene.

David

Ok I finally got something! Thanks for supporting David. I’ll soon post the code.

Claudio

I found some time to wrap up my solution and post it on the Code Snippet forum. For anyone interested, here is the link

[url]Cartoon Painter]

Claudio