Сделайте свет независимым от вида в модели Фонга

Я пытаюсь реализовать модель затенения Фонга, но сталкиваюсь с чем-то довольно странным. Когда я меняю положение просмотра, кажется, что свет ведет себя по-другому, как если бы он зависел от вида. Например, если я нахожусь близко к объекту, я вижу только эффекты окружающего света, а если я ухожу далеко от него, я начинаю видеть вклад рассеянного света.

Это мои шейдеры:

//Vertex Shader

attribute vec4 vPosition;
attribute vec4 vNormal;
varying vec3 N, L, E;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform vec4 lightPosition;

void main()
{
    vec3 pos = -(modelViewMatrix * vPosition).xyz;
    vec3 light = lightPosition.xyz;
    L = normalize(light - pos);
    E = -pos;
    N = normalize((modelViewMatrix * vNormal).xyz);
    gl_Position = projectionMatrix * modelViewMatrix * vPosition;
}
//Fragment Shader

uniform vec4 ambientProduct;
uniform vec4 diffuseProduct;
uniform vec4 specularProduct;
uniform float shininess;
varying vec3 N, L, E;

void main()
{
    vec4 fColor;
    vec3 H = normalize(L + E);
    vec4 ambient = ambientProduct;
    float Kd = max(dot(L, N), 0.0);
    vec4 diffuse = Kd * diffuseProduct;
    float Ks = pow(max(dot(N, H), 0.0), shininess);
    vec4 specular = Ks * specularProduct;
    if (dot(L, N) < 0.0) {
        specular = vec4(0.0, 0.0, 0.0, 1.0);
    }
    fColor = ambient + diffuse + specular;
    fColor.a = 1.0;
    gl_FragColor = fColor;
}

Что я делаю неправильно? Как сделать так, чтобы свет вел себя независимо от положения зрителя?

Обновление 1:

После ответа @Rabbid76 я отредактировал вершинный шейдер, добавив эти строки (а также передав отдельные модели и матрицы представления, но я опущу это для краткости):

    vec3 pos = (modelViewMatrix * vPosition).xyz;
    vec3 light = (viewMatrix * lightPosition).xyz;

И я также обновил вычисление вектора N, поскольку предыдущий способ сделать это, казалось, фактически не допускал затенения для каждого фрагмента:

    N = normalize(mat3(modelViewMatrix) * vNormal.xyz);

Тем не менее, тень, кажется, движется вместе с вращением камеры. Я думаю, это может быть связано с тем, что свет умножается на viewMatrix?


person memememe    schedule 31.08.2019    source источник
comment
Возможно, эта статья окажется полезной.   -  person gman    schedule 01.09.2019


Ответы (2)


Расчет вектора света неверен.

L = normalize(light - pos);

В то время как pos — это позиция в пространстве обзора, light — это позиция в мировом пространстве. light - это положение света в мире. Так что light - pos вообще не имеет никакого смысла. Оба вектора должны быть связаны с одними и теми же системами отсчета.

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

Конечно, преобразование также можно выполнить в коде шейдера:

uniform mat4 viewMatrix;
uniform mat4 modelViewMatrix;
uniform vec4 lightPosition;

void main()
{
    vec3 pos   = (modelViewMatrix * vPosition).xyz;
    vec3 light = (viewMatrix * lightPosition).xyz;
    L = normalize(light - pos);

    // ...
}

Кроме того, обратите внимание, что положение в пространстве обзора не должно быть инвертировано. Должно быть

vec3 pos = (modelViewMatrix * vPosition).xyz; 

скорее, чем

vec3 pos = -(modelViewMatrix * vPosition).xyz;
person Rabbid76    schedule 31.08.2019
comment
У меня все еще есть некоторые странные эффекты, кажется, что свет все еще вращается вместе с видом. - person memememe; 31.08.2019
comment
Обратите внимание, яркость зеркального отражения зависит от направления взгляда. - person Rabbid76; 31.08.2019
comment
Спасибо, что указали на это, но странно то, что вместе с зеркальным бликом тень также перемещается в зависимости от направления взгляда! - person memememe; 31.08.2019
comment
Я отредактировал вопрос с этими исправлениями и еще одним, который я сделал, потому что без него у меня не было бы затенения по фрагментам. - person memememe; 31.08.2019
comment
Это была плохая идея, потому что мой ответ отвечает на вопрос, которого больше нет. - person Rabbid76; 31.08.2019
comment
Тогда вынесу правки в отдельный раздел, извините - person memememe; 31.08.2019
comment
Только что сделал это. Это почти работает, но есть одна последняя деталь, которая меня беспокоит. - person memememe; 31.08.2019
comment
Является ли ligtPosition пространством просмотра или позицией в мировом пространстве? - person Rabbid76; 31.08.2019
comment
Мировое космическое положение. - person memememe; 31.08.2019
comment
@memememe Опять же, зеркальная часть должна как-то вращаться вместе с видом. Только диффузная часть статична. - person Rabbid76; 31.08.2019
comment
@memememe Kd зависит от N и L, но Ks зависит от N, L и V. - person Rabbid76; 31.08.2019
comment
@memememe См. ламбертовский диффузный соответственно отражение Фонга. Возможно, вас интересует еще более сложная легкая модель. - person Rabbid76; 31.08.2019
comment
Я прочитал их все, но все еще что-то я не понимаю. В Фонге у меня есть материальные и световые коэффициенты для 3 компонентов света (окружающий, рассеянный, зеркальный), и эти коэффициенты регулируют цвет каждого компонента (например, если материал имеет [1.0, 0.0, 0.0] в качестве зеркального компонента у него будет красная зеркальная подсветка), но как мне отрегулировать интенсивность зеркальной подсветки? Как мне, например, создать только диффузный материал без бликов? - person memememe; 01.09.2019
comment
@memememe Как мне, например, создать только рассеянный материал без бликов вообще Установите (0, 0, 0, 0) на specularProduct - person Rabbid76; 01.09.2019
comment
Это имеет смысл, но каким-то образом у меня все еще есть движущиеся тени, даже если зеркальный компонент равен 0. Я в основном думал, что артефакт возникает из-за зеркального компонента материала земли, движущегося при вращении камеры, но это не так. - person memememe; 01.09.2019

Рабочий сниппет в вашем вопросе всегда полезен !

Проблемы

  1. Свет и положение должны быть в одном пространстве.

    Это может быть мировое пространство или пространство просмотра, но они должны быть одним и тем же пространством.

    Код имел позицию E в пространстве просмотра, но lightPosition в мировом пространстве.

  2. Вы не можете умножить нормальный на modelViewMatrix

    Вам нужно удалить перевод. Вы также потенциально должны иметь дело с проблемами масштабирования. См. эту статью.

  3. Код вычисляет значения в вершинном шейдере, поэтому они будут интерполированы при передаче во фрагментный шейдер. Это означает, что они больше не будут единичными векторами, поэтому вам нужно их повторно нормализовать.

  4. При вычислении полувектора нужно добавить их направления

    Код добавлял L (направление от поверхности к источнику света) к положению вида поверхности вместо направления от поверхности к виду.

  5. При вычислении направления света от поверхности это было бы light - pos, но код отрицал pos. Конечно, вам также нужно, чтобы pos было отрицательным, чтобы поверхность смотрела в направлении E.

const gl = document.querySelector('canvas').getContext('webgl');
const m4 = twgl.m4;

const vs = `
attribute vec4 vPosition;
attribute vec4 vNormal;
varying vec3 N, L, E;
uniform mat4 viewMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
uniform vec4 lightPosition;

void main()
{
    vec3 pos = (modelViewMatrix * vPosition).xyz;
    vec3 light = (viewMatrix * lightPosition).xyz;
    L = light - pos;
    E = -pos;
    N = mat3(modelViewMatrix) * vNormal.xyz;
    gl_Position = projectionMatrix * modelViewMatrix * vPosition;
}
`;

const fs = `
precision highp float;

uniform vec4 ambientProduct;
uniform vec4 diffuseProduct;
uniform vec4 specularProduct;
uniform float shininess;
varying vec3 N, L, E;

void main()
{
    vec4 fColor;
    vec3 normal = normalize(N);
    vec3 surfaceToLightDir = normalize(L);
    vec3 surfaceToViewDir = normalize(E);
    vec3 H = normalize(surfaceToLightDir + surfaceToViewDir);
    vec4 ambient = ambientProduct;
    float Kd = max(dot(surfaceToLightDir, normal), 0.0);
    vec4 diffuse = Kd * diffuseProduct;
    float Ks = pow(max(dot(normal, H), 0.0), shininess);
    vec4 specular = Ks * specularProduct;
    if (dot(surfaceToLightDir, normal) < 0.0) {
        specular = vec4(0.0, 0.0, 0.0, 1.0);
    }
    fColor = ambient + diffuse + specular;
    fColor.a = 1.0;
    gl_FragColor = fColor;
}
`;

// compiles shaders, links program, looks up locations
const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
const vertices = twgl.primitives.createSphereVertices(
    2, // radius
    8, // subdivision around
    6, // subdivisions down
);
// calls gl.createBuffer, gl.bindBuffer, gl.bufferData
const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
  vPosition: vertices.position,
  vNormal: vertices.normal,
  indices: vertices.indices,
});

function render(time) {
  time *= 0.001;  // convert to seconds
  
  gl.enable(gl.DEPTH_TEST);
  gl.enable(gl.CULL_FACE);
  
  gl.useProgram(programInfo.program);

  // calls gl.bindBuffer, gl.enableVertexAttribArray, gl.vertexAttribPointer
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  
  const projectionMatrix = m4.perspective(
      60 * Math.PI / 180,  // field of view
      gl.canvas.clientWidth / gl.canvas.clientHeight,  // aspect
      0.1,  // znear
      100,  // zfar
  );
  
  const eye = [
    Math.sin(time) * 5, 
    3, 
    3 + Math.cos(time) * 5,
  ];
  const target = [0, 2, 3];
  const up = [0, 1, 0];
  const cameraMatrix = m4.lookAt(eye, target, up);
  const viewMatrix = m4.inverse(cameraMatrix);
  
  const worldMatrix = m4.translation([0, 2, 3]);
  
  const modelViewMatrix = m4.multiply(viewMatrix, worldMatrix);

  const uniforms = {
    viewMatrix,
    modelViewMatrix,
    projectionMatrix,
    lightPosition: [4, 3, 1, 1],
    ambientProduct: [0, 0, 0, 1], 
    diffuseProduct: [1, 1, 1, 1],
    specularProduct: [1, 1, 1, 1],
    shininess: 50,
  };

  // calls gl.uniformXXX
  twgl.setUniforms(programInfo, uniforms);

  // calls gl.drawArrays or gl.drawElements
  twgl.drawBufferInfo(gl, bufferInfo);
  
  // -- not important to answer --
  drawLightAndGrid(uniforms)  

  requestAnimationFrame(render);
}
requestAnimationFrame(render);

// -- ignore below this line. The only point is to give a frame
// of reference.
let gridBufferInfo;
function drawLightAndGrid(sphereUniforms) {
  if (!gridBufferInfo) {
    const vPosition = [];
    const s = 100;
    for (let x = -s; x <= s; x += 2) {
      vPosition.push(x, 0, -s);
      vPosition.push(x, 0,  s);
      vPosition.push(-s, 0, x);
      vPosition.push( s, 0, x);
    }
    gridBufferInfo = twgl.createBufferInfoFromArrays(gl, {
      vPosition,
      vNormal: { value: [0, 0, 0], }
    });
  }
  
  const worldMatrix = m4.translation(sphereUniforms.lightPosition);
  m4.scale(worldMatrix, [0.1, 0.1, 0.1], worldMatrix);
  const uniforms = Object.assign({}, sphereUniforms, {
    modelViewMatrix: m4.multiply(sphereUniforms.viewMatrix, worldMatrix),
    ambientProduct: [1, 0, 0, 1],
    diffuseProduct: [0, 0, 0, 0],
    specularProduct: [0, 0, 0, 0],
  });
  
  twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
  twgl.setUniforms(programInfo, uniforms);
  twgl.drawBufferInfo(gl, bufferInfo);
  twgl.setBuffersAndAttributes(gl, programInfo, gridBufferInfo);
  twgl.setUniforms(programInfo, {
    modelViewMatrix: sphereUniforms.viewMatrix,
    ambientProduct: [0, 0, 1, 1],
  });
  twgl.drawBufferInfo(gl, gridBufferInfo, gl.LINES);
}
canvas { border: 1px solid black }
<script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>
<canvas></canvas>

Лично мне трудно следовать коротким загадочным именам переменных, но это личное предпочтение.

person gman    schedule 01.09.2019
comment
Нельзя умножать нормаль на modelViewMatrix. Вам нужно удалить перевод. Могу я спросить вас, почему? Мы должны как-то двигать нормали вместе с объектом, не так ли? - person memememe; 01.09.2019
comment
Нет, нормали - это только направление. Если вы переведете куб на 15 единиц по оси X, все нормали по-прежнему будут указывать в одном и том же направлении. Если вы добавите +15 к каждой нормали, то нормализованные левые нормали (-1,0,0) теперь будут обращены вправо normalize(-1 + 15, 0, 0) = 1,0,0. Один из способов удалить перевод — взять верхнюю подматрицу 3x3 из modelViewMatrix. Другой — установить normal.w в 0 перед умножением. Это все еще оставляет проблему масштабирования. См. эту статью. - person gman; 01.09.2019