CrossHatch Post Processing Filter – Let’s go manga!

Hey folks!



I’m not sure wether this post belongs here or in the contributers forum, but as I do not identify myself as a contributer yet (not one in italic anyway) I decided to go with the user code forum.



I have been interested in the general idea of shaders for a while now but never dared to try writing them myself. Through some other post on the forum here I stumbled upon this and truly, I love the idea of games that look like they were drawn while actually being 3D. As there was an example attached I decided to give this thing a shot and now, 5 hours later, I finished my first shader, and I still can’t believe how easy that went :slight_smile:

Anyhow, to show the results I got (yes, I implemented it in jME3 right away, how cool is that?), here are some pics:





Anime image on a box with black lines





Same anime image with colors leaking through





My avatar with different color settings for both lines and background





An image of a test game I was making with an early version of the filter applied



I think that when applied correctly with a nice cartoonedge filter and some anime style models we could very well create an anime style drawn effect, as shown in the first 2 pictures.As I doubt I will use this shader much myself, I sincerely hope this shader will be included in the jME packages as part of the standard library so it is of use to someone :wink:



Please test this shader on any system you have and report whether it works, I had 0,0 experience writing these things until this afternoon and it all seems so logical, I can’t believe it’s that simple… I’m sure I must have made ahuge mistake, just need to find it yet :stuck_out_tongue:



The files I created:



CrossHatchFilter.java

[java]package shaders;



import com.jme3.asset.AssetManager;

import com.jme3.material.Material;

import com.jme3.math.ColorRGBA;

import com.jme3.post.Filter;

import com.jme3.renderer.RenderManager;

import com.jme3.renderer.Renderer;

import com.jme3.renderer.ViewPort;



/*

  • A Post Processing filter that makes the screen look like it was drawn as
  • diagonal lines with a pen.
  • Try combining this with a cartoon edge filter to obtain manga style visuals.

    *
  • Based on an article from Geeks3D:
  • (Shader Library) Crosshatching GLSL Filter | Geeks3D

    *
  • @author: Roy Straver a.k.a. Baal Garnaal

    /

    public class CrossHatchFilter extends Filter {



    private ColorRGBA lineColor = ColorRGBA.Black.clone();

    private ColorRGBA paperColor = ColorRGBA.White.clone();

    private float colorInfluenceLine = 0.0f;

    private float colorInfluencePaper = 0.0f;

    private float fillValue = 0.9f;



    private float luminance1 = 0.9f;

    private float luminance2 = 0.7f;

    private float luminance3 = 0.5f;

    private float luminance4 = 0.3f;

    private float luminance5 = 0.0f;



    private int lineThickness = 1;

    private int lineDistance = 4;



    public CrossHatchFilter() {

    super("CrossHatchFilter");

    }



    public CrossHatchFilter(ColorRGBA lineColor, ColorRGBA paperColor) {

    this();

    this.lineColor = lineColor;

    this.paperColor = paperColor;

    }



    @Override

    public boolean isRequiresDepthTexture() {

    return false;

    }



    @Override

    public void initFilter(AssetManager manager, RenderManager renderManager, ViewPort vp, int w, int h) {

    material = new Material(manager, "/MatDefs/Post/CrossHatch.j3md");

    material.setColor("LineColor", lineColor);

    material.setColor("PaperColor", paperColor);



    material.setFloat("ColorInfluenceLine", colorInfluenceLine);

    material.setFloat("ColorInfluencePaper", colorInfluencePaper);



    material.setFloat("FillValue", fillValue);



    material.setFloat("Luminance1", luminance1);

    material.setFloat("Luminance2", luminance2);

    material.setFloat("Luminance3", luminance3);

    material.setFloat("Luminance4", luminance4);

    material.setFloat("Luminance5", luminance5);



    material.setInt("LineThickness", lineThickness);

    material.setInt("LineDistance", lineDistance);

    }



    @Override

    public Material getMaterial() {

    return material;

    }



    @Override

    public void preRender(RenderManager renderManager, ViewPort viewPort) {

    }



    @Override

    public void cleanUpFilter(Renderer r) {

    }



    /

  • Sets color used to draw lines

    /

    public void setLineColor(ColorRGBA lineColor){

    this.lineColor = lineColor;

    if (material != null)

    material.setColor("LineColor", lineColor);

    }



    /

  • Sets color used as background

    /

    public void setPaperColor(ColorRGBA paperColor){

    this.paperColor = paperColor;

    if (material != null)

    material.setColor("PaperColor", paperColor);

    }

    /

  • Sets color influence of original image on lines drawn

    /

    public void setColorInfluenceLine(float colorInfluenceLine){

    this.colorInfluenceLine = colorInfluenceLine;

    if (material != null)

    material.setFloat("ColorInfluenceLine", colorInfluenceLine);

    }



    /

  • Sets color influence of original image on non-line areas

    /

    public void setColorInfluencePaper(float colorInfluencePaper){

    this.colorInfluencePaper = colorInfluencePaper;

    if (material != null)

    material.setFloat("ColorInfluencePaper", colorInfluencePaper);

    }



    /

  • Sets line/paper color ratio for areas with values < luminance5,
  • really dark areas get no lines but a filled blob instead

    /

    public void setFillValue(float fillValue){

    this.fillValue = fillValue;

    if (material != null)

    material.setFloat("FillValue", fillValue);

    }



    /

  • Sets minimum luminance levels for lines drawn
  • Luminance1: Top-left to down right 1
  • Luminance2: Top-right to bottom left 1
  • Luminance3: Top-left to down right 2
  • Luminance4: Top-right to bottom left 2
  • Luminance5: Blobs

    /

    public void setLuminanceLevels(float luminance1, float luminance2, float luminance3, float luminance4, float luminance5){

    this.luminance1 = luminance1;

    this.luminance2 = luminance2;

    this.luminance3 = luminance3;

    this.luminance4 = luminance4;

    this.luminance5 = luminance5;



    if (material != null){

    material.setFloat("Luminance1", luminance1);

    material.setFloat("Luminance2", luminance2);

    material.setFloat("Luminance3", luminance3);

    material.setFloat("Luminance4", luminance4);

    material.setFloat("Luminance5", luminance5);

    }

    }



    /

  • Sets the thickness of lines drawn

    /

    public void setLineThickness(int lineThickness){

    this.lineThickness = lineThickness;

    if (material != null)

    material.setInt("LineThickness", lineThickness);

    }



    /

  • Sets minimum distance between lines drawn
  • Primary lines are drawn at 2*lineDistance
  • Secondary lines are drawn at lineDistance

    /

    public void setLineDistance(int lineDistance){

    this.lineDistance = lineDistance;

    if (material != null)

    material.setInt("LineDistance", lineDistance);

    }



    /

  • Returns line color

    /

    public ColorRGBA getLineColor() {

    return lineColor;

    }



    /

  • Returns paper background color

    /

    public ColorRGBA getPaperColor() {

    return paperColor;

    }



    /

  • Returns current influence of image colors on lines

    /

    public float getColorInfluenceLine(){

    return colorInfluenceLine;

    }



    /

  • Returns current influence of image colors on paper background

    /

    public float getColorInfluencePaper(){

    return colorInfluencePaper;

    }



    /

  • Returns line/paper color ratio for blobs

    /

    public float getFillValue() {

    return fillValue;

    }



    /

  • Returns the thickness of the lines drawn

    /

    public int getLineThickness() {

    return lineThickness;

    }



    /

  • Returns minimum distance between lines

    /

    public int getLineDistance() {

    return lineDistance;

    }



    /

  • Returns treshold for lines 1

    /

    public float getLuminance1() {

    return luminance1;

    }



    /

  • Returns treshold for lines 2

    /

    public float getLuminance2() {

    return luminance2;

    }



    /

  • Returns treshold for lines 3

    /

    public float getLuminance3() {

    return luminance3;

    }



    /

  • Returns treshold for lines 4

    /

    public float getLuminance4() {

    return luminance4;

    }



    /

  • Returns treshold for blobs

    /

    public float getLuminance5() {

    return luminance5;

    }

    }[/java]



    CrossHatch.jm3d

    [java]MaterialDef CrossHatch {



    MaterialParameters {

    Int NumSamples

    Texture2D Texture;

    Vector4 LineColor;

    Vector4 PaperColor;

    Float ColorInfluenceLine;

    Float ColorInfluencePaper;

    Float FillValue;

    Float Luminance1;

    Float Luminance2;

    Float Luminance3;

    Float Luminance4;

    Float Luminance5;

    Int LineThickness;

    Int LineDistance;

    }



    Technique {

    VertexShader GLSL150: Common/MatDefs/Post/Post15.vert

    FragmentShader GLSL150: MatDefs/Post/CrossHatch15.frag



    WorldParameters {

    WorldViewProjectionMatrix

    }

    }



    Technique {

    VertexShader GLSL100: Common/MatDefs/Post/Post.vert

    FragmentShader GLSL100: MatDefs/Post/CrossHatch.frag



    WorldParameters {

    WorldViewProjectionMatrix

    }

    }



    Technique FixedFunc {

    }



    }[/java]



    CrossHatch15.frag

    [java]#import "Common/ShaderLib/MultiSample.glsllib"



    uniform COLORTEXTURE m_Texture;

    in vec2 texCoord;



    uniform vec4 m_LineColor;

    uniform vec4 m_PaperColor;

    uniform float m_ColorInfluenceLine;

    uniform float m_ColorInfluencePaper;



    uniform float m_FillValue;

    uniform float m_Luminance1;

    uniform float m_Luminance2;

    uniform float m_Luminance3;

    uniform float m_Luminance4;

    uniform float m_Luminance5;



    uniform int m_LineDistance;

    uniform int m_LineThickness;





    void main() {

    vec4 texVal = getColor(m_Texture, texCoord);

    float linePixel = 0;



    float lum = texVal.r
    0.2126 + texVal.g0.7152 + texVal.b0.0722;



    if (lum < m_Luminance1){

    if (mod(gl_FragCoord.x + gl_FragCoord.y, m_LineDistance * 2) < m_LineThickness)

    linePixel = 1;

    }

    if (lum < m_Luminance2){

    if (mod(gl_FragCoord.x - gl_FragCoord.y, m_LineDistance * 2) < m_LineThickness)

    linePixel = 1;

    }

    if (lum < m_Luminance3){

    if (mod(gl_FragCoord.x + gl_FragCoord.y - m_LineDistance, m_LineDistance) < m_LineThickness)

    linePixel = 1;

    }

    if (lum < m_Luminance4){

    if (mod(gl_FragCoord.x - gl_FragCoord.y - m_LineDistance, m_LineDistance) < m_LineThickness)

    linePixel = 1;

    }

    if (lum < m_Luminance5){ // No line, make a blob instead

    linePixel = m_FillValue;

    }



    // Mix line color with existing color information

    vec4 lineColor = mix(m_LineColor, texVal, m_ColorInfluenceLine);

    // Mix paper color with existing color information

    vec4 paperColor = mix(m_PaperColor, texVal, m_ColorInfluencePaper);



    gl_FragColor = mix(paperColor, lineColor, linePixel);

    }[/java]



    CrossHatch.frag

    [java]uniform sampler2D m_Texture;

    varying vec2 texCoord;



    uniform vec4 m_LineColor;

    uniform vec4 m_PaperColor;

    uniform float m_ColorInfluenceLine;

    uniform float m_ColorInfluencePaper;



    uniform float m_FillValue;

    uniform float m_Luminance1;

    uniform float m_Luminance2;

    uniform float m_Luminance3;

    uniform float m_Luminance4;

    uniform float m_Luminance5;



    uniform int m_LineDistance;

    uniform int m_LineThickness;





    void main() {

    vec4 texVal = texture2D(m_Texture, texCoord);

    float linePixel = 0;



    float lum = texVal.r0.2126 + texVal.g0.7152 + texVal.b*0.0722;



    if (lum < m_Luminance1){

    if (mod(gl_FragCoord.x + gl_FragCoord.y, m_LineDistance * 2) < m_LineThickness)

    linePixel = 1;

    }

    if (lum < m_Luminance2){

    if (mod(gl_FragCoord.x - gl_FragCoord.y, m_LineDistance * 2) < m_LineThickness)

    linePixel = 1;

    }

    if (lum < m_Luminance3){

    if (mod(gl_FragCoord.x + gl_FragCoord.y - m_LineDistance, m_LineDistance) < m_LineThickness)

    linePixel = 1;

    }

    if (lum < m_Luminance4){

    if (mod(gl_FragCoord.x - gl_FragCoord.y - m_LineDistance, m_LineDistance) < m_LineThickness)

    linePixel = 1;

    }

    if (lum < m_Luminance5){ // No line, make a blob instead

    linePixel = m_FillValue;

    }



    // Mix line color with existing color information

    vec4 lineColor = mix(m_LineColor, texVal, m_ColorInfluenceLine);

    // Mix paper color with existing color information

    vec4 paperColor = mix(m_PaperColor, texVal, m_ColorInfluencePaper);



    gl_FragColor = mix(paperColor, lineColor, linePixel);

    }[/java]



    Update: Added lineThickness option and now updates shader after initialization wehn changes setters are used
5 Likes

Hey, great work :slight_smile: The advertising talk about “so easy to use shaders in jme3 that you can learn them in the time you save applying them to your models” seems to be true eh? :wink:

Well, I believe I got that advertisement somehow although I have no clue where :slight_smile: But yeah, it appears it was true.



Anyhow, a friend of mine just suggested I should allow for thicker lines, already found a way to do that easily, I’ll update the code tomorrow :slight_smile:



I frigging love this,

Wow this is brilliant, thanks for sharing! This is the perfect way to get away with fairly simple art assets without affecting overall presentation. Actually, this reminds me a great deal of a game design doc I once typed up. Maybe I’ll dig it up and put it out here.



Alright, I found an old draft of the game, “Black Matters”

Black Matters - Google Docs



I dare say I’m a much more accomplished design doc writer today ^^ Yet I always liked this idea. If anyone could be interested in prototyping the concept, using techniques like the ones demonstrated above, I’d be happy to flesh out the design.

ooooh Nice!!

I like Filter contribution.

If you have some feedback on the Filter implementation (was it easy to implement your own?, easy to understand?, etc…) , i’m eager to hear it.



A remark though.

In your Filter you set the parameters to the material in the initFilter, which is good practice. But you need to set them in the setters too or changing the filter’s parameters in the main loop of the game won’t have any effect.



Nice job, let me know when you consider it finished, i’ll add it to the core.

Thanks

@erlend_sh

Getting rid of the necessity of good art assets was exactly one of the reasons I liked this filter, so many people get stuck there :stuck_out_tongue: I think it would be fun making games without textures, just colors and mostly simple open gl lighting would do using this kind of techniques, perhaps combined with some extra shadow shaders. I just read through that game design doc, not sure what to expect of that game but it seems like an original idea :stuck_out_tongue: Might be fun to see some sort of demo of it, and I’d love to see this filter put to work. :wink:



@nehon

Thanks for the remark, I’ll fix that right away. As I had no clue on implementing a shader I just followed the fog shader included in jME, lot’s of copy/paste and edit stuff done and some trail and error to find out how to work with it. Was already wondering about that :wink:



For the feedback on filter implementation on my own, well… There was a bit of guessing, good guessing tho. Everything was quite easy to understand once I looked a little closer and tried changing things. jME nicely told me what was wrong whenever I made an error (yeah, I programmed it in jMP too =]). I looked at the advanced shader tutorials on the wiki pages but it seemed these were not on post processing (at least, that’s what I understood).

One thing to mention that I found pretty much magic was the passing around of values: from the java class to the .jm3d material was done by setFloat etc (no magic), then when I got into my .frag file I had to dig them back up as m_SameName. It’s definately not hard to see, it just was a bit of a suprise to me, but then again, I had no experience doing this whatsoever tho I did recognize the m_ in front right away. Plus I think I know why these things are done this way, as I understand you have to glue java and opengl together somehow and this is a pretty neat way to do so.

In short:

Easy to implement on your own? Definately, just take an example, copy all parts and play with it until it does what you want. After a few hours, everything becomes crystal clear and you can do anything you imagine.

Easy to understand? Hell yes. See the result and consider the fact I only started this as a first try.

OH I didn’t notice you even made a glsl 1.5 with multisampling

Very nice work!!



yeah the m_ part needs to be documented a bit more.



Thanks for the feed back :wink:

Updated! See the end of my topic start for changes :slight_smile: Should be final this way unless someone has some great idea or sees a miserable mistake.



@nehon I suppose that’s a good thing? Like I said, just followed the fog shader wherever it went to, actually started with the glsl1.5 and ported it back to 1.0 :wink:

yep that’s great, this way I just have to copy your files in the core, and Done! :stuck_out_tongue:



I updated the shader doc to explain the “m_” thing, see the “User’s uniform” section.

https://wiki.jmonkeyengine.org/legacy/doku.php/jme3:advanced:jme3_shaders



I’ll integrate your Filter in the core tonight.

@nehon

That definately clears things up :slight_smile:



I’m gonna go ahead and try some other shaders now, is there a list of certain shaders that need to be done? Some kind of request list, preferentially with an article that describes how it works? Just need to practise a bit, could start off with the easier ones and progress to more difficult sorts of shading as I go. Programming these things seems an art on its own: Simple, but very, very powerful…



I’m thinking about posterization and dream-vision filters at the moment, any suggestions? :wink:



Edit: Guess I should make a kind of list…

Posterization

Dream-vision

Film-grain

´Looks awesome

@baalgarnaal go ahead with posterization and dream-vision it can be nice.

The filters remaining on my todo are motion blur and under water (which will probably be an enhancement of the current Water filter), but those are pretty advanced.





Other “simple” ideas could be night vision, or heat vision, infra red etc…

Also a heat distortion filter could be nice like this





This would be some kind of distance blur with some distortion

Ok the CrossHatchFilter is in



Thanks :wink:

Great :smiley: Just browsed google source a bit, can’t wait to see your test for it hehe :stuck_out_tongue:

I did notice tho that the links I put in seem to have taken some extra load with them (tags and crap around them), it’s not very important but I suppose that may be removed some day :stuck_out_tongue:



Edit: Not really sure where to put this, don’t want to start a new topic for this, but I just found this lovely presentation that gives an introduction to GLSL stuff. I think it’s quite useful as a quick reference card to find out what is possible (although I figured out most of it myself already anyway). Oh, and I’m working on a funny shader that draws some heat-above-ground defraction using only the frag file. I think it’s a bit heavy on sampling but it is quite well able to determine where the effect should be drawn, plus I haven’t found anyone doing this yet. I still need to find a good defraction function tho, horizontal sampling using a sine is good for underwater effects but sucks for warmth, although it is a good place holder. Any suggestions? (Oh, I can make the sine-underwater thing too if anyone wants it, just need to extract it, shouldn’t take much more than an hour to make. I think @pspeed would love to see that.)

The test is very basic, I just copy/paste the bloomTest and just replace the filter



I’m gonna link this presentation in the wiki shader doc



For the heat shader i’m eager to see that!!

I’m interested in the sine diffraction for under water. The thing is, for underwater i want this kind of deformation, but also dust in the water , light scattering from the surface and caustics projection on the floor.



To place the heat, i guess you’ll have to relate on th depth buffer.

@nehon: I did some research, I think this kind of water shader is a bit too much for me to make right now but I suppose we could ask the author for his source code (he was planning to put it on the web anyway, found it ‘coming soon’ on his webpage). It looks very good in my opinion and apparantly the fps is quite okay too, perhaps we could tweak it a bit:

Paper

Website



Looks promising to me.



About the heat shader thing, well, I think I did okay-ish on finding the right area for defraction but the sine functions still keep me a bit from saying it’s great, I mean it should be more… line-ish I think. Been tweaking it for numerous days now, I suppose I need another thought on it as I just can’t come up with something better right now. A screenshot would be a bit stupid to post as it’s hard to see the actual diffraction that way, perhaps a vid? :wink:

hehe yeah I read those papers earlier, it’s really good yep.

The only thing is, we don’t need caustics an god rays to be exactly like they should in real life. i think a scrolling/animated caustic texture projected on the ground would do the trick and would be a lot faster.

But definitely this paper can help a lot.



For the heat, yeah go ahead make a vid :wink:



PS : I edited your links so they display correctly