[RESOLVED] My first shader: A spritesheet animation

But note: you can create whatever crazy material parameters that you want… you just can’t assign arrays to them in j3m.

1 Like

Even if I reference the same material instance (loaded via AssetManager)?

geom.setMaterial(mat);
1 Like

Well, the implication is that you’ve taken something that should be mesh-specific and made it material-specific. So where you could have batched 1000 sprites together before now you can’t.

But anyway, if you want to do it you can just create four separate material parameters for each corner.

1 Like

Hmm - maybe I can explain where I’m trying to get and we can see if that sheds light on this. Whenver someone in my game dies, a trophy/prize is spawn instead (for someone else to pick up). Every trophy would be a seperate geometry, attached to a trophy-node.

The quad I attach to the geometry is displaying an animation based on a spritesheet. In my shader, I simply use the base TexCoord (as mentioned above) and add an offset to this based on time and animation speed.

Perhaps there are more options here:

  1. Attach the same material to different geometries (and/or attach the same quad to different geometries)
  2. Batch all trophy nodes (dont know anything about that process, so entirely unsure about any pros or cons)

This is my TestCase:

package mygame;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Quad;

public class MainShaderTest extends SimpleApplication {

public static void main(String[] args) {
    MainShaderTest app = new MainShaderTest();
    app.start();
}
Quad quad;
private Geometry geom;
private Material mat;
@Override
public void simpleInitApp() {
    
    quad = new Quad(0.5f, 0.5f);
    geom = new Geometry("Bounty", quad);
    mat = assetManager.loadMaterial("Materials/AnimateSpriteShader.j3m");
    geom.setMaterial(mat);
    geom.setLocalTranslation(0,0,5);
    rootNode.attachChild(geom);
}
}

This is my material:

Material BountyMaterial : MatDefs/AnimateSpriteShader.j3md {
     MaterialParameters {
        AniTexMap : Textures/Subspace/prizes.png
        numTilesU : 10
        TileWidth : 16  
        TileHeight : 16
        Speed : 20                      
     }
    AdditionalRenderState {
      Blend Alpha
      Wireframe Off
    }
}

My vertex shader:

uniform mat4 g_WorldViewProjectionMatrix;
uniform float g_Tpf;
uniform float g_Time;

uniform int m_numTilesU;
uniform float m_Speed; 
uniform int m_TileWidth;

attribute vec3 inPosition; //The view
attribute vec2 inTexCoord;

varying vec2 texCoordAni; //parameter to calculate the correct texture coordinates and pass it to the frag shader

void main(){
gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);

float tileDistance = float(g_Time*m_Speed);
int selectedTile = int(mod(float(tileDistance), m_numTilesU));

texCoordAni.x = (float(float(inTexCoord.x/m_numTilesU) + float(selectedTile)/float(m_numTilesU)));
texCoordAni.y = inTexCoord.y;
}

My fragment shader:

varying vec2 texCoordAni;
uniform sampler2D m_AniTexMap;

void main(){
    vec4 AniTex = texture2D(m_AniTexMap, vec2(texCoordAni));
    gl_FragColor = AniTex;
}

My material definition:

MaterialDef AnimateSpriteShader {
    
    MaterialParameters {
        Int numTilesU
        Float Speed
        Int TileWidth
        Int TileHeight
        Texture2D AniTexMap
    }
    Technique {
        VertexShader GLSL120 :   MatDefs/AnimateSpriteShader.vert
        FragmentShader GLSL120 : MatDefs/AnimateSpriteShader.frag

        WorldParameters {
            WorldViewProjectionMatrix
            Time
        }
    }
}
1 Like

Just a random note:
Given that you already know how many tiles are in your texture, a full four texture coordinates seems a bit redundant when one cell ID would do. You could then calculate the texture coordinates in your shader based on the cell ID and those material parameters.

But I don’t understand why that needs to be in the material when it’s kind of mesh-specific. Why not just have a quad per trophy type or whatever. You can even reuse them if you like.

Then a unique “trophy” simply becomes a particular Quad… and not a Quad + Geometry + Material to define it.

1 Like

I guess it doesn’t :stuck_out_tongue: Thanks for the help! It motivates beyond words that JME has such a helpful community!

1 Like

I updated the code above to reflect the final solution (in case anybody else needs that kind of animated sprite shader).

1 Like

Ahem… why don’t you take a look at my batched sample app?

The “missing” feature is that tpf is managed by Java and not by the shader… (which for me is a plus as I have more control over the animation) however if you prefer to experiment on your own is ok :slight_smile:

2 Likes

I did notice your MonkeySheet code, but as this was a challenge as much for my curiousity (on how Shaders work) as for the game, I continued with Shaders. I’m using time elapsed instead of tpf, so I guess if the shader is lagging it will catch up in the next draw?

My goal was to remove complexity from my CPU and move it to the GPU, so a self controlled Shader - where I control everything from each material - seemed a sensible solution.

1 Like

Cool but what if have several animations (run, die, jump) and I want to switch according to ingame events? What if I want to spawn a projectile when the animation is 25% complete?

On the MonkeySheet philosophy the CPU is always aware of the animation status…

1 Like

In this very simple) shader, you just switch the material on the quad whenver your logic predicates it. For my game, I do not have this requirement :slight_smile:

1 Like

But this would ultimately lead to an appstate and a solution very much like the MonkeySheet I guess.However, in my case - the only thing residing on the CPU is switching material. In your case, you set the tile per update call:

 public void update(float tpf) {
        tTPF+=(tpf);
        localScale= (float) (5+24*(Math.sin(1)-Math.sin(1+tTPF)));
        geo.setLocalScale(localScale);
        c += (60 * tpf);
        for (int i = 0; i < SIZE; i++) {
            quads[i].setSFrame((int) (c + i) % 20); // HERE?
        }
        msBatcher.updateAnim();
    }

Which is overkill for my use

1 Like

Yeah, this is a work in progress (non-batched Materials update the tile once every a configurable tickduration).

1 Like

For posterity, this is a multi-line sprite shader:

j3md:

MaterialDef AnimateSpriteShader {
    
    MaterialParameters {
        Int numTilesX
        Int numTilesY
        Float Speed
        Int numTilesOffsetX
        Int numTilesOffsetY
        Texture2D AniTexMap
        Float StartTime
    }
    Technique {
        VertexShader GLSL120 :   MatDefs/AnimateMultilineSpriteShader.vert
        FragmentShader GLSL120 : MatDefs/AnimateMultilineSpriteShader.frag

        WorldParameters {
            WorldViewProjectionMatrix
            Time
        }
    }
}

Vert:

uniform mat4 g_WorldViewProjectionMatrix;
uniform float g_Tpf;
uniform float g_Time;

uniform int m_numTilesX;
uniform int m_numTilesY;
uniform float m_Speed; 
uniform int m_numTilesOffsetX;
uniform int m_numTilesOffsetY;
uniform float m_StartTime;

attribute vec3 inPosition; //The view
attribute vec2 inTexCoord;

varying vec2 texCoordAni; //parameter to calculate the correct texture coordinates and pass it to the frag shader

void main(){
    gl_Position = g_WorldViewProjectionMatrix * vec4(inPosition, 1.0);
    
    float tileDistance = float((g_Time-m_StartTime)*m_Speed);
    int selectedTileX = int(mod(float(tileDistance), m_numTilesX))+m_numTilesOffsetX;
    int selectedTileY = (m_numTilesY-1) - (int(mod(float(tileDistance / m_numTilesX), m_numTilesY))+m_numTilesOffsetY);
    //int selectedTileY = (m_numTilesY-1)- m_numTilesOffsetY;

    texCoordAni.x = (float(float(inTexCoord.x/m_numTilesX) + float(selectedTileX)/float(m_numTilesX)));
    texCoordAni.y = (float(float(inTexCoord.y/m_numTilesY) + float(selectedTileY)/float(m_numTilesY)));
}

Frag:

varying vec2 texCoordAni;
varying float completed;
uniform sampler2D m_AniTexMap;

void main(){
    vec4 AniTex = texture2D(m_AniTexMap, vec2(texCoordAni));

    if(AniTex.rgb == vec3(0.0, 0.0, 0.0)) 
        discard;

    gl_FragColor = AniTex;
}

Mat:

Material My Material : MatDefs/AnimateMultilineSpriteShader.j3md {
    MaterialParameters {
        AniTexMap : Flip Textures/Subspace/wormhole.bm2
        numTilesX : 4
        numTilesY : 6
        Speed : 20
        numTilesOffsetX : 0
        numTilesOffsetY : 0
     }
    AdditionalRenderState {
      Blend Alpha
      Wireframe Off
    }
}

Note: it discards black pixels to make it transparent.

5 Likes