• Runtimes
  • [Spine webgl] Use + normal map implementation

Hi,

I would like to start using Spine with Phaser but I'm having hard time trying to understand the spine-webgl implementation. I do not know if I should modify the code or use it as it is.

I would like to specify the drawing position on the screen. Is there a way without modifying the spine runtime? The only way I found is to use a RTT, and then use that texture

And

I would like to implement normal map lightning but I can't figure where to implement it:

  • Using spine skins, then render twice on textures and use the both for the final texture

  • Support FFD, - Not sure making it part of the spine projet is a good idea, and heavy
  • Modify the spine-webgl code to make it work in the drawing process

I have no idea on how to implement it with the PolygonBatcher

Thanks !

Related Discussions
...
  • Изменено

We currently don't have a dedicated Spine Phaser runtime, but a 3rd party created one here https://github.com/orange-games/phaser-spine. It's pretty much up-to-date and should work as intended. It probably makes more sense to start there to implement normal mapping.

We have a libGDX normal mapping example here spine-runtimes/NormalMapTest.java at 3.6 The spine-webgl API is very similar to libGDX, so it should be possible to port this over.

The way it works:

  1. You load your skeleton .json/.skel file and atlas file as usual.
  2. You also have one or more textures that look exactly the same as the atlas pages, except instead of color information, they contain the normals information
  3. You create a new shader that takes both the diffuse atlas textures and normal maps into account
  4. Set up the shader, then use SkeletonRenderer to render as usual.

Thank you for theses information

I tried the whole day to make it work, but unsuccessfully :bang:
There is some things I still do not understand:

atlasTexture = atlas.getRegions().first().getTexture();

Is used (NormalMapTest.java) as the diffuse texture :nerd:
How is the atlas handled with the pixel Shader texCoords ?

I'm currently stuck there, as far as I understand, Mesh.ts .bind() method seems to handle the shader's attributes, but i'm stuck with the error : "Error: Couldn't find location for attribute a_texCoords"

Here is my code:

var transitionsDemo = function(canvas, loadingComplete, bgColor) {
   var OUTLINE_COLOR = new spine.Color(0, 0.8, 0, 1);

   var canvas, gl, renderer, input, assetManager;
   var skeleton, skeletonNoMix, state, stateNoMix, bounds;
   var timeSlider, timeSliderLabel;
   var timeKeeper;

   var DEMO_NAME = "TransitionsDemo";

   if (!bgColor) bgColor = new spine.Color(235 / 255, 239 / 255, 244 / 255, 1);


   var ambiantColor = [1, 1, 1];
   var lightColor = [1, 0, 1];
   var lightPos = [100, 0, 0];
   var nm_texture = null;
   var df_texture = null;
   var ShaderProgram = null;

   var uniformSetters;
   var attribSetters;

   function init () {
      timeSlider = $("#transitions-timeslider").data("slider");
      timeSlider.set(0.5);
      timeSliderLabel = $("#transitions-timeslider-label")[0];

  canvas.width = canvas.clientWidth; canvas.height = canvas.clientHeight;
  gl = canvas.ctx.gl;

  renderer = new spine.webgl.SceneRenderer(canvas, gl);
  shaderProgram = createShader();
  renderer.batcherShader = shaderProgram;
  assetManager = spineDemos.assetManager;
  var textureLoader = function(img) { return new spine.webgl.GLTexture(gl, img); };

  assetManager.loadTexture(DEMO_NAME, textureLoader, "atlas1.png");
  assetManager.loadTexture(DEMO_NAME, textureLoader, "atlas1_NORM.png");
  assetManager.loadText(DEMO_NAME, "atlas1.atlas");
  assetManager.loadJson(DEMO_NAME, "demos.json");

  input = new spine.webgl.Input(canvas);
  timeKeeper = new spine.TimeKeeper();
   }

   function loadingComplete () {

  skeleton = loadSkeleton("spineboy");
  skeletonNoMix = new spine.Skeleton(skeleton.data);
  state = createState(0.25);
  state.multipleMixing = true;
  setAnimations(state, 0, 0);
  stateNoMix = createState(0);
  setAnimations(stateNoMix, -0.25, 0);

  state.apply(skeleton);
  skeleton.updateWorldTransform();
  bounds = { offset: new spine.Vector2(), size: new spine.Vector2() };
  skeleton.getBounds(bounds.offset, bounds.size, []);
  setupInput();
  $("#transitions-overlay").removeClass("overlay-hide");
  $("#transitions-overlay").addClass("overlay");
   }

   function setupInput() {
      $("#transitions-die").click(function () {
         var entry = state.setAnimation(0, "death", false);
         setAnimations(state, 0, true, 0);
         entry.next.mixDuration = 0.1;

     var entry = stateNoMix.setAnimation(0, "death", false);
     setAnimations(stateNoMix, -0.25, -0.25 + -0.1);
  });
   }

   function createState (mix) {
      var stateData = new spine.AnimationStateData(skeleton.data);
      stateData.defaultMix = mix;
      var state = new spine.AnimationState(stateData);
      return state;
   }

   function setAnimations (state, delay, first) {
      state.addAnimation(0, "idle", true, first);
      state.addAnimation(0, "walk", true, 0.6);
      state.addAnimation(0, "jump", false, 1);
      state.addAnimation(0, "run", true, delay);
      state.addAnimation(0, "walk", true, 1.2);
      state.addAnimation(0, "run", true, 0.5);
      state.addAnimation(0, "jump", false, 1);
      state.addAnimation(0, "run", true, delay);
      state.addAnimation(0, "jump", true, 0.5);
      state.addAnimation(0, "walk", true, delay).listener = {
         start: function (trackIndex) {
            setAnimations(state, delay, 0.6);
         }
      };
   }

   function loadSkeleton(name) {
      var atlas = new spine.TextureAtlas(assetManager.get(DEMO_NAME, "atlas1.atlas"), function(path) {
         return assetManager.get(DEMO_NAME, path);
      });
      df_texture = assetManager.get(DEMO_NAME, "atlas1.png");

  nm_texture = assetManager.get(DEMO_NAME, "atlas1_NORM.png");

  var atlasLoader = new spine.AtlasAttachmentLoader(atlas);
  var skeletonJson = new spine.SkeletonJson(atlasLoader);
  var skeletonData = skeletonJson.readSkeletonData(assetManager.get(DEMO_NAME, "demos.json")[name]);
  var skeleton = new spine.Skeleton(skeletonData);
  skeleton.setSkinByName("default");
  return skeleton;
   }

   function render () {
      timeKeeper.update();
      var delta = timeKeeper.delta * timeSlider.get();
      if (timeSliderLabel) {
         var oldValue = timeSliderLabel.textContent;
         var newValue = Math.round(timeSlider.get() * 100) + "%";
         if (oldValue !== newValue) timeSliderLabel.textContent = newValue;
      }

  var offset = bounds.offset;
  var size = bounds.size;

  renderer.camera.position.x = offset.x + size.x - 50;
  renderer.camera.position.y = offset.y + size.y / 2 - 40;
  renderer.camera.viewportWidth = size.x * 2;
  renderer.camera.viewportHeight = size.y * 2;
  renderer.resize(spine.webgl.ResizeMode.Fit);

  gl.clearColor(bgColor.r, bgColor.g, bgColor.b, bgColor.a);
  gl.clear(gl.COLOR_BUFFER_BIT);
  

  renderer.begin();
  shaderProgram.bind();
  shaderProgram.setUniformi("u_texture", df_texture.texture);
  shaderProgram.setUniformi("u_normals", nm_texture.texture);
  shaderProgram.setUniform2f("resolution", size.x*2, size.y*2);
  shaderProgram.setUniform3f("ambientColor", ambiantColor[0], ambiantColor[1], ambiantColor[2]);
  shaderProgram.setUniform3f("light", lightPos[0], lightPos[1], lightPos[2]);
  shaderProgram.setUniform3f("lightColor", lightColor[0], lightColor[1], lightColor[2]);


  state.update(delta);
  state.apply(skeleton);
  skeleton.updateWorldTransform();
  skeleton.x = -200;
  skeleton.y = -100;

  renderer.drawSkeleton(skeleton, true);


  renderer.end();
   }

   function createShader () {
      var vert = "attribute vec4 a_position;\n" //
         + "attribute vec4 a_color;\n" //
         + "attribute vec2 a_texCoord0;\n" //
         + "uniform mat4 u_proj;\n" //
         + "uniform mat4 u_trans;\n" //
         + "uniform mat4 u_projTrans;\n" //
         + "varying vec4 v_color;\n" //
         + "varying vec2 v_texCoords;\n" //
         + "\n" //
         + "void main()\n" //
         + "{\n" //
         + "   v_color = a_color;\n" //
         + "   v_texCoords = a_texCoord0;\n" //
         + "   gl_Position =  u_projTrans * a_position;\n" //
         + "}\n" //
         + "";

  var frag = "#ifdef GL_ES\n" //
     + "precision mediump float;\n" //
     + "#endif\n" //
     + "varying vec4 v_color;\n" //
     + "varying vec2 v_texCoords;\n" //
     + "uniform sampler2D u_texture;\n" //
     + "uniform sampler2D u_normals;\n" //
     + "uniform vec3 light;\n" //
     + "uniform vec3 ambientColor;\n" //
     + "float ambientIntensity = 1.0; \n" //
     + "uniform vec2 resolution;\n" //
     + "uniform vec3 lightColor;\n" //
     + "bool useNormals = true;\n" //
     + "bool useShadow = false;\n" //
     + "vec3 attenuation = vec3(0.5, 0.5, 0.5);\n" //
     + "float strength = 1.0;\n" //
     + "bool yInvert = false;\n" //
     + "\n" //
     + "void main() {\n" //
     + "  // sample color & normals from our textures\n" //
     + "  vec4 color = texture2D(u_texture, v_texCoords.st);\n" //
     + "  vec3 nColor = texture2D(u_normals, v_texCoords.st).rgb;\n" //
     + "\n" //
     + "  // some bump map programs will need the Y value flipped..\n" //
     + "  nColor.g = yInvert ? 1.0 - nColor.g : nColor.g;\n" //
     + "\n" //
     + "  // this is for debugging purposes, allowing us to lower the intensity of our bump map\n" //
     + "  vec3 nBase = vec3(0.5, 0.5, 1.0);\n" //
     + "  nColor = mix(nBase, nColor, strength);\n" //
     + "\n" //
     + "  // normals need to be converted to [-1.0, 1.0] range and normalized\n" //
     + "  vec3 normal = normalize(nColor * 2.0 - 1.0);\n" //
     + "\n" //
     + "  // here we do a simple distance calculation\n" //
     + "  vec3 deltaPos = vec3( (light.xy - gl_FragCoord.xy) / resolution.xy, light.z );\n" //
     + "\n" //
     + "  vec3 lightDir = normalize(deltaPos);\n" //
     + "  float lambert = useNormals ? clamp(dot(normal, lightDir), 0.0, 1.0) : 1.0;\n" //
     + "  \n" //
     + "  // now let's get a nice little falloff\n" //
     + "  float d = sqrt(dot(deltaPos, deltaPos));  \n" //
     + "  float att = useShadow ? 1.0 / ( attenuation.x + (attenuation.y*d) + (attenuation.z[i]d[/i]d) ) : 1.0;\n" //
     + "  \n" //
     + "  vec3 result = (ambientColor * ambientIntensity) + (lightColor.rgb * lambert) * att;\n" //
     + "  result *= color.rgb;\n" //
     + "  \n" //
     + "  gl_FragColor = v_color * vec4(result, color.a);\n" //
     + "}";


     // CREATING COMMON WAY
     /*
     var program = gl.createProgram();

     // Lier les shaders préexistants
     var vertShader = gl.createShader(gl.VERTEX_SHADER);
     gl.shaderSource(vertShader, vert);
     gl.compileShader(vertShader);
        

     var fragShader = gl.createShader(gl.FRAGMENT_SHADER);
     gl.shaderSource(fragShader, frag);
     gl.compileShader(fragShader);

     gl.attachShader(program, vertShader);
     gl.attachShader(program, fragShader);

     gl.linkProgram(program);

     if ( !gl.getProgramParameter( program, gl.LINK_STATUS) ) {
     var info = gl.getProgramInfoLog(program);
     throw 'Impossible de compiler le program WebGL.\n\n' + info;
     }
     uniformSetters = webglUtils.createUniformSetters(gl, program);
     attribSetters  = webglUtils.createAttributeSetters(gl, program);
     */


     var program = new spine.webgl.Shader(gl, vert, frag);
     
     return program;
}

   transitionsDemo.loadingComplete = loadingComplete;
   transitionsDemo.render = render;
   transitionsDemo.DEMO_NAME = DEMO_NAME;
   init();
};

And here is the result with the firefox canvas tool analysis :

I would be glad if you could take the time to take a look at this code

Looking at your shader code, the attribute is called a_texCoord0.

23 дня спустя

This was not about Phaser, but about our own WebGL runtime.