在两个以上的目标之间使用 mix() 在 WebGL 着色器中变形

Morphing in WebGL shaders with mix() between more then two targets

我正在尝试使用 three.js 构建一个图像滑块,但我很难将适当的状态传递给 glsl 着色器,以便我可以在幻灯片之间转换。我可以轻松地在两个目标(无论是纹理还是 models)之间完成,只需在 0 和 1 之间缓和并将其作为属性浮点数传递,如下所示:

attribute float mix;
vec4 color = mix(tex1, tex2, mix);

但我不明白如何用超过 2 个目标来处理它。我应该传递一个数字并做一堆 if 语句吗?

我设置了我的缓冲平面几何体和我的着色器 material,其中包含我的 3 个纹理,如下所示:

const uniforms = {
  time: { value: 0 },
  tex1: { type: 't', value: null },
  tex2: { type: 't', value: null },
  tex3: { type: 't', value: null },
  activeTexture: { type: 'i', value: 0 },
  mixFactor:  { value: 0 }
}
const vertexShader = document.querySelector('#vertex-shader').text
const fragmentShader = document.querySelector('#fragment-shader').text

const geometry = new THREE.PlaneBufferGeometry(80, 40, 20, 20)
    const material = new THREE.ShaderMaterial({
    uniforms,
    vertexShader,
    fragmentShader
})

// textures are loaded here...

// transition using GSAP
function shift ()  {
    let ease = Power3.easeInOut
    if (counter === 0) {
      TweenMax.to(uniforms.mixFactor, 2, { value: 1, ease, onStart () {
        uniforms.activeTexture.value = 1
      } })
    } else if (counter === 1) {
      TweenMax.to(uniforms.mixFactor, 2, { value: 1, ease, onComplete () {
        uniforms.activeTexture.value = 2
      } })
   } else if (counter === 2) {
      TweenMax.to(uniforms.mixFactor, 2, { value: 2, ease, onComplete () {
        uniforms.activeTexture.value = 0
   } })


   console.log(uniforms.activeTexture.value)

   counter += 1
   if (counter === 3) counter = 0
}


// glsl
// morph between different targets depending on the passed int attribute

void main () {
  vec4 texColor = vec4(0.0);
  if (activeTexture == 0) {
  texColor = transition(tex1, tex2, vUv, mixFactor);
  } else if (activeTexture == 1) {
    texColor = transition(tex2, tex3, vUv, mixFactor);
  } else if (activeTexture == 2) {
    texColor = transition(tex3, tex1, vUv, mixFactor);
  }
  gl_FragColor = texColor;
}

这并没有给我想要的效果(纹理突然在彼此之间切换,没有过渡到位,而且有点难看)。我是三个人的新手,我什至不知道该如何解决这个问题。如何做到这一点?

我会在 GLSL 中进行混合,然后在管理绘制内容的着色器之外休息。您可以让一个着色器采用 2 个或更多纹理,在它们之间转换,但是一旦它们达到 0 或 1,就用另一个纹理切换纹理。如果你只需要三个......这就太过分了。

大致如下:

const myTransitionMaterial = new THREE.ShaderMaterial({
    uniforms:{
        uLerp: {value: 0},
        uTexA: {value: null},
        uTexB: {value: null},
    },
    vertexShader: vs,
    fragmentShader: fs,

})

//lets say you have a list of a bunch of textures, and you add them
myTransitionMaterial.textures = [tex1,tex2,tex3]

//and you want to lerp through them linearly using 0-1 regardless of how many there are
myTransitionMaterial.lerp = (normalizedFactor)=>{

    const length = myTransitionMaterial.textures.length

    const index = normalizedFactor * length // 0-3

    //at 0.00 we want 0-1 indecis and 0.00 f
    //at 0.99 we want 0-1 indecis and 0.99 f 
    //at 1.00 we want 1-2 indecis and 0.00 f
    //at 1.99 we want 1-2 indecis and 0.99 f
    //at 2.00 we want 2-3 indecis and 0.00 f
    //at 2.99 we want 2-3 indecis and 0.99 f
    //at 3.00 we want 3-4 indecis and 0.00 f

    const f = index - Math.floor(index)

    const i0 = Math.floor(index)
    const i1 = i0 <= length ? i0 + 1 : null //catch edge 

    this.uniforms.uLerp.value = f
    this.uniforms.uTexA.value = this.textures[i0]
    this.uniforms.uTexB.value = this.textures[i1]

}.bind(myTransitionMaterial)

对比:

varying vec2 vUv;

void main(){
    vUv = uv;
    gl_Position = vec4(position.xy,0.,1.);
}

fs:

uniform float uLerp;
uniform sampler2D uTexA;
uniform sampler2D uTexB;

varying vec2 vUv;

void main(){

    gl_FragColor = vec4( mix( texture2D(uTexA, vUv).xyz, texture2D(uTexB, vUv).xyz, uLerp ), 1. );

}

这里要指出的一个重要概念是,如果您执行这样的操作,并且第一次尝试 lerp,您的帧速率将在第一次显示纹理时变得不稳定。发生这种情况是因为渲染器会在第一次遇到它们时自动将它们上传到 gpu。例如,如果您使用每个纹理渲染一次帧,甚至在进行此过渡之前,它就会像黄油一样平滑。

如果纹理的数量已经设置(无论如何它应该是统一的)我会做一些不同的:

我会定义一个浮动制服作为你的混合器,然后使用 0-1 值在两者之间转换。通过这种方式,您可以根据需要为混合器变量设置动画,并且 GLSL 保持非常简单:

uniform sampler2d t1;
uniform sampler2d t2;
uniform sampler2d t3;
uniform float mixer;
void main(){
    vec4 c1 = texture2D(t1,vUv);
    vec4 c4 = c1; //create a duplicate so you can loop back
    vec4 c2 = texture2D(t2,vUv);
    vec4 c3 = texture2D(t3,vUv);
    float mp1 = .33333; //define the locations of t2 
    float mp2 = .66666; //and t3
    float w= .33333; //define the width
    c1 *= 1-mix(0.0,w,abs(mixer)); //this is at 1 when mixer is 0 &  0 when .333
    c2 *= 1-mix(0.0,w, abs(mixer-mp1)); //this is 1 when .333 & 0 when 0<mixer>.666
    c3 *= 1-mix(0.0,w,abs(mixer-mp2)); //this is 1 when .666 & 0 when .333<mixer>1.0
    c4 *= 1-mix(0.0,w,abs(mixer-1.0)); //this is 1 when 1 & 0 when .666<mixer
    gl_FragColor=c1+c2+c3+c4; //now it will only ever be a mixture of 2 textures
}

然后你在混频器上做一些边界功能,这样

if(mixer > 1)mixer --;
if(mixer < 0)mixer ++;

然后您可以通过从 0-0.3333 补间从 T1 转到 T2。您可以通过从 .333 到 .666 补间从 T2 到 T3,通过从 .6666 到 1.0 补间从 T3 到 T1 等等。

然后你只需要做一些管理,这样你的补间就可以循环 - 即,如果从当前位置到目标位置的距离大于 1/3,你可以从 0 跳到 1 或者从 1 到 0

我带了 5 戈佩卡:)

比如我们要对多张图片进行转场。所以我们可以在制服中使用数组。

我们开始了

var uniforms = {
  textures: {
    value: []
  },
  transition: {
    value: 0
  }
};
var textureLoader = new THREE.TextureLoader();
textureLoader.setCrossOrigin("");
var pics = [
  "https://threejs.org/examples/textures/UV_Grid_Sm.jpg",
  "https://threejs.org/examples/textures/colors.png",
  "https://threejs.org/examples/textures/planets/moon_1024.jpg",
  "https://threejs.org/examples/textures/decal/decal-normal.jpg"
];
pics.forEach((p, idx)=>{
    textureLoader.load(p, function(tex){
    uniforms.textures.value[idx] = tex;
    tex.needsUpdate = true;
  })
});

我们的几何体和顶点着色器是常用的:

var planeGeom = new THREE.PlaneBufferGeometry(10, 10);
var vertShader = `
    varying vec2 vUv;
    void main()
  {
    vUv = uv;
    vec4 mvPosition = modelViewMatrix * vec4(position, 1.0 );
    gl_Position = projectionMatrix * mvPosition;
  }
`;

我们的片段着色器神奇地出现了,它是动态构建的,并基于我们的数组长度和图片链接:

var fragShader = `
  uniform sampler2D textures[` + pics.length + `];
  uniform float transition;
  varying vec2 vUv;

  vec4 getTexture(int index){
    for(int i = 0; i < ` + pics.length + `; i++){
         if (i == index){   return texture2D(textures[i],vUv); }
    }
  }
  void main()
  {
    if (transition == 1.){
        gl_FragColor = texture2D(textures[` + (pics.length - 1) + `], vUv); // show last
    }
    else {
      float chunk = 1. / ` + (pics.length - 1) + `.; // amount of transitions = amount of pics - 1
        float t = floor(transition / chunk);
      int idx0 = int(t);
      int idx1 = int(t) + 1; 
      gl_FragColor = mix(
        getTexture(idx0),
        getTexture(idx1),
        (transition - (float(t) * chunk)) * ` + (pics.length - 1) + `.
      );
    }
  }
`;

该解决方案足够灵活,因此您可以根据需要进行任意数量的转换。

jsfiddle 示例 r86