GLSL шейдер для кубической проекции текстуры - PullRequest
0 голосов
/ 04 октября 2018

Я пытаюсь реализовать кубическую проекцию текстуры внутри моего шейдера WebGL, как показано на рисунке ниже:

Cubic projection

Что я пробовал до сих пор:

Я передаю ограничивающий прямоугольник моего объекта (прямоугольник в середине рисунка) следующим образом:

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);

Мне удалось реализовать сферические и цилиндрические проекции, но я застрялТеперь, как получить кубическую карту такого типа, текстура должна растягиваться на всю ограничивающую рамку, соотношение сторон не имеет значения.

Возможно, мне не хватает некоторых ключевых точек, и мне нужны некоторые подсказки.Как должна выглядеть математика для кубической проекции?

Ответы [ 2 ]

0 голосов
/ 09 ноября 2018

Ключевой момент здесь: нормали должны быть в объектном пространстве.Обратите внимание, что ответ 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).

Результат:

enter image description here

РЕДАКТИРОВАТЬ:

Стоит связать здесь еще один ответ от gman , который содержит дополнительную информацию по этой теме, а также некоторые интересные методы оптимизации, чтобы избежать условных выражений внутри шейдеров GLSL: Как реализовать textureCube с использованием 6 sampler2D .

0 голосов
/ 05 октября 2018

Честно говоря, я не знаю, правильно ли это или нет, но ...

Посмотрим, как работает кубическое отображение, есть таблица в спецификации 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).Другими словами, вы передаете в нормальном режиме, и он возвращает 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>
...