Curved world shader does not work with dynamic batching

I have used the following curved world shader in the game.
https://playcanvas.com/editor/scene/1353984

Problem : Curve shader works with static batching but does not work with dynamic batching.

Here is the test project for the issue.
https://playcanvas.com/editor/scene/1837572

Hi @yash_mehrotra,

That’s correct, when enabling dynamic batching the engine overrides the curved world transform chunk removing the effect. Sadly currently there isn’t any elegant way around that. There is a feature request on this:

1 Like

Faced the same problem. When batching is enabled, the curve effect is canceled. Now is there any solution (elegant or not)?

Is it marked as solved?

Changed code from example

var CurvedWorld = pc.createScript('curvedWorld');

CurvedWorld.attributes.add('curvePower', {
  type: 'vec4',
  default: [1, -1, 0, 0],
  description: 'Determines the amount of curviness to be applied.',
});

CurvedWorld.prototype.initialize = function () {
  CurvedWorld.instance = this;
  // --- variables
  //this.activeCamera = this.app.root.findByName('Camera');

  // --- make sure all materials are loaded before calling this
  this.materials = this.app.assets.filter(function (asset) {
    return asset.type === 'material';
  }).map(o => o.resource);

  // --- update the material shader
  this.materials.forEach(material => 
  {
    this.updateShader(material);;
    material.setParameter('curvePower', [this.curvePower.x * 0.001, this.curvePower.y * 0.001, this.curvePower.z * 0.001, this.curvePower.w * 0.001])
  });

  // --- events
  this.on('attr', () => {
    this.materials.forEach(material => 
    {
      material.setParameter('curvePower', [this.curvePower.x * 0.001, this.curvePower.y * 0.001, this.curvePower.z * 0.001, this.curvePower.w * 0.001]);
      this.updateShaderUniforms(material)
    });
  });
};

CurvedWorld.prototype.postInitialize = function(){
  console.log(this.app.batcher._batchList);
};

CurvedWorld.prototype.updateShader = function (material) {
  //console.log(material.chunks.transformVS);
  material.chunks.transformVS =
    //'uniform vec3 curveDirection;\n' +
    //'uniform float curvePower;\n' +
    'uniform vec4 curvePower;\n' +
    'uniform vec3 curveCenterPos;\n' +
    //'uniform vec3 curveCameraDirection;\n' +
    '#ifdef PIXELSNAP\n' +
    'uniform vec4 uScreenSize;\n' +
    '#endif\n' +
    '#ifdef MORPHING\n' +
    'uniform vec4 morph_weights_a;\n' +
    'uniform vec4 morph_weights_b;\n' +
    '#endif\n' +
    '#ifdef MORPHING_TEXTURE_BASED\n' +
    'uniform vec4 morph_tex_params;\n' +
    'vec2 getTextureMorphCoords() {\n' +
    '    float vertexId = morph_vertex_id;\n' +
    '    vec2 textureSize = morph_tex_params.xy;\n' +
    '    vec2 invTextureSize = morph_tex_params.zw;\n' +
    '    // turn vertexId into int grid coordinates\n' +
    '    float morphGridV = floor(vertexId * invTextureSize.x);\n' +
    '    float morphGridU = vertexId - (morphGridV * textureSize.x);\n' +
    '    // convert grid coordinates to uv coordinates with half pixel offset\n' +
    '    return (vec2(morphGridU, morphGridV) * invTextureSize) + (0.5 * invTextureSize);\n' +
    '}\n' +
    '#endif\n' +
    '#ifdef MORPHING_TEXTURE_BASED_POSITION\n' +
    'uniform highp sampler2D morphPositionTex;\n' +
    '#endif\n' +
    'mat4 getModelMatrix() {\n' +
    '    #ifdef DYNAMICBATCH\n' +
    '    return getBoneMatrix(vertex_boneIndices);\n' +
    '    #elif defined(SKIN)\n' +
    '    return matrix_model * getSkinMatrix(vertex_boneIndices, vertex_boneWeights);\n' +
    '    #elif defined(INSTANCING)\n' +
    '    return mat4(instance_line1, instance_line2, instance_line3, instance_line4);\n' +
    '    #else\n' +
    '    return matrix_model;\n' +
    '    #endif\n' +
    '}\n' +
    'vec4 getPosition() {\n' +
    '    dModelMatrix = getModelMatrix();\n' +
    '    vec3 localPos = vertex_position;\n' +
    '    #ifdef NINESLICED\n' +
    '    // outer and inner vertices are at the same position, scale both\n' +
    '    localPos.xz *= outerScale;\n' +
    '    // offset inner vertices inside\n' +
    '    // (original vertices must be in [-1;1] range)\n' +
    '    vec2 positiveUnitOffset = clamp(vertex_position.xz, vec2(0.0), vec2(1.0));\n' +
    '    vec2 negativeUnitOffset = clamp(-vertex_position.xz, vec2(0.0), vec2(1.0));\n' +
    '    localPos.xz += (-positiveUnitOffset * innerOffset.xy + negativeUnitOffset * innerOffset.zw) * vertex_texCoord0.xy;\n' +
    '    vTiledUv = (localPos.xz - outerScale + innerOffset.xy) * -0.5 + 1.0; // uv = local pos - inner corner\n' +
    '    localPos.xz *= -0.5; // move from -1;1 to -0.5;0.5\n' +
    '    localPos = localPos.xzy;\n' +
    '    #endif\n' +
    '    #ifdef MORPHING\n' +
    '    #ifdef MORPHING_POS03\n' +
    '    localPos.xyz += morph_weights_a[0] * morph_pos0;\n' +
    '    localPos.xyz += morph_weights_a[1] * morph_pos1;\n' +
    '    localPos.xyz += morph_weights_a[2] * morph_pos2;\n' +
    '    localPos.xyz += morph_weights_a[3] * morph_pos3;\n' +
    '    #endif // MORPHING_POS03\n' +
    '    #ifdef MORPHING_POS47\n' +
    '    localPos.xyz += morph_weights_b[0] * morph_pos4;\n' +
    '    localPos.xyz += morph_weights_b[1] * morph_pos5;\n' +
    '    localPos.xyz += morph_weights_b[2] * morph_pos6;\n' +
    '    localPos.xyz += morph_weights_b[3] * morph_pos7;\n' +
    '    #endif // MORPHING_POS47\n' +
    '    #endif // MORPHING\n' +
    '    #ifdef MORPHING_TEXTURE_BASED_POSITION\n' +
    '    // apply morph offset from texture\n' +
    '    vec2 morphUV = getTextureMorphCoords();\n' +
    '    vec3 morphPos = texture2D(morphPositionTex, morphUV).xyz;\n' +
    '    localPos += morphPos;\n' +
    '    #endif\n' +
    '    vec4 posW = dModelMatrix * vec4(localPos, 1.0);\n' +
    // --- START custom curve code
    // '    float amountX = curveCameraDirection.x * pow(posW.x - curveCenterPos.x, 2.0);\n' +
    // '    float amountY = curveCameraDirection.y * pow(posW.y - curveCenterPos.y, 2.0);\n' +
    // '    float amountZ = curveCameraDirection.z * pow(posW.z - curveCenterPos.z, 2.0);\n' +
    // '    float amountSum = (amountX * curveCameraDirection.x + amountY * curveCameraDirection.y + amountZ * curveCameraDirection.z) * curvePower;\n' +
    // '    posW = posW + vec4(-amountX * curveCameraDirection.x * curvePower.x,-amountY * curveCameraDirection.y * curvePower.y,-amountZ * curveCameraDirection.z * curvePower.z,0);\n' +
    // '    posW.x += -amountSum;\n' +
    '    float amountX = pow(posW.x - curveCenterPos.x, 2.0);\n' +
    //'    float amountY = pow(posW.y - curveCenterPos.y, 2.0);\n' +
    '    float amountZ = pow(posW.z - curveCenterPos.z, 2.0);\n' +
    //'    float amountSum = amountX + amountY + amountZ;\n' +
    '    float amountSum = amountX + amountZ;\n' +
    '    posW += vec4(curvePower.x, curvePower.y, curvePower.z, curvePower.w) * amountSum;\n' +
    // --- END custom curve code

    '    #ifdef SCREENSPACE\n' +
    '    posW.zw = vec2(0.0, 1.0);\n' +
    '    #endif\n' +
    '    dPositionW = posW.xyz;\n' +
    '    vec4 screenPos;\n' +
    '    #ifdef UV1LAYOUT\n' +
    '    screenPos = vec4(vertex_texCoord1.xy * 2.0 - 1.0, 0.5, 1);\n' +
    '    #else\n' +
    '    #ifdef SCREENSPACE\n' +
    '    screenPos = posW;\n' +
    '    #else\n' +
    '    screenPos = matrix_viewProjection * posW;\n' +
    '    #endif\n' +
    '    #ifdef PIXELSNAP\n' +
    '    // snap vertex to a pixel boundary\n' +
    '    screenPos.xy = (screenPos.xy * 0.5) + 0.5;\n' +
    '    screenPos.xy *= uScreenSize.xy;\n' +
    '    screenPos.xy = floor(screenPos.xy);\n' +
    '    screenPos.xy *= uScreenSize.zw;\n' +
    '    screenPos.xy = (screenPos.xy * 2.0) - 1.0;\n' +
    '    #endif\n' +
    '    #endif\n' +
    '    return screenPos;\n' +
    '}\n' +
    'vec3 getWorldPosition() {\n' +
    '    return dPositionW;\n' +
    '}\n';
  
  material.update();
  
  //console.log(material.chunks.transformVS);

  this.updateShaderUniforms(material);
};

CurvedWorld.prototype.postUpdate = function () {
  if (!this.materials) return;

  this.materials.forEach(material => this.updateShaderUniforms(material));
};

CurvedWorld.prototype.updateShaderUniforms = function (material) {

  this.activeCamera = this.app.root.findByName('Camera');
  if(!this.activeCamera) return;
  if(!this.activeCamera.followCamera) return;
  
  //const targetPos = this.activeCamera.getPosition();
  const targetPos = this.activeCamera.followCamera.followTarget.getPosition();
  material.setParameter('curveCenterPos', [targetPos.x, targetPos.y, targetPos.z]);
};

This post from another thread also looks like a solution, but I can’t figure out how to implement it.

Hey! I’m trying to apply a curved world effect for my game too. Were you able to finally get it working? Mine currently works for static/primitive meshes but not for skinned meshes with animation. I’m able to apply the effect for their positioning with transformVS but I can’t seem to get the shader to affect the material properly and seems like with my normalPS I’m creating invalid normals because the material is just all black. Below are the two shader chunks, any help would be appreciated!

if (isSkinned) {
    // Skinned mesh: Override position, and override normalPS to modify world-space normal using new API
    const shaderChunks = material.getShaderChunks(pc.SHADERLANGUAGE_GLSL);
    shaderChunks.set("transformVS",
      'uniform float curvePower;\n' +
      'uniform vec3 curveCenterPos;\n' +
      'uniform vec3 curveCameraDirection;\n' +
      'vec4 getPosition() {\n' +
      '    vec4 posW = getModelMatrix() * vertex_position;\n' +
      '    float amountX = curveCameraDirection.x * pow(posW.x - curveCenterPos.x, 2.0);\n' +
      '    float amountY = curveCameraDirection.y * pow(posW.y - curveCenterPos.y, 2.0);\n' +
      '    float amountZ = curveCameraDirection.z * pow(posW.z - curveCenterPos.z, 2.0);\n' +
      '    float amountSum = (amountX * curveCameraDirection.x + amountY * curveCameraDirection.y + amountZ * curveCameraDirection.z) * curvePower;\n' +
      '    posW.y += -amountSum;\n' +
      '    dPositionW = posW.xyz;\n' +
      '    return matrix_viewProjection * posW;\n' +
      '}\n' +
      'vec3 getWorldPosition() {\n' +
      '    return dPositionW;\n' +
      '}\n'
    );
    shaderChunks.set("normalPS",
      'uniform float curvePower;\n' +
      'uniform vec3 curveCenterPos;\n' +
      'uniform vec3 curveCameraDirection;\n' +
      'vec3 getNormal() {\n' +
      '    vec3 n = dNormalW;\n' +
      '    float amountX = curveCameraDirection.x * pow(dPositionW.x - curveCenterPos.x, 2.0);\n' +
      '    float amountY = curveCameraDirection.y * pow(dPositionW.y - curveCenterPos.y, 2.0);\n' +
      '    float amountZ = curveCameraDirection.z * pow(dPositionW.z - curveCenterPos.z, 2.0);\n' +
      '    float amountSum = (amountX * curveCameraDirection.x + amountY * curveCameraDirection.y + amountZ * curveCameraDirection.z) * curvePower;\n' +
      '    float blend = clamp(abs(amountSum) * 0.1, 0.0, 0.5);\n' +
      '    n = normalize(mix(n, vec3(0.0, 1.0, 0.0), blend));\n' +
      '    return n;\n' 
    );
  }

I haven’t returned to this script in a while, but here is the latest version that worked for me for all needs including skinned meshes.

The script is designed for one-time initialization, in my case it exists in the Main scene, which is never unloaded.

class CurvedWorld extends pc.ScriptType{}
pc.registerScript(CurvedWorld, 'curvedWorld');

/**@type {CurvedWorld} */
CurvedWorld.instance;

CurvedWorld.attributes.add('ignoreMaterials', {type:'asset', assetType:'material', array:true});

CurvedWorld.attributes.add('curvePower', {
  type: 'vec4',
  default: [-1, -1, 0, 0],
  description: 'Determines the amount of curviness to be applied.',
});

CurvedWorld.prototype.initialize = function () {
  CurvedWorld.instance = this;
  // --- variables
  //this.activeCamera = this.app.root.findByName('Camera');

  // --- make sure all materials are loaded before calling this
  this.materials = this.app.assets.filter(function (asset) {
    return asset.type === 'material' && CurvedWorld.instance.ignoreMaterials.includes(asset) === false;
  }).map(o => o.resource);

  // --- update the material shader
  this.materials.forEach(material => 
  {
    this.updateShader(material);
    material.setParameter('curvePower', [this.curvePower.x * 0.001, this.curvePower.y * 0.001, this.curvePower.z * 0.001, this.curvePower.w * 0.001])
  });

  // --- events
  this.on('attr', () => {
    this.materials.forEach(material => 
    {
      material.setParameter('curvePower', [this.curvePower.x * 0.001, this.curvePower.y * 0.001, this.curvePower.z * 0.001, this.curvePower.w * 0.001]);
      this.updateShaderUniforms(material)
    });
  });
};

// CurvedWorld.prototype.postInitialize = function(){
//   console.log(this.app.batcher._batchList);
// };

CurvedWorld.prototype.updateShader = function (material) {
  //console.log(material.chunks.transformVS);
  material.chunks.transformVS =
    //'uniform vec3 curveDirection;\n' +
    //'uniform float curvePower;\n' +
    'uniform vec4 curvePower;\n' +
    'uniform vec3 curveCenterPos;\n' +
    //'uniform vec3 curveCameraDirection;\n' +
    '#ifdef PIXELSNAP\n' +
    'uniform vec4 uScreenSize;\n' +
    '#endif\n' +
    '#ifdef MORPHING\n' +
    'uniform vec4 morph_weights_a;\n' +
    'uniform vec4 morph_weights_b;\n' +
    '#endif\n' +
    '#ifdef MORPHING_TEXTURE_BASED\n' +
    'uniform vec4 morph_tex_params;\n' +
    'vec2 getTextureMorphCoords() {\n' +
    '    float vertexId = morph_vertex_id;\n' +
    '    vec2 textureSize = morph_tex_params.xy;\n' +
    '    vec2 invTextureSize = morph_tex_params.zw;\n' +
    '    // turn vertexId into int grid coordinates\n' +
    '    float morphGridV = floor(vertexId * invTextureSize.x);\n' +
    '    float morphGridU = vertexId - (morphGridV * textureSize.x);\n' +
    '    // convert grid coordinates to uv coordinates with half pixel offset\n' +
    '    return (vec2(morphGridU, morphGridV) * invTextureSize) + (0.5 * invTextureSize);\n' +
    '}\n' +
    '#endif\n' +
    '#ifdef MORPHING_TEXTURE_BASED_POSITION\n' +
    'uniform highp sampler2D morphPositionTex;\n' +
    '#endif\n' +
    'mat4 getModelMatrix() {\n' +
    '    #ifdef DYNAMICBATCH\n' +
    '    return getBoneMatrix(vertex_boneIndices);\n' +
    '    #elif defined(SKIN)\n' +
    '    return matrix_model * getSkinMatrix(vertex_boneIndices, vertex_boneWeights);\n' +
    '    #elif defined(INSTANCING)\n' +
    '    return mat4(instance_line1, instance_line2, instance_line3, instance_line4);\n' +
    '    #else\n' +
    '    return matrix_model;\n' +
    '    #endif\n' +
    '}\n' +
    'vec4 getPosition() {\n' +
    '    dModelMatrix = getModelMatrix();\n' +
    '    vec3 localPos = vertex_position;\n' +
    '    #ifdef NINESLICED\n' +
    '    // outer and inner vertices are at the same position, scale both\n' +
    '    localPos.xz *= outerScale;\n' +
    '    // offset inner vertices inside\n' +
    '    // (original vertices must be in [-1;1] range)\n' +
    '    vec2 positiveUnitOffset = clamp(vertex_position.xz, vec2(0.0), vec2(1.0));\n' +
    '    vec2 negativeUnitOffset = clamp(-vertex_position.xz, vec2(0.0), vec2(1.0));\n' +
    '    localPos.xz += (-positiveUnitOffset * innerOffset.xy + negativeUnitOffset * innerOffset.zw) * vertex_texCoord0.xy;\n' +
    '    vTiledUv = (localPos.xz - outerScale + innerOffset.xy) * -0.5 + 1.0; // uv = local pos - inner corner\n' +
    '    localPos.xz *= -0.5; // move from -1;1 to -0.5;0.5\n' +
    '    localPos = localPos.xzy;\n' +
    '    #endif\n' +
    '    #ifdef MORPHING\n' +
    '    #ifdef MORPHING_POS03\n' +
    '    localPos.xyz += morph_weights_a[0] * morph_pos0;\n' +
    '    localPos.xyz += morph_weights_a[1] * morph_pos1;\n' +
    '    localPos.xyz += morph_weights_a[2] * morph_pos2;\n' +
    '    localPos.xyz += morph_weights_a[3] * morph_pos3;\n' +
    '    #endif // MORPHING_POS03\n' +
    '    #ifdef MORPHING_POS47\n' +
    '    localPos.xyz += morph_weights_b[0] * morph_pos4;\n' +
    '    localPos.xyz += morph_weights_b[1] * morph_pos5;\n' +
    '    localPos.xyz += morph_weights_b[2] * morph_pos6;\n' +
    '    localPos.xyz += morph_weights_b[3] * morph_pos7;\n' +
    '    #endif // MORPHING_POS47\n' +
    '    #endif // MORPHING\n' +
    '    #ifdef MORPHING_TEXTURE_BASED_POSITION\n' +
    '    // apply morph offset from texture\n' +
    '    vec2 morphUV = getTextureMorphCoords();\n' +
    '    vec3 morphPos = texture2D(morphPositionTex, morphUV).xyz;\n' +
    '    localPos += morphPos;\n' +
    '    #endif\n' +
    '    vec4 posW = dModelMatrix * vec4(localPos, 1.0);\n' +
    // --- START custom curve code
    // '    float amountX = curveCameraDirection.x * pow(posW.x - curveCenterPos.x, 2.0);\n' +
    // '    float amountY = curveCameraDirection.y * pow(posW.y - curveCenterPos.y, 2.0);\n' +
    // '    float amountZ = curveCameraDirection.z * pow(posW.z - curveCenterPos.z, 2.0);\n' +
    // '    float amountSum = (amountX * curveCameraDirection.x + amountY * curveCameraDirection.y + amountZ * curveCameraDirection.z) * curvePower;\n' +
    // '    posW = posW + vec4(-amountX * curveCameraDirection.x * curvePower.x,-amountY * curveCameraDirection.y * curvePower.y,-amountZ * curveCameraDirection.z * curvePower.z,0);\n' +
    // '    posW.x += -amountSum;\n' +
    '    float amountX = pow(posW.x - curveCenterPos.x, 2.0);\n' +
    //'    float amountY = pow(posW.y - curveCenterPos.y, 2.0);\n' +
    '    float amountZ = pow(posW.z - curveCenterPos.z, 2.0);\n' +
    //'    float amountSum = amountX + amountY + amountZ;\n' +
    '    float amountSum = amountX + amountZ;\n' +
    '    posW += vec4(curvePower.x, curvePower.y, curvePower.z, curvePower.w) * amountSum;\n' +
    // --- END custom curve code

    '    #ifdef SCREENSPACE\n' +
    '    posW.zw = vec2(0.0, 1.0);\n' +
    '    #endif\n' +
    '    dPositionW = posW.xyz;\n' +
    '    vec4 screenPos;\n' +
    '    #ifdef UV1LAYOUT\n' +
    '    screenPos = vec4(vertex_texCoord1.xy * 2.0 - 1.0, 0.5, 1);\n' +
    '    #else\n' +
    '    #ifdef SCREENSPACE\n' +
    '    screenPos = posW;\n' +
    '    #else\n' +
    '    screenPos = matrix_viewProjection * posW;\n' +
    '    #endif\n' +
    '    #ifdef PIXELSNAP\n' +
    '    // snap vertex to a pixel boundary\n' +
    '    screenPos.xy = (screenPos.xy * 0.5) + 0.5;\n' +
    '    screenPos.xy *= uScreenSize.xy;\n' +
    '    screenPos.xy = floor(screenPos.xy);\n' +
    '    screenPos.xy *= uScreenSize.zw;\n' +
    '    screenPos.xy = (screenPos.xy * 2.0) - 1.0;\n' +
    '    #endif\n' +
    '    #endif\n' +
    '    return screenPos;\n' +
    '}\n' +
    'vec3 getWorldPosition() {\n' +
    '    return dPositionW;\n' +
    '}\n';
  
  material.update();
  
  //console.log(material.chunks.transformVS);

  this.updateShaderUniforms(material);
};

CurvedWorld.prototype.postUpdate = function () {
  if (!this.materials) return;

  this.materials.forEach(material => this.updateShaderUniforms(material));
};

CurvedWorld.prototype.updateShaderUniforms = function (material) {
  //console.log(Game.instance);
  if(!Game.instance || Game.instance.enabled === false || !Game.instance.camera || !Game.instance.camera.followCamera) 
  {
    material.setParameter('curveCenterPos', [0, 0, 0]);
    material.setParameter('curvePower', [0, 0, 0, 0])
    return;
  }
  else {
    //change for your pose
    const targetPos = Game.instance.camera.followCamera.followTarget.getPosition();
    material.setParameter('curveCenterPos', [targetPos.x, targetPos.y, targetPos.z]);
    material.setParameter('curvePower', [this.curvePower.x * 0.001, this.curvePower.y * 0.001, this.curvePower.z * 0.001, this.curvePower.w * 0.001])
  }
};

Thanks for the quick response! This is definitely helpful, any chance the project you are working with is public? If not I can manage with this, would just be helpful to follow along with the rest of the project!