How to load gltf files in react.js - reactjs

I am trying to load a glTF file in my reactjs application using gltf loader. I manage to display a basic cube on the canvas, but using the gltf loader I only get the blank canvas without the 3D object.
I have tried several paths to the file but it seems to sit in the correct position (same folder as component). I have also tried other glTF files without luck. Do i need to include the bin file somewhere?
Here is my code:
import React, { Component } from 'react';
import * as THREE from 'three';
import GLTFLoader from 'three-gltf-loader';
const OrbitControls = require("three-orbit-controls") (THREE);
class Viewer extends Component{
componentDidMount(){
const width = this.mount.clientWidth
const height = this.mount.clientHeight
//ADD SCENE
this.scene = new THREE.Scene()
//ADD CAMERA
this.camera = new THREE.PerspectiveCamera(
75,
width / height,
0.1,
1000
)
this.camera.position.z = 4
//ADD RENDERER
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setClearColor('#888888')
this.renderer.setSize(width, height)
this.mount.appendChild(this.renderer.domElement)
//ADD ORBIT CONTROL
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
//ADD CUBE
const geometry = new THREE.BoxGeometry(1, 1, 1)
const material = new THREE.MeshBasicMaterial({ color: '#433F81' })
this.cube = new THREE.Mesh(geometry, material)
this.scene.add(this.cube)
//ADD OBJECT
const loader = new GLTFLoader();
loader.load('model.gltf', (object) => {
this.scene.add(object.scene);
});
this.animate();
}
componentWillUnmount(){
this.stop()
this.mount.removeChild(this.renderer.domElement)
}
animate = () => {
this.renderScene()
this.frameId = window.requestAnimationFrame(this.animate)
}
renderScene = () => {
this.renderer.render(this.scene, this.camera)
}
render(){
return(
<div
style={{ width: '400px', height: '400px' }}
ref={(mount) => { this.mount = mount }}
/>
)
}
}
export default Viewer;
The application should show the 3D file on the canvas.
NOTE: I'm new to three.js, so layman terms would be appreciated.

You can turn gltf files to JSX and use react-three-fiber to reconcile threejs. Here's an example: https://github.com/react-spring/react-three-fiber/tree/3.x/tools three-fiber wraps threejs similar to how react-dom wraps html, reconcilers don't necessarily change the rules, which makes it less of an abstraction as it seems.

Related

Threejs and WebGL not rendering 3d objects in firefox

Hello everyone I am creating my own website using reactjs and I want to include 3d object in my website.
To do that I am using three js with a WebGL renderer, it usually works fine but SOMETIMES my 3d object does not render and I get a black frame instead (see the pictures). Now the issue is that this bug happens sometimes and I can't understand what produces it. The bug is only happening on firefox, chrome works fine.
3d object bug
3d object working
I know how to solve it for my computer (I have to enable graphic performances for firefox in config panel), yet it was working perfectly fine sometimes without graphic performances enabled. My fear is that if I deploy my website this bug occurs for other user.
(Edit after some research this is EXACTLY the issue https://bugzilla.mozilla.org/show_bug.cgi?id=1712486 but i don't get how to solve the issue)
I am pretty sure that this is due to the WebGL renderer which seems to sometimes have trouble to load textures with my graphic card... not sure why.
Can anyone help me to find a fix (for every user not only for my computer) or is there a way to check if this rendering bug happens in order to display an image instead ?
Here is the code to generate my object :
import React from 'react';
import './object3d.css';
import { useState, useEffect, useRef } from "react";
import * as THREE from "three";
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
var sat;
function loadGLTFModel(scene, glbPath) {
return new Promise((resolve, reject) => {
const loader = new RGBELoader();
//loader.load(
loader.load( 'royal_esplanade_1k.hdr', ( texture ) => {
texture.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = texture;
const loader2 = new GLTFLoader();
loader2.load( glbPath, function ( gltf ) {
sat = gltf.scene
scene.add( sat );
resolve(sat);
} );
},
undefined,
function (error) {
console.log(error);
reject(error);
} );
});
}
const Object = ({path_file}) => {
const refContainer = useRef();
const [loading, setLoading] = useState(true);
const [renderer, setRenderer] = useState();
useEffect(() => {
const { current: container } = refContainer;
if (container && !renderer) {
const scW = container.clientWidth;
const scH = container.clientHeight;
//console.log(scW);
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
premultipliedAlpha : false
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(scW, scH);
renderer.outputEncoding = THREE.sRGBEncoding;
container.appendChild(renderer.domElement);
setRenderer(renderer);
const scene = new THREE.Scene();
const scale = 25;
const camera = new THREE.OrthographicCamera(
-scale,
scale,
scale,
-scale,
0.01,
50000
);
camera.position.y = 25;
camera.position.x = 2;
camera.position.z = 0;
camera.rotation.y = 45* Math.PI /180;
camera.rotation.z = 45* Math.PI /180;
const ambientLight = new THREE.AmbientLight(0xcccccc, 1);
scene.add(ambientLight);
loadGLTFModel(scene, path_file, {
}).then((sat) => {
animate(sat);
setLoading(false);
});
let req = null;
const animate = () => {
req = requestAnimationFrame(animate);
sat.rotation.y+=0.01;
renderer.render(scene, camera);
};
return () => {
cancelAnimationFrame(req);
renderer.dispose();
};
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div className='container_3d'
ref={refContainer}
>
{loading && (
<span style={{ left: "50%", top: "50%" }}>
Loading...
</span>
)}
</div>
);
};
const Object3dsat = ({path_file}) => {
return (
<Object path_file={path_file}/>
)
}
export default Object3dsat
I notted that when the bug occurs I got multiple lines in the Fail Logs :
"RenderDXGITextureHost Failed to open shared texture, hr=0x80070057"
More Over my config :
GPU 1 : AMD radeon RX Vega 10
GPU 2 : Nvidia GeForce RTX 2060
CPU : AMD Ryzen 7 3750H with Radeon Vega Mobile Gfx
and I am using Firefox Version: 105.0.1
(I also notted that the GPU used for Web rendering is the AMD Radeon GPU but it was working even with this GPU)
Hope you guys can help me
Thank you for your help !

Loading .ply file with three.js in react

I'm pretty new to React and am currently trying to visualize a point cloud (.ply file) with the help of three.js and react.
When trying to load the file though, I don't get any errors, the point cloud doesn't appear at all. Everything else does though...
Only thing I receive via the console is Infinty% loaded.
Click here to download the .ply file used in this example
Here's my code:
import React from "react";
import * as THREE from "three";
import { PLYLoader } from "three/examples/jsm/loaders/PLYLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
var container;
var camera, scene, renderer, controls, loader;
export default function Visualization() {
return <div></div>;
}
init();
animate();
function init() {
//Creating the container for the ply
container = document.createElement("div");
document.body.appendChild(container);
//initializing the camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.01, 2000);
camera.position.z = 2;
camera.position.set(0, 9, 1500);
//initializing the scene
scene = new THREE.Scene();
scene.add(new THREE.AxesHelper(30));
//initializing renderer
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.outputEncoding = THREE.sRGBEncoding;
//adding renderer to DOM
container.appendChild(renderer.domElement);
//initializing interactive controls
controls = new OrbitControls(camera, renderer.domElement);
controls.update();
//rendering ply file
const plyLoader = new PLYLoader();
plyLoader.load(
"./Crankcase.ply",
function (geometry) {
const mesh = new THREE.Points(geometry);
mesh.rotateX(-Math.PI / 2);
scene.add(mesh);
},
// called when loading is in progress
function (xhr) {
console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
},
// called when loading has errors
function (error) {
console.log("An error happened");
console.log(error);
}
);
}
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
controls.update();
}
A late reply to your question.
All you need to do is move your *.ply file to the public folder of your react application and in loader just use it like this:
plyLoader.load(
"Crankcase.ply",
function (geometry) {
const mesh = new THREE.Points(geometry);
mesh.rotateX(-Math.PI / 2);
scene.add(mesh);
},
// called when loading is in progress
function (xhr) {
console.log((xhr.loaded / xhr.total) * 100 + "% loaded");
},
// called when loading has errors
function (error) {
console.log("An error happened");
console.log(error);
}
);
This worked for me

How to import TrackballControls from three.js module using react?

I am using react js boiler plate (create-react-app) and imported three.js. I am trying to use TrackballControls for a particular project but it isn't working. It throws an error like "Attempted import error: 'TrackballControls' is not exported from 'three' (imported as 'THREE')" . Now I understand that it is in the examples folder and it's not an official export if I correctly understood from the forum. Some one please help me with this, how do I import TrackballControls in a react component? Help will be highly appreciated!
import React, { useState, useEffect } from 'react'
import * as THREE from 'three'
const OrbitControls = require('three-orbit-controls')(THREE)
import "../src/assets/sass/home.scss"
const X = () => {
const [y, setY] = useState(0)
const [ masses, setMasses ] = useState([])
let parent, renderer, scene, camera, controls
useEffect(() => {
// renderer
renderer = new THREE.WebGLRenderer()
renderer.setSize( window.innerWidth, window.innerHeight )
document.body.appendChild( renderer.domElement )
// scene
scene = new THREE.Scene()
// camera
camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 100 )
camera.position.set( 20, 20, 20 )
// controls
controls = new THREE.TrackballControls( camera, sphere )
controls.minDistance = 5
controls.maxDistance = 250
controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
controls.dampingFactor = 0.05;
// axes
// scene.add(new THREE.AxisHelper( 20 ))
// geometry
let geometry = new THREE.SphereGeometry( 2, 8, 6, 0, 6.3, 0, 3.1)
// material
let material = new THREE.MeshBasicMaterial({
wireframe: true,
wireframeLinewidth: 1
})
let sphere = new THREE.Mesh( geometry, material )
// parent
parent = new THREE.Object3D()
scene.add( parent )
scene.add( sphere )
function animate() {
requestAnimationFrame( animate )
parent.rotation.z += 0.01
controls.update()
renderer.render( scene, camera )
}
animate()
}
,[])
return <div></div>
}
export default X
All JavaScript files from the examples directory are now available as ES6 modules in the three npm package. There is actually a guide that explains how you can import them in your application:
https://threejs.org/docs/index.html#manual/en/introduction/Import-via-modules
For your particular case, it would look like so:
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls.js';
Notice that third-party packages like three-orbit-controls are not necessary anymore. When using the modules from the three package, it's guaranteed that you work with the latest (and supported) versions.
three.js R109
I know there's no one here to see this but I'm posting my solution if someone comes across this issue in the future. So here's what I figured out -
I haven't mentioned in my question that I was using a SSR (server side render) react framework called next.js. The import works perfectly fine with a pure react app. But in server side frameworks, the import related stuff should be done inside the useEffect (or componentDidMount) along with the rest of the threejs stuff. So I dynamically imported it like this -
let dynamicallyImportPackage = async () => {
let TrackballControls
await import('three/examples/jsm/controls/TrackballControls')
// you can now use the package in here
.then(module => {
TrackballControls = module.TrackballControls
})
.catch(e => console.log(e))
return TrackballControls
}
then, I used it in my useEffect like so -
let TrackbackControls = await dynamicallyImportPackage()
// controls
controls = new TrackbackControls(camera, container)
controls.minDistance = 5
controls.maxDistance = 250
Arigato Gosaimasu!

How to change font family dynamically in a functional component?

I have a component that fetches a random font from Google Font and pass it down to a functional component to render it.
function Palette() {
const url = `https://www.googleapis.com/webfonts/v1/webfonts?key=${key}`;
const [font, setFont] = useState("");
useEffect(() => {
const fetchFont = async url => {
const res = await axios(url);
const items = res.data.items;
const idx = Math.floor(Math.random() * items.length - 1);
setFont(items[idx].family);
};
fetchFont(url);
}, [url]);
return (
<div className="App">
<h1>Palette</h1>
<Card fontFamily={font} bgColor="green" color="white">
{font}
</Card>
</div>
);
}
How can I import this font dynamically inside the child component for styling?
How about this?
<Card style={{fontFamily: font, backgroundColor: 'green', color: 'white'}}>
{font}
</Card>
You can just set inline-css style in react like above. :)
You can append the the link tag that loads the font like this
const font = null;
const id = 'dynamic-font';
if (document.getElementById(id)) {
font = document.getElementById(id);
} else {
font = document.createElement('link');
font.rel='stylesheet'
font.id = id
document.head.appendChild(font)
}
font.href = // url to the font;
Then you can use the font in your CSS.
Another solution would be to actually download the .ttf / .otf files, then write the appropriate #font-face css class, create a CSS class that has the font-family set to the created font-face and apply the class to the desired element.

Cloning nodes and appending to separate Layers in React-Konva

I'm trying to recreate an effect similar to the hover effect found on this site: http://tabotabo.com
Currently, what I'm doing is have the video playing on a layer with a second scaled up layer also playing the video with an additional Text object with a destination-in compositing operation. This is currently working well enough but I was curious if there would be a more efficient way to achieve this by either caching or cloning the first layer and sending that through to the second layer instead of having two separate video objects running in tandem.
Here is the relevant code, if it helps.
Main Render:
<Stage width={width} height={height} ref={ref => (this.stage = ref)}>
<Layer hitGraphEnabled={false}>
<CanvasVideo
src={this.state.background}
settings={{id: 'main', width: width, height: height }}
ref={(el) => this.main = el }
/>
</Layer>
<Layer hitGraphEnabled={false} scaleX={hoverScale} scaleY={hoverScale} x={scaleOffsetX} y={scaleOffsetY}>
<CanvasVideo
src={this.state.background}
settings={{id: 'main2', width: width, height: height }}
ref={(el) => this.main2 = el }
/>
<Text
id="hoverText"
text={this.state.menu.hoverText}
globalCompositeOperation='destination-in'
fontSize={200}
fill="white"
opacity={hoverOpacity}
height={height}
width={width}
align="center"
verticalAlign='middle'
/>
</Layer>
</Stage>
Video Container Class:
import React, { Component } from 'react';
import Konva from 'konva';
import { render } from 'react-dom';
import { Stage, Layer, Image } from 'react-konva';
class CanvasVideo extends Component {
constructor(props) {
super(props);
const video = document.createElement('video');
video.muted = true;
video.autoplay = false;
video.loop = true;
video.src = props.src;
this.state = {
video: video
};
video.addEventListener('canplay', () => {
video.play();
this.image.getLayer().batchDraw();
this.requestUpdate();
});
}
requestUpdate = () => {
this.image.getLayer().batchDraw();
requestAnimationFrame(this.requestUpdate);
}
render() {
let { settings } = this.props
return (
<Image
{...settings}
image={this.state.video}
ref={node => { this.image = node; }}
/>
);
}
}
CanvasVideo.defaultProps = {
settings: null,
};
export default CanvasVideo;
Any explanations or insights would be greatly appreciated.
Thank you!
Currently, there is no way to reuse Konva.Image or any other Konva.Node inside different parents. Konva.Node can have only one parent.
I see only one optimization here is to reuse <video> element you created in CanvasVideo component. It can be like this:
const cache = {};
function createVideo(url) {
if (cache[url]) {
return cache[url];
}
const video = document.createElement('video');
video.src = url;
cache[url] = video;
return video;
}
const video = createVideo(props.src);

Resources