MeshStandardMaterial with color, metalness, and roughness applied to cube shows up as black

I’ve been having problems getting the MeshStandardMaterial to display my colorMap, metalnessMap, and roughnessMap textures I’ve baked, with the material instead showing up as black. So, I created a simple demo involving a red, metallic, reflective cube to demonstrate.
In Blender:
In Blender
In threeJs, imported as glb:
In threeJs

import * as THREE from "three"
import {GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader.js"
import {RGBELoader} from "three/examples/jsm/loaders/RGBELoader.js"
import {OrbitControls} from "three/examples/jsm/controls/OrbitControls.js"

(async () => {
    const gScene = new THREE.Scene()
    const canvas = document.querySelector("#c")
    const gCamera = new THREE.PerspectiveCamera(50, canvas.clientWidth/canvas.clientHeight, 0.1, 1000)
    const gltfLoader = new GLTFLoader()
    const textureLoader = new THREE.TextureLoader()

    const gRenderer = new THREE.WebGLRenderer({canvas})
    gRenderer.setPixelRatio( window.devicePixelRatio );
    gRenderer.setSize( window.innerWidth, window.innerHeight );
    gRenderer.toneMapping = THREE.ACESFilmicToneMapping;
    gRenderer.toneMappingExposure = 1;
    gRenderer.outputEncoding = THREE.sRGBEncoding;

    gCamera.position.x = 5
    gCamera.position.z = 5
    const controls = new OrbitControls(gCamera, canvas)
    controls.target.set(0,0,0)
    controls.update()
    const pmremGenerator = new THREE.PMREMGenerator( gRenderer );
    pmremGenerator.compileEquirectangularShader();
    const rgbeLoader = new RGBELoader()
    const envmap = await new Promise((resolve,reject) => {
        rgbeLoader
        .setDataType(THREE.UnsignedByteType)
        .load("./cubemap.hdr",
    (tex)=> {
        const envMap = pmremGenerator.fromEquirectangular(tex).texture
        gScene.background = envMap
        gScene.environment = envMap
        resolve(envMap)
    } )

})

    const colorMap = textureLoader.load("ColorMap.png")
    const metalnessMap = textureLoader.load("MetalnessMap.png")
    const roughnessMap = textureLoader.load("RoughnessMap.png")
    colorMap .flipY = false
    metalnessMap.flipY = false
    roughnessMap.flipY = false
    const cubeMaterial = new THREE.MeshStandardMaterial({map: colorMap, metalness: 1,metalnessMap: metalnessMap, roughness: 1,roughnessMap: roughnessMap, envMap: envmap})

    gltfLoader.load('./scene.glb', (glb) => {
            glb.scene.traverse((child) => {
                if(child.type==='Mesh' ){
                    child.material = cubeMaterial 
                    gScene.add(child)
                }

            })

    })

    window.addEventListener('resize', onWindowResize)
    function onWindowResize(){
        gCamera.aspect = window.innerWidth / window.innerHeight
        gCamera.updateProjectionMatrix()
        gRenderer.setSize(window.innerWidth, window.innerHeight)
    }
    function render(){
        gRenderer.render(gScene,gCamera)
        requestAnimationFrame(render)
    }

    requestAnimationFrame(render)
})()

ColorMap.png:ColorMap.png - Google Drive
RoughnessMap.png: RoughnessMap.png - Google Drive
MetalnessMap.png: MetalnessMap.png - Google Drive
scene.glb: scene.glb - Google Drive
cubemap.hdr: cubemap.hdr - Google Drive

1 Like

Physically Based Rendering and Lighting | Discover three.js

“To use PBR materials such as the MeshStandardMaterial , we must add a light to the scene. This makes sense - if there is no light, we cannot see.”

1 Like

I specified an environment map though

I read that entire article, and added a light to the scene, but my imported cube mesh still show up as black. What’s weird is that if I create a new BoxBufferGeometry() cube mesh in threJs, instead of using my imported version, that cube shows up, and allows me to modify its metalness, and roughness.

Cube imported from Blender vs BoxBufferGeometry():

Code:


import * as THREE from "three"
import {GLTFLoader} from "three/examples/jsm/loaders/GLTFLoader.js"
import {RGBELoader} from "three/examples/jsm/loaders/RGBELoader.js"
import {OrbitControls} from "three/examples/jsm/controls/OrbitControls.js"
import { BoxBufferGeometry, Mesh, MeshStandardMaterial } from "three"

(async () => {
    const gScene = new THREE.Scene()
    const canvas = document.querySelector("#c")
    const gCamera = new THREE.PerspectiveCamera(50, canvas.clientWidth/canvas.clientHeight, 0.1, 1000)
    const gltfLoader = new GLTFLoader()
    const textureLoader = new THREE.TextureLoader()

    

    const gRenderer = new THREE.WebGLRenderer({canvas})
    gRenderer.setPixelRatio( window.devicePixelRatio );
    gRenderer.setSize( window.innerWidth, window.innerHeight );
    gRenderer.toneMapping = THREE.ACESFilmicToneMapping;
    gRenderer.physicallyCorrectLights = true
    gRenderer.toneMappingExposure = 1;
    gRenderer.outputEncoding = THREE.sRGBEncoding;
    const dLight = new THREE.DirectionalLight('white', 8)
    dLight.position.set(10,10,10)
    gScene.add(dLight)
    gCamera.position.x = 5
    gCamera.position.z = 5
    const controls = new OrbitControls(gCamera, canvas)

    controls.target.set(0,0,0)
    controls.update()
    const pmremGenerator = new THREE.PMREMGenerator( gRenderer );
    pmremGenerator.compileEquirectangularShader();
    const rgbeLoader = new RGBELoader()
    const envmap = await new Promise((resolve,reject) => {
        rgbeLoader
        .setDataType(THREE.UnsignedByteType)
        .load("./cubemap.hdr",
    (tex)=> {
        const envMap = pmremGenerator.fromEquirectangular(tex).texture
        gScene.background = envMap
        gScene.environment = envMap
        resolve(envMap)
    } )
})

    const colorMap = textureLoader.load("ColorMap.png")
    const metalnessMap = textureLoader.load("MetalnessMap.png")
    const roughnessMap = textureLoader.load("RoughnessMap.png")
    colorMap .flipY = false
    metalnessMap.flipY = false
    roughnessMap.flipY = false
    const material = new MeshStandardMaterial({color: 'purple', roughness: 0, metalness: 1})
    gltfLoader.load('./scene.glb', (glb) => {
            glb.scene.traverse((child) => {
                if(child.type==='Mesh' ){
                    child.material = material
                    gScene.add(child)
                }
            })
    })

    const geometry = new BoxBufferGeometry(2,2,2)
    const cube = new Mesh(geometry, material)
    cube.position.x = 3
    cube.position.z = -3
    gScene.add(cube)
    window.addEventListener('resize', onWindowResize)
    function onWindowResize(){
        gCamera.aspect = window.innerWidth / window.innerHeight
        gCamera.updateProjectionMatrix()
        gRenderer.setSize(window.innerWidth, window.innerHeight)

    }

    function render(){
        gRenderer.render(gScene,gCamera)
        requestAnimationFrame(render)
    }
    requestAnimationFrame(render)

})()

Note that the scalar properties you specify on the material (color, roughness, metalness) are multiplied against the texture values. Two dark colors, like solid ‘purple’ multiplied by a dark color map, are going to give you even darker results. If you want textures shown “as-is” then use properties like:

  • color: 0xFFFFFF
  • metalness: 1
  • roughness: 1

You may find it easier to configure your material adding textures in Blender or the three.js editor, where you can preview the results and export the final GLB to be used in your code.

1 Like

Yeah, but In the second code snippit, where I apply the same MeshStandardMaterial({color: 'purple', roughness: 0, metalness: 1}) to the imported glb cube, and the BoxBufferGeometry() cube, the material only shows up on the second cube(as shown in the screenshot). Why would that be? What could be wrong/different with my simple cube exported as a glb from Blender as opposed to a BoxBufferGeometry()?

Different vertex normals and UVs, most likely. Occasionally vertex colors but I don’t see them here.

You may also want to set…

material.flatShading = true

… on the new material, because the GLB does not include vertex normals. This would be set automatically on the material returned initially from GLTFLoader.

3 Likes

because the GLB does not include vertex normals

That was it. Up until now I’ve only been using MeshBasicMaterial, so I haven’t needed to check the “export normals” box when exporting as gltf. However, with that box checked, it now works! Thanks so much!

1 Like