Skip to content

progressiveLightMap: Missing vertex normals or flatShading causes errors and flatShading not preserved #31799

@universyu

Description

@universyu

Description

I’ve attached a html containing a minimal reproduction.

The issue occurs in two cases:

Geometry without vertex normals but material with flatShading = true

Normally this renders fine, since flatShading handles the shading without requiring vertex normals.
In progressiveLightMap, however, the material gets replaced with the internal uvMat, which has flatShading = false.
As a result, the object renders completely black.

Image

Geometry without vertex normals and material without flatShading

In this case, the code does not raise any error or warning.
This leads to silent failures and incorrect results during rendering.

Reproduction steps

1.Copy the provided HTML code into a local file (e.g. index.html).
2.Download the model from Horse.glb.
3.Open index.html in a browser.
4.Use the Choose File button to select the downloaded Horse.glb.
5.Click Load Model.

Code

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>three.js webgl - progressive lightmap accumulation</title>
    <meta charset="utf-8" />
    <meta
      name="viewport"
      content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0"
    />
    <style>
      body {
        margin: 0;
        font-family: Arial, sans-serif;
      }
      #upload-container {
        position: absolute;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: rgba(255, 255, 255, 0.9);
        padding: 20px;
        border-radius: 10px;
        text-align: center;
        z-index: 1000;
        box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      }
      #upload-container.hidden {
        display: none;
      }
      #file-input {
        display: none;
      }
      #file-select-btn {
        margin: 10px 0;
        padding: 10px 20px;
        border: 2px dashed #ccc;
        border-radius: 5px;
        background: #f9f9f9;
        cursor: pointer;
        display: inline-block;
        transition: background-color 0.3s;
      }
      #file-select-btn:hover {
        background: #e9e9e9;
      }
      #file-name {
        margin: 10px 0;
        font-style: italic;
        color: #666;
      }
      #upload-btn {
        background: #007cff;
        color: white;
        border: none;
        padding: 10px 20px;
        border-radius: 5px;
        cursor: pointer;
        font-size: 14px;
      }
      #upload-btn:hover {
        background: #0056b3;
      }
      #upload-btn:disabled {
        background: #ccc;
        cursor: not-allowed;
      }
      #loading {
        display: none;
        margin-top: 10px;
        color: #666;
      }
    </style>
  </head>
  <body>
    <div id="container"></div>
    <div id="info">
      <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a>
      - Progressive Lightmaps by
      <a href="https://github.com/zalo" target="_blank" rel="noopener">zalo</a
      ><br />
      [Inspired by
      <a
        href="http://madebyevan.com/shaders/lightmap/"
        target="_blank"
        rel="noopener"
        >evanw's Lightmap Generation</a
      >]
    </div>

    <!-- File Upload Interface -->
    <div id="upload-container">
      <h3>Upload 3D Model File</h3>
      <p>Supported formats: .glb, .gltf</p>
      <input type="file" id="file-input" accept=".glb,.gltf" />
      <div id="file-select-btn">Choose File</div>
      <div id="file-name">No file selected</div>
      <button id="upload-btn" disabled>Load Model</button>
      <div id="loading">Loading model...</div>
    </div>

    <script type="importmap">
      {
        "imports": {
          "three": "https://unpkg.com/[email protected]/build/three.module.js",
          "three/addons/": "https://unpkg.com/[email protected]/examples/jsm/"
        }
      }
    </script>

    <script type="module">
      import * as THREE from "three";
      import { GUI } from "three/addons/libs/lil-gui.module.min.js";
      import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
      import { OrbitControls } from "three/addons/controls/OrbitControls.js";
      import { TransformControls } from "three/addons/controls/TransformControls.js";
      import { ProgressiveLightMap } from "three/addons/misc/ProgressiveLightMap.js";

      // ShadowMap + LightMap Res and Number of Directional Lights
      const shadowMapRes = 512,
        lightMapRes = 1024,
        lightCount = 8;
      let camera,
        scene,
        renderer,
        controls,
        object = new THREE.Mesh(),
        lightOrigin = null,
        progressiveSurfacemap;
      const dirLights = [],
        lightmapObjects = [];
      const params = {
        Enable: true,
        "Blur Edges": true,
        "Blend Window": 200,
        "Light Radius": 50,
        "Ambient Weight": 0.5,
        "Debug Lightmap": true,
      };

      // File upload related elements
      const uploadContainer = document.getElementById("upload-container");
      const fileInput = document.getElementById("file-input");
      const uploadBtn = document.getElementById("upload-btn");
      const loadingDiv = document.getElementById("loading");

      init();
      setupFileUpload();

      /**
       * Initialize Three.js scene
       */
      function init() {
        // renderer
        renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setAnimationLoop(animate);
        renderer.shadowMap.enabled = true;
        document.body.appendChild(renderer.domElement);

        // camera
        camera = new THREE.PerspectiveCamera(
          70,
          window.innerWidth / window.innerHeight,
          1,
          1000
        );
        camera.position.set(0, 100, 200);
        camera.name = "Camera";

        // scene
        scene = new THREE.Scene();
        scene.background = new THREE.Color(0xebebeb);
        scene.fog = new THREE.Fog(0x949494, 1000, 3000);

        // progressive lightmap
        progressiveSurfacemap = new ProgressiveLightMap(renderer, lightMapRes);

        // directional lighting "origin"
        lightOrigin = new THREE.Group();
        lightOrigin.position.set(60, 150, 100);
        scene.add(lightOrigin);

        // create 8 directional lights to speed up the convergence
        for (let l = 0; l < lightCount; l++) {
          const dirLight = new THREE.DirectionalLight(
            0xffffff,
            Math.PI / lightCount
          );
          dirLight.name = "Dir. Light " + l;
          dirLight.position.set(200, 200, 200);
          dirLight.castShadow = true;
          dirLight.shadow.camera.near = 100;
          dirLight.shadow.camera.far = 5000;
          dirLight.shadow.camera.right = 150;
          dirLight.shadow.camera.left = -150;
          dirLight.shadow.camera.top = 150;
          dirLight.shadow.camera.bottom = -150;
          dirLight.shadow.mapSize.width = shadowMapRes;
          dirLight.shadow.mapSize.height = shadowMapRes;
          lightmapObjects.push(dirLight);
          dirLights.push(dirLight);
        }

        // ground
        const groundMesh = new THREE.Mesh(
          new THREE.PlaneGeometry(600, 600),
          new THREE.MeshPhongMaterial({ color: 0xffffff, depthWrite: true })
        );
        groundMesh.position.y = -0.1;
        groundMesh.rotation.x = -Math.PI / 2;
        groundMesh.name = "Ground Mesh";
        groundMesh.material.color = new THREE.Color(0xebebeb);
        scene.add(groundMesh);
        lightmapObjects.push(groundMesh);

        // controls
        controls = new OrbitControls(camera, renderer.domElement);
        controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
        controls.dampingFactor = 0.05;
        controls.screenSpacePanning = true;
        controls.minDistance = 100;
        controls.maxDistance = 500;
        controls.maxPolarAngle = Math.PI / 1.5;
        controls.target.set(0, 100, 0);

        window.addEventListener("resize", onWindowResize);
      }

      /**
       * Setup file upload functionality
       */
      function setupFileUpload() {
        const fileSelectBtn = document.getElementById("file-select-btn");
        const fileName = document.getElementById("file-name");

        // Custom file select button click event
        fileSelectBtn.addEventListener("click", function () {
          fileInput.click();
        });

        // File selection event
        fileInput.addEventListener("change", function (event) {
          const file = event.target.files[0];
          if (file) {
            fileName.textContent = file.name;
            uploadBtn.disabled = false;
          } else {
            fileName.textContent = "No file selected";
            uploadBtn.disabled = true;
          }
        });

        // Upload button click event
        uploadBtn.addEventListener("click", function () {
          const file = fileInput.files[0];
          if (file) {
            loadModel(file);
          }
        });
      }

      /**
       * Load 3D model file
       * @param {File} file - User selected file
       */
      function loadModel(file) {
        // Show loading state
        loadingDiv.style.display = "block";
        uploadBtn.disabled = true;

        const loader = new GLTFLoader();
        const url = URL.createObjectURL(file);

        loader.load(
          url,
          function (gltf) {
            // Clean up previous model
            if (object && object.parent) {
              scene.remove(object);
            }

            // Clean up old models from lightmapObjects
            for (let i = lightmapObjects.length - 1; i >= 0; i--) {
              if (lightmapObjects[i].name === "Loaded Mesh") {
                lightmapObjects.splice(i, 1);
              }
            }

            object = gltf.scene.children[0] || gltf.scene;
            object.position.set(0, 50, 0);

            object.traverse(function (child) {
              if (child.isMesh) {
                child.name = "Loaded Mesh";
                lightmapObjects.push(child);
              } else {
                child.layers.disableAll(); // Disable Rendering for this
              }
            });

            progressiveSurfacemap.addObjectsToLightMap(lightmapObjects);
            scene.add(object);

            // Hide upload interface and create GUI
            uploadContainer.classList.add("hidden");
            createGUI();

            // Release object URL
            URL.revokeObjectURL(url);
          },
          function (progress) {
            // Loading progress can be handled here if needed
          },
          function (error) {
            alert(
              "Model loading failed. Please check if the file format is correct."
            );
            loadingDiv.style.display = "none";
            uploadBtn.disabled = false;
            URL.revokeObjectURL(url);
          }
        );
      }

      /**
       * Create GUI control panel
       */
      function createGUI() {
        const gui = new GUI({ title: "Accumulation Settings" });
        gui.add(params, "Enable");
        gui.add(params, "Blur Edges");
        gui.add(params, "Blend Window", 1, 500).step(1);
        gui.add(params, "Light Radius", 0, 200).step(10);
        gui.add(params, "Ambient Weight", 0, 1).step(0.1);
        gui.add(params, "Debug Lightmap");
      }

      /**
       * Handle window resize
       */
      function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(window.innerWidth, window.innerHeight);
      }

      /**
       * Animation loop function
       */
      function animate() {
        // Update the inertia on the orbit controls
        controls.update();

        // Accumulate Surface Maps
        if (params["Enable"]) {
          progressiveSurfacemap.update(
            camera,
            params["Blend Window"],
            params["Blur Edges"]
          );

          if (!progressiveSurfacemap.firstUpdate) {
            progressiveSurfacemap.showDebugLightmap(params["Debug Lightmap"]);
          }
        }

        // Manually Update the Directional Lights
        for (let l = 0; l < dirLights.length; l++) {
          if (Math.random() > params["Ambient Weight"]) {
            dirLights[l].position.set(
              lightOrigin.position.x + Math.random() * params["Light Radius"],
              lightOrigin.position.y + Math.random() * params["Light Radius"],
              lightOrigin.position.z + Math.random() * params["Light Radius"]
            );
          } else {
            const lambda = Math.acos(2 * Math.random() - 1) - 3.14159 / 2.0;
            const phi = 2 * 3.14159 * Math.random();
            dirLights[l].position.set(
              Math.cos(lambda) * Math.cos(phi) * 300 + object.position.x,
              Math.abs(Math.cos(lambda) * Math.sin(phi) * 300) +
                object.position.y +
                20,
              Math.sin(lambda) * 300 + object.position.z
            );
          }
        }
        renderer.render(scene, camera);
      }
    </script>
  </body>
</html>

Live example

none

Version

@0.160.0/

Device

No response

Browser

No response

OS

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions