问题
I am trying to build an image slider using three.js and am having difficulties with wrapping my head around passing the appropriate state to the glsl shaders so I can transition between the slides. I can easily do it between two targets (be it textures or models) with simply easing between 0 and 1 and passing it as an attrib float like this:
attribute float mix;
vec4 color = mix(tex1, tex2, mix);
But I can't understand how to approach it with more then 2 targets. Should I pass a number and do a bunch of if statements?
I set up my buffer plane geometry and my shader material, which contains my 3 textures, like this:
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;
}
This doesn't give me the desired effect (the textures abruptly switch between one another, don't transition into place, also it's a bit ugly). I am new to three and am clueless how should I even approach the problem. How does one do this?
回答1:
I brought my 5 kopeikas :)
For example, we want to have transition for several pics. So we can use arrays in our uniforms.
Here we go
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;
})
});
Our geometry and vertex shader are usual:
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;
}
`;
Magic comes here in our fragment shader, which built dynamically and based on the length of our array with links to pics:
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) + `.
);
}
}
`;
The solution is flexible enough, thus you can have as many transitions as you want.
jsfiddle example r86
回答2:
I would do the mix in GLSL and rest outside the shaders managing what gets drawn. You can have one shader that takes 2 or more textures, transition between them, but once they get to 0 or 1, switch out the texture with another one. If you need just three though... this is overkill.
Something along the lines of this:
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)
vs:
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. );
}
An important concept to point out here is that if you do something like this, and try to lerp for the first time, your frame rate will get choppy as textures are displayed for the first time. This happens because the renderer will automatically upload them to the gpu as it first encounters them. For example, if you render a frame with each texture once, before even doing this transition, it's going to be smooth as butter.
回答3:
if the number of textures is already set (and it should be as uniforms anyway) I would do it a little different:
i would define a float uniform that is your mixer and then use a 0-1 value to transition between the two. In this way you can animate the mixer variable however you like and the GLSL stays pretty simple:
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
}
So then you do some border function on mixer so that
if(mixer > 1)mixer --;
if(mixer < 0)mixer ++;
and then you can go from T1 to T2 by tweening from 0-0.3333. You can go from T2 to T3 by tweening from .333 to .666, and from T3 to T1 by tweening from .6666 to 1.0 and so on.
Then you just need to do a little management so that your tweens go circularly- ie, if the distance from current position to a target position is greater than 1/3 some amount you do a jump from 0 to 1 or from 1 to 0
来源:https://stackoverflow.com/questions/44768839/morphing-in-webgl-shaders-with-mix-between-more-then-two-targets