Node Material - Ambient Occlusion problem

I’ve created the following custom material by using the babylon node material editor: https://nme.babylonjs.com/#8F45L0#1
By importing a few meshes (.glb) into my babylon scene and assigning them the aforementioned material everything works ok except for the global ambient occlusion texture (see image).
I set it up in order to be mapped according the 2nd uv set (uv2) but it doesn’t seem to work at all.
Here’s a screenshot of the model in Blender (I’m exporting the glb files from an FBX model, 2 separated uv set) and the final babylon result (lower-right part of the image).

What am I doing wrong?

Welcome aboard!

It’s hard to tell because the node material seems ok as the AO texture takes its inputs from uv2: have you checked that the uv2 channel has the right data after the object is loaded in Babylon? What happens if you take your inputs from the uv channel instead?

Also, are you able to setup a repro in the Playground as it would be easier to diagnose?

You can use this doc if you need to host your assets: Using External Assets In the Playground | Babylon.js Documentation

Is it that you can’t create the PG by itself or that you can but the problem does not show up there?

You can’t really throw it away, it holds some important transformation for the right/left handed handling. If you still want to remove it, you should set scene.useRightHandedSystem = true so that this transformation is the identity matrix.

You can display in the console mesh.getVerticesData("uv2") and see if the data “seem right”: meaning, values are around the [0,1] range and not all 0 for eg.

What if you try to display your file in the sandbox (https://sandbox.babylonjs.com/)?

By removing the root object you remove a mirror on Z, so it may be your problem. Try to either remove your code that tamper with the root node or set scene.useRightHandedSystem = true.

Here’s the PG example: Babylon.js Playground

The funny thing is that in the PG the ambient occlusion texture works as expected, I even copied the PG code in my application and it works like a charm.

The main difference between the PG and my application is that in the node material pointed by the PG the textures are embedded in the node, while in my application I use the node material generated code (with some addition in order to handle the dynamic texture change).

Here’s the function I use in order to create a material:

const createPBRMaterial = (
  alphaMapTextureUrl,
  baseMapTextureUrl,
  metalnessMapTextureUrl,
  roughnessMapTextureUrl,
  normalMapTextureUrl,
  gridMapTextureUrl,
  globalAmbientOcclusionTextureUrl
) => {
  const nodeMaterial = new BABYLON.NodeMaterial('node')

  // InputBlock
  const position = new BABYLON.InputBlock('position')
  position.setAsAttribute('position')

  // TransformBlock
  const WorldPos = new BABYLON.TransformBlock('WorldPos')
  WorldPos.complementZ = 0
  WorldPos.complementW = 1

  // InputBlock
  const World = new BABYLON.InputBlock('World')
  World.setAsSystemValue(BABYLON.NodeMaterialSystemValues.World)

  // TransformBlock
  const Worldnormal = new BABYLON.TransformBlock('World normal')
  Worldnormal.complementZ = 0
  Worldnormal.complementW = 0

  // InputBlock
  const normal = new BABYLON.InputBlock('normal')
  normal.setAsAttribute('normal')

  // TransformBlock
  const Worldnormal1 = new BABYLON.TransformBlock('World normal')
  Worldnormal1.complementZ = 0
  Worldnormal1.complementW = 0

  // PerturbNormalBlock
  const Perturbnormal = new BABYLON.PerturbNormalBlock('Perturb normal')
  Perturbnormal.invertX = false
  Perturbnormal.invertY = false

  // TransformBlock
  const Worldposition = new BABYLON.TransformBlock('World position')
  Worldposition.complementZ = 0
  Worldposition.complementW = 1

  // TransformBlock
  const Worldtangent = new BABYLON.TransformBlock('World tangent')
  Worldtangent.complementZ = 0
  Worldtangent.complementW = 0

  // InputBlock
  const tangent = new BABYLON.InputBlock('tangent')
  tangent.setAsAttribute('tangent')

  // InputBlock
  const uv = new BABYLON.InputBlock('uv')
  uv.setAsAttribute('uv')

  // TextureBlock
  const normalmap = new BABYLON.TextureBlock('normal map')
  normalmap.texture = new BABYLON.Texture(normalMapTextureUrl, null)
  normalmap.texture.wrapU = 1
  normalmap.texture.wrapV = 1
  normalmap.texture.uAng = 0
  normalmap.texture.vAng = 0
  normalmap.texture.wAng = 0
  normalmap.texture.uOffset = 0
  normalmap.texture.vOffset = 0
  normalmap.texture.uScale = 1
  normalmap.texture.vScale = 1
  normalmap.convertToGammaSpace = false
  normalmap.convertToLinearSpace = false

  // TextureBlock
  const basemap = new BABYLON.TextureBlock('base map')
  basemap.texture = new BABYLON.Texture(baseMapTextureUrl, null)
  basemap.texture.wrapU = 1
  basemap.texture.wrapV = 1
  basemap.texture.uAng = 0
  basemap.texture.vAng = 0
  basemap.texture.wAng = 0
  basemap.texture.uOffset = 0
  basemap.texture.vOffset = 0
  basemap.texture.uScale = 1
  basemap.texture.vScale = 1
  basemap.convertToGammaSpace = false
  basemap.convertToLinearSpace = false

  // LerpBlock
  const Lerp = new BABYLON.LerpBlock('Lerp')
  Lerp.visibleInInspector = false
  Lerp.visibleOnFrame = false

  // MultiplyBlock
  const Multiply = new BABYLON.MultiplyBlock('Multiply')
  Multiply.visibleInInspector = false
  Multiply.visibleOnFrame = false

  // TextureBlock
  const gridmap = new BABYLON.TextureBlock('grid map')
  gridmap.texture = new BABYLON.Texture(gridMapTextureUrl, null)
  gridmap.texture.wrapU = 1
  gridmap.texture.wrapV = 1
  gridmap.texture.uAng = 0
  gridmap.texture.vAng = 0
  gridmap.texture.wAng = 0
  gridmap.texture.uOffset = 0
  gridmap.texture.vOffset = 0
  gridmap.texture.uScale = 1
  gridmap.texture.vScale = 1
  gridmap.convertToGammaSpace = false
  gridmap.convertToLinearSpace = false

  // VectorMergerBlock
  const VectorMerger = new BABYLON.VectorMergerBlock('VectorMerger')
  VectorMerger.visibleInInspector = false
  VectorMerger.visibleOnFrame = false

  // AddBlock
  const Add = new BABYLON.AddBlock('Add')
  Add.visibleInInspector = false
  Add.visibleOnFrame = false

  // MultiplyBlock
  const Multiply1 = new BABYLON.MultiplyBlock('Multiply')
  Multiply1.visibleInInspector = false
  Multiply1.visibleOnFrame = false

  // InputBlock
  const gridhorizontalshiftspeed = new BABYLON.InputBlock('grid horizontal shift speed')
  gridhorizontalshiftspeed.value = 0.1
  gridhorizontalshiftspeed.min = 0
  gridhorizontalshiftspeed.max = 0
  gridhorizontalshiftspeed.isBoolean = false
  gridhorizontalshiftspeed.matrixMode = 0
  gridhorizontalshiftspeed.animationType = BABYLON.AnimatedInputBlockTypes.None
  gridhorizontalshiftspeed.isConstant = false

  // InputBlock
  const Time = new BABYLON.InputBlock('Time')
  Time.value = 0
  Time.min = 0
  Time.max = 0
  Time.isBoolean = false
  Time.matrixMode = 0
  Time.animationType = BABYLON.AnimatedInputBlockTypes.Time
  Time.isConstant = false

  // MultiplyBlock
  const Multiply2 = new BABYLON.MultiplyBlock('Multiply')
  Multiply2.visibleInInspector = false
  Multiply2.visibleOnFrame = false

  // InputBlock
  const gridverticalshiftspeed = new BABYLON.InputBlock('grid vertical shift speed')
  gridverticalshiftspeed.value = 0.1
  gridverticalshiftspeed.min = 0
  gridverticalshiftspeed.max = 0
  gridverticalshiftspeed.isBoolean = false
  gridverticalshiftspeed.matrixMode = 0
  gridverticalshiftspeed.animationType = BABYLON.AnimatedInputBlockTypes.None
  gridverticalshiftspeed.isConstant = false

  // AddBlock
  const Add1 = new BABYLON.AddBlock('Add')
  Add1.visibleInInspector = false
  Add1.visibleOnFrame = false

  // VectorSplitterBlock
  const VectorSplitter = new BABYLON.VectorSplitterBlock('VectorSplitter')
  VectorSplitter.visibleInInspector = false
  VectorSplitter.visibleOnFrame = false

  // InputBlock
  const gridcolor = new BABYLON.InputBlock('grid color')
  gridcolor.value = new BABYLON.Color3(1, 1, 1)
  gridcolor.isConstant = false

  // PBRMetallicRoughnessBlock
  const PBRMetallicRoughness = new BABYLON.PBRMetallicRoughnessBlock('PBRMetallicRoughness')
  PBRMetallicRoughness.lightFalloff = 0
  PBRMetallicRoughness.useAlphaTest = false
  PBRMetallicRoughness.alphaTestCutoff = 0.5
  PBRMetallicRoughness.useAlphaBlending = true
  PBRMetallicRoughness.useRadianceOverAlpha = true
  PBRMetallicRoughness.useSpecularOverAlpha = true
  PBRMetallicRoughness.enableSpecularAntiAliasing = false
  PBRMetallicRoughness.realTimeFiltering = false
  PBRMetallicRoughness.realTimeFilteringQuality = 8
  PBRMetallicRoughness.useEnergyConservation = true
  PBRMetallicRoughness.useRadianceOcclusion = true
  PBRMetallicRoughness.useHorizonOcclusion = true
  PBRMetallicRoughness.unlit = false
  PBRMetallicRoughness.forceNormalForward = false
  PBRMetallicRoughness.debugMode = 0
  PBRMetallicRoughness.debugLimit = 0
  PBRMetallicRoughness.debugFactor = 1

  // InputBlock
  const view = new BABYLON.InputBlock('view')
  view.setAsSystemValue(BABYLON.NodeMaterialSystemValues.View)

  // InputBlock
  const cameraPosition = new BABYLON.InputBlock('cameraPosition')
  cameraPosition.setAsSystemValue(BABYLON.NodeMaterialSystemValues.CameraPosition)

  // TextureBlock
  const metalnessmap = new BABYLON.TextureBlock('metalness map')
  metalnessmap.texture = new BABYLON.Texture(metalnessMapTextureUrl, null)
  metalnessmap.texture.wrapU = 1
  metalnessmap.texture.wrapV = 1
  metalnessmap.texture.uAng = 0
  metalnessmap.texture.vAng = 0
  metalnessmap.texture.wAng = 0
  metalnessmap.texture.uOffset = 0
  metalnessmap.texture.vOffset = 0
  metalnessmap.texture.uScale = 1
  metalnessmap.texture.vScale = 1
  metalnessmap.convertToGammaSpace = false
  metalnessmap.convertToLinearSpace = false

  // TextureBlock
  const roughnessmap = new BABYLON.TextureBlock('roughness map')
  roughnessmap.texture = new BABYLON.Texture(roughnessMapTextureUrl, null)
  roughnessmap.texture.wrapU = 1
  roughnessmap.texture.wrapV = 1
  roughnessmap.texture.uAng = 0
  roughnessmap.texture.vAng = 0
  roughnessmap.texture.wAng = 0
  roughnessmap.texture.uOffset = 0
  roughnessmap.texture.vOffset = 0
  roughnessmap.texture.uScale = 1
  roughnessmap.texture.vScale = 1
  roughnessmap.convertToGammaSpace = false
  roughnessmap.convertToLinearSpace = false

  // TextureBlock
  const GlobalAO = new BABYLON.TextureBlock('Global AO')
  GlobalAO.texture = new BABYLON.Texture(globalAmbientOcclusionTextureUrl, null)
  GlobalAO.texture.wrapU = 1
  GlobalAO.texture.wrapV = 1
  GlobalAO.texture.uAng = 0
  GlobalAO.texture.vAng = 0
  GlobalAO.texture.wAng = 0
  GlobalAO.texture.uOffset = 0
  GlobalAO.texture.vOffset = 0
  GlobalAO.texture.uScale = 1
  GlobalAO.texture.vScale = 1
  GlobalAO.convertToGammaSpace = false
  GlobalAO.convertToLinearSpace = false

  // InputBlock
  const uv1 = new BABYLON.InputBlock('uv2')
  uv1.setAsAttribute('uv2')

  // TextureBlock
  const alphamap = new BABYLON.TextureBlock('alpha map')
  alphamap.texture = new BABYLON.Texture(alphaMapTextureUrl, null)
  alphamap.texture.wrapU = 1
  alphamap.texture.wrapV = 1
  alphamap.texture.uAng = 0
  alphamap.texture.vAng = 0
  alphamap.texture.wAng = 0
  alphamap.texture.uOffset = 0
  alphamap.texture.vOffset = 0
  alphamap.texture.uScale = 1
  alphamap.texture.vScale = 1
  alphamap.convertToGammaSpace = false
  alphamap.convertToLinearSpace = false

  // ReflectionBlock
  const Reflection = new BABYLON.ReflectionBlock('Reflection')
  Reflection.useSphericalHarmonics = true
  Reflection.forceIrradianceInFragment = false

  // InputBlock
  const Color = new BABYLON.InputBlock('Color3')
  Color.value = new BABYLON.Color3(1, 1, 1)
  Color.isConstant = false

  // FragmentOutputBlock
  const FragmentOutput = new BABYLON.FragmentOutputBlock('FragmentOutput')
  FragmentOutput.visibleInInspector = false
  FragmentOutput.visibleOnFrame = false

  // InputBlock
  const normalstrength = new BABYLON.InputBlock('normal strength')
  normalstrength.value = 1
  normalstrength.min = 0
  normalstrength.max = 0
  normalstrength.isBoolean = false
  normalstrength.matrixMode = 0
  normalstrength.animationType = BABYLON.AnimatedInputBlockTypes.None
  normalstrength.isConstant = false

  // TransformBlock
  const WorldPosViewProjectionTransform = new BABYLON.TransformBlock('WorldPos * ViewProjectionTransform')
  WorldPosViewProjectionTransform.complementZ = 0
  WorldPosViewProjectionTransform.complementW = 1

  // InputBlock
  const ViewProjection = new BABYLON.InputBlock('ViewProjection')
  ViewProjection.setAsSystemValue(BABYLON.NodeMaterialSystemValues.ViewProjection)

  // VertexOutputBlock
  const VertexOutput = new BABYLON.VertexOutputBlock('VertexOutput')
  VertexOutput.visibleInInspector = false
  VertexOutput.visibleOnFrame = false

  // Connections
  position.output.connectTo(WorldPos.vector)
  World.output.connectTo(WorldPos.transform)
  WorldPos.output.connectTo(WorldPosViewProjectionTransform.vector)
  ViewProjection.output.connectTo(WorldPosViewProjectionTransform.transform)
  WorldPosViewProjectionTransform.output.connectTo(VertexOutput.vector)
  WorldPos.output.connectTo(PBRMetallicRoughness.worldPosition)
  normal.output.connectTo(Worldnormal.vector)
  World.output.connectTo(Worldnormal.transform)
  Worldnormal.output.connectTo(PBRMetallicRoughness.worldNormal)
  view.output.connectTo(PBRMetallicRoughness.view)
  cameraPosition.output.connectTo(PBRMetallicRoughness.cameraPosition)
  position.output.connectTo(Worldposition.vector)
  World.output.connectTo(Worldposition.transform)
  Worldposition.output.connectTo(Perturbnormal.worldPosition)
  normal.output.connectTo(Worldnormal1.vector)
  World.output.connectTo(Worldnormal1.transform)
  Worldnormal1.output.connectTo(Perturbnormal.worldNormal)
  tangent.output.connectTo(Worldtangent.vector)
  World.output.connectTo(Worldtangent.transform)
  Worldtangent.output.connectTo(Perturbnormal.worldTangent)
  uv.output.connectTo(Perturbnormal.uv)
  uv.output.connectTo(normalmap.uv)
  normalmap.rgb.connectTo(Perturbnormal.normalMapColor)
  normalstrength.output.connectTo(Perturbnormal.strength)
  Perturbnormal.output.connectTo(PBRMetallicRoughness.perturbedNormal)
  uv.output.connectTo(basemap.uv)
  basemap.rgb.connectTo(Lerp.left)
  gridhorizontalshiftspeed.output.connectTo(Multiply1.left)
  Time.output.connectTo(Multiply1.right)
  Multiply1.output.connectTo(Add.left)
  uv.output.connectTo(VectorSplitter.xyIn)
  VectorSplitter.x.connectTo(Add.right)
  Add.output.connectTo(VectorMerger.x)
  VectorSplitter.y.connectTo(Add1.left)
  Time.output.connectTo(Multiply2.left)
  gridverticalshiftspeed.output.connectTo(Multiply2.right)
  Multiply2.output.connectTo(Add1.right)
  Add1.output.connectTo(VectorMerger.y)
  VectorMerger.xy.connectTo(gridmap.uv)
  gridmap.rgb.connectTo(Multiply.left)
  gridcolor.output.connectTo(Multiply.right)
  Multiply.output.connectTo(Lerp.right)
  gridmap.a.connectTo(Lerp.gradient)
  Lerp.output.connectTo(PBRMetallicRoughness.baseColor)
  uv.output.connectTo(metalnessmap.uv)
  metalnessmap.r.connectTo(PBRMetallicRoughness.metallic)
  uv.output.connectTo(roughnessmap.uv)
  roughnessmap.r.connectTo(PBRMetallicRoughness.roughness)
  uv1.output.connectTo(GlobalAO.uv)
  GlobalAO.r.connectTo(PBRMetallicRoughness.ambientOcc)
  uv.output.connectTo(alphamap.uv)
  alphamap.r.connectTo(PBRMetallicRoughness.opacity)
  position.output.connectTo(Reflection.position)
  World.output.connectTo(Reflection.world)
  Color.output.connectTo(Reflection.color)
  Reflection.reflection.connectTo(PBRMetallicRoughness.reflection)
  PBRMetallicRoughness.lighting.connectTo(FragmentOutput.rgb)
  PBRMetallicRoughness.alpha.connectTo(FragmentOutput.a)

  // Output nodes
  nodeMaterial.addOutputNode(VertexOutput)
  nodeMaterial.addOutputNode(FragmentOutput)
  nodeMaterial.build()

  return nodeMaterial
}

You can try to use the generated code in the PG and see if that works.

Yes, I’ll definitely do this, thank you!

I generated the code of the material with the embedded textures from here https://nme.babylonjs.com/#8F45L0#3

Updated PG: Babylon.js Playground

I get the ERR_INVALID_URL while trying to load the embdedded textures. The texture url has been generated by the node material editor, is it a bug?

If I replaced the data:octet/stream generated urls with public images urls it should still work, right?

I fixed the urls by replacing the data:octet/stream with dropbox-served resources but I get the same problem I have on the application.

ParseFromSnippetAsync PG: https://playground.babylonjs.com/#3XTXYR#2
Generated code PG: Babylon.js Playground

It looks like the material returned by the ParseFromSnippetAsync function is somehow different from the one created by the node material generated code.

Could you generate a glb file from Blender with the AOMap in? that way we would have a point of comparison with something that actually works.

Unfortunately I can’t use Blender yet, but I’ll ask for a friend of mine who knows it very well.

That said, this https://playground.babylonjs.com/#3XTXYR#2 is the desired result while this Babylon.js Playground is what I get instead.

Is there a way to compare two different babylon javascript material objects? I tried the JSON.stringify way but it tells me that the material are circular objects.

Some parameters of the texture and reflection blocks were not correctly serialized when generating code, this PR should fix the problem:

1 Like

Thank you very much Evgeni!!!