ShadowOnlyMaterial doesn't work on GLB mesh with ReactBabylon.JS?

I am new to babylonjs and I have developed babylonjs application with imperative method. However, when I integrate ReactBabylon library, the shadow of the mesh only appear on StandardMaterial ground.

Here is the full source code including the main App as entrypoint, Camera component, Lights component, SceneSetup component and ShadowGround component

import { useState, useEffect } from "react";
import { Vector3 } from "@babylonjs/core";
import { Scene, Engine } from "react-babylonjs";
import "@babylonjs/loaders";
import Camera from "./components/Camera.js"
import ShadowGround from "./components/ShadowGround.js";
import Lights from "./components/Lights";
import SceneSetup from "./components/SceneSetup.js";
import Furniture from "./components/Furniture.js";
import DiningChair from "./assets/furnitures/dining_chair.glb";
import Sofa from "./assets/furnitures/sofa.glb";


const App = () => {
  const [shadowGenerator, setShadowGenerator] = useState(null);

  useEffect(() => {
   
  }, [shadowGenerator]);

  return (
    <Engine antialias adaptToDeviceRatio canvasId="babylon-js">
      <Scene>
        <SceneSetup />
        <Camera />
        <Lights setShadowGeneratorRef={setShadowGenerator}/>
        <ShadowGround position={new Vector3(0, -5, 0)} />
        {shadowGenerator && (
          <>
            <Furniture
            key={`chair-${shadowGenerator.getLight()?.name}`}
              shadowGenerator={shadowGenerator}
              model={DiningChair}
              position={new Vector3(-5, -5, -3)}
              scaling={new Vector3(4, 4, 4)}
              rotation={new Vector3(0, 0, 0)}
            />
            <Furniture
            key={`sofa-${shadowGenerator.getLight()?.name}`}
              shadowGenerator={shadowGenerator}
              model={Sofa}
              position={new Vector3(5, -5, -3)}
              scaling={new Vector3(4, 4, 4)}
              rotation={new Vector3(0, 0, 0)}
            />
          </>
        )}
      </Scene>
    </Engine>
  );
};

export default App;

import { Vector3, StandardMaterial } from "@babylonjs/core";
import { ShadowOnlyMaterial } from "@babylonjs/materials";
import { useScene } from "react-babylonjs";

console.log("ShadowOnlyMaterial exists:", ShadowOnlyMaterial);

const ShadowGround = ({ position }) => {
  const scene = useScene();
  return (
    <ground
      name="ground"
      width={200}
      height={200}
      position={position}
      receiveShadows
      onCreated={(ground) => {
        console.log("Ground created:", ground);
        console.log("Scene lights:", scene.lights);
        const shadowMaterial = new ShadowOnlyMaterial("shadowMat", scene);
        shadowMaterial.alpha = 1.0;
        ground.material = shadowMaterial;
      }}
    />
  );
};

export default ShadowGround;

import { Vector3, ShadowGenerator } from "@babylonjs/core";

const Lights = ({ setShadowGeneratorRef }) => {
  return (
    <>
      <hemisphericLight
        name="hemisphericLight"
        intensity={0.3}
        direction={new Vector3(0, 1, 0)}
      />
      <directionalLight
        name="directionalLight"
        intensity={1}
        direction={new Vector3(0, -1, 1)} // Ensure correct shadow direction
        position={new Vector3(0, 10, -10)} // Adjusted to cast shadows correctly
        onCreated={(instance) => {
          instance.shadowEnabled = true;
          const shadowGenerator = new ShadowGenerator(2048, instance);
          setShadowGeneratorRef(shadowGenerator);
        }}
      />
    </>
  );
};

export default Lights;
import { useEffect } from "react";
import { useScene } from "react-babylonjs";
import { SceneLoader, PointerDragBehavior, Vector3 } from "@babylonjs/core";
import "@babylonjs/loaders";

const Furniture = ({ shadowGenerator, model, position, scaling, rotation }) => {
  const scene = useScene();

  useEffect(() => {
    if (!scene || !shadowGenerator) return;

    SceneLoader.ImportMeshAsync("", model, "", scene)
      .then((result) => {
        const furniture = result.meshes[0];
        furniture.position = position;
        furniture.scaling = scaling;
        furniture.rotation = rotation;

        const dragBehavior = new PointerDragBehavior({
          dragPlaneNormal: new Vector3(0, 1, 0),
        });
        furniture.addBehavior(dragBehavior);

        // Ensure shadow is added before rendering
        shadowGenerator.addShadowCaster(furniture, true);
        furniture.receiveShadows = true;
        console.log(
          "Shadow casters:",
          shadowGenerator.getShadowMap()?.renderList
        );
        console.log("Furniture mesh:", furniture);
        console.log("Is furniture casting shadows?", furniture.receiveShadows);
        console.log("Light used for shadows:", shadowGenerator.getLight());
      })
      .catch((err) => console.error("Error loading GLB:", err));
  }, [scene, shadowGenerator, model, position, scaling, rotation]);

  return null;
};

export default Furniture;

import { Vector3 } from "@babylonjs/core";

const Camera = () => {
  return (
    <freeCamera
      name="camera"
      position={new Vector3(0, 0, -10)}
      setTarget={[Vector3.Zero()]}
      fov={1.6}
      onCreated={(camera) => {
        camera.inputs.clear();
      }}
    />
  );
};

export default Camera;

import { useEffect } from "react";
import { useScene } from "react-babylonjs";
import { HDRCubeTexture } from "@babylonjs/core";
import background from "../assets/backgrounds/background.hdr";
const SceneSetup = () => {
  const scene = useScene();

  useEffect(() => {
    if (!scene) return;

    const hdrTexture = new HDRCubeTexture(background, scene, 1024);
    hdrTexture.level = 10;
    scene.environmentTexture = hdrTexture;
    scene.createDefaultSkybox(hdrTexture, true, 1000);
  }, [scene]);
};

export default SceneSetup;

I have tried to work with ChatGPT to troubleshoot the issue, with all the log messages and using a plain mesh object. But it just don’t work.

With ShadowOnlyMaterial

With StandardMaterial

My current version of @babylonjs/core and @babylonjs/materials are:
── @babylonjs/core@7.47.2
├─┬ @babylonjs/gui@7.47.2
│ └── @babylonjs/core@7.47.2 deduped
├─┬ @babylonjs/loaders@7.47.2
│ └── @babylonjs/core@7.47.2 deduped
├─┬ @babylonjs/materials@7.47.2
│ └── @babylonjs/core@7.47.2 deduped
├─┬ babylonjs-hook@0.1.1
│ └── @babylonjs/core@7.47.2 deduped
└─┬ react-babylonjs@3.2.2
└── @babylonjs/core@7.47.2 deduped
Other libraries in package.json
“react”: “^18.3.1”,
“react-babylonjs”: “^3.2.2”,
“react-dom”: “^18.3.1”,

Appreciate that! Feel free to ask for any further information. Always ready to support.

cc @brianzinn the react version daddy.

Can you confirm it only happens in the React version ? if not a repro in the playground would be amazing.

Hmm I started the project with a little bit of React style approach until it is started to get messy with state control. Then, I spent a day refactoring them into React component with declarative style. Here was the previous code. I hope it helps

import React, { useEffect, useState, useRef } from "react";
import { Engine, Scene, Color3 } from "@babylonjs/core";
import Camera from "./components/Camera";
import Lights from "./components/Lights";
import Furniture from "./components/Furniture";
import Ground from "./components/Ground";
import { HDRCubeTexture } from "@babylonjs/core";

import "@babylonjs/loaders";
import background from "./assets/backgrounds/background.hdr";

const App = () => {
  const reactCanvas = useRef(null);

  useEffect(() => {
    const canvas = reactCanvas.current;
    if (!canvas) return;

    const engine = new Engine(canvas, true);
    const scene = new Scene(engine);

    // Create HDR texture
    const hdrTexture = new HDRCubeTexture(background, scene, 1048);
    hdrTexture.level = 10;
    scene.environmentTexture = hdrTexture;

    scene.createDefaultSkybox(hdrTexture, true, 1000);

    // Initialize lights and other components
    const shadowGenerator = Lights(scene);
    const camera = Camera(scene, canvas);
    Ground(scene);
    Furniture(scene, shadowGenerator, camera, engine);

    // Render loop
    engine.runRenderLoop(() => {
      scene.render();
    });

    // Resize the engine on window resize
    const resize = () => engine.resize();
    window.addEventListener("resize", resize);

    // Cleanup on component unmount
    return () => {
      engine.dispose();
      window.removeEventListener("resize", resize);
    };
  }, []);
  return (
    <canvas ref={reactCanvas} style={{ width: "100%", height: "100vh" }} />
  );
};

export default App;
import { FreeCamera, Vector3 } from "@babylonjs/core";

const Camera = (scene, canvas) => {
  const camera = new FreeCamera("camera", new Vector3(0, 0, -10), scene);
  camera.fov = 1.7;
  camera.setTarget(Vector3.Zero());
  return camera;
};

export default Camera;
import {
  SceneLoader,
  Vector3,
  PointerDragBehavior,
  Color3,
  ActionManager,
  ExecuteCodeAction,
} from "@babylonjs/core";
import "@babylonjs/loaders";
import createButtonPanel from "./ButtonPanel";
import DiningChair from "../assets/furnitures/dining_chair.glb";
import Sofa from "../assets/furnitures/sofa.glb";

const Furniture = (scene, shadowGenerator, camera, engine) => {
  const loadFurniture = (model) => {
    SceneLoader.ImportMeshAsync("", model, "", scene)
      .then((result) => {
        const furniture = result.meshes[0];
        furniture.position = new Vector3(0, -5, 5);
        furniture.scaling = new Vector3(4, 4, 4);
        furniture.rotation = new Vector3(0, 0, 0);

        shadowGenerator.addShadowCaster(furniture, true);
        // Enable dragging
        const dragBehavior = new PointerDragBehavior({
          dragPlaneNormal: new Vector3(0, 1, 0),
        });
        furniture.addBehavior(dragBehavior);
      })
      .catch((err) => console.error("Error loading GLB:", err));
  };

  loadFurniture(DiningChair);
  loadFurniture(Sofa);
};

export default Furniture;
import { MeshBuilder } from "@babylonjs/core";
import { ShadowOnlyMaterial } from "@babylonjs/materials";

const Ground = (scene) => {
  const ground = new MeshBuilder.CreateGround(
    "ground",
    { width: 100, height: 100 },
    scene
  );
  ground.position.y = -5;
  const shadowOnlyMaterial = new ShadowOnlyMaterial("shadowMat", scene);
  shadowOnlyMaterial.alpha = 1; // Transparent ground
  ground.material = shadowOnlyMaterial;
  ground.receiveShadows = true;
};

export default Ground;
import {
  DirectionalLight,
  HemisphericLight,
  Vector3,
  ShadowGenerator,
} from "@babylonjs/core";

const Lights = (scene) => {
  const directionalLight = new DirectionalLight(
    "directionalLight",
    new Vector3(0, -10, 5),
    scene
  );
  directionalLight.intensity = 1;
  directionalLight.position = new Vector3(0, 10, -5);

  const shadowGenerator = new ShadowGenerator(1024, directionalLight);
  shadowGenerator.useBlurExponentialShadowMap = true;
  shadowGenerator.blurKernel = 32;

  const hemisphericLight = new HemisphericLight(
    "hemisphericLight",
    new Vector3(0, 1, 0),
    scene
  );
  hemisphericLight.intensity = 0.3;
  return shadowGenerator;
};

export default Lights;

Hmm is it possible to put React code in the playground? It is 1AM now. Let me try to move them into playground tomorrow.
Feel free to replace the furniture GLB into a basic shape as I tried it with a sphere on my new code this afternoon and it has no shadow on the ShadowOnlyMaterial.

works fine for me. make sure to set the “activeLight” on the material. here is a working demo:
App.tsx - nodebox - CodeSandbox

Search this page for “limitation”:
ShadowOnly Material | Babylon.js Documentation

edit: your example is probably choosing the hemispheric light, which cannot be used to cast shadows.

I need to use codesandbox instead of the react-babylonjs playground, since @babylonjs/materials is used.

I already defer the creation of the shadowgenerator until the light is created react-babylonjs does some deferred instantiation when a constructor requires another instance.

Also, you can “teach” the reconciler the @babylonjs/materials and use them declaratively - there is a dynamic registration example here with GridMaterial:
Extending materials - React Babylonjs

2 Likes

Oh wow. After reading the “limitation” section, it can be solved with a much simpler way. I swapped the order between both lights, with directionalLight comes first. But to prevent unexpected issue, I will pass dirLightRef to the ground too. Thanks!

 <>
      <directionalLight
        name="directionalLight"
        intensity={1}
        direction={new Vector3(0, -1, 1)} // Ensure correct shadow direction
        position={new Vector3(0, 10, -10)} // Adjusted to cast shadows correctly
        onCreated={(instance) => {
          instance.shadowEnabled = true;
          const shadowGenerator = new ShadowGenerator(2048, instance);
          setShadowGeneratorRef(shadowGenerator);
        }}
      />
      <hemisphericLight
        name="hemisphericLight"
        intensity={0.3}
        direction={new Vector3(0, 1, 0)}
      />
    </>
2 Likes

I looked at the code briefly - without digging too much. I think the light selection is flawed. In react-babylonjs you can create a ShadowGenerator imperatively. So, you don’t need to use the “onCreated” callback as you have done. The example I provided does it declaratively:

<directionalLight name="dl" intensity={0.6} direction={...} ...>
    <shadowGenerator mapSize={1024} useBlurExponentialShadowMap ...>
</directionalLight>

I will only attach the shadowGenerator to certain lights - this is what I have in my codegen:

switch (className) {
    case 'DirectionalLight':
    case 'PointLight':
    case 'SpotLight':
    case 'ShadowLight': // I think it's abstract.  Anyway, it can still be created.
      metadata.isShadowLight = true
      break
    case 'CascadedShadowLight': // I think it's abstract.  Anyway, it can still be created.
      metadata.isShadowLight = true
      break
  }
}

In other words, if you create a <shadowGenerator ../> in react-babylonjs only those lights will be automatically selected by the ShadowGenerator instantiation (and that instantiation is delayed waiting for notification that the light was created).

It looks like “ShadowOnlyMaterial” is trying to use the Hemispheric light, while I think it should be skipped over. It’s probably an easy fix in the materials library. It would adhere closer to what is stated in the docs “ShadowOnly material is dead simple to use.”! I think active light should only need to be provided when there are multiple shadow lights available to the shadow generator. Glad you got it working though - cheers.

1 Like

Thanks for the detailed thought process. I did tried declaring a ShadowGenerator imperatively. If I recall correctly, it still didn’t work as expected. Since I need to use the ShadowGenerator instance in my Furniture component for shadowGenerator.addShadowCaster(loadedFurniture, true);, I decided to just use the “onCreated” callback instead. It might work if I pass ref around in the shadowGenerator tag?

I will revisit this issue again soon and see if your suggestion works.

Side note: This community is amazing :orange_heart:

2 Likes