I've been using the Add a 3D Model Mapbox GL example as a template to add 3D objects to my Mapbox map. It works but sometimes has issues with textures.
I'm loading an OBJ file and adding it to the scene. Here's what the model looks like in a plain THREE.js scene:
Here's a distilled version of the code to render that scene (see gist for full details):
async function addOBJ(path, mtlPath) {
const loader = new OBJLoader2();
const matLoader = new MTLLoader();
matLoader.load(mtlPath, mtlParseResult => {
const materials = MtlObjBridge.addMaterialsFromMtlLoader(mtlParseResult);
loader.addMaterials(materials);
loader.load(path, group => {
scene.add(group);
});
});
}
function init() {
camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 0.001, 1000 );
camera.position.set(86, 100, -5);
scene = new THREE.Scene();
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setSize( window.innerWidth, window.innerHeight );
document.body.appendChild( renderer.domElement );
addOBJ('silo_p2.obj', 'silo_p2.mtl');
}
When I try to add the same model in my Mapbox map, the texture comes out oddly:
What's going on in my THREE.js + Mapbox scene? It looks like the texture has turned to noise.
Full source available here, but this is the gist:
function getSpriteMatrix(coord, sceneCenter) {
const rotationX = new THREE.Matrix4().makeRotationAxis(new THREE.Vector3(1, 0, 0), 90 * Math.PI / 180);
const s = sceneCenter.meterInMercatorCoordinateUnits();
return new THREE.Matrix4()
.makeTranslation(coord.x - sceneCenter.x, coord.y - sceneCenter.y, coord.z - sceneCenter.z)
.scale(new THREE.Vector3(s, -s, s))
.multiply(rotationX);
}
class SpriteCustomLayer {
type = 'custom';
renderingMode = '3d';
constructor(id) {
this.id = id;
this.model = loadObj('silo_p2.obj', 'silo_p2.mtl');
}
async onAdd(map, gl) {
this.camera = new THREE.Camera();
const centerLngLat = map.getCenter();
this.center = MercatorCoordinate.fromLngLat(centerLngLat, 0);
const {x, y, z} = this.center;
this.cameraTransform = new THREE.Matrix4().makeTranslation(x, y, z);
this.map = map;
this.scene = this.makeScene();
// use the Mapbox GL JS map canvas for three.js
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true,
});
// From https://threejs.org/docs/#examples/en/loaders/GLTFLoader
this.renderer.gammaOutput = true;
this.renderer.gammaFactor = 2.2;
this.renderer.autoClear = false;
this.addModel();
}
makeScene() {
const scene = new THREE.Scene();
scene.add(new THREE.AmbientLight( 0xffffff, 0.25 ));
for (const y of [-70, 70]) {
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(0, y, 100).normalize();
scene.add(directionalLight);
}
return scene;
}
async addModel() {
const model = await this.model;
flipSides(model);
const scene = model.clone();
const matrix = getSpriteMatrix(
MercatorCoordinate.fromLngLat([-74.0445, 40.6892], 0),
this.center,
);
scene.applyMatrix(matrix);
this.scene.add(scene);
}
render(gl, matrix) {
this.camera.projectionMatrix = new THREE.Matrix4()
.fromArray(matrix)
.multiply(this.cameraTransform);
this.renderer.state.reset();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
}
}
map.on('load', () => {
const layer = new SpriteCustomLayer('statue');
map.addLayer(layer);
});
How can I get the texture to look right on this Object in the Mapbox view? I'm using THREE.js r109 and Mapbox GL JS v1.4.0. Because of where these models are coming from, using a different format like GLTF isn't an option for me.
Related
I downloaded the following model from sketchfab and imported to my scene but model does not seem looking correctly especially the glass blur.
here is my code
import "./sass/main.scss";
import { Scene, PerspectiveCamera, WebGLRenderer, DirectionalLight, ACESFilmicToneMapping, sRGBEncoding, Object3D, Mesh, MeshStandardMaterial, ReinhardToneMapping, AmbientLight, EquirectangularReflectionMapping,
} from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { RGBELoader } from "three/examples/jsm/loaders/RGBELoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const scene = new Scene();
const camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new WebGLRenderer({
canvas: document.querySelector("canvas#webgl"),
antialias: true
});
renderer.toneMapping = ACESFilmicToneMapping;
renderer.outputEncoding = sRGBEncoding;
renderer.physicallyCorrectLights = true;
renderer.toneMappingExposure = 1
renderer.shadowMap.enabled = true;
const controls = new OrbitControls(camera, renderer.domElement);
camera.rotation.reorder("YXZ")
camera.position.set(2, 1.5, 3.5);
camera.rotation.set(-0.25, Math.PI * 0.25, 0);
renderer.setSize(window.innerWidth, window.innerHeight)
const gltfLoader = new GLTFLoader();
const rgbeLoader = new RGBELoader();
const environmentMap = await rgbeLoader.loadAsync("./assets/environment/puresky.hdr")
environmentMap.mapping = EquirectangularReflectionMapping;
scene.environment = environmentMap;
await gltfLoader.loadAsync("./assets/models/scene.gltf").then(gltf => {
scene.add(gltf.scene);
});
mapAllElements();
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
function mapAllElements() {
scene.traverse((child) => {
if (child instanceof Mesh && child.material instanceof MeshStandardMaterial) {
child.material.envMap = environmentMap;
child.material.envMapIntensity = 1;
child.material.needsUpdate = true;
}
})
}
I searched my problem on the internet but couldn't find anything useful and I also tried other gltf models from sketchfab but other models also does not looking properly.
My code is using top-level-await feature and I enabled from webpack.config.js
The way the model looks is tied to the Viewer in which it is loaded. If you want to recreate the effects you can create a new material using the THREE.MeshPhysicalMaterial class, which is a physically-based material that can simulate realistic lighting and materials.
A barebones example is as follows:
scene.traverse((child) => {
if (child instanceof Mesh && child.material instanceof MeshStandardMaterial) {
if (child.name.includes("glass")) {
// Create a new MeshPhysicalMaterial for the glass object
const glassMaterial = new THREE.MeshPhysicalMaterial({
color: 0xffffff,
metalness: 0,
roughness: 0.1,
transparent: true,
transmission: 0.9,
opacity: 0.7,
envMap: environmentMap,
envMapIntensity: 1,
side: THREE.DoubleSide,
});
// Replace the existing material with the new glass material
child.material = glassMaterial;
} else {
// For non-glass objects, just add the environment map
child.material.envMap = environmentMap;
child.material.envMapIntensity = 1;
}
child.material.needsUpdate = true;
}
});
}
If you don't want to re-invent the wheel, you can also take a look at WebGi, where you have much finer grained control through an accessible plugin system.
Im trying to add two textures. One for the wall and another for the floor. After rendering Im just getting a solid color instead of the texture.
Here is my scene & camera configuration:
const tempScene = new THREE.Scene();
tempScene.background = new THREE.Color(0x021D49);
const ambient = new THREE.AmbientLight(0xffffff, 0.6);
tempScene.add(ambient);
const directionalLight = new THREE.DirectionalLight(0xfafafa, 0.6);
directionalLight.position.set(0, 300, 300);
tempScene.add(directionalLight);
const tempCamera = new THREE.PerspectiveCamera(
60,
el.clientWidth / el.clientHeight,
0.1,
10000,
);
tempCamera.up.set(0, 0, 1);
tempCamera.position.set(0, 300, 300);
tempCamera.lookAt(0, 0, 0);
const tempControls = new OrbitControls(tempCamera, el);
tempControls.maxPolarAngle = Math.PI / 2;
tempControls.minDistance = 10;
tempControls.maxDistance = 500;
tempControls.enablePan = false;
const tempRenderer = new THREE.WebGLRenderer({ canvas: el, antialias: true });
tempRenderer.setSize(el.clientWidth, el.clientHeight);
tempRenderer.setPixelRatio(window.devicePixelRatio);
tempRenderer.shadowMap.enabled = true;
tempRenderer.shadowMap.type = THREE.PCFSoftShadowMap;
tempRenderer.outputEncoding = THREE.sRGBEncoding;
Here is the code for texture:
const floorTex = new THREE.TextureLoader().load(floorTexture);
const wallMaterial = new THREE.MeshBasicMaterial({ map: wallTex, side: THREE.DoubleSide });
const floorMaterial = new THREE.MeshStandardMaterial({ map: floorTex });
const lineMaterial = new THREE.LineBasicMaterial({ color: 0x000000 });
const wallGeometry = new THREE.BufferGeometry();
const wallVertices = new Float32Array([
-x1,
y1,
0, // vertex 1
-x1,
y1,
10, // vertex 2
-x2,
y2,
0, // vertex 3
-x2,
y2,
10, // vertex 4
]);
wallGeometry.setAttribute('position', new THREE.BufferAttribute(wallVertices, 3));
wallGeometry.setIndex([0, 2, 1, 2, 3, 1]);
const wall = new THREE.Mesh(wallGeometry, wallMaterial);
const geo = new THREE.EdgesGeometry(wall.geometry);
const wireframe = new THREE.LineSegments(geo, lineMaterial);
wall.add(wireframe);
wall.castShadow = true;
wall.receiveShadow = true;
scene!.add(wall);
const floorGeometry = new THREE.Shape(floorPoints.map((point) => new THREE.Vector2(point[0], point[1])));
const shapeGeometry = new THREE.ShapeGeometry(floorGeometry);
const floor = new THREE.Mesh(shapeGeometry, floorMaterial);
floor.receiveShadow = true;
scene!.add(floor);
This is how it looks
But in real the texture look like this:
floorTexture
wallTexture
Your wall does not get a texture applied since you define no texture coordinates for wallGeometry. Next to position you need a uv buffer attribute holding these data.
Unfortunately, the code snippet does not provide enough information to investigate why the floor has no texture. Instances of ShapeGeometry do have a texture coordinates so I could imagine there is a loading issue with your texture.
BTW: If you add tempRenderer.outputEncoding = THREE.sRGBEncoding; to your code, you have to set the encoding property of all color textures to THREE.sRGBEncoding as well. Otherwise your sRGB workflow is incomplete.
How to adjust the position of the model?
When the scene is created, the model does not land on the plane as the photo shows.
I want it to position itself in the middle on the plan.
Model position
public async createScene(canvas: ElementRef < HTMLCanvasElement > ) {
console.log('create scene function');
setTimeout(() => {
this.loadingMessage = 'Creating the scene';
}, 100);
this.isErrorHappened = false;
this.canvas = canvas.nativeElement;
this.renderer = new THREE.WebGLRenderer({
canvas: this.canvas,
alpha: true,
antialias: true
});
this.renderer.setSize(this.size.width, this.size.height);
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.scene = new THREE.Scene();
this.scene.background = new THREE.Color(0xffffff);
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.camera.position.x = -25;
this.camera.position.y = 15;
this.camera.position.z = 0;
this.ambientLight = new THREE.AmbientLight(this.lightColor, 0.5);
this.scene.add(this.ambientLight);
this.directionalLight = new THREE.DirectionalLight(this.lightColor, 1);
this.directionalLight.position.set(0, 10, 0);
this.directionalLight.target.position.set(-5, 0, 0);
this.scene.add(this.directionalLight);
this.scene.add(this.directionalLight.target);
this.gridHelper = new THREE.GridHelper(50, 30);
this.scene.add(this.gridHelper);
this.axes = new THREE.AxesHelper(20);
this.axes.renderOrder = 1;
this.scene.add(this.axes);
this.controls = new OrbitControls(this.camera, this.canvas);
}
By default, IFC.js locates the model in the position specified in the IFC file. This means that the position of that file is probably wrong.
IFC.js also provides a method to ignore this and just place the model in the center, while storing the original transform of the model: ifcLoader.ifcManager.applyWebIfcConfig(). Here you have a link to the official docs. You can use it like this:
await ifcLoader.ifcManager.applyWebIfcConfig({
COORDINATE_TO_ORIGIN: true
});
Just move the position of the object to the origin?
myObject.position.set(0,0,0);
I have 3D models as such:
I want to add a cast shadow similar to this:
And I have the following piece of code responsible for the model:
var ambLight = new THREE.AmbientLight( 0x404040 );
this.scene.add(ambLight)
var loader = new THREE.GLTFLoader();
loader.load(path,function (gltf) {
gltf.scene.traverse( function( model ) {
if (model.isMesh){
model.castShadow = true;
}
});
this.scene.add(gltf.scene);
}
I added the castSHadow part as seen in this StackOverflow post.
I've tried model.castShadow = true and I've tried removing the if condition and just leave the castShadow but that doesn't work either. Am I missing a step? The full custom layer code is here if it helps.
You only have an instance of AmbientLight in your scene which is no shadow-casting light.
3D objects can only receive shadow if they set Object3D.receiveShadow to true and if the material is not unlit. Meaning MeshBasicMaterial would not work as the ground's material.
You have to globally enable shadows via: renderer.shadowMap.enabled = true;
I suggest you have a closer look to the shadow setup of this official example.
Based on your code, you're trying to add a shadow on top of Mapbox.
For that, apart from the suggestions from #Mugen87, you'll need to create a surface to receive the shadow, and place it exactly below the model you're loading, considering also the size of the object you're loading to avoid the shadow goes out of the plane surface... and then you'll get this.
Relevant code in this fiddle I have created. I slightly changed the light and I added a light helper for clarity.
var customLayer = {
id: '3d-model',
type: 'custom',
renderingMode: '3d',
onAdd: function(map, gl) {
this.camera = new THREE.Camera();
this.scene = new THREE.Scene();
const dirLight = new THREE.DirectionalLight(0xffffff, 1);
dirLight.position.set(0, 70, 100);
let d = 1000;
let r = 2;
let mapSize = 8192;
dirLight.castShadow = true;
dirLight.shadow.radius = r;
dirLight.shadow.mapSize.width = mapSize;
dirLight.shadow.mapSize.height = mapSize;
dirLight.shadow.camera.top = dirLight.shadow.camera.right = d;
dirLight.shadow.camera.bottom = dirLight.shadow.camera.left = -d;
dirLight.shadow.camera.near = 1;
dirLight.shadow.camera.far = 400000000;
//dirLight.shadow.camera.visible = true;
this.scene.add(dirLight);
this.scene.add(new THREE.DirectionalLightHelper(dirLight, 10));
// use the three.js GLTF loader to add the 3D model to the three.js scene
var loader = new THREE.GLTFLoader();
loader.load(
'https://docs.mapbox.com/mapbox-gl-js/assets/34M_17/34M_17.gltf',
function(gltf) {
gltf.scene.traverse(function(model) {
if (model.isMesh) {
model.castShadow = true;
}
});
this.scene.add(gltf.scene);
// we add the shadow plane automatically
const s = new THREE.Box3().setFromObject(gltf.scene).getSize(new THREE.Vector3(0, 0, 0));
const sizes = [s.x, s.y, s.z];
const planeSize = Math.max(...sizes) * 10;
const planeGeo = new THREE.PlaneBufferGeometry(planeSize, planeSize);
const planeMat = new THREE.ShadowMaterial();
planeMat.opacity = 0.5;
let plane = new THREE.Mesh(planeGeo, planeMat);
plane.rotateX(-Math.PI / 2);
plane.receiveShadow = true;
this.scene.add(plane);
}.bind(this)
);
this.map = map;
// use the Mapbox GL JS map canvas for three.js
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true
});
this.renderer.autoClear = false;
this.renderer.shadowMap.enabled = true;
},
render: function(gl, matrix) {
var rotationX = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(1, 0, 0),
modelTransform.rotateX
);
var rotationY = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 1, 0),
modelTransform.rotateY
);
var rotationZ = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
modelTransform.rotateZ
);
var m = new THREE.Matrix4().fromArray(matrix);
var l = new THREE.Matrix4()
.makeTranslation(
modelTransform.translateX,
modelTransform.translateY,
modelTransform.translateZ
)
.scale(
new THREE.Vector3(
modelTransform.scale,
-modelTransform.scale,
modelTransform.scale
)
)
.multiply(rotationX)
.multiply(rotationY)
.multiply(rotationZ);
this.camera.projectionMatrix = m.multiply(l);
this.renderer.state.reset();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
}
};
I have a 3D gltf model of a building which I'm trying to put on a Mapbox Map, but when using MeshPhongMaterial or any other variation it just won't change the color.
Everything else works except the color.
Here's my code:
var modelOrigin = [17.1956,44.7871];
var modelAltitude = 0;
var modelRotate = [Math.PI / 2, 0, 0];
var modelAsMercatorCoordinate = mapboxgl.MercatorCoordinate.fromLngLat(
modelOrigin,
modelAltitude
);
// transformation parameters to position, rotate and scale the 3D model onto the map
var modelTransform = {
translateX: modelAsMercatorCoordinate.x,
translateY: modelAsMercatorCoordinate.y,
translateZ: modelAsMercatorCoordinate.z,
rotateX: modelRotate[0],
rotateY: modelRotate[1],
rotateZ: modelRotate[2],
/* Since our 3D model is in real world meters, a scale transform needs to be
* applied since the CustomLayerInterface expects units in MercatorCoordinates.
*/
scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits()
};
var THREE = window.THREE;
// configuration of the custom layer for a 3D model per the CustomLayerInterface
var customLayer = {
id: '3d-model',
type: 'custom',
renderingMode: '3d',
onAdd: function (map, gl) {
this.camera = new THREE.Camera();
this.scene = new THREE.Scene();
// create two three.js lights to illuminate the model
var directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(0, -70, 100).normalize();
this.scene.add(directionalLight);
var directionalLight2 = new THREE.DirectionalLight(0xffffff);
directionalLight2.position.set(0, 70, 100).normalize();
this.scene.add(directionalLight2);
// use the three.js GLTF loader to add the 3D model to the three.js scene
var loader = new THREE.GLTFLoader();
loader.load(
'Apartment Building_17_gltf.gltf',
function (gltf) {
this.scene.add(gltf.scene);
}.bind(this)
);
this.map = map;
// use the Mapbox GL JS map canvas for three.js
this.renderer = new THREE.WebGLRenderer({
canvas: map.getCanvas(),
context: gl,
antialias: true
});
this.renderer.autoClear = false;
},
render: function (gl, matrix) {
var material = new THREE.MeshStandardMaterial({color: 0x5da8de});
material.needsUpdate = true;
material.colorsNeedUpdate=true;
var rotationX = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(1, 0, 0),
modelTransform.rotateX
);
var rotationY = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 1, 0),
modelTransform.rotateY
);
var rotationZ = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
modelTransform.rotateZ
);
var m = new THREE.Matrix4().fromArray(matrix);
var l = new THREE.Matrix4()
.makeTranslation(
modelTransform.translateX,
modelTransform.translateY,
modelTransform.translateZ
)
.scale(
new THREE.Vector3(
modelTransform.scale/9,
-modelTransform.scale/9,
modelTransform.scale/9
)
)
.multiply(rotationX)
.multiply(rotationY)
.multiply(rotationZ);
//var pink = new THREE.Color(0xfca4c5);
//gltf.scene.Color = pink;
//var boja =
//var vel = new THREE.Matrix4.scale(new THREE.Vector3(0.5,0.5,0.5));
this.camera.projectionMatrix = m.multiply(l);
this.renderer.state.reset();
this.renderer.render(this.scene, this.camera);
this.map.triggerRepaint();
}
};
map.on('style.load', function () {
map.addLayer(customLayer, 'waterway-label');
If you want to change the material of a loaded glTF asset, do it like so:
gltf.scene.traverse( ( object ) => {
if ( object.isMesh ) object.material = newMaterial;
} );