javascriptthree.js

Three.js: How to add maps to an OBJ object to get a photorealistic metallic material?


For a little project, I would like to add different maps to an OBJ object in a Three.js 3D scene to get a photorealistic metallic effect. Unfortunately, I have some problems with it.

Directly embedding the code here in a working way doesn't work. So I created this as template: https://codepen.io/Anna_B/pen/NWroEMP

The material should look like here, if you add under THREE.MeshStandardMaterial the envMaps, map, and roughnessMap.

I have tried to write it like this:

import * as THREE from "https://threejs.org/build/three.module.js";

import {
  OBJLoader
} from "https://threejs.org/examples/jsm/loaders/OBJLoader.js";

var container;

var camera, scene, renderer;

var mouseX = 0,
  mouseY = 0;

var windowHalfX = window.innerWidth / 2;
var windowHalfY = window.innerHeight / 2;


const textureLoader = new TextureLoader();

const envMaps = (function() {

  const path = '../../examples/textures/cube/SwedishRoyalCastle/';
  const format = '.jpg';
  const urls = [
    path + 'px' + format, path + 'nx' + format,
    path + 'py' + format, path + 'ny' + format,
    path + 'pz' + format, path + 'nz' + format
  ];

  const reflectionCube = cubeTextureLoader.load(urls);
  reflectionCube.format = RGBFormat;

  const refractionCube = cubeTextureLoader.load(urls);
  refractionCube.mapping = CubeRefractionMapping;
  refractionCube.format = RGBFormat;

  return {
    none: null,
    reflection: reflectionCube,
    refraction: refractionCube
  };

})();


const roughnessMaps = (function() {

  const bricks = textureLoader.load('../../examples/textures/brick_roughness.jpg');
  bricks.wrapT = RepeatWrapping;
  bricks.wrapS = RepeatWrapping;
  bricks.repeat.set(9, 1);

  return {
    none: null,
    bricks: bricks
  };

})();


var object;

init();
animate();

function init() {
  container = document.createElement("div");
  container.className = "object";
  document.body.appendChild(container);

  camera = new THREE.PerspectiveCamera(
    45,
    window.innerWidth / window.innerHeight,
    1,
    2000
  );
  camera.position.z = 250;

  // scene

  scene = new THREE.Scene();

  var ambientLight = new THREE.AmbientLight(0xcccccc, 0.4);
  scene.add(ambientLight);

  var pointLight = new THREE.PointLight(0xffffff, 2);
  pointLight.position.set(100, 100, 50);

  camera.add(pointLight);
  scene.add(camera);

  // manager

  function loadModel() {
    object.traverse(function(child) {
      //This allow us to check if the children is an instance of the Mesh constructor
      if (child instanceof THREE.Mesh) {
        child.material = new THREE.MeshStandardMaterial({
          color: "#555",
          roughness: 0.1,
          metalness: 0.4,
          texture: textureLoader,
          envMap: envMaps,
          roughnessMaps: roughnessMap

        });
        child.material.flatShading = false;

        //Sometimes there are some vertex normals missing in the .obj files, ThreeJs will compute them
      }
    });
    object.position.y = -90;
    scene.add(object);
  }

  var manager = new THREE.LoadingManager(loadModel);

  manager.onProgress = function(item, loaded, total) {
    console.log(item, loaded, total);
  };

  // model

  function onProgress(xhr) {
    if (xhr.lengthComputable) {
      var percentComplete = (xhr.loaded / xhr.total) * 100;
      console.log("model " + Math.round(percentComplete, 2) + "% downloaded");
    }
  }

  function onError() {}

  var loader = new OBJLoader(manager);

  loader.load(
    "https://threejs.org/examples/models/obj/female02/female02.obj",
    function(obj) {
      object = obj;
    },
    onProgress,
    onError
  );

  //

  renderer = new THREE.WebGLRenderer({
    alpha: true
  });
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  container.appendChild(renderer.domElement);

  document.addEventListener("mousemove", onDocumentMouseMove, false);

  //

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

function onWindowResize() {
  windowHalfX = window.innerWidth / 2;
  windowHalfY = window.innerHeight / 2;

  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth, window.innerHeight);
}

function onDocumentMouseMove(event) {
  mouseX = (event.clientX - windowHalfX) / 2;
  mouseY = (event.clientY - windowHalfY) / 2;
}

//

function animate() {
  requestAnimationFrame(animate);
  render();
}

function render() {
  camera.position.x += (mouseX - camera.position.x) * 0.05;
  camera.position.y += (-mouseY - camera.position.y) * 0.05;

  camera.lookAt(scene.position);

  renderer.render(scene, camera);
}

I think something is completely wrong. It would be sooooo nice if somebody could help me! I would be so thankful!!


Solution

  • If you're looking for a ThreeJS tool to experiment with metallic effects, try...

    https://threejs.org/examples/webgl_materials_displacementmap.html

    ...which includes a set of controls to easily adjust critical mesh material parameters, and immediately see the affects. After using this tool, in your specific example, I came up with the following parameters for your mesh material, giving the object a very photo realistic metallic effect, and changing the color of the object to "#88f" for a bluish tint...

    THREE.MeshStandardMaterial({
        color: "#88f",
        metalness: 1,
        roughness: 0.37,
        aoMapIntensity: 1.0,
        ambientIntensity: 0.42,
        envMapIntensity: 2.2,
        displacementScale: 2.1,
        normalScale: 1
    });
    

    Obviously you'll have to experiment with the aforementioned tool to obtain the specific metallic effect you desire. Hopefully this assists your efforts...

    EDIT The environment map feature depends on environment images that are eventually reflected or refracted onto the object. The codepen example appears to be missing this critical element (ie, the THREE.CubeTextureLoader()). Based on the code in the question, it appears that it was originally adapted from https://threejs.org/examples/#webgl_materials_cubemap, which I believe has the necessary elements of the desired environment map concept. Using that ThreeJS example as a baseline, and injecting the female02.obj model into the scene, we arrive at...

    Note that the female02.obj is composed of over a dozen meshes, so the object has to be cloned() and then the mesh material set for each of the individual meshes.

    <html lang="en">
      <head>
        <meta charset="utf-8" />
        <title>Three.js ESM + Import Map (Stack Snippets)</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <style>
          html, body { margin:0; height:100%; }
          #container { width:100%; height:100%; overflow:hidden; }
          #info {
            position:absolute; top:0; left:0; padding:8px 10px;
            background:rgba(0,0,0,0.5); color:#fff; font:13px/1.3 system-ui,sans-serif;
          }
          #info a { color:#8cf; }
        </style>
    
        <!-- Import map tells the browser how to resolve 'three' and addons -->
        <script type="importmap">
        {
          "imports": {
            "three": "https://cdn.jsdelivr.net/npm/three@0.159.0/build/three.module.js",
            "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.159.0/examples/jsm/"
          }
        }
        </script>
      </head>
      <body>
        <div id="container"></div>
        <div id="info">
          <a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> – cube mapping (ESM).<br/>
          Texture by <a href="http://www.humus.name/index.php?page=Textures" target="_blank" rel="noopener">Humus</a>
        </div>
    
        <script type="module">
          // Now we can use bare specifiers thanks to the import map
          import * as THREE from 'three';
          import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
          import { OBJLoader } from 'three/addons/loaders/OBJLoader.js';
    
          let container, camera, scene, renderer, pointLight;
    
          init();
          animate();
    
          function init() {
            container = document.getElementById('container');
    
            // Renderer
            renderer = new THREE.WebGLRenderer({ antialias: true });
            renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
            renderer.setSize(window.innerWidth, window.innerHeight);
            renderer.outputColorSpace = THREE.SRGBColorSpace;
            container.appendChild(renderer.domElement);
    
            // Camera
            camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 1, 5000);
            camera.position.z = 2000;
    
            // Scene
            scene = new THREE.Scene();
    
            // Cubemap
            const path = 'https://threejs.org/examples/textures/cube/SwedishRoyalCastle/';
            const files = ['px','nx','py','ny','pz','nz'].map(f => `${path}${f}.jpg`);
            const cubeLoader = new THREE.CubeTextureLoader();
            const reflectionCube = cubeLoader.load(files);
            const refractionCube = cubeLoader.load(files);
            reflectionCube.colorSpace = THREE.SRGBColorSpace;
            refractionCube.colorSpace = THREE.SRGBColorSpace;
            refractionCube.mapping = THREE.CubeRefractionMapping;
            scene.background = reflectionCube;
    
            // Lights
            scene.add(new THREE.AmbientLight(0xffffff, 0.6));
            pointLight = new THREE.PointLight(0xffffff, 1.4);
            pointLight.position.set(0, 0, 1000);
            scene.add(pointLight);
    
            // Materials (modern settings)
            const matReflectWhite = new THREE.MeshLambertMaterial({
              color: 0xffffff, envMap: reflectionCube, envMapIntensity: 1.0
            });
            const matRefractYellow = new THREE.MeshLambertMaterial({
              color: 0xffee00, envMap: refractionCube, refractionRatio: 0.95, envMapIntensity: 1.0
            });
            const matReflectOrange = new THREE.MeshLambertMaterial({
              color: 0xff6600, envMap: reflectionCube, envMapIntensity: 0.3
            });
    
            // Model
            const loader = new OBJLoader();
            loader.setPath('https://threejs.org/examples/models/obj/female02/');
            loader.load('female02.obj', (object) => {
              object.scale.multiplyScalar(10);
              object.position.y = -1200;
    
              const a = object.clone();
              a.traverse(o => { if (o.isMesh) o.material = matReflectWhite; });
              scene.add(a);
    
              const b = object.clone();
              b.position.x = -900;
              b.traverse(o => { if (o.isMesh) o.material = matRefractYellow; });
              scene.add(b);
    
              const c = object.clone();
              c.position.x = 900;
              c.traverse(o => { if (o.isMesh) o.material = matReflectOrange; });
              scene.add(c);
            });
    
            // Controls
            const controls = new OrbitControls(camera, renderer.domElement);
            controls.enableZoom = false;
            controls.enablePan = false;
            controls.minPolarAngle = Math.PI / 4;
            controls.maxPolarAngle = Math.PI / 1.5;
    
            window.addEventListener('resize', onWindowResize);
          }
    
          function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(window.innerWidth, window.innerHeight);
          }
    
          function animate() {
            requestAnimationFrame(animate);
            pointLight.position.copy(camera.position).add(new THREE.Vector3(0,0,100));
            renderer.render(scene, camera);
          }
        </script>
      </body>
    </html>