modifying triangles and vertices on react-three-fiber mesh after render - reactjs

I'm trying to recreate some old Unity QuadSphere code with a #react-three/fiber version, and I've written a quick test that creates a custom class, QuadGeometry extending from THREE.BufferGeometry in which I generate my vertices and indices. The starting configuration looks something like:
6-7-8
|\ /|
3 4 5
|/ \|
0-1-2
where 0 through 8 are groups of 3 numbers creating my vertices array which is then added as an attribute using setAttribute('position', new Float32BufferAttribute(vertices, 3)) and my indices are [0,4,2,2,4,8,8,4,6,6,4,0] which are specified using setIndex(indices). this all works perfectly well and displays correctly. the problem occurs when I go to update the indices so that my quad has activated sides such that it would look like:
6-7-8
|\|/|
3-4-5
|/|\|
0-1-2
the above change only modifies the indices to be like [0,4,1,1,4,2...6,4,3,3,4,0] (more indices for more triangles) and I set these by again calling setIndex(indices), but my mesh isn't updating to reflect the change in faces (indices). I've setup my mesh using the following so that it will update the indices every 5 seconds:
export function QuadMesh(props: QuadMeshProps) {
const meshRef = useRef<Mesh>();
const [state, setState] = useState<QuadState>('unified');
const quad = useMemo<QuadGeometry>(() => {
console.info('creating new QuadGeometry!', {props});
return new QuadGeometry({
centre: {x: props.position[0] ?? 0, y: props.position[1] ?? 0, z: props.position[2] ?? 0},
radius: props.radius ?? 1
});
}, [props]);
let nextChangeAt: number;
const changeFrequency = 5; // 5 seconds
useFrame(({ clock }) => {
const time = clock.getElapsedTime(); // in seconds
if (nextChangeAt == null) {
nextChangeAt = time + changeFrequency;
}
if (time >= nextChangeAt) {
nextChangeAt = time + changeFrequency;
const geometry = meshRef.current.geometry as QuadGeometry;
setState(modifyQuadGeometry(geometry, state));
}
});
return (
<mesh ref={meshRef} dispose={null} geometry={quad} castShadow receiveShadow>
<meshBasicMaterial attach="material" wireframe={true} />
</mesh>
);
}
function modifyQuadGeometry(geometry: QuadGeometry, state: QuadState): QuadState {
let outState: QuadState;
/* update to indices happens here on geometry obj */
const { position } = geometry.attributes;
position.needsUpdate = true;
geometry.computeVertexNormals();
return outState;
}
so how should I go about triggering an update of the faces / triangles in my mesh?
NOTE:
if I modify the indices before I generate the mesh then it renders with the expected additional faces, but I specifically want to modify it at runtime as this is meant to be used to generate a level of detail effect (eventually).
You can see the code at: https://github.com/bicarbon8/QuadSphere/blob/in-javascript/src/components/shapes.tsx
and a demo at: https://bicarbon8.github.io/QuadSphere/

ok, so I consider this a bit of a hack, but for the sake of anyone who is trying to do something similar, I managed to get something similar to what I want by forcing the mesh to fully re-load and render by adding a key property based on the QuadGeometry id and active sides and then also creating a recursive function to render all child QuadGeometry objects in their own mesh and not rendering a mesh of my QuadGeometry if it has any children. the updated code looks like this
export function QuadMesh(props: QuadMeshProps) {
const [level, setLevel] = useState<number>(0);
const [offset, setOffset] = useState<number>(1);
const registry = useMemo<QuadRegistry>(() => {
console.info('creating new QuadRegistry!');
return new QuadRegistry();
}, [props]);
const quad = useMemo<QuadGeometry>(() => {
console.info('creating new QuadGeometry!', {props});
return new QuadGeometry({
centre: {x: props.position[0] ?? 0, y: props.position[1] ?? 0, z: props.position[2] ?? 0},
radius: props.radius ?? 1,
registry: registry
});
}, [props]);
return MeshBufferGeom({quad});
}
function MeshBufferGeom(props: {quad: QuadGeometry}) {
const meshes = new Array<MeshProps>();
if (!props.quad.hasChildren()) {
const positions = new Float32Array(props.quad.vertices);
const indices = new Uint16Array(props.quad.indices);
meshes.push(
<mesh key={`${props.quad.id}-${props.quad.activeSides.join('-')}`} castShadow receiveShadow>
<bufferGeometry>
<bufferAttribute
attach="attributes-position"
array={positions}
count={positions.length / 3}
itemSize={3} />
<bufferAttribute
attach="index"
array={indices}
count={indices.length}
itemSize={1} />
</bufferGeometry>
<meshBasicMaterial attach="material" wireframe={true} />
</mesh>
);
} else {
meshes.push(...[
props.quad.bottomleftChild,
props.quad.bottomrightChild,
props.quad.topleftChild,
props.quad.toprightChild
].map(c => MeshBufferGeom({quad: c})));
}
return (
<>
{...meshes}
</>
);
}
where MeshBufferGeom is the recursive function that returns either a single or multiple <mesh>...</mesh> elements (if multiple it's all of the child and children's children, etc. mesh objects). I'm still hoping someone can tell me a better way to update the faces on an existing mesh so I can reduce the number of throw-away objects I'm creating and disposing of each time the QuadGeometry activates a side, but for now this will work

Related

how to update an uniform vec2 when the mouse is moving,

I'm trying to update an uniform vec2 when the mouse is moving but i get an error: TypeError: Failed to execute 'uniform2fv' on 'WebGL2RenderingContext': Overload resolution failed.
for this I create a ref uniforms
const uniformRef = useRef({
uAlpha: 0,
uOffset: { x: 0.0, y: 0.0 },
uTexture: imageTexture,
});
then I created a useEffect to listen the event "mousemove"
useEffect(() => {
document.addEventListener("mousemove", onMouseMove);
return () => document.removeEventListener("mousemove", onMouseMove);
}, [onMouseMove]);
and finally i create the functiopn call on "mousemove"
const onMouseMove = useCallback((e: MouseEvent) => {
if (!planeRef.current || planeRef.current === undefined) return;
// mouse coordinate
let x = (e.clientX / window.innerWidth) * 2 - 1;
let y = -(e.clientY / window.innerHeight) * 2 + 1;
const position = new THREE.Vector3(x, y, 0);
// change position of the mesh
gsap.to(planeRef.current?.position, {
duration: 1,
x,
y,
ease: Power4.easeOut,
onUpdate: () => onPositionUpdate(position),
});
}, []);
// update the offset
const onPositionUpdate = (position: THREE.Vector3) => {
if (planeRef.current) {
let offset = planeRef.current.position
.clone()
.sub(position)
.multiplyScalar(-0.25);
gsap.to(uniformRef.current, {
uOffset: { x: offset.x, y: offset.y },
duration: 1,
ease: Power4.easeOut,
});
}
};
And this is the initialisation of my shader
const ColorShiftMaterial = shaderMaterial(
{
uTexture: new THREE.Texture(),
uOffset: new THREE.Vector2(0.0, 0.0),
uAlpha: 0,
},
...)
If you could help me on this or just give tips, it will help me a lot !
Thanks
I tried a lot of things, but every time i tried someting new, I still get the error, I was thinking it may be because i give to uOffset a vec3. So I change to a vec2 but even with this I still get the error.
First off, you should use react-three fiber when dealing with 3JS and react.
By naming convention you should name your uniforms like u_mouse (with an underscore).
In three fiber there is an onPointerMove that you can use to get your mouse movement.
For example:
<mesh onPointerMove={(e) => handleMove(e)}>
To pass your values to the shader, you need to have uniforms.
const uniforms = {
u_time: { value: 0.0 },
u_mouse: { value: { x: 0.0, y: 0.0 } },
u_resolution: { value: { x: 0, y: 0 } },
};
You can use a useEffect to get the values of the screensize and use these values in your resolution uniform. Then you can use these values in your shader to calculate the movement of the mouse regardless of the screen size.
I'm not sure how you have written your shader, but you can create a vec2 for the mouse and a vec2 for the resolution and do something like
vec2 v = u_mouse/u_resolution; // mouseposition / screensize
vec3 color = vec3(v.x, (v.y/v.x+0.2), v.y); // use the v-value for the x- and y-axis
gl_FragColor = vec4(color, 1.0); // add the new colors to your output
It all depends on what you want to happen and how you want it to look.

React Hooks - Infinite loops when setting a state within a map

For reference: I am working on a sliding puzzle game.
So I have a const Board function and I have defined 2 states named:
puzzlePieces
hiddenIndexNumber
The point of 'hiddenIndexNumber' is to keep track of the hidden block index within the game. So before the game starts, I loop through a new array that I create for puzzlePieces and use map to return HTML elements. When looping, I want to make sure that I get the hidden block index for my hiddenIndexNumber to keep track of the hidden block.
This is how my code (partially) looks:
const Board = () => {
const totalPieces = 9
const hiddenNumber = totalPieces
const[hiddenIndexNumber, setHiddenIndex] = useState(-1)
// here I create an array of 9 elements and shuffle them with underline
const [puzzlePieces, changePuzzlePieceContent] = useState(
_.shuffle( [ ...Array( totalPieces ).keys() ].map( num => num + 1 ) )
)
let puzzleElements = [ ...Array( totalPieces ).keys() ].map( index => {
// the problem here is that setHiddenIndex makes the framework rerender
// the DOM after setting the index number and I don't know how to solve the issue here
if( puzzlePieces[index] === hiddenNumber ) {
setHiddenIndex(index)
}
return <Puzzle
key = { index }
index = { index }
number = { puzzlePieces[index] }
hidden = { puzzlePieces[index] === hiddenNumber && true }
onChange = { handleChange }
/>
} )
}
The problem is with this code:
if( puzzlePieces[index] === hiddenNumber ) {
setHiddenIndex(index)
}
How do I make sure that I set hiddenIndexNumber without requesting for rerendering the DOM?
I would suggest you to look into shouldComponentUpdate() and useEffect() Hooks.
It's well described here: shouldComponentUpdate()? with an example.

I'm not sure why my state is not increasing in this code

I'm trying to use the html5 canvas element to create a grid in React. I'm using state to store the start points of each square, but for some reason my state is not incrementing. The code is as follows:
const [row, setRow] = useState(0);
const [col, setCol] = useState(0);
useEffect(() => {
simToggle();
}, [])
const simToggle = () => {
let canvas = document.getElementById('canvas');
let ctx = canvas.getContext('2d');
for(let i = 0; i < 901; i++) {
if (col > 600) {
setCol(0);
setRow(row + 20);
}
ctx.rect(col, row, 20, 20);
ctx.stroke();
setCol(col + 20);
}
}
I have a button that when clicked runs the simToggle function, and each time I click that the 'col' state increments by 20. Why is it only incrementing once per function call instead of each iteration of the for loop it is in?
React does not update the state immediately. It adds your call to a queue that it will process after your code is finished running. Since you are calling the set state method in a loop, only one call will get processed after it's done.
See the following link for details ...

Is it possible to get / return Highcharts Axis values?

I would like to be able to use the values printed on Highcharts x and y axes when the chart renders in another part of my app, so for example an array of number from 0 to n.
I am using highcharts-react-official - I can't find any documentation on methods that return the values as they are printed exactly on the screen.
Is this possible?
Update
With this code I can return an array of numbers that represent the tick values on the y axis:
const options: Highcharts.Options = {
yAxis: {
labels: {
formatter(): string {
console.log('YAXIS', Object.keys(this.axis.ticks)
}
}
}
}
Although the array includes a negative number which I cannot see in the chart axis itself.
The same trick does not work for the x axis.
Is there a cleaner approach do getting the correct result?
Update 2
Following the solution below I finally got what I was looking for:
This does not work as you would expect:
const res = this.yAxis
.map((axis) => axis.ticks)
.map((axisTicks) => Highcharts.objectEach(axisTicks, (tick) => tick.label.textStr));
console.log(res); // undefined;
However this does:
const yAxisVals: string[][] = [];
const axisTicks = this.yAxis.map((axis) => axis.ticks);
axisTicks.forEach((item, idx) => {
yAxisVals[idx] = [];
Highcharts.objectEach(item, (tick) => yAxisVals[idx].push(tick.label.textStr));
});
console.log(yAxisVals);
You can use render event and get the values by Axis.tickPositions property:
chart: {
events: {
render: function() {
var xAxisTickPositions = this.xAxis[0].tickPositions,
yAxisTickPositions = this.yAxis[0].tickPositions;
console.log(xAxisTickPositions, yAxisTickPositions);
}
}
}
Live demo: http://jsfiddle.net/BlackLabel/6m4e8x0y/4960/
API Reference:
https://api.highcharts.com/highmaps/chart.events.render
https://api.highcharts.com/class-reference/Highcharts.Axis#tickPositions

Getting dotted line on html Canvas

I am using React to develop a real-time paint app. So, the idea is to store mouse events in an array(to pass it through socket) and pass it to draw function. However, when I move mouse fast, I'm getting a dotted line instead of smooth line. If I directly draw using mouse events instead of an array, I'm getting a smooth line. So I guess the issue is in pushing mouse events into the array.
This is my output:
The following is my PaintCanvas component
function PaintCanvas(props) {
let ctx;
const canvasRef = useRef("");
const [isDrawing, changeIsDrawing] = useState(false);
let strokes = [];
const mouseDownFunction = e => {
changeIsDrawing(true);
if (ctx) {
wrapperForDraw(e);
}
};
const mouseUpFunction = e => {
if (ctx) {
ctx.beginPath();
}
changeIsDrawing(false);
};
const mouseMoveFunction = e => {
if (ctx) {
wrapperForDraw(e);
}
};
const wrapperForDraw = e => {
if (!isDrawing) return;
strokes.push({
x: e.clientX,
y: e.clientY
});
drawFunction(strokes);
};
const drawFunction = strokes => {
let { top, left } = canvasRef.current.getBoundingClientRect();
if (!isDrawing) return;
ctx.lineWidth = 3;
ctx.lineCap = "round";
for (let i = 0; i < strokes.length; i++) {
ctx.beginPath();
//adding 32px to offset my custom mouse icon
ctx.moveTo(strokes[i].x - left, strokes[i].y - top + 32);
ctx.lineTo(strokes[i].x - left, strokes[i].y - top + 32);
ctx.closePath();
ctx.stroke();
}
};
useEffect(() => {
let canvas = canvasRef.current;
ctx = canvas.getContext("2d");
});
return (
<div>
<canvas
ref={canvasRef}
width="500px"
height="500px"
onMouseDown={mouseDownFunction}
onMouseUp={mouseUpFunction}
onMouseMove={mouseMoveFunction}
className={styles.canvasClass}
/>
</div>
);
}
export default PaintCanvas;
How can I get a smooth line using the array implementation.
In your loop where you are drawing the lines, you don't need to call moveTo every iteration. Each call of lineTo() automatically adds to the current sub-path, which means that all the lines will all be stroked or filled together.
You need to pull beginPath out of the loop, remove the moveTo call and take the stroke outside the loop for efficiency.
ctx.beginPath();
for (let i = 0; i < strokes.length; i++) {
//adding 32px to offset my custom mouse icon
//ctx.moveTo(strokes[i].x - left, strokes[i].y - top + 32); // Remove this line
ctx.lineTo(strokes[i].x - left, strokes[i].y - top + 32);
}
// Its also more efficient to call these once for the whole line
ctx.stroke();

Resources