Mario Galaxy-style velvet shader?


#1

You guys know what I’m talking about, right? Well, in Maya this effect is created using a sampler info node called Facing Ratio. You can create this node in the hypergraph, connect it to the outV of a ramp, and control different aspects of a shader such as color, reflectivity, etc.

link to see it in action: http://stevenchan.us/publications/mayafacingratio

Obviously I’m wondering if there’s anything like this in Panda 3d? This could really liven up a scene with that soft, rim-lighting effect that makes Mario Galaxy glow!


#2

maybe you can make useage of this one http://www.panda3d.net/phpbb2/viewtopic.php?t=1864

might need some slight modifications like the TexGenAttrib… maybe want to change it to “my camera” or something. you also might want to change the texture-blend mode to additive ones.


#3

Something like this ?

Then it’s simple, just like smooth shading, but the dot product is inverted.


#4

bump … shaders are cool. and this one really rox even thought such simple.


#5

hmm… yeah thats it! If its easy to do, would you please show us how? :slight_smile:


#6

I’m still not sure if I did this correctly, see for yourself.

from pandac.PandaModules import *
loadPrcFileData('','''
framebuffer-multisample 1
''')
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from direct.gui.OnscreenText import OnscreenText
from direct.interval.IntervalGlobal import *
from direct.actor import Actor
from direct.task import Task
from random import random
import sys


class World(DirectObject):
  def __init__(self):
      self.accept('escape',sys.exit)
      self.accept('space',self.randomizeEnvirLightColor)
      self.acceptKeys( (
                       'arrow_up',
                       'arrow_up-repeat',
                       ), self.adjustFacingRatioPower,[.05])
      self.acceptKeys( (
                       'arrow_down',
                       'arrow_down-repeat',
                       ), self.adjustFacingRatioPower,[-.05])

      balloon = loader.loadModel("frowney")
      balloon.reparentTo(render)
      balloon.setScale(3)
      balloon.setX(-4)

      model2= loader.loadModel("../samples/Sample-Programs--Teapot-on-TV/models/mechman_idle")
      model2.reparentTo(render)
      model2.setX(5)

      tex1=loader.loadTexture('black.png')

      model4=loader.loadModel("virus")
      model4.reparentTo(render)
      model4.setZ(5)
      model4.setTexture(tex1)

      self.model3= Actor.Actor("ripple",{'move':'ripple'})
      self.model3.reparentTo(render)
      self.model3.setScale(5)
      self.model3.setY(-5)
      self.model3.setP(-90)
      self.model3.loop('move')
      self.model3.setTexture(tex1,1)

      panda=loader.loadModel("panda")
      panda.reparentTo(render)
      panda.setPos(0,7,-5)

      loc='../samples/Sample-Programs--Roaming-Ralph/models/'
      self.ralph=Actor.Actor(loc+'ralph',{'walk':loc+'ralph-walk'})
      self.ralph.reparentTo(render)
      self.ralph.setX(10)
      self.ralph.loop('walk')

      tron=loader.loadModel('../samples/Sample-Programs--Glow-Filter/models/tron')
      tron.reparentTo(render)
      tron.setScale(.5)
      tron.setX(-10)

      robot=loader.loadModel('../samples/Sample-Programs--Boxing-Robots/models/robot')
      robot.reparentTo(render)
      robot.setScale(.5)
      robot.setTexture(tex1)

      lightDum=render.attachNewNode('lightDummy')
      light=loader.loadModel('misc/sphere')
      light.reparentTo(lightDum)
      light.setPos(15,0,5)
      lightDum.setShaderOff(1)
      lightDum.hprInterval(5,Vec3(360,0,0)).loop()
      
      # default values
      self.facingRatioPower=.7
      self.envirLightColor=Vec4(1,1,1,0)
      
      textcolor=(0,1,0,1)
      self.facingRatioPowerText=OnscreenText('facing ratio power = %.2f' %self.facingRatioPower,
         fg=textcolor,pos=(0,-.83),scale=.045,mayChange=1)
      OnscreenText('[ ARROW UP/DOWN ] : adjust value',fg=textcolor,pos=(0,-.90),scale=.045)
      OnscreenText('[ SPACE ] : randomize environment light color',fg=textcolor,pos=(0,-.97),scale=.045)

      sha=Shader.load('facingRatio1.sha')
      render.setShader(sha)
      render.setShaderInput('cam',camera)
      render.setShaderInput('light',light)
      render.setShaderInput('envirLightColor',self.envirLightColor*self.facingRatioPower)

      render.setAntialias(AntialiasAttrib.MMultisample)
      taskMgr.doMethodLater(5,self.printCamTransform,'printCamTransform')

  def acceptKeys(self,keys,method,extraArgs=[]):
      for k in keys:
          self.accept(k,method,extraArgs)
          
  def adjustFacingRatioPower(self,amount):
      self.facingRatioPower+=amount
      render.setShaderInput('envirLightColor',self.envirLightColor*self.facingRatioPower)
      self.facingRatioPowerText['text']='facing ratio power = %.2f' %self.facingRatioPower

  def randomizeEnvirLightColor(self):
      self.envirLightColor=Vec4(random(),random(),random(),0)
      render.setShaderInput('envirLightColor',self.envirLightColor*self.facingRatioPower)

  def printCamTransform(self,t):  # print it out while trying to get the best view angle
      camera.printTransform()
      print
      return Task.again

camera.setPos(-21.17, -27.05, 7.82)
camera.setHpr(-33.07, -13.63, -3.24)
mat=Mat4(camera.getMat())
mat.invertInPlace()
base.mouseInterfaceNode.setMat(mat)
base.setBackgroundColor(0,0,0,1)

World()
run()

facingRatio1.sha :

//Cg

void vshader(
             float4 vtx_position : POSITION,
             float2 vtx_texcoord0 : TEXCOORD0,
             float3 vtx_normal : NORMAL,
             uniform float4x4 mat_modelproj,
             uniform float4 mspos_cam,
             uniform float4 mspos_light,

   	    out float  l_smooth,
   	    out float  l_facingRatio,
             out float2 l_texcoord0 : TEXCOORD0,
             out float4 l_position : POSITION
             )
{
  // SMOOTH SHADING : dot product ranges from -1~1, scale it so I get 0~1,
  // but before it, I set the ambient light to .2,
  // so it's .5/2.5 (2.5 is -1.5~1 range), add the .5 to the dark side
  l_smooth = smoothstep( -1.5,1,dot(vtx_normal, normalize(mspos_light-vtx_position)) );
  l_facingRatio = pow( 1.0-saturate( dot(vtx_normal, normalize(mspos_cam-vtx_position)) ), 2 );
  l_position = mul(mat_modelproj, vtx_position);
  l_texcoord0=vtx_texcoord0;
}

void fshader(
             in float2 l_texcoord0: TEXCOORD0,
   	    in float  l_smooth,
             in float  l_facingRatio,
             uniform float4 k_envirLightColor,
             sampler2D tex_0 : TEXUNIT0,

             out float4 o_color:COLOR)
{
  float4 tex = tex2D(tex_0, l_texcoord0);
//   o_color = float4( tex.rgb*l_smooth, tex.a );
  o_color = float4( lerp(tex.rgb*l_smooth, k_envirLightColor.rgb, l_facingRatio) , tex.a );
}

virus.egg.pz
I made frowney transparent by editing frowney.rgb, black.png is 1x1 pixel texture, I use 20,20,20 value.


#7

that seems pretty close, though I haven’t tried it with multiple light sources (directional lights for example). If you look at this screen, you can see that Mario is lit with a cool blue light to his left but a warmer yellow light to his right. Is this possible with your version of the shader?

ynjh_jo: I’ve noticed you on these boards before, you are always quick with solutions! Amazing!

I think this shader would work even better if the color was based on the texture. To do this, one might use 2 textures. If its possible, one texture could replace the light (ie a pre-lit “extra bright” texture) so that the rim-lighting ties into the main texture (a “normal brightness” texture).

If you can improve the results (either with multiple lights or textures) I will happily include my Klonoa model into a sample folder tutorial for other users. :astonished:


#8

That shader works to mimic fully lit environment by an ambient light source. I haven’t tought of multiple environment lights. I haven’t had any idea in my mind, how about you ? Do you have any usable reference ?
Don’t get me wrong, I’m not an expert at all, and my attempts were simply to leverage my fLLLLLLLLat experience. If people could take advantage of it, oh good for them, otherwise, nobody cares.

About Mario pic, I looked at it closely. It seems the right light is not warm yellow, it’s white, as you can see in the right side of his blue clothing. It’s white, so what you saw on his face is skin color burn, simply because it’s so bright. Ok, forget that, no importance.

Are you sure want to use 2 textures for all of your models ? Just for lighting ? Sure you can lerp between two texture lookup values. But, that way you wouldn’t be able to change the light’s color, or have you found any trick/scenario about it ?
Instead of using extra texture, I replaced o_color calculation with this :

o_color = float4( lerp(tex.rgb*l_smooth, (tex.rgb+k_envirLightColor.rgb)*.5, l_facingRatio) , tex.a );

I used the average of base/main texture and the light color : i*.5[/i] instead of light color only. It’s like laying a transparent light color layer over the base texture, so there is color blending (50:50).
The result for orange environment light :


Output color for white/gray/black (all 3 channels are equal) textures remains orange, while for blue one (frowney), it’s purple.

Oh, you can change the light’s eccentricity too (borrowing Maya’s term if you’re familiar with it) :

  l_facingRatio = pow( 1.0-saturate( dot(vtx_normal, normalize(mspos_cam-vtx_position)) ), 4 );

recently I’ve used 4.

[EDIT] :
I just noticed that you added a screenshot there. Sure it looks that way, it’s because not enough smooth triangles to build a smooth curvature surface for the light. If you use low poly model, you must want to use normal map instead of interpolating vertex normals. Look at my new 2 gray models, they are quite high poly, so the result is smooth.


#9

Nice! I think this blends better with the underlying texture now. Very cool. One issue I can think of, is that polygons mapped with alpha transparency will look strange (textured leaves on a large single polygon for example), so there needs to be a way to apply the shader on a per-object basis rather than the entire scene. I don’t know if this is easy to do or not.

…as you might have guessed I am just taking some babysteps in programming so I’m not sure how to do something like adding for example 2 directional lights into the CG shader. While the 1 environment light source is cool for creating a general glow, multiple lights (directionals would probably work ok) could create a nice bounce light coming from the ground plane, for instance.

I was just throwing out the idea of multiple textures because I am not experienced in programming.

However, I like what you have done and I think its fully usable in this form, especially for some sort of visor effect in a FPS. If I ever manage to create something worthwhile and use this, I’ll be sure to credit you. Thank you for taking some time to show us this cool effect!

Hopefully you can find it useful in your own projects.

By the way, anyone who wants to take a look at my Klonoa model can download him here: http://rapidshare.com/files/80191760/klonoa.rar.html. I should point out he doesn’t have any animations, just the static pose. (mods, feel free to distribute him with a sample tutorial or something in future releases of Panda).


#10

CONGRATULATIONS !!!
You’ve got a HEAVY present at this year end. It’s so HEAVY until I can’t feel my SLOW graphics card.
As you wish, multi environment lights. I thought using cubemap would solve this nicely, it’s indeed nice, but so slooooow.
Perhaps I did something stupid. Finally I got world cubemap texcoord generation working, not sure if it’s the fastest way. For shaders experts : any help about this would be GREAT.

I’ve used a fullcolor cubemap with 6 colors (1 for each side) :
X+ : purple
X- : yellow
Y+ : yellow-orange
Y- : blue
Z+ : white
Z- : green
envLightsCubemap1.zip

Results :

      self.facingRatioPower=4.5
      cubeMap = loader.loadCubeMap('render_#.jpg')
      render.setShaderInput('envirCubeMap',cubeMap)
//Cg

void vshader(
             float4 vtx_position : POSITION,
             float2 vtx_texcoord0 : TEXCOORD0,
             float3 vtx_normal : NORMAL,
             uniform float4x4 mat_modelproj,
             uniform float4x4 trans_model_to_world,
             uniform float4 mspos_cam,
             uniform float4 mspos_light,

             out float  l_smooth,
             out float  l_facingRatio,
             out float2 l_texcoord0 : TEXCOORD0,
             out float3 l_texcoord1 : TEXCOORD1,
             out float4 l_position : POSITION
             )
{
  // SMOOTH SHADING : dot product ranges from -1~1, scale it so I get 0~1,
  // but before it, I set the ambient light to .2,
  // so it's .5/2.5 (2.5 is -1.5~1 range), add the .5 to the dark side
  l_smooth = smoothstep( -1.5,1,dot(vtx_normal, normalize(mspos_light-vtx_position)) );
  l_facingRatio = pow( 1.0-saturate( dot(vtx_normal, normalize(mspos_cam-vtx_position)) ), 4 );
  l_position = mul(mat_modelproj, vtx_position);
  l_texcoord0=vtx_texcoord0;

  // cubemap texcoord generation, in world space
  l_texcoord1=mul(trans_model_to_world, float4(vtx_normal,0)).xyz;
}

void fshader(
             in float2 l_texcoord0: TEXCOORD0,
             in float3 l_texcoord1: TEXCOORD1,
             in float  l_smooth,
             in float  l_facingRatio,
             uniform float4 k_facingRatioPower,
             uniform samplerCUBE k_envirCubeMap,
             sampler2D tex_0 : TEXUNIT0,

             out float4 o_color:COLOR)
{
  float4 tex = tex2D(tex_0, l_texcoord0);
  float3 cubeMap = texCUBE(k_envirCubeMap,l_texcoord1).xyz * k_facingRatioPower.x;
//   o_color = float4( cubeMap, tex.a ); // cubemap only
  o_color = float4( lerp(tex.rgb*l_smooth, (tex.rgb+cubeMap)*.5, l_facingRatio) , tex.a );
}

#11

this looks cool. The low frame rate might have something to do with the polycounts on your models, especially the cartoony cat (?). I tried to run this but I can’t seem to get it to work, I get the error:

:gobj(error): facingRatio1.sha (0) : error C0000: syntax error, unexpected $end at token “”
:gobj(error): facingRatio1.sha (0) : error C0501: type name expected at token “”
:gobj(error): facingRatio1.sha (0) : error C0000: syntax error, unexpected $end at token “”
:gobj(error): facingRatio1.sha (0) : error C0501: type name expected at token “”

after replacing your previous shader with the new code. I don’t want to go into the CG shader and mess around with it to try and fix it, because I wouldn’t know where to begin. :frowning:

Also, could you please be a little clearer where the .py code needs to be inserted? Does it just go under the #default values? Thanks again for this wonderful New Year’s gift!


#12

The error indicates that you missed to copy the closing curly bracket for (presumably) the fragment shader. :slight_smile:
Yes, insert the code there.
Happy new year !!!


#13

argh… ok I fixed that stupid mistake I made. But when I enter this, I no longer get any effect:

from pandac.PandaModules import *
loadPrcFileData('','''
framebuffer-multisample 1
''')
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from direct.gui.OnscreenText import OnscreenText
from direct.interval.IntervalGlobal import *
from direct.actor import Actor
from direct.task import Task
from random import random
import sys


class World(DirectObject):
  def __init__(self):
      self.accept('escape',sys.exit)
      self.accept('space',self.randomizeEnvirLightColor)
      self.acceptKeys( (
                       'arrow_up',
                       'arrow_up-repeat',
                       ), self.adjustFacingRatioPower,[.05])
      self.acceptKeys( (
                       'arrow_down',
                       'arrow_down-repeat',
                       ), self.adjustFacingRatioPower,[-.05])

      balloon = loader.loadModel("frowney")
      balloon.reparentTo(render)
      balloon.setScale(3)
      balloon.setX(-4)

      model2= loader.loadModel("../samples/Sample-Programs--Teapot-on-TV/models/mechman_idle")
      model2.reparentTo(render)
      model2.setX(5)

      panda=loader.loadModel("panda")
      panda.reparentTo(render)
      panda.setPos(0,7,-5)

      loc='../samples/Sample-Programs--Roaming-Ralph/models/'
      self.ralph=Actor.Actor(loc+'ralph',{'walk':loc+'ralph-walk'})
      self.ralph.reparentTo(render)
      self.ralph.setX(10)
      self.ralph.loop('walk')

      tron=loader.loadModel('../samples/Sample-Programs--Glow-Filter/models/tron')
      tron.reparentTo(render)
      tron.setScale(.5)
      tron.setX(-10)
      
      tex1=loader.loadTexture('../models/maps/black.png')

      robot=loader.loadModel('../samples/Sample-Programs--Boxing-Robots/models/robot')
      robot.reparentTo(render)
      robot.setScale(.5)
      robot.setTexture(tex1)

      lightDum=render.attachNewNode('lightDummy')
      light=loader.loadModel('misc/sphere')
      light.reparentTo(lightDum)
      light.setPos(15,0,5)
      lightDum.setShaderOff(1)
      lightDum.hprInterval(5,Vec3(360,0,0)).loop()
     
      # default values
      self.envirLightColor=Vec4(0,0,0,1)
      self.facingRatioPower=4.5
      cubeMap = loader.loadCubeMap('../models/maps/render_#.jpg')
      render.setShaderInput('envirCubeMap',cubeMap)
     
      textcolor=(0,1,0,1)
      self.facingRatioPowerText=OnscreenText('facing ratio power = %.2f' %self.facingRatioPower,
         fg=textcolor,pos=(0,-.83),scale=.045,mayChange=1)
      OnscreenText('[ ARROW UP/DOWN ] : adjust value',fg=textcolor,pos=(0,-.90),scale=.045)
      OnscreenText('[ SPACE ] : randomize environment light color',fg=textcolor,pos=(0,-.97),scale=.045)

      sha=Shader.load('facingRatio1.sha')
      render.setShader(sha)
      render.setShaderInput('cam',camera)
      render.setShaderInput('light',light)
      render.setShaderInput('envirCubeMap',self.envirLightColor*self.facingRatioPower)

      render.setAntialias(AntialiasAttrib.MMultisample)
      taskMgr.doMethodLater(5,self.printCamTransform,'printCamTransform')

  def acceptKeys(self,keys,method,extraArgs=[]):
      for k in keys:
          self.accept(k,method,extraArgs)
         
  def adjustFacingRatioPower(self,amount):
      self.facingRatioPower+=amount
      render.setShaderInput('envirCubeMap',self.envirLightColor*self.facingRatioPower)
      self.facingRatioPowerText['text']='facing ratio power = %.2f' %self.facingRatioPower

  def randomizeEnvirLightColor(self):
      self.envirLightColor=Vec4(0,0,0,1)
      render.setShaderInput('envirCubeMap',self.envirLightColor*self.facingRatioPower)

  def printCamTransform(self,t):  # print it out while trying to get the best view angle
      camera.printTransform()
      print
      return Task.again

camera.setPos(-21.17, -27.05, 7.82)
camera.setHpr(-33.07, -13.63, -3.24)
mat=Mat4(camera.getMat())
mat.invertInPlace()
base.mouseInterfaceNode.setMat(mat)
base.setBackgroundColor(0,0,0,1)

World()
run()

:blush:


#14

You haven’t set facingRatioPower shader input yet :

      render.setShaderInput('facingRatioPower',Vec4(self.facingRatioPower,0,0,0))

[EDIT] :
VJ, be very careful with your code, you set envirCubeMap shader input twice. It’s the latest will be used by the shader.
And you’re no longer need self.envirLightColor.


#15

Hi! I looked over the code after your suggestions and got it to work. However, there is one problem that was giving me a crash. Here’s my code:

from pandac.PandaModules import *
loadPrcFileData('','''
framebuffer-multisample 1
''')
import direct.directbase.DirectStart
from direct.showbase.DirectObject import DirectObject
from direct.gui.OnscreenText import OnscreenText
from direct.interval.IntervalGlobal import *
from direct.actor import Actor
from direct.task import Task
from random import random
import sys


class World(DirectObject):
  def __init__(self):
      self.accept('escape',sys.exit)
      self.acceptKeys( (
                       'arrow_up',
                       'arrow_up-repeat',
                       ), self.adjustFacingRatioPower,[.05])
      self.acceptKeys( (
                       'arrow_down',
                       'arrow_down-repeat',
                       ), self.adjustFacingRatioPower,[-.05])

      tex2=loader.loadTexture('../models/maps/KlonoaUVs.jpg')
      Klonoa = loader.loadModel('../models/Klonoa2')
      Klonoa.setScale(2)
      Klonoa.reparentTo(render)
      Klonoa.setTexture(tex2)

      lightDum=render.attachNewNode('lightDummy')
      light=loader.loadModel('misc/sphere')
      light.reparentTo(lightDum)
      light.setPos(15,0,5)
      lightDum.setShaderOff(1)
      lightDum.hprInterval(5,Vec3(360,0,0)).loop()
     
      # default values
      self.facingRatioPower=4.5
      cubeMap = loader.loadCubeMap('../models/maps/render_#.jpg')
      render.setShaderInput('envirCubeMap',cubeMap)
      render.setShaderInput('facingRatioPower',Vec4(self.facingRatioPower,0,0,0))
     
      textcolor=(0,1,0,1)
      self.facingRatioPowerText=OnscreenText('facing ratio power = %.2f' %self.facingRatioPower,
         fg=textcolor,pos=(0,-.83),scale=.045,mayChange=1)
      OnscreenText('[ ARROW UP/DOWN ] : adjust value',fg=textcolor,pos=(0,-.90),scale=.045)

      sha=Shader.load('facingRatio1.sha')
      render.setShader(sha)
      render.setShaderInput('cam',camera)
      render.setShaderInput('light',light)
      render.setShaderInput('envirCubeMap'*self.facingRatioPower)

      render.setAntialias(AntialiasAttrib.MMultisample)
      taskMgr.doMethodLater(5,self.printCamTransform,'printCamTransform')

  def acceptKeys(self,keys,method,extraArgs=[]):
      for k in keys:
          self.accept(k,method,extraArgs)
         
  def adjustFacingRatioPower(self,amount):
      self.facingRatioPower+=amount
      render.setShaderInput('envirCubeMap'*self.facingRatioPower)
      self.facingRatioPowerText['text']='facing ratio power = %.2f' %self.facingRatioPower

  def printCamTransform(self,t):  # print it out while trying to get the best view angle
      camera.printTransform()
      print
      return Task.again

camera.setPos(-21.17, -27.05, 7.82)
camera.setHpr(-33.07, -13.63, -3.24)
mat=Mat4(camera.getMat())
mat.invertInPlace()
base.mouseInterfaceNode.setMat(mat)
base.setBackgroundColor(0,0,0,1)

World()
run()

the problem is this:

      # default values
      self.facingRatioPower=4

If I set it to a non-integer (4.5 like you have it in your screenshot), I get this error:

File “cubemap.py”, line 55, in init
render.setShaderInput(‘envirCubeMap’*self.facingRatioPower)
TypeError: can’t multiply sequence by non-int

Anyway, this means it crashes any time I try to increase/decrese the amount when running the file.


#16

I’m pretty sure that is a syntax error, and the correct line is:

render.setShaderInput('envirCubeMap',self.facingRatioPower) 

#17

nope, if I do that the shader won’t show up (though thanks for trying to help).

Oh by the way ynjh_jo, I get a pretty steady 45-60 fps in this scene (about 32000 polygons). So I guess its a graphics card issue?

Anyway thanks again for all your help, this shader is gorgeous!


#18

Oh, I see what’s going on. This line looks a bit ridiculous:

      render.setShaderInput('envirCubeMap'*self.facingRatioPower) 

First of all, assigning a float to a cubemap input is nonsense, That is, if that code did actually assign something to the shader. You are multiplying the envirCubeMap with a number. If it’s an int it would do something odd (after multiplying an int by a string your shader won’t read it anymore) but if its a float it just errors out. Logically.
I think you accidentally deleted a part. This should be the correct:

      render.setShaderInput('facingRatioPower',Vec4(self.facingRatioPower,0,0,0))

I’d eat up my hat if that still doesnt work.


#19

You shouldn’t do that. facingratiopower is simply passed to the shader, and the calculation is performed in the shader, so you shouldn’t multiply them.
I guess you want to gain some more performance by calculating the end-power so the multiplication doesn’t have to be done for every rendered pixel.
I guess the only possible thing for that is by brightening the cubemap images with your image editor.
And don’t forget to remove any reference to k_facingratiopower in the shader.

[EDIT] :
after doing that, change your config-tuning to this :

loadPrcFileData('','''
framebuffer-multisample 1
video-sync 0
''')

to avoid framerate capping, and show us how fast it could be.
oh my latest scene on the pic is 75k tris.


#20

ok I updated the code as per your instructions pro-rsoft, you were right that code was nonsense! ha… well I am still learning by adjusting the code provided to me…

anyway, I took out the framerate cap and tripled the number of characters making for a total of more than 96000 tris and its still running at a constant 30, sometimes going into the mid 40s. So its pretty good, I think! :open_mouth: I have a radeon 9600 graphics card.

the facingratio power is still not properly connected to the left/right keys. The indicated value is updated correctly but the shader itself does not (Panda doesn’t crash though, so thats nice).