Interactively changing color of mesh face or small region - R3F + NodeMaterial

Hello everyone,

I would like to allow users to interact with a map, so that clicking on the map will cause a section of the map, a circle around their click point, to change color.

I have generated the map by displacing the vertices of a plane. There’s no textures, but I am using TSL NodeMaterials for the shader.

I’ve got the click point using a raycaster ( and can position a pointer above this on the map )

But I’m stumped as to how I can update the color of the map. Would it be best to update the material? Maybe something to do with faces? Perhaps vertexColors={true} is involved?

Seems like this would be a problem that’s come up before…

Any ideas would be greatly appreciated!

All the best

B

Draw your circle with your shader?
Three raycaster always return the UV coordinates of the ray.

It’s even more simple with TSL as you can switch color node without changing material or combine multiple shader pieces together in real time :star_struck:
This mean you can use a “normal” version of your node and switch anytime to a combined version with a circle drawned on top.

const myNormalNode = Fn(() => {
	//shader map code here
	return result;
})();

const myCircledNode = Fn(() => {
	const myCircle = //draw a circle here
	const colors = myNormalNode; //get map shader code
	const result = vec3(0,0,0).toVar(); //empty color variable 
	result.assign( colors.mul( myCircle ) ); //add the circle to the map
	return result;
})();

//before mouse click
myMaterial.colorNode = myNormalNode;

//on mouse click
myMaterial.colorNode = myCircledNode;

myMaterial.needsUpdate = true

This is obviously very abstract, since you didn’t posted any sample of your code.

1 Like

Thanks @Oxyn, this looks like it could be perfect.

I’ll have a stab at putting it into practice then post some code once it either is or isn’t working.

All the best

B

I’m using this strategy to draw circles as well. I may be able to provide detailed code in case you need it. But I’m not sure it’s compatible with yours ( like UV 0.0 origins in the shader code can change lot of things)

Hi @Oxyn, that would be amazing. Let me have a go first, then we can compare notes.

It’ll also make me tidy the code up!

All the best

B

OK, I have a texture which shows a circle given a uv co-ordinate (see code below) but I cannot figure out how to change this on mouse click (also see below). Any ideas there would be fantastic.

Thanks!

N

Sorry, but I can’t get this to work on jsfiddle or similar. Anyone got a good template for that?

So, here’s an image of what it looks like:

Here’s what I have for mouse click change. This basically does nothing. Doesn’t kill the scene but doesn’t change it either…


    const handleClick =(e) =>
            {
                const intersects = e.intersections
                
                const myCircledNode = Fn(() => {
                    
                    const iSect = intersects[ 0 ]
                    const circleCentre = iSect.uv
                    const currentColors = pRef.current.material.colorNode; 
                    console.log("CC:", currentColors)
                    const colorD = color("#1a9815");
                    const newCols = mix(  colorD , mix('#005500','#FF0011', (positionLocal.z)).mul(0.06), step(0.05, distance(uv(),circleCentre)) )
                    return newCols;

                })();

        if ( intersects.length > 0 ) {
            
            const newPos = intersects[ 0 ].point
            newPos.y += 2.5
            
            coneRef.current.position.copy( newPos);
            pRef.current.material.colorNode = myCircledNode
        }
    }

And here’s the shader:

export const MyCircleNodeMaterial = (colorz, hitPos=vec2(0.4,0.4), circleSize=0.15) => {
    
    const circleCentre = hitPos;
    const insideCircle = distance(positionLocal.xy, circleCentre)

    const { nodes } = useMemo( () => {
        const colorA = uniform(color(colorz.colors[0]));
        const colorB = uniform(color(colorz.colors[1]));
    
        const colorD = uniform(color("#1a9815"));
 
        const tDis =  uniform( 0.9 );
        const tAmb = uniform( 0.1 );
        const tAtt = uniform( 0.1 );
        const tPow  = uniform( 1.0 );
        const tSca = uniform( 2.0 );


    console.log("canda", colorA)
    console.log("Inside:", insideCircle)
        return {
        nodes: {

            colorNode: mix(  colorD , mix(colorA, colorB, (positionLocal.z)).mul(0.06), step(circleSize, distance(uv(),circleCentre)) ),
            thicknessColorNode: vec3( 0.5, 0.3, 0.0 ) ,
            thicknessDistortionNode: tDis,
            thicknessAmbientNode: tAmb,
            thicknessAttenuationNode:tAtt,
            thicknessPowerNode:tPow,
            thicknessScaleNode:tSca,
        },
        
        };
    }, []);

  return <meshSSSNodeMaterial {...nodes} 
    side={THREE.DoubleSide}
    //vertexColors={true}
    roughness ={ 0.3}
    
    />
};


Here is one, but no R3F and no nodes:

https://codepen.io/boytchev/full/XJbPJMQ

5 Likes

That’s amazing, thanks @PavelBoytchev !

B

1 Like

Try as I might, I cannot get this to work with R3F and NodeMaterials. I’m sure it SHOULD work but how is totally beyond me.

Any ideas would be massively appreciated. I remain convinced it’s probably a simple thing I’m overlooking or misunderstanding…

I’ve stripped it back to a very simple situation.

  1. A mesh with a simple planeGeometry
  2. A meshStandardNodeMaterial applied
  3. The colorNode of this material set by useState

CODE:

import { useRef, useState } from 'react'
import {   uv,color, Fn, uniform, varying, vec2, vec3,} from 'three/tsl';
import * as THREE from 'three/webgpu'
import { extend } from '@react-three/fiber'
import { OrbitControls } from '@react-three/drei';


export default function Terrain(){

    extend (THREE)

    const pRef = useRef();
    let [cNode, setcNode ] = useState(color('green'))
   
  const handleClick=(e) =>
            {
                const intersects = e.intersections

                if ( intersects.length > 0 ) {
                    console.log("pREF:", pRef.current)
                    pRef.current.material.colorNode = color('red');
                    setcNode(color('red'));
                }
            }


    return <>
        
        <OrbitControls makeDefault/>
        <ambientLight intensity={0.5}  />
            <directionalLight position={[5,3,0]}  />


           <mesh ref={pRef}  rotation-x={Math.PI *-0.5} onDoubleClick={(e) => handleClick(e)} >
            <planeGeometry args={[40,40,4,4]} / >
            <meshStandardNodeMaterial colorNode={cNode}/>
           </mesh>
      
        
    </>
}
  • This works to set the initial color of the plane and the onDoubleClick fires but the color of the plane on screen never changes.

  • Using pRef.current.material.colorNode = "this is a string" I can see that the colorNode is updated, but still nothing changes on the actual material/mesh.

Hello everyone, small update. I’m a tiny bit closer to understanding this problem.

The following code updates the plane with a new random color BUT only when the page is refreshed. NOT each frame. Does this seem odd, or am I missing something obvious?

If there’s any R3F folks out there, any insight would be terrific. I’m stuck in a ‘but that should work’ loop.

Update the plane color on each frame. Note the rotation works fine.

const pRef = useRef();

    useFrame(()=>{
      //pRef.current.rotation.y += 0.010;
        pRef.current.material.colorNode = color(Math.random(),Math.random(), Math.random())
    })

Component returns:

return <>
           <mesh ref={pRef}  rotation-x={Math.PI *-0.5} onClick={(e) => handleClick(e)} >
                <planeGeometry args={[40,40,4,4]} / >
                <meshSSSNodeMaterial />
           </mesh>
       </>

What’s weird is, if you un-comment the rotation, that happens fine. The plane rotates.

But the color just stays the same as the first random value…

Thanks!
B

Hey @Oxyn, have you had success implementing a nodeMaterial color change like this?
It’s been driving me insane trying to get it working the last couple of days.
Thanks
B

Sadly I’m only using javascript. I can’t understand your code and will not dare to suggest fixes (could end more broken than before lol)
One thing is sure tho: each time you switch node, you need to update the material for changes to take effect. I realize I omitted to specify this in my sample earlier. Sorry

myMaterial.needsUpdate = true; should come after the switch

1 Like

@Oxyn That seems to have solved it! Thanks so much.

I actually did this quite early on but must have fudged the syntax or something because it didn’t help…

Will update here once I’ve back-filled the original example.

All the best

B

1 Like