Line disappear when camera move in three js - javascript

I'm trying to render d3 force graph in three.js, I'm using standard Line and BoxGeometry with photo texture on it. On force graph update I call draw function, which is also called on in
controls.addEventListener('change', () => {this.redraw()});
but when I move camera, some lines disappear, when I'm closer, it seems worst, besides it does not look like there is any rule, even when I'm close to graph, it look like lines to disappear are chosen at random.
There line is
And this is if I move camera a little in angle
This is from one side of the graph:
And this is when from other:
I tried to scale down units
And also frustum = false
Whole code:
import {
WebGLRenderer,
Scene,
PerspectiveCamera,
Texture,
MeshBasicMaterial,
SphereGeometry,
Mesh,
Geometry,
Vector3,
LineBasicMaterial,
Line,
LineSegments,
BoxGeometry,
TextureLoader
} from 'three';
import * as three from 'three';
import { ViewModel, Link, Node } from './Model';
import { event } from 'd3-selection';
import * as selection from 'd3-selection';
import { drag } from 'd3-drag';
// Old module syntax
declare function require(name:String);
let OrbitControls = require('./../../../node_modules/three-orbit-controls/index')(three);
interface IView {
render():void;
}
class ViewNode {
public vector:Vector3;
public mesh:Mesh;
public node:Node;
}
export class Full3DView implements IView {
private canvas: Element;
private renderer: WebGLRenderer;
private scene: Scene;
private lineMaterial: LineBasicMaterial;
private camera: PerspectiveCamera;
private controls: any;
private nodes:ViewNode[] = [];
private lines:Geometry[] = [];
constructor(private model:ViewModel) {
this.canvas = document.querySelector('#view3d2');
this.model.onChange(() => {this.render()});
}
render(): void {
this.buildScene();
this.model.simulation.on('tick', () => this.redraw());
this.model.linkForce.distance(40);
this.model.collideForce.radius(30);
}
private buildScene() {
this.scene = new Scene();
this.camera = new PerspectiveCamera( 90, window.innerWidth/window.innerHeight, 1, 20000 );
this.renderer = new WebGLRenderer();
this.renderer.setSize( this.canvas.clientWidth, this.canvas.clientHeight );
this.canvas.appendChild( this.renderer.domElement );
this.controls = new OrbitControls( this.camera, this.renderer.domElement);
this.controls.addEventListener('change', () => {this.redraw()});
this.lineMaterial = new LineBasicMaterial({ color: 0xccff00, linewidth: 3});
let vectorIndex:Map<String, Vector3> = new Map();
let textureLoader = new TextureLoader();
this.model.nodes.forEach((node:Node) => {
this.buildNode(vectorIndex, textureLoader, node);
});
this.model.links.forEach((link:Link) => {
this.buildEdge(vectorIndex, link);
});
this.camera.position.z = 5000;
}
private buildNode(vectorIndex:Map<String, Vector3>, textureLoader:TextureLoader, node:Node) {
let material = new MeshBasicMaterial();
let geometry = new BoxGeometry( 30, 30, 30);
let mesh = new Mesh( geometry, material );
mesh.lookAt(this.camera.position);
this.scene.add( mesh );
mesh.position.set(node.x, node.y, 0);
mesh.rotation.x += 1;
vectorIndex.set(node.index, mesh.position);
this.nodes.push({
vector: mesh.position,
mesh: mesh,
node: node
});
textureLoader.load('/data/images/' + node.id + '.jpg', (texture:Texture) => {
material.map = texture;
material.needsUpdate = true;
});
}
private buildEdge(vectorIndex:Map<String, Vector3>, link:Link) {
let geometry = new Geometry();
geometry.vertices.push(
vectorIndex.get(link.source.index).copy(vectorIndex.get(link.source.index).setZ(0)),
vectorIndex.get(link.target.index).copy(vectorIndex.get(link.target.index).setZ(0))
);
geometry.computeLineDistances();
this.lines.push(geometry);
let line = new Line(geometry, this.lineMaterial);
this.scene.add(line);
}
private redraw() {
this.nodes.forEach((node:ViewNode) => {
node.vector.setX(node.node.x * 10);
node.vector.setY(node.node.y * 10);
node.mesh.lookAt(this.camera.position);
node.mesh.frustumCulled = false;
});
this.lines.forEach((line:Geometry) => {
line.verticesNeedUpdate = true;
});
this.renderer.render(this.scene, this.camera)
}
}

The actual answer I was looking for is in the comment above from WestLangley.
A possible explanation for your issue is that when you update the vertices of a geometry, you should call geometry.computeBoundingSphere(). The renderer calls it for you on the first render call, but after that, if you modify vertices, the bounding sphere is no longer correct, and you need to update it. Alternatively, you can set mesh.frustumCulled = false;

I was unable to get it working using Line object, but if I use LineSegments and push all pairs of vertices to one Geometry, it is working well.
So in function buildScene I use instead of
this.lineMaterial = new LineBasicMaterial({ color: 0xccff00, linewidth: 3});
lines
this.linesGeometry = new Geometry();
this.scene.add(new LineSegments(this.linesGeometry, new LineBasicMaterial({ color: 0xccff00, linewidth: 3})));
and then content of buildEdge is
this.linesGeometry.vertices.push(
vectorIndex.get(link.source.index).copy(vectorIndex.get(link.source.index).setZ(0)),
vectorIndex.get(link.target.index).copy(vectorIndex.get(link.target.index).setZ(0))
);
and in redraw function I just do
this.linesGeometry.verticesNeedUpdate = true;
instead of
this.lines.forEach((line:Geometry) => {
line.verticesNeedUpdate = true;
});

Related

Gltf model does not looking properly in three.js

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.

Can't get material from a gltf loaded scene in threejs

I'd like to modify the material in a gltf loaded model to set its opacity/transparency.
I can't seem to get any meshes. I have created this fiddle that says 'no mesh found'. I have found quite a few examples of getting material and they all seem to do it this way (making sure it's mesh then getting the material).
<style>
canvas {
display: block;
width: 98%;
height: 98%;
}
</style>
<canvas id="canvasid"></canvas>
<script src="https://cdn.jsdelivr.net/npm/three#0.143/examples/js/controls/OrbitControls.js"></script>-->
<script async src="https://unpkg.com/es-module-shims#1.3.6/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three#0.143/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { GLTFLoader } from 'https://cdn.jsdelivr.net/npm/three#0.143/examples/jsm/loaders/GLTFLoader.js';
import { OrbitControls } from 'https://cdn.jsdelivr.net/npm/three#0.143/examples/jsm/controls/OrbitControls.js';
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x6699cc);
const lights = [];
lights[0] = new THREE.PointLight(0xffffff, 1, 0);
lights[1] = new THREE.PointLight(0xffffff, 1, 0);
lights[2] = new THREE.PointLight(0xffffff, 1, 0);
lights[0].position.set(0, 100, 0);
lights[1].position.set(100, 100, 100);
lights[2].position.set(- 100, - 100, - 100);
scene.add(lights[0]);
scene.add(lights[1]);
scene.add(lights[2]);
const loader = new GLTFLoader();
loader.load("https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf", function (gltf) {
const mesh = gltf.scene.children[0];
scene.add(mesh);
//my main question is about this code - looks like everyone can test if isMesh, then can get the material. I have tried multiple models and can never find any mesh. If I just assumme a material that doesn't work either
gltf.scene.traverse((o) => {
if (o.isMesh) {
console.log("found mesh");
console.log(o && o.material);
} else {
console.log("no mesh found");
}
});
}, undefined, function (error) {
console.error(error);
});
var canvas = document.getElementById("canvasid");
var renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true
});
renderer.setSize(canvas.parentElement.clientWidth, canvas.parentElement.clientHeight);
var camera = new THREE.PerspectiveCamera(1, canvas.parentElement.clientWidth / canvas.parentElement.clientHeight, 1, 1000);
camera.position.z = 100;
const controls = new OrbitControls(camera, renderer.domElement);
controls.update();
controls.autoRotate = true;
controls.enableDamping = true;
var animate = function () {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
window.addEventListener('resize', function () {
renderer.setSize(canvas.parentElement.clientWidth, canvas.parentElement.clientHeight);
camera.aspect = canvas.parentElement.clientWidth / canvas.parentElement.clientHeight;
camera.updateProjectionMatrix();
}, false);
animate();
How can I access the mesh/material?
The traverse does not work since you add the first child of gltf.scene to scene. That means you change the object hierarchy and thus break the traversal.
To fix this, simply move the below two lines of code after the call of Object3D.traverse().
const mesh = gltf.scene.children[0];
scene.add(mesh);
Also, consider to just do this since a glTF asset does not necessarily only have a single child mesh:
scene.add(gltf.scene);
Updated fiddle: https://jsfiddle.net/ftcnby9e/

why is three.js cast shadow not working on a 3D model

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();
}
};

Broken textures with Mapbox GL and THREE.js

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.

THREE.JS Mesh position smooth animation

I have a pretty easy question for you with three.js about x position translation of an imported .obj mesh.
I'm fairly new to three.js and was wandering if someone could give me some lead on what to do or solve this problem.
So... I have this mesh on (0,-200,0) and i just wanted to move it to (50,-200,0) with a smooth translation through a button back and forth to the two positions.
objLoader = new THREE.OBJLoader();
objLoader.load('models/map_real.obj', function (obj) {
var blackMat = new THREE.MeshStandardMaterial({color:0xfaf9c9});
obj.traverse(function (child) {
if (child instanceof THREE.Mesh) {
child.material = blackMat;
}
});
obj.castShadow = true;
obj.receiveShadow = true;
obj.position.set(0,-200,0)
obj.rotation.y = getDeg(-20);
scene.add(obj);
objWrap = obj;
});
I have in my main.js an init() which contains all the functions such as camera(), animate(), render() etc... and from the variable objWrap.position.x it logs the correct position.
I've tried to capture the click (as the snippet below shows) on my #test button and only increment the position by 0.5 - - i get this, is not in the animate loop so it cant keep add 0.5.
$('#test').click(function(){
if (objWrap.position.x <= 50) {
objWrap.position += 0.5}
});
So the final result that i want is a button that toggle back and forth a smooth animation that goes from objWrap.position.x = 0 to objWrap.position.x = 50
I hope to have been clear, feel free to ask if you need to know more, i'll respond in seconds... All the help is truly appreciate!
Just an example of how you can do it with Tween.js:
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.setScalar(50);
camera.lookAt(scene.position);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
scene.add(new THREE.GridHelper(200, 100));
var box = new THREE.Mesh(new THREE.BoxGeometry(2, 2, 2), new THREE.MeshBasicMaterial({
color: "aqua"
}));
scene.add(box);
btnMove.addEventListener("click", onClick, false);
var forth = true;
function onClick() {
new TWEEN.Tween(box.position)
.to(box.position.clone().setX(forth ? 50 : 0), 1000)
.onStart(function() {
btnMove.disabled = true;
})
.onComplete(function() {
btnMove.disabled = false;
forth = !forth;
})
.start();
}
render();
function render() {
requestAnimationFrame(render);
TWEEN.update();
renderer.render(scene, camera);
}
body {
overflow: hidden;
margin: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/94/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/tween.js/17.2.0/Tween.min.js"></script>
<button id="btnMove" style="position:absolute;">Move</button>
This works with ReactJS,
import TWEEN.
import {TWEEN} from "three/examples/jsm/libs/tween.module.min";
Enable drag controller and tween animation.
const { gl, camera } = useThree();
const objects = []; // objects.push(ref.current)
useEffect(() => {
const controls = new DragControls( objects, camera, gl.domElement );
controls.addEventListener( 'dragend', (e) => {
new TWEEN.Tween(e.object.position)
.to(new Vector3(x, y, z), 1000)
.easing(TWEEN.Easing.Back.InOut)
.start();
});
})
UseFrame to show the animation.
useFrame(() => {
TWEEN.update();
});
Hope to help someone.

Categories

Resources