用于定向线性渐变的WebGL着色器

问题描述 投票:0回答:2

我正在用Three js实现定向线性渐变着色器。这是我第一次使用着色器,但是这里有一个起点:

    uniforms: {
            color1: {
                value: new Three.Color('red')
            },
            color2: {
                value: new Three.Color('purple')
            },
            bboxMin: {
                value: geometry.boundingBox.min
            },
            bboxMax: {
                value: geometry.boundingBox.max
            }
        },
        vertexShader: `
            uniform vec3 bboxMin;
            uniform vec3 bboxMax;

            varying vec2 vUv;

            void main() {

                vUv.y = (position.y - bboxMin.y) / (bboxMax.y - bboxMin.y);

                gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
            }
        `,
        fragmentShader: `
            uniform vec3 color1;
            uniform vec3 color2;

            varying vec2 vUv;

            void main() {

                gl_FragColor = vec4(mix(color1, color2, vUv.y), 1.0);
            }
        `,

这对于“自下而上”的线性渐变效果非常好,其中color1(红色)在底部,而color2(紫色)在顶部。我正在尝试找出如何旋转渐变的方向。我知道它需要编辑void main() {函数,但是我对所需的数学有些迷惑。

基本上,我正在尝试重新实现svg渐变定义:

viewBox="0 0 115.23 322.27">
  <defs>
    <linearGradient id="linear-gradient" x1="115.95" y1="302.98" x2="76.08" y2="143.47" gradientUnits="userSpaceOnUse">
      <stop offset="0" stop-color="#8e0000"/>
      <stop offset="1" stop-color="#42210b"/>
    </linearGradient>

所以我必须将视图框和x1,y1,x2,y2以及将两种以上“停止”颜色转换为制服和某种有效的逻辑的可能性

three.js glsl shader webgl linear-gradients
2个回答
2
投票

使用纹理。

[This answer显示使用纹理进行渐变。

作为证明,这通常是解决方案here is a canvas 2d implementation in WebGL,这里是the code in Skia,用于Chrome,Android和Firefox中以绘制SVG和Canvas2D渐变

然后,您可以通过操纵纹理坐标来像其他任何纹理一样偏移,旋转和缩放渐变的应用方式。在three.js中,您可以通过设置texture.offsettexture.repeattexture.rotation或通过更新几何中的纹理坐标来实现。

body {
  margin: 0;
}
#c {
  width: 100vw;
  height: 100vh;
  display: block;
}
<canvas id="c"></canvas>
<script type="module">
import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.module.js';

function main() {
  const canvas = document.querySelector('#c');
  const renderer = new THREE.WebGLRenderer({canvas});

  const fov = 75;
  const aspect = 2;  // the canvas default
  const near = 0.1;
  const far = 5;
  const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
  camera.position.z = 2;

  const scene = new THREE.Scene();

  const geometry = new THREE.PlaneBufferGeometry(1, 1);

  const tempColor = new THREE.Color();
  function get255BasedColor(color) {
    tempColor.set(color);
    return tempColor.toArray().map(v => v * 255);
  }
  
  function makeRampTexture(stops) {
    // let's just always make the ramps 256x1
    const res = 256;
    const data = new Uint8Array(res * 3);
    const mixedColor = new THREE.Color();
    
    let prevX = 0;
    for (let ndx = 1; ndx < stops.length; ++ndx) {
      const nextX = Math.min(stops[ndx].position * res, res - 1);
      if (nextX > prevX) {
        const color0 = stops[ndx - 1].color;
        const color1 = stops[ndx].color;
        const diff = nextX - prevX;
        for (let x = prevX; x <= nextX; ++x) {
          const u = (x - prevX) / diff;
          mixedColor.copy(color0);
          mixedColor.lerp(color1, u);
          data.set(get255BasedColor(mixedColor), x * 3);
        }
      }
      prevX = nextX;
    }
    
    return new THREE.DataTexture(data, res, 1, THREE.RGBFormat);
  }
  

  function makeInstance(geometry, x, scale, rot) {
    const texture = makeRampTexture([
      { position: 0, color: new THREE.Color('red'), },
      { position: 0.7, color: new THREE.Color('yellow'), },
      { position: 1, color: new THREE.Color('blue'), },
    ]);
    texture.repeat.set(1 / scale, 1 / scale);
    texture.rotation = rot;

    const material = new THREE.MeshBasicMaterial({map: texture});

    const cube = new THREE.Mesh(geometry, material);
    scene.add(cube);

    cube.position.x = x;

    return cube;
  }

  const cubes = [
    makeInstance(geometry,  0, 1, 0),
    makeInstance(geometry, -1.1, 1.42, Math.PI / 4),
    makeInstance(geometry,  1.1, 1, Math.PI / 2),
  ];

  function resizeRendererToDisplaySize(renderer) {
    const canvas = renderer.domElement;
    const width = canvas.clientWidth;
    const height = canvas.clientHeight;
    const needResize = canvas.width !== width || canvas.height !== height;
    if (needResize) {
      renderer.setSize(width, height, false);
    }
    return needResize;
  }

  function render(time) {
    time *= 0.001;

    if (resizeRendererToDisplaySize(renderer)) {
      const canvas = renderer.domElement;
      camera.aspect = canvas.clientWidth / canvas.clientHeight;
      camera.updateProjectionMatrix();
    }

    renderer.render(scene, camera);

    requestAnimationFrame(render);
  }

  requestAnimationFrame(render);
}

main();
</script>

请注意,这很不幸,但是three.js并未将纹理矩阵设置(偏移,重复,旋转)与纹理数据本身分开,这意味着您不能使用相同的数据以不同的方式使用渐变纹理。您必须为每一个制作独特的纹理。

您可以为每个几何图形制作不同的纹理坐标,但这也不理想。

另一种解决方案是制作自己的着色器,该着色器使用纹理矩阵并通过该矩阵传递偏移,重复,旋转,如果要避免资源重复。

幸运的是,256x1 RGB渐变纹理不是那么大,所以我只制作多个渐变纹理而不必担心。


0
投票

我由gman赞成的答案是正确的,但是我发现我不得不做一些额外的事情,因为我没有使用标准的几何图形,而是使用了由svg图标制成的几何图形,并且答案也无法完全解释如何使用线性渐变定义以三种颜色创建纹理,该渐变可以具有多种颜色和定义的方向(需要弄清楚的东西)。

[这些步骤仅适用于偶然遇到此问题并同样处理非标准几何并困惑如何在《三分之三》中制作线性渐变纹理的个人:

使用画布2d生成渐变纹理。您需要事先计算出脸部对象的大小,对我来说,我只是进行了bbox测量,或者,如果您不希望精确的渐变到脸部位置匹配,则可以使用任意大小。

function generateTextureGradient(size, x1, y1, x2, y2, colors){
    let width = size.width
    let height = size.height
    const canvas = new self.OffscreenCanvas(width, height)
    let context = canvas.getContext('2d')
    context.rect(0, 0, width, height)
    let gradient = context.createLinearGradient(x1, y1, x2, y2)
    for (let color of colors) {
        gradient.addColorStop(color.props.offset, color.props['stop-color'])
    }
    context.fillStyle = gradient
    context.fill()
    return canvas
}

展开您的几何UV,我的svg几何有80个面但0个faceVertexes,使用此循环生成faceVertexUVs,因此三人了解如何将纹理放置到网格上。

for (var i = 0; i < geometry.faces.length; i++) {
        var face = geometry.faces[i];
        var faceUVs = geometry.faceVertexUvs[0][i] || [
            new Three.Vector2(),
            new Three.Vector2(),
            new Three.Vector2()
        ]
        var va = geometry.vertices[geometry.faces[i].a]
        var vb = geometry.vertices[geometry.faces[i].b]
        var vc = geometry.vertices[geometry.faces[i].c]
        var vab = new Three.Vector3().copy(vb).sub(va)
        var vac = new Three.Vector3().copy(vc).sub(va)
        //now we have 2 vectors to get the cross product of...
        var vcross = new Three.Vector3().copy(vab).cross(vac);
        //Find the largest axis of the plane normal...
        vcross.set(Math.abs(vcross.x), Math.abs(vcross.y), Math.abs(vcross.z))
        var majorAxis = vcross.x > vcross.y ? (vcross.x > vcross.z ? 'x' : vcross.y > vcross.z ? 'y' : vcross.y > vcross.z) : vcross.y > vcross.z ? 'y' : 'z'
        //Take the other two axis from the largest axis
        var uAxis = majorAxis == 'x' ? 'y' : majorAxis == 'y' ? 'x' : 'x';
        var vAxis = majorAxis == 'x' ? 'z' : majorAxis == 'y' ? 'z' : 'y';
        faceUVs[0].set(va[uAxis], va[vAxis])
        faceUVs[1].set(vb[uAxis], vb[vAxis])
        faceUVs[2].set(vc[uAxis], vc[vAxis])
        geometry.faceVertexUvs[0][i] = faceUVs
 }
geometry.elementsNeedUpdate = geometry.verticesNeedUpdate = geometry.uvsNeedUpdate = true;

通过将纹理添加到材质中然后制作网格来完成

var texture = new Three.CanvasTexture(
    generateTextureGradient(
        {width, height},
        grad.x1,
        grad.y1,
        grad.x2,
        grad.y2,
        grad.colors
    )
)
var material = new Three.MeshBasicMaterial({
    side: Three.DoubleSide,
    map: texture,
    wireframe: false
})
var mesh = new Three.Mesh(geometry, material)
© www.soinside.com 2019 - 2024. All rights reserved.