I am running a three.js component inside a React (Typescript) app, everything was working fine rendering my 3D object, until I started to add some more functionality with vertex colors and now I am getting an error "Cannot read properties of undefined (reading 'position')" at line 27:
const count = geometry.attributes.position.count;
Is there an issue I am facing here with scope or its implementation as a component?
import React from "react";
import ReactDOM from "react-dom";
import * as THREE from "three";
class MyBox extends React.Component {
componentDidMount() {
let mouseX = 0;
let mouseY = 0;
let windowHalfX = window.innerWidth / 2;
let windowHalfY = window.innerHeight / 2;
const radius = 1;
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth / 2, window.innerHeight / 2);
this.mount.appendChild(renderer.domElement);
var geometry = new THREE.IcosahedronGeometry(radius, 1);
const count = geometry.attributes.position.count;
geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(count * 3), 3));
const color = new THREE.Color();
const positions = geometry.attributes.position;
const colors = geometry.attributes.color;
for (let i = 0; i < count; i++) {
color.setHSL((positions.getY(i) / radius + 1) / 2, 1.0, 0.5);
colors.setXYZ(i, color.r, color.g, color.b);
}
var material = new THREE.MeshPhongMaterial({ color: 0x7e31eb, vertexColours: true, shininess: 100 });
var cube = new THREE.Mesh(geometry, material);
scene.add(cube);
var wiregeometry = new THREE.IcosahedronGeometry(radius + 0.01, 1);
var wirematerial = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true });
var wirecube = new THREE.Mesh(wiregeometry, wirematerial);
scene.add(wirecube);
const light = new THREE.HemisphereLight(0xffffbb, 0x080820, 1);
scene.add(light);
camera.position.z = 2;
document.addEventListener('mousemove', onDocumentMouseMove);
function onDocumentMouseMove(event) {
mouseX = (event.clientX - windowHalfX);
mouseY = (event.clientY - windowHalfY);
}
var animate = function() {
requestAnimationFrame(animate);
wirecube.rotation.x += mouseY / 20000;
wirecube.rotation.y += mouseX / 20000;
wirecube.rotation.z += 0.00;
cube.rotation.x += mouseY / 20000;
cube.rotation.y += mouseX / 20000;
cube.rotation.z += 0.00;
renderer.render(scene, camera);
};
animate();
renderer.render(scene, camera);
}
render() {
return ( <
div ref = { ref => (this.mount = ref) }
/>
)
}
}
const rootElement = document.getElementById("root")
ReactDOM.render( < MyBox / > , rootElement);
export default MyBox;
Similarly, if I write
var position = geometry.getAttribute('position');
I get a "geometry.getAttribute is not a function" error, which seems odd because that code would usually work for me.
Users encountered problems when mixing ThreeJS version with incompatible plugins. Try using the files for everything from the same release.
Example of a user using incompatible versions: https://discourse.threejs.org/t/buffergeometry-setattribute-is-not-a-function-error/22856
Related
I am using THREE.js in react.
I want to fill 500 number of 1X1X1 box geometry in a 200x200x200 area randomly.
The following code shows how to get the position.xyz with the range from -100 to 100.
However, how to get the position that all 1X1X1 box geometry could not touch each other (no overlapping)?
I have no idea.
function Geometry() {
const geo_num = 500;
const box_length = 200;
const geo_position = useMemo(() => {
const array = []
for (let i = 0; i < geo_num; i++) {
const xdirection = Math.round(Math.random()) * 2 - 1;
const ydirection = Math.round(Math.random()) * 2 - 1;
const zdirection = Math.round(Math.random()) * 2 - 1;
const xpos = Math.random() * box_length * xdirection * 0.5;
const ypos = Math.random() * box_length * ydirection * 0.5;
const zpos = Math.random() * box_length * zdirection * 0.5;
array.push(new THREE.Vector3(xpos, ypos, zpos))
}
return array
})
return (
<>
{
geo_position.map((element, index) => (
<mesh position={element} key={index} >
<boxGeometry args={[1, 1, 1]} />
</mesh>
))
}
</>
)
}
A very rough concept of how you can do it, using an array and its .includes() method:
body{
overflow: hidden;
margin: 0;
}
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three#0.136.0";
import {OrbitControls} from "https://cdn.skypack.dev/three#0.136.0/examples/jsm/controls/OrbitControls";
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(50, 50, 250);
let renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener("resize", event => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let contols = new OrbitControls(camera, renderer.domElement);
let boxCount = 500;
let range = [-100, 100];
let v3Array = [];
let counter = 0;
let v3 = new THREE.Vector3();
while(counter < boxCount){
let v3 = [
THREE.MathUtils.randInt(range[0], range[1]).toFixed(0),
THREE.MathUtils.randInt(range[0], range[1]).toFixed(0),
THREE.MathUtils.randInt(range[0], range[1]).toFixed(0),
].join("|");
if (!v3Array.includes(v3)){
v3Array.push(v3);
counter++
}
}
v3Array.map( p => {
let o = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial({color: Math.random() * 0xffffff, wireframe: true}));
let pos = p.split("|").map(c => {return parseInt(c)});
o.position.set(pos[0], pos[1], pos[2]);
scene.add(o);
});
let boxHelper = new THREE.Box3Helper(new THREE.Box3(new THREE.Vector3().setScalar(range[0] - 0.5), new THREE.Vector3().setScalar(range[1] + 0.5)), "yellow");
scene.add(boxHelper);
renderer.setAnimationLoop( () => {
renderer.render(scene, camera);
})
</script>
I want use image background in react-chartjs-2 but its not working.
I tried in plane chartjs page and its working without any problem.
const image = new Image();
image.src = 'https://www.chartjs.org/img/chartjs-logo.svg';
const plugin = {
id: 'custom_canvas_background_image',
beforeDraw: (chart) => {
if (image.complete) {
const ctx = chart.ctx;
const {top, left, width, height} = chart.chartArea;
const x = left + width / 2 - image.width / 2;
const y = top + height / 2 - image.height / 2;
ctx.drawImage(image, x, y);
} else {
image.onload = () => chart.draw();
}
}
};
export default function Chart() {
return <Line options = {
options
}
data = {
data
}
plugins = {
plugin
}
/>;
}
plugins must be defined as an array as follows:
plugins = {[
plugin
]}
I couldn't find this information on the react-chartjs-2 page directly but taking a look at types.ts on GitHub, I found this...
/**
* The plugins array that is passed into the Chart.js chart
* #see https://www.chartjs.org/docs/latest/developers/plugins.html
* #default []
*/
plugins?: Plugin<TType>[];
I know this is a trivial question but I assure you I've been trying to solve this for several hours.
The color prop is a string (e.g '#f56d3d') and it should change the mesh material color. I'm using useEffect to initialize the scene and I've tried to use another to change the color
function Scene({ color, shape }) {
const cubeRef = useRef(null);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer({ alpha: true });
const geometry = new THREE.BoxGeometry();
const material = new THREE.MeshBasicMaterial({ color });
const cube = new THREE.Mesh(geometry, material);
const animate = () => {
requestAnimationFrame(animate);
cube.material.color.setStyle(color);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
};
useEffect(() => {
renderer.setSize(window.innerWidth, window.innerHeight);
cubeRef.current.appendChild(renderer.domElement);
scene.add(cube);
camera.position.z = 5;
animate();
},[]);
return (
<div ref={cubeRef}>
</div>
)
}
export default Scene
What comes to my mind is a way to let know the renderer that the cube inside the scene has changed.
What I've tried so far is:
cube.material.needsUpdate = true;
renderer.render(scene, camera);
cube.material = new THREE.MeshBasicMaterial({ color });
All of these inside a useEffect with `color as dep.
Thanks
If you dont mind creating a new object then you can rearrange the variables in the useEffect like this:
const cubeRef = useRef(null);
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 );
const renderer = new THREE.WebGLRenderer({ alpha: true });
const geometry = new THREE.BoxGeometry();
let cube, material;
const animate = () => {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
};
useEffect(() => {
material = new THREE.MeshBasicMaterial({color});
cube = new THREE.Mesh(geometry, material);
renderer.setSize(window.innerWidth, window.innerHeight);
cubeRef.current.innerHTML = '';
cubeRef.current.appendChild(renderer.domElement);
scene.add(cube);
camera.position.z = 5;
animate();
}, [color])
Also you might need to save the previous positioning or rotation somewhere.
I'm trying to use Three.js in Typescript react, I render Dodecahedron figure and random stars, I want to add some mark up to my three.js with React but when I render Three.js canvas into HTML it dissapears my root div, and I'm not able to add some other components
THREE.JS
import * as THREE from "three";
export function ThreeCanvas() {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.innerHTML = "";
document.body.appendChild(renderer.domElement);
const geometry = new THREE.DodecahedronBufferGeometry(1.7, 0);
const material = new THREE.MeshStandardMaterial({
color: "#00FF95",
});
const Stars = () => {
const starGeometry = new THREE.SphereGeometry(0.1, 24, 24)
const starMaterial = new THREE.MeshStandardMaterial({color: 0xffffff})
const star = new THREE.Mesh(starGeometry, starMaterial)
const [x, y, z] = Array(3).fill(1).map(() => THREE.MathUtils.randFloatSpread(70))
star.position.set(x, y, z)
scene.add(star)
}
Array(200).fill(100).forEach(Stars)
const light = new THREE.PointLight(0xffffff)
light.position.set(20, 20, 20)
scene.add(light)
camera.position.z = 5;
camera.position.x = 3.5
const Figure = new THREE.Mesh(geometry, material);
scene.add(Figure);
const animate = () => {
requestAnimationFrame(animate);
Figure.rotation.x += 0.01;
Figure.rotation.y += 0.01;
renderer.render(scene, camera);
};
animate();
window.addEventListener("resize", () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});
return null
}
this is my three.js code what I do in react
React
import ReactDOM from "react-dom"
import { ThreeCanvas } from "./Components/Three";
import Landing from "./Components/Landing";
import "./Style/Style.css"
const FirstSection = () => {
return (
<div className="container">
<Landing />
<ThreeCanvas />;
</div>
);
}
ReactDOM.render(<FirstSection />, document.getElementById("root"));
Landing is my markup component when I open console I dont see anywhere my Landing element but in react tools I see, how to fix that issue I have no idea
You're removing document.body in Three.JS component which is why the body only contains the canvas. You might want to use a reference to the element instead of targeting document.body so that it's not disturbing the DOM structure which is why your markdown does not show. As a rule of thumb, you should never be interacting with the DOM via the document.
document.body.innerHTML = "";
I've quickly refactored the Three.JS component to use a React element reference so that you can add additional markup.
Refactored ThreeCanvas Component
import * as React from "react";
import * as THREE from "three";
export function ThreeCanvas() {
const ref = React.useRef();
const [loaded, setLoaded] = React.useState(false);
React.useEffect(() => {
if (!loaded && ref) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
const geometry = new THREE.DodecahedronBufferGeometry(1.7, 0);
const material = new THREE.MeshStandardMaterial({
color: "#00FF95"
});
const Stars = () => {
const starGeometry = new THREE.SphereGeometry(0.1, 24, 24);
const starMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff
});
const star = new THREE.Mesh(starGeometry, starMaterial);
const [x, y, z] = Array(3)
.fill(1)
.map(() => THREE.MathUtils.randFloatSpread(70));
star.position.set(x, y, z);
scene.add(star);
};
Array(200).fill(100).forEach(Stars);
const light = new THREE.PointLight(0xffffff);
light.position.set(20, 20, 20);
scene.add(light);
camera.position.z = 5;
camera.position.x = 3.5;
const Figure = new THREE.Mesh(geometry, material);
scene.add(Figure);
const animate = () => {
requestAnimationFrame(animate);
Figure.rotation.x += 0.01;
Figure.rotation.y += 0.01;
renderer.render(scene, camera);
};
const resize = () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
animate();
ref.current.appendChild(renderer.domElement);
window.addEventListener("resize", resize);
setLoaded(true);
return () => window.removeEventListener("resize", resize);
}
}, [ref, loaded]);
return <div ref={ref} />;
}
export default ThreeCanvas;
Typescript Version: ThreeCanvas.tsx
import * as React from "react";
import * as THREE from "three";
export function ThreeCanvas() {
const ref = React.useRef<HTMLDivElement>(null);
const [loaded, setLoaded] = React.useState(false);
React.useEffect(() => {
if (!loaded && ref.current) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
const geometry = new THREE.DodecahedronBufferGeometry(1.7, 0);
const material = new THREE.MeshStandardMaterial({
color: "#00FF95"
});
const Stars = () => {
const starGeometry = new THREE.SphereGeometry(0.1, 24, 24);
const starMaterial = new THREE.MeshStandardMaterial({
color: 0xffffff
});
const star = new THREE.Mesh(starGeometry, starMaterial);
const [x, y, z] = Array(3)
.fill(1)
.map(() => THREE.MathUtils.randFloatSpread(70));
star.position.set(x, y, z);
scene.add(star);
};
Array(200).fill(100).forEach(Stars);
const light = new THREE.PointLight(0xffffff);
light.position.set(20, 20, 20);
scene.add(light);
camera.position.z = 5;
camera.position.x = 3.5;
const Figure = new THREE.Mesh(geometry, material);
scene.add(Figure);
const animate = () => {
requestAnimationFrame(animate);
Figure.rotation.x += 0.01;
Figure.rotation.y += 0.01;
renderer.render(scene, camera);
};
const resize = () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
};
animate();
ref.current.appendChild(renderer.domElement);
window.addEventListener("resize", resize);
setLoaded(true);
return () => window.removeEventListener("resize", resize);
}
}, [ref, loaded]);
return <div ref={ref} />;
}
export default ThreeCanvas;
I want to create an animated canvas in ReactJs. I have a functional component. I created a class named circle with two methods: draw and update. When I instance circle, I want to call draw, but i have this error from the browser:
TypeError: cir.draw is not a function
I tried with const draw = function() {...} but is the same.
If you know a better way to crate an animated canvas I'm opened for suggestions. In Js it is easiest for me, but React has some difficulties for do that, I think.
This is my code.
import React, { useState, useEffect } from "react";
export default function Home() {
var canvas;
var c;
var radius = 40;
var circle = function (x, y, dx, dy) {
const draw = () => {
c.beginPath();
c.arc(x, y, radius, 0, Math.PI * 2, false);
c.strokeStyle = "#ccc";
c.stroke();
};
const update = () => {
if (x + radius > window.innerWidth || x - radius < 0) dx = -dx;
if (y + radius > window.innerHeight || y - radius < 0) dy = -dy;
x += dx;
y += dy;
};
};
var Animate = () => {
requestAnimationFrame(Animate);
var x = Math.random() * window.innerWidth;
var y = Math.random() * window.innerHeight;
var dx = (Math.random() - 0.5) * 15;
var dy = (Math.random() - 0.5) * 50;
let cir = new circle(x, y, dx, dy, c);
cir.draw();
console.log(cir);
};
useEffect(() => {
var canvas = document.querySelector("canvas");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var c = canvas.getContext("2d");
console.log("a");
Animate();
}, []);
return (
<div>
<canvas></canvas>
</div>
);
}