用于纹理立方体投影的 GLSL 着色器

GLSL shader for texture cubic projection

我正在尝试在我的 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);

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

也许我遗漏了一些关键点,我需要一些提示。立方投影的数学应该是什么样的?

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

查看立方体映射的工作原理,在 OpenGL ES 2.0 规范

中有一个table
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|
--------------------+---------------------------+---+---+---+

Table 3.21: Selection of cube map images based on major axis direction of texture coordinates

用我写的这个函数

#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 并且它 returns 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>

这里的关键点是:法线应该在object-space中。请注意 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).

结果:

编辑:

link 这里值得一提的是 gman 的另一个答案,其中包含有关此主题的更多信息以及一些很酷的优化技术,以避免 GLSL 着色器中的条件:.

实现 textureCube