Change gradient shader dynamically with Spring

Hi everybody,

I’m affraid this is more a R3F issue but I’m not sure of the feasibility of this.
So, I’ve got a shaderMaterial that I populate with those :

export const vertexShader = `
    varying vec2 vUv;

    void main() {
      vUv = uv;
      gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
    }
  `
export const fragmentShader = `
    uniform vec3 color1;
    uniform vec3 color2;
  
    varying vec2 vUv;
    
    void main() {
      
      gl_FragColor = vec4(mix(color1, color2, vUv.y), 1.0);
    }

Now I’ve got to set the uniforms like this (example) :

let uniforms = {
      color1: {
        value: new THREE.Color("red")
      },
      color2: {
        value: new THREE.Color("blue")
      }
    }

And this works as a charm
Now I want my shader colors to fade when pressing a button for animation purpose.

If my material was a meshBasicMaterial, I would have done that :

const { color1anim } = useSpring({
      color1anim: !custom ? "red" : "yellow",
      config: config.smooth,
      delay: 300
    })
  return (
      <meshBasicMaterial color={color1anim}/>
  )

And then it works well…
But now i’d like to do the same with my shaderMaterial… which args are an array containing this object, and uniforms will work as passed in the comments :

<animated.shaderMaterial
            /* uniforms={{
              color1: {
                value: new THREE.Color("#1b00e6")
              },
              color2: {
                value: new THREE.Color("#ff08da")
              }
            }} */
            args={[{
              uniforms:{
                color1: {
                  value: new THREE.Color("blue")
                },
                color2: {
                  value: new THREE.Color("red")
                }
              },
              vertexShader: vertexShader,
              fragmentShader: fragmentShader,
            }]} 
        />

But when I use my spring color values instead, like the next example, they’ll always be undefined.


const { color1anim } = useSpring({
      color1anim: !custom ? "red" : "yellow",
      config: config.smooth,
      delay: 300
    })

    const { color2anim } = useSpring({
      color2anim: !custom ? "green" : "blue",
      config: config.smooth,
      delay: 300
    })

<animated.shaderMaterial
            /* uniforms={{
              color1: {
                value: new THREE.Color("#1b00e6")
              },
              color2: {
                value: new THREE.Color("#ff08da")
              }
            }} */
            args={[{
              uniforms:{
                color1: {
                  value: new THREE.Color(color1anim)
                },
                color2: {
                  value: new THREE.Color(color2anim)
                }
              },
              vertexShader: vertexShader,
              fragmentShader: fragmentShader,
            }]} 
        />
  )

I think there’s a problem coming from the way props are passed through the component, but does someone has a clue about updating corectly my Spring changing colors in this type of props (again, no problem if passed in a simple prop like rotation-x, color, scale) ?

Thanks a lot !

i dont think thats a good way in general to create a shader material, you should make a class with setter/getters just like you’d do it in plain threejs.

“animating” args would mean you re-create the material 120 times per second. <foo args={[1, 2, 3]} /> is the same as new THREE.Foo(1, 2, 3). if args changes the object must be re-created from scratch because args refers to constructor arguments.

do this instead:

class FooMaterial extends THREE.ShaderMaterial {
  constructor() {
    super({ uniforms: { ... }, vertex............ })
  }
  get color1() { return this.uniforms.color1.value }
  set color1(v) { this.uniforms.color1.value = v } 
}

extend({ FooMaterial })

...

<mesh>
  <fooMaterial color1="hotpink" />

drei has a shortcut/helper for that called shaderMaterial: Threejs journey - CodeSandbox it creates the setter/getters automatically.

if you want to animate it with react-spring:

// After the extend ...
import { a, useSpring } from '@react-spring/three'

const AnimatedFooMaterial = a('fooMaterial')

const props = useSpring({ color1: condition ? "red" : "blue" })
<mesh>
  <AnimatedFooMaterial {...props} />
1 Like
1 Like

Here it was :slight_smile: ! You were right, thanks a lot !

import React, { useRef } from 'react'
import { shaderMaterial } from '@react-three/drei'
import useStore from '../../../../../store'
import * as THREE from "three"
import { vertexShader, fragmentShader } from './index.js'
import { useSpring, config, a } from '@react-spring/three'
import {extend} from '@react-three/fiber'


export default function Shader() { 
    const color1 = useStore(state => state.color1)
    const color2 = useStore(state => state.color2)

    const color1custom = useStore(state => state.color1custom)
    const color2custom = useStore(state => state.color2custom)

    const custom = useStore(state => state.custom)

    const portalMaterial = useRef()
    const AnimatedFooMaterial = a('portalMaterial')

    const { color1anim } = useSpring({
      color1anim: !custom ? color1 : color1custom,
      config: config.smooth,
      delay: 300
    })

    const { color2anim } = useSpring({
      color2anim: !custom ? color2 : color2custom,
      config: config.smooth,
      delay: 300
    })
    
    return (
      <AnimatedFooMaterial color1={color1anim} color2={color2anim} ref={portalMaterial}/>
    )
}

extend({
  PortalMaterial: shaderMaterial(
    {color1: new THREE.Color('yellow'), color2: new THREE.Color('pink') },
      vertexShader,
      fragmentShader,
  ),
})