So I've been out of react since before hooks and haven't used threeJS before, but I'm trying to hit 2 birds with one stone so excuse me if it's a rookie mistake.
what I'm trying to do is render a Three.js scene inside a react document body, how I'm trying to do that is through running the three.js code in useEffect() and setting a reference to my react document using useRef(), however, apparently useEffect runs before the document is rendered, hence breaking, so I tried using a ref callback like this
import { useRef, useEffect, useCallback } from "react";
// Packages
import * as THREE from "three";
// Styling
import "./homePage.scss";
function HomePage() {
// Declare a new mounting reference
const mountRef = useCallback((node) => {
if (node !== null) {
useRef(null);
}
}, []);
// Lifecycle hook
useEffect(() => {
console.log(mountRef);
// === THREE.JS CODE START ===
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
// use ref as a mount point of the Three.js scene instead of the document.body
mountRef.appendChild(renderer.domElement);
var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
var animate = function () {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
};
animate();
// === THREE.JS CODE END ===
}, []);
return <div ref={mountRef} />;
}
export default HomePage;
However, now I have the following error
React Hook "useRef" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook
How can I fix this situation? Thanks!
You use useRef at the top level of your component, not in a callback (don't use useCallback for it):
const mountRef = useRef(null);
Then in your useEffect, declare mountRef.current as a dependency and only use it if it exists, see *** comments:
useEffect(() => {
// *** If we don't have the DOM element yet, wait for it
if (!mountRef.current) {
return;
}
// === THREE.JS CODE START ===
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
// use ref as a mount point of the Three.js scene instead of the document.body
mountRef.current.appendChild(renderer.domElement);
// ^^^^^^^^−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− *** Use the DOM element
var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
var animate = function () {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
};
animate();
// === THREE.JS CODE END ===
}, []);
You probably want to include a cleanup callback (a function you return from useEffect) to remove the Three DOM element from the div on unmount:
useEffect(() => {
// ***
const { current } = mountRef;
if (!current) {
return;
}
// === THREE.JS CODE START ===
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
// use ref as a mount point of the Three.js scene instead of the document.body
// ***
const {domElement} = renderer;
current.appendChild(domElement);
var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
var animate = function () {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
};
animate();
// === THREE.JS CODE END ===
// ***
return () => {
current.removeChild(domElement);
};
}, []);
Note there how I grabbed mountRef.current and renderer.domElement to local constants. That makes the cleanup callback more reliable, since those properties can be changed outside the context of the useEffect callback.
Live Example:
const {useRef, useEffect} = React;
function HomePage() {
// Declare a new mounting reference
const mountRef = useRef(null);
// Lifecycle hook
useEffect(() => {
const { current } = mountRef;
console.log("current", current);
// If we don't have the DOM element yet, wait for it
if (!current) {
return;
}
// === THREE.JS CODE START ===
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
// use ref's DOM ELEMENT as a mount point of the Three.js scene instead of the document.body
const { domElement } = renderer;
current.appendChild(renderer.domElement);
var geometry = new THREE.BoxGeometry(1, 1, 1);
var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);
camera.position.z = 5;
var animate = function () {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
};
animate();
// === THREE.JS CODE END ===
// Cleanup callback on component unmount
return () => {
current.removeChild(domElement);
};
}, []);
return <div ref={mountRef} />;
}
ReactDOM.render(
<HomePage/>,
document.getElementById("root")
);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r124/three.min.js"></script>
You can't define hooks in callbacks, see Rules of Hooks.
To get the div ref, you only need to provide the reference from useRef, no useCallback needed at all, see docs examples.
const mountRef = useRef(null);
<div ref={mountRef} />
Related
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/
I just started working with Three.js & everytime I try to use the onchange event, I keep getting this non-passive event listener violation for my onchange event. please help.
ERROR : [Violation] Added non-passive event listener to a scroll-blocking 'touchstart' event. Consider marking event handler as 'passive' to make the page more responsive. See https://www.chromestatus.com/feature/5745543795965952
CODE :
"use strict";
import * as THREE from "https://unpkg.com/three#0.141.0/build/three.module";
import * as dat from "https://cdn.skypack.dev/dat.gui";
// Adding the gui //
const gui = new dat.GUI();
const world = {
plane: {
width: 10
}
}
gui.add(world.plane, 'width', 1, 20).
onchange = function() {
console.log(planeMesh.geometry);
// planeMesh.geometry.dispose();
// planeMesh.geometry = new THREE.planeGeometry(world.plane.width, 10, 10, 10);
}
// Instantiating scene, camera & renderer //
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth,window.innerHeight);
document.body.appendChild(renderer.domElement);
// Building the geometry & material //
const planeGeometry = new THREE.PlaneGeometry(10, 10, 10, 10);
const planeMaterial = new THREE.MeshPhongMaterial({
color: 0xff0000,
side: THREE.DoubleSide,
flatShading: THREE.FlatShading
});
// Binding the geometry and material //
const planeMesh = new THREE.Mesh(planeGeometry,planeMaterial);
// Adding the planeMesh to the scene //
scene.add( planeMesh );
const {array} = planeMesh.geometry.attributes.position;
for(let i=0; i<array.length; i += 3){
const x = array[i];
const y = array[i+1];
const z = array[i+2];
array[i+2] = z + Math.random();
}
// Adding light //
const light = new THREE.DirectionalLight(0xffffff, 1);
light.position.set(0,0,1);
scene.add(light);
// Animating & rendering the design //
function animate() {
requestAnimationFrame( animate );
renderer.render( scene, camera );
};
animate();
I'm trying to access elements in this array, but for some reason it's saying "undefined" and giving back a length of 0, but there's clearly a length of 5. The picture above is the result of this code.
console.log(scene.children[1].children);
The result of this code below is 0
console.log(scene.children[1].children.length);
The result of this code below is undefined
console.log(scene.children[1].children[0]);
Here's the previous paths if it helps with the context:
console.log(scene);
console.log(scene.children[1]);
Per comments: here is full code:
import "./styles.css";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import gsap from "gsap";
import blueHouseBaked from "./static/Bluehouse.png";
import blueHouse from "./static/Bluehouse.glb";
import greenHouseBaked from "./static/Greenhouse.png";
import greenHouse from "./static/Greenhouse.glb";
import redHouseBaked from "./static/Redhouse.png";
import redHouse from "./static/Redhouse.glb";
import purpleHouseBaked from "./static/Purplehouse.png";
import purpleHouse from "./static/Purplehouse.glb";
import groundBaked from "./static/Ground.png";
import ground from "./static/Ground.glb";
import "regenerator-runtime/runtime";
const canvas = document.querySelector("canvas.experience");
// Scene
const scene = new THREE.Scene();
/**
* Sizes
*/
const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};
window.addEventListener("resize", () => {
// Update sizes
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;
// Update camera
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();
// Update renderer
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});
/**
* Camera
*/
// Base camera
const camera = new THREE.PerspectiveCamera(
75,
sizes.width / sizes.height,
0.1,
100
);
camera.position.x = 0;
camera.position.y = 17;
camera.position.z = 22;
camera.rotation.x = -Math.PI / 8;
scene.add(camera);
//Controls -------------------
//Three.js vector lerping
let cameraTargetPosition = new THREE.Vector3(0, 17, 22);
window.addEventListener("mousedown", () => {
window.addEventListener("mousemove", updateCamera);
});
window.addEventListener("mouseup", () => {
window.removeEventListener("mousemove", updateCamera);
});
// Controls
function updateCamera(ev) {
ev.preventDefault();
if (ev.movementX > 0) {
cameraTargetPosition.x -= ev.movementX * 0.015;
} else {
cameraTargetPosition.x -= ev.movementX * 0.015;
}
if (ev.movementY > 0) {
cameraTargetPosition.z -= ev.movementY * 0.015;
} else {
cameraTargetPosition.z -= ev.movementY * 0.015;
}
}
/**
* Everything load
*/
let group = new THREE.Group();
async function loadModels(model, texture) {
const gltfLoader = new GLTFLoader();
const textureLoader = new THREE.TextureLoader();
const mesh = await gltfLoader.loadAsync(model);
const bakedTexture = textureLoader.load(texture);
const bakedMaterial = new THREE.MeshBasicMaterial({
map: bakedTexture,
});
bakedTexture.flipY = false;
bakedTexture.encoding = THREE.sRGBEncoding;
mesh.scene.children[0].material = bakedMaterial;
group.add(mesh.scene.children[0]);
}
loadModels(blueHouse, blueHouseBaked);
loadModels(greenHouse, greenHouseBaked);
loadModels(redHouse, redHouseBaked);
loadModels(purpleHouse, purpleHouseBaked);
loadModels(ground, groundBaked);
scene.add(group);
/**
* Renderer
*/
const renderer = new THREE.WebGLRenderer({
canvas: canvas,
antialias: true,
});
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
/**
* Animate
*/
const clock = new THREE.Clock();
let lastElapsedTime = 0;
// Ray Casting stuff
let raycaster = new THREE.Raycaster();
let mouse = new THREE.Vector2();
let currentIntersect = null;
window.addEventListener("mousemove", (event) => {
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
});
console.log(scene.children[1].children[0]);
const tick = () => {
camera.position.lerp(cameraTargetPosition, 0.1);
const elapsedTime = clock.getElapsedTime();
const deltaTime = elapsedTime - lastElapsedTime;
lastElapsedTime = elapsedTime;
// Raycaster stuff
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects([scene.children[1].children]);
if (intersects.length) {
if (!currentIntersect) {
console.log("mouse enter");
}
currentIntersect = intersects[0];
} else {
if (currentIntersect) {
console.log("mouse leave");
}
currentIntersect = null;
}
// Render
renderer.render(scene, camera);
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
tick();
The problem you are facing is due to asynchronous code. The loadModels function loads model asynchronously which means that the rest of the code finishes first before that asynchronous method is finished for all of its calls.
Here is a great video of the event loop and how it works to better understand why this happens - https://www.youtube.com/watch?v=8aGhZQkoFbQ.
Loading GLTF models can also take a bit of time depending on their size so it may be a few ms before they are available in the group.
You can try logging on every tick, so changing you tick method to -
const tick = () => {
camera.position.lerp(cameraTargetPosition, 0.1);
const elapsedTime = clock.getElapsedTime();
const deltaTime = elapsedTime - lastElapsedTime;
lastElapsedTime = elapsedTime;
// Raycaster stuff
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects([scene.children[1].children]);
if (intersects.length) {
if (!currentIntersect) {
console.log("mouse enter");
}
currentIntersect = intersects[0];
} else {
if (currentIntersect) {
console.log("mouse leave");
}
currentIntersect = null;
}
// Render
renderer.render(scene, camera);
console.log(scene.children[0].children[1])
// Call tick again on the next frame
window.requestAnimationFrame(tick);
};
However, when we worked with threejs, it was much easier to have the scene assigned to the global window for easy access in chrome.
Simply add the following lines in your code after creating the scene
window.scene = scene
import "./styles.css";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import gsap from "gsap";
import blueHouseBaked from "./static/Bluehouse.png";
import blueHouse from "./static/Bluehouse.glb";
import greenHouseBaked from "./static/Greenhouse.png";
import greenHouse from "./static/Greenhouse.glb";
import redHouseBaked from "./static/Redhouse.png";
import redHouse from "./static/Redhouse.glb";
import purpleHouseBaked from "./static/Purplehouse.png";
import purpleHouse from "./static/Purplehouse.glb";
import groundBaked from "./static/Ground.png";
import ground from "./static/Ground.glb";
import "regenerator-runtime/runtime";
const canvas = document.querySelector("canvas.experience");
// Scene
const scene = new THREE.Scene();
window.scene = scene
.......
Then you can access the group from the chrome console once the GLTF models are loaded since scene is now accessible from the devtools. You can just write scene there and you would be able to access it, or you can write scene.children[0].children[1] and you would see your group.
Edit: Meant this just for debugging.
So I found a solution, but I still don't know why the fundamental problem still exists. If I put the console.log(scene.children[1].children[0]); INSIDE of the tick It outputs this:
undefined
mesh
mesh
mesh
So the first loop through is undefined. Therefore the solution is just to check if it's undefined and now I get no errors and everything works:
// Raycaster stuff
raycaster.setFromCamera(mouse, camera);
if (
typeof scene.children[1].children[0] !== "undefined" &&
typeof scene.children[1].children[1] !== "undefined" &&
typeof scene.children[1].children[2] !== "undefined" &&
typeof scene.children[1].children[3] !== "undefined"
) {
const intersects = raycaster.intersectObjects([
scene.children[1].children[0],
scene.children[1].children[1],
scene.children[1].children[2],
scene.children[1].children[3],
]);
if (intersects.length) {
if (!currentIntersect) {
console.log("mouse enter");
}
currentIntersect = intersects[0];
} else {
if (currentIntersect) {
console.log("mouse leave");
}
currentIntersect = null;
}
}
But again, I do NOT understand why it's undefined in the first few loops.
I have 20 objects and I'm creating mesh and textures with three.js. You can see the example object.
What I'm trying to do is exactly, I display my first object, then let it stay on the screen for a while, then turn it off and display my new object. In this way, I want to show my 20 objects in order. The waiting time is important to me, I want this time to be around 0.5. How can I do it.
Adding all meshes a array:
for ( var i = 0; i < count; i ++ ) {
geometry = // new geometry ...
material = // new material ...
//the lines above are unnecessary
var mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
objects.push(mesh);
}
And this is the render part:
function render() {
requestAnimationFrame(render);
for ( var i = 0; i < count; i ++ ) {
var mesh = objects[ i ];
}
renderer.render(scene, camera);
}
As I said, 0.5 seconds after showing my first object, I have to turn it off and show my other object. But I couldn't find a solution. What should I do?
You just need something like this:
let index = Math.floor(t/500) % meshes.length;
Full example:
const renderer = new THREE.WebGLRenderer({antialias:true});
document.body.appendChild(renderer.domElement);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, 1, 0.1, 10);
camera.position.setScalar(1.3);
camera.lookAt(scene.position);
const material = new THREE.MeshNormalMaterial();
const meshes = [
new THREE.BoxGeometry(),
new THREE.SphereGeometry(),
new THREE.CylinderGeometry(),
new THREE.ConeGeometry(),
new THREE.IcosahedronGeometry(),
new THREE.OctahedronGeometry(),
new THREE.TorusGeometry(),
new THREE.TorusKnotGeometry(),
new THREE.DodecahedronGeometry(),
].map(geometry => new THREE.Mesh( geometry, material ));
let currentIndex = null;
requestAnimationFrame(function render(t) {
if (renderer.width !== innerWidth || renderer.height !== innerHeight){
renderer.setSize(innerWidth, innerHeight);
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
}
const index = Math.floor(t/500) % meshes.length;
if (currentIndex !== index) {
scene.remove(meshes[currentIndex]);
scene.add(meshes[index])
currentIndex = index;
}
scene.rotation.y += 0.01;
renderer.render(scene, camera);
requestAnimationFrame(render);
});
body { margin: 0; overflow:hidden}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/107/three.min.js"></script>
I am trying to use three.js in a React application however even the basic sample from the three.js docs fails to load on the canvas.
I first made a vanilla.js sandbox implementation with the sample which works fine. Then I ported it over to create a react+three.js minimal sandbox implementation which fails to work.
Can anyone have a look at it and point me in the right direction ?
class Viewer extends Component {
state = {};
scene = null;
camera = null;
renderer = new WebGLRenderer();
inst = 0;
viewerRef = React.createRef();
componentDidMount() {
const { domElement } = this.renderer;
this.scene = new Scene();
this.scene.background = new Color("#ccc");
this.camera = new Camera(
75,
domElement.innerWidth / domElement.innerHeight,
0.1,
1000
);
this.renderer.setSize(domElement.innerWidth, domElement.innerHeight);
this.viewerRef.current.appendChild(this.renderer.domElement);
const geometry = new BoxGeometry(1, 1, 1);
const material = new MeshBasicMaterial({ color: 0x05ff00 });
const cube = new Mesh(geometry, material);
this.scene.add(cube);
this.camera.position.z = 5;
this.display();
}
display = () => {
this.renderer.render(this.scene, this.camera);
requestAnimationFrame(this.display);
};
render = () => <div className="viewer" ref={this.viewerRef} />;
}
Here is CodeSandBox that works with a basic React wrapper code for Three.js. It also has THREE.OrbitControls integration and scale on resize code:
https://codesandbox.io/s/github/supromikali/react-three-demo
Live demo: https://31yp61zxq6.codesandbox.io/
Here is a full code snippet in the case above listed links don't work for you:
index.js code
import React, { Component } from "react";
import ReactDOM from "react-dom";
import THREE from "./three";
class App extends Component {
componentDidMount() {
// BASIC THREE.JS THINGS: SCENE, CAMERA, RENDERER
// Three.js Creating a scene tutorial
// https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
var renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
// MOUNT INSIDE OF REACT
this.mount.appendChild(renderer.domElement); // mount a scene inside of React using a ref
// CAMERA CONTROLS
// https://threejs.org/docs/index.html#examples/controls/OrbitControls
this.controls = new THREE.OrbitControls(camera);
// ADD CUBE AND LIGHTS
// https://threejs.org/docs/index.html#api/en/geometries/BoxGeometry
// https://threejs.org/docs/scenes/geometry-browser.html#BoxGeometry
var geometry = new THREE.BoxGeometry(2, 2, 2);
var material = new THREE.MeshPhongMaterial( {
color: 0x156289,
emissive: 0x072534,
side: THREE.DoubleSide,
flatShading: true
} );
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);
var 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, 200, 0 );
lights[ 1 ].position.set( 100, 200, 100 );
lights[ 2 ].position.set( - 100, - 200, - 100 );
scene.add( lights[ 0 ] );
scene.add( lights[ 1 ] );
scene.add( lights[ 2 ] );
// SCALE ON RESIZE
// Check "How can scene scale be preserved on resize?" section of Three.js FAQ
// https://threejs.org/docs/index.html#manual/en/introduction/FAQ
// code below is taken from Three.js fiddle
// http://jsfiddle.net/Q4Jpu/
// remember these initial values
var tanFOV = Math.tan( ( ( Math.PI / 180 ) * camera.fov / 2 ) );
var windowHeight = window.innerHeight;
window.addEventListener( 'resize', onWindowResize, false );
function onWindowResize( event ) {
camera.aspect = window.innerWidth / window.innerHeight;
// adjust the FOV
camera.fov = ( 360 / Math.PI ) * Math.atan( tanFOV * ( window.innerHeight / windowHeight ) );
camera.updateProjectionMatrix();
camera.lookAt( scene.position );
renderer.setSize( window.innerWidth, window.innerHeight );
renderer.render( scene, camera );
}
// ANIMATE THE SCENE
var animate = function() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
};
animate();
}
render() {
return <div ref={ref => (this.mount = ref)} />;
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
three.js code (THREE.OrbitControls import)
import * as THREE from 'three';
window.THREE = THREE; // THREE.OrbitControls expects THREE to be a global object
require('three/examples/js/controls/OrbitControls');
export default {...THREE, OrbitControls: window.THREE.OrbitControls};
Result should look like this:
I am not sure why nobody has mentioned react-three-fiber. It's a React renderer for ThreeJS.
Best part is, using rollupjs, we can even tree-shake a lot of unnecessary code, that we won't use in our React ThreeJS app. Also, using react-three-fiber in a desired way, we can achieve 60FPS for our ThreeJS animations 🥰
To learn basics, just take a look into react-three-fiber Examples
There's a few issues with your sandbox at the moment, which aren't necessarily related to React.
innerWidth and innerHeight are not defined. clientWidth and clientHeight are probably the fields you're looking for to get the dimensions of the dom element.
The dimensions of the canvas are being read before it has been added into the document, meaning the dimensions of the element will always be 0 0. Insert the element first before computing the camera aspect ratio and setting the render size.
Camera isn't intended to be used directly. The docs specify it as an abstract base class for cameras -- use PerspectiveCamera instead.
Here's a sample of the code with the above fixes
const { domElement } = this.renderer;
// Fix 1
this.viewerRef.current.appendChild(domElement);
// Fix 2
const width = domElement.clientWidth;
const height = domElement.clientHeight;
this.scene = new Scene();
this.scene.background = new Color("#ccc");
// Fix 3
this.camera = new PerspectiveCamera(
75,
width / height,
0.1,
1000
);
this.renderer.setSize(width, height);
const geometry = new BoxGeometry(1, 1, 1);
const material = new MeshBasicMaterial({ color: 0x05ff00 });
const cube = new Mesh(geometry, material);
this.scene.add(cube);
this.camera.position.z = 5;
this.display();
Hope that helps!