【问题标题】:GLSL shader for texture cubic projection用于纹理三次投影的 GLSL 着色器
【发布时间】:2019-03-09 19:15:06
【问题描述】:

我正在尝试在我的 WebGL 着色器中实现纹理三次投影,如下图所示:

到目前为止我尝试了什么:

我正在传递我的对象的边界框(图片中间的框)如下:

uniform vec3 u_bbmin;
uniform vec3 u_bbmax;

...所以我的投影框的八个顶点是:

vec3 v1 = vec3(u_bbmin.x, u_bbmin.y, u_bbmin.z);
vec3 v2 = vec3(u_bbmax.x, u_bbmin.y, u_bbmin.z);
vec3 v3 = vec3(u_bbmin.x, u_bbmax.y, u_bbmin.z);
...other combinations
vec3 v8 = vec3(u_bbmax.x, u_bbmax.y, u_bbmax.z);

最后,为了从我的纹理中采样,我需要一张如下形式的贴图:

varying vec3 v_modelPos;
...
uniform sampler2D s_texture;
vec2 tCoords = vec2(0.0);

tCoords.s = s(x,y,z)
tCoords.t = t(y,y,z)

vec4 color = texture2D(s_texture, tCoords);

我能够实现球形和圆柱形投影,但我现在卡住了如何获得这种立方体贴图,纹理应该拉伸到整个边界框,纵横比无关紧要。

也许我遗漏了一些关键点,我需要一些提示。三次投影的数学应该是怎样的?

【问题讨论】:

    标签: webgl projection fragment-shader glsles


    【解决方案1】:

    老实说,我不知道这是否正确,但是...

    查看立方体映射的工作原理,在 OpenGL ES 2.0 规范中有一个表格

    Major Axis Direction|        Target             |sc |tc |ma |
    --------------------+---------------------------+---+---+---+
           +rx          |TEXTURE_CUBE_MAP_POSITIVE_X|−rz|−ry| rx|
           −rx          |TEXTURE_CUBE_MAP_NEGATIVE_X| rz|−ry| rx|
           +ry          |TEXTURE_CUBE_MAP_POSITIVE_Y| rx| rz| ry|
           −ry          |TEXTURE_CUBE_MAP_NEGATIVE_Y| rx|−rz| ry|
           +rz          |TEXTURE_CUBE_MAP_POSITIVE_Z| rx|−ry| rz|
           −rz          |TEXTURE_CUBE_MAP_NEGATIVE_Z|−rx|−ry| rz|
    --------------------+---------------------------+---+---+---+
    

    表 3.21:基于纹理坐标长轴方向的立方体贴图图像选择

    使用我写的这个函数

    #define RX 0
    #define RY 1
    #define RZ 2
    #define S 0
    #define T 1
    
    void majorAxisDirection(vec3 normal, inout mat4 uvmat) {
       vec3 absnorm = abs(normal);
       if (absnorm.x > absnorm.y && absnorm.x > absnorm.z) {
         // x major
         if (normal.x >= 0.0) {
           uvmat[RZ][S] = -1.;
           uvmat[RY][T] = -1.;
         } else {
           uvmat[RZ][S] =  1.;
           uvmat[RY][T] = -1.;
         }
       } else if (absnorm.y > absnorm.z) {
         // y major
         if (normal.y >= 0.0) {
           uvmat[RX][S] =  1.;
           uvmat[RZ][T] =  1.;
         } else {
           uvmat[RX][S] =  1.;
           uvmat[RZ][T] = -1.;
         }
       } else {
         // z major
         if (normal.z >= 0.0) {
           uvmat[RX][S] =  1.;
           uvmat[RY][T] = -1.;
         } else {
           uvmat[RX][S] = -1.;
           uvmat[RY][T] = -1.;
         }
       }
    }
    

    您传入一个矩阵,它会将其设置为将正确的 X、Y 或 Z 移动到 X 和 Y 列(以转换为 s 和 t)。换句话说,你传入 normal 并返回 s 和 t。

    这将有效地给出一个单位立方体投影在原点的正侧。添加另一个矩阵,我们可以移动和缩放那个立方体。

    如果您希望它完全适合立方体,那么您需要设置比例、平移和方向以匹配立方体。

    "use strict";
    
    /* global document, twgl, requestAnimationFrame */
    
    const vs = `
    uniform mat4 u_model;
    uniform mat4 u_viewProjection;
    
    attribute vec4 position;
    attribute vec3 normal;
    attribute vec2 texcoord;
    
    varying vec2 v_texCoord;
    varying vec3 v_normal;
    varying vec3 v_position;
    
    void main() {
      v_texCoord = texcoord;
      vec4 position = u_model * position;
      gl_Position = u_viewProjection * position;
      v_position = position.xyz;
      v_normal = (u_model * vec4(normal, 0)).xyz;
    }
    `;
    const fs = `
    precision mediump float;
    
    varying vec3 v_position;
    varying vec2 v_texCoord;
    varying vec3 v_normal;
    
    uniform mat4 u_cubeProjection;
    uniform sampler2D u_diffuse;
    
    #define RX 0
    #define RY 1
    #define RZ 2
    #define S 0
    #define T 1
    
    #if BOX_PROJECTION
    
    void majorAxisDirection(vec3 normal, inout mat4 uvmat) {
       vec3 absnorm = abs(normal);
       if (absnorm.x > absnorm.y && absnorm.x > absnorm.z) {
         // x major
         if (normal.x >= 0.0) {
           uvmat[RZ][S] = -1.;
           uvmat[RY][T] = -1.;
         } else {
           uvmat[RZ][S] =  1.;
           uvmat[RY][T] = -1.;
         }
       } else if (absnorm.y > absnorm.z) {
         // y major
         if (normal.y >= 0.0) {
           uvmat[RX][S] =  1.;
           uvmat[RZ][T] =  1.;
         } else {
           uvmat[RX][S] =  1.;
           uvmat[RZ][T] = -1.;
         }
       } else {
         // z major
         if (normal.z >= 0.0) {
           uvmat[RX][S] =  1.;
           uvmat[RY][T] = -1.;
         } else {
           uvmat[RX][S] = -1.;
           uvmat[RY][T] = -1.;
         }
       }
    }
    
    #else  // cube projection
    
    void majorAxisDirection(vec3 normal, inout mat4 uvmat) {
       vec3 absnorm = abs(normal);
       if (absnorm.x > absnorm.y && absnorm.x > absnorm.z) {
         // x major
         uvmat[RZ][S] =  1.;
         uvmat[RY][T] = -1.;
       } else if (absnorm.y > absnorm.z) {
         uvmat[RX][S] =  1.;
         uvmat[RZ][T] =  1.;
       } else {
         uvmat[RX][S] =  1.;
         uvmat[RY][T] = -1.;
       }
    }
    
    #endif
    
    void main() {
      vec3 normal = normalize(v_normal);
      mat4 uvmat = mat4(
        vec4(0, 0, 0, 0),
        vec4(0, 0, 0, 0),
        vec4(0, 0, 0, 0),
        vec4(0, 0, 0, 1));
      majorAxisDirection(normal, uvmat);
      uvmat = mat4(
        abs(uvmat[0]),
        abs(uvmat[1]),
        abs(uvmat[2]),
        abs(uvmat[3]));
      
      vec2 uv = (uvmat * u_cubeProjection * vec4(v_position, 1)).xy;
      
      gl_FragColor = texture2D(u_diffuse, uv);
    }
    `;
    
    const m4 = twgl.m4;
    const gl = twgl.getWebGLContext(document.getElementById("c"));
    // compile shaders, look up locations
    const cubeProjProgramInfo = twgl.createProgramInfo(gl,
        [vs, '#define BOX_PROJECTION 0\n' + fs]);
    const boxProjProgramInfo = twgl.createProgramInfo(gl, 
        [vs, '#define BOX_PROJECTION 1\n' + fs]);
    
    let progNdx = 1;
    const programInfos = [
      cubeProjProgramInfo,
      boxProjProgramInfo,
    ];
    
    // create buffers
    const cubeBufferInfo = twgl.primitives.createCubeBufferInfo(gl, 2);
    const sphereBufferInfo = twgl.primitives.createSphereBufferInfo(gl, 1, 60, 40);
    
    const ctx = document.createElement("canvas").getContext("2d");
    ctx.canvas.width = 256;
    ctx.canvas.height = 256;
    ctx.fillStyle = `hsl(${360}, 0%, 30%)`;
    ctx.fillRect(0, 0, 256, 256);
    for (let y = 0; y < 4; ++y) {
      for (let x = 0; x < 4; x += 2) {
        ctx.fillStyle = `hsl(${(x + y) / 16 * 360}, 100%, 75%)`;
        ctx.fillRect((x + (y & 1)) * 64, y * 64, 64, 64);
      }
    }
    ctx.lineWidth = 10;
    ctx.strokeRect(0, 0, 256, 256);
    ctx.font = "240px sans-serif";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillStyle = 'red';
    ctx.fillText("F", 128, 128);
    
    const texture = twgl.createTexture(gl, {
      src: ctx.canvas,
      wrap: gl.CLAMP_TO_EDGE,
      min: gl.LINEAR,  // no mips
    });
    
    function addElem(parent, type) {
      const elem = document.createElement(type);
      parent.appendChild(elem);
      return elem;
    }
    
    function makeRange(parent, obj, prop, min, max, name) {
      const divElem = addElem(parent, 'div');
      const inputElem = addElem(divElem, 'input');
      Object.assign(inputElem, {
        type: 'range',
        min: 0,
        max: 1000,
        value: (obj[prop] - min) / (max - min) * 1000,
      });
      const valueElem = addElem(divElem, 'span');
      valueElem.textContent = obj[prop].toFixed(2);
      const labelElem = addElem(divElem, 'label');
      labelElem.textContent = name;
      
      function update() {
        inputElem.value = (obj[prop] - min) / (max - min) * 1000,
        valueElem.textContent = obj[prop].toFixed(2);
      }
      
      inputElem.addEventListener('input', (e) => {
        obj[prop] = (e.target.value / 1000 * (max - min) + min);
        update();
      });
      
      return update;
    }
    
    const models = [
      cubeBufferInfo,
      sphereBufferInfo,
      cubeBufferInfo,
    ];
    const rotateSpeeds = [
      1,
      1,
      0,
    ];
    let modelNdx = 0;
    const ui = document.querySelector('#ui');
    const cubeMatrix = m4.translation([0.5, 0.5, 0.5]);
    const updaters = [
      makeRange(ui, cubeMatrix,  0, -2, 2, 'sx'),
      makeRange(ui, cubeMatrix,  5, -2, 2, 'sy'),
      makeRange(ui, cubeMatrix, 10, -2, 2, 'sz'),
      makeRange(ui, cubeMatrix, 12, -2, 2, 'tx'),
      makeRange(ui, cubeMatrix, 13, -2, 2, 'ty'),
      makeRange(ui, cubeMatrix, 14, -2, 2, 'tz'),
    ];
    document.querySelectorAll('input[name=shape]').forEach((elem) => {
      elem.addEventListener('change', (e) => {
        if (e.target.checked) {
          modelNdx = parseInt(e.target.value);
          if (modelNdx == 2) {
            m4.scaling([1/2, 1/2, 1/2], cubeMatrix);
            m4.translate(cubeMatrix, [1, 1, 1], cubeMatrix);
            updaters.forEach(f => f());
          }
        }
      })
    });
    document.querySelectorAll('input[name=proj]').forEach((elem) => {
      elem.addEventListener('change', (e) => {
        if (e.target.checked) {
          progNdx = parseInt(e.target.value);
        }
      })
    });
    
    
    const uniforms = {
      u_diffuse: texture,
      u_cubeProjection: cubeMatrix,
    };
    
    function render(time) {
      time *= 0.001;
      twgl.resizeCanvasToDisplaySize(gl.canvas);
      gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
      
      const programInfo = programInfos[progNdx];
      const bufferInfo = models[modelNdx];
    
      gl.enable(gl.DEPTH_TEST);
      gl.enable(gl.CULL_FACE);
      gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    
      const fov = 30 * Math.PI / 180;
      const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
      const zNear = 0.5;
      const zFar = 10;
      const projection = m4.perspective(fov, aspect, zNear, zFar);
      const eye = [0, 4, -4];
      const target = [0, 0, 0];
      const up = [0, 1, 0];
    
      const camera = m4.lookAt(eye, target, up);
      const view = m4.inverse(camera);
      const viewProjection = m4.multiply(projection, view);
      const model = m4.rotationY(time * rotateSpeeds[modelNdx]);
    
      uniforms.u_viewProjection = viewProjection;
      uniforms.u_model = model;
    
      gl.useProgram(programInfo.program);
      twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
      twgl.setUniforms(programInfo, uniforms);
      gl.drawElements(gl.TRIANGLES, bufferInfo.numElements, gl.UNSIGNED_SHORT, 0);
    
      requestAnimationFrame(render);
    }
    requestAnimationFrame(render);
    body {
      margin: 0;
      font-family: monospace;
      color: white;
    }
    canvas {
      display: block;
      width: 100vw;
      height: 100vh;
      background: #444;
    }
    #ui {
      position: absolute;
      left: 0;
      top: 0;
    }
    #ui span {
      display: inline-block;
      width: 4em;
      text-align: right;
    }
    <canvas id="c"></canvas>
    
    <script src="https://twgljs.org/dist/3.x/twgl-full.min.js"></script>
    <div id="ui">
      <div>
        <input type="radio" name="proj" id="sphere" value="0">
        <label for="sphere">cubic projection</label>
        <input type="radio" name="proj" id="cube" value="1" checked>
        <label for="cube">box projection</label>
      </div> 
      <div>
        <input type="radio" name="shape" id="sphere" value="1">
        <label for="sphere">sphere</label>
        <input type="radio" name="shape" id="cube" value="0" checked>
        <label for="cube">cube</label>
        <input type="radio" name="shape" id="cube" value="2">
        <label for="cube">cube match</label>
      </div> 
    </div>

    【讨论】:

    • 是的,我也尝试过类似的方法,但无法使其正常工作。顺便说一句,我认为内部分支应该引用 normal,而不是 abs(normal)。无论如何,我今天找到了这个教程here,它正是展示了我所需要的。我将深入研究它,试图完全理解该主题。
    • 伟大的人,感谢您的努力!我们随时欢迎您的建议。
    【解决方案2】:

    这里的关键点是:法线应该在对象空间中。请注意gman 的答案比我的更优雅,通过使用矩阵进行uv 计算。我使用的是边界框坐标,它已经作为uniform 传递给顶点着色器用于其他一般目的。

    而且,我什至不需要区分所有六个主轴,我只需要三个侧面投影,所以可以简化下来。当然,纹理会被镜像到相对的面上。

    float sX = u_bbmax.x - u_bbmin.x;
    float sY = u_bbmax.y - u_bbmin.y;
    float sZ = u_bbmax.z - u_bbmin.z;
    
    /* --- BOX PROJECTION - THREE SIDES --- */
    if( (abs(modelNormal.x) > abs(modelNormal.y)) && (abs(modelNormal.x) > abs(modelNormal.z)) ) {
      uvCoords = modelPos.yz / vec2(sY, -sZ); // X axis
    } else if( (abs(modelNormal.z) > abs(modelNormal.x)) && (abs(modelNormal.z) > abs(modelNormal.y)) ) {
      uvCoords = modelPos.xy / vec2(sX, -sY); // Z axis
    } else {
      uvCoords = modelPos.xz / vec2(sX, -sZ); // Y axis
    }
    uvCoords += vec2(0.5);
    

    解释:

    1. 纹理投影的方向由 modelPos 坐标的顺序。 示例:纹理可以通过使用旋转 90 度 modelPos.yx 而不是 modelPos.xy
    2. 纹理投影的方向由 modelPos 坐标。 示例:纹理可以通过使用在 Y 轴上进行镜像 vec2(sX, sY) 而不是 vec2(sX, -sY)

    结果:

    编辑:

    值得在这里链接来自gman 的另一个答案,其中包含有关此主题的其他信息以及一些很酷的优化技术,以避免在 GLSL 着色器中出现条件:How to implement textureCube using 6 sampler2D

    【讨论】:

      猜你喜欢
      • 2011-10-04
      • 1970-01-01
      • 2015-02-23
      • 1970-01-01
      • 2010-10-27
      • 2011-09-25
      • 1970-01-01
      • 1970-01-01
      • 2020-12-03
      相关资源
      最近更新 更多