Пользовательский шейдер на объекте не корректен, когда объект вращается

1

https://codepen.io/im_paul_hi/pen/bObYOy?editors=0010

Я пытаюсь написать базовый шейдер рассеянного освещения, но когда я применяю какие-либо изменения положения/поворота к моему объекту, кажется, что затенение не обновляется. Если я переместлю позицию "Custom Point Light", она, похоже, будет работать нормально (затенение куба обновится точно), но если сам куб движется, затенение выглядит некорректно.

Если вы раскомментируете строки 183 и 184, вращение будет применено, и затенение будет неправильным.

cube.rotation.x += rotSpeed.x;
cube.rotation.y += rotSpeed.y;

class PromisedLoad {

  static GenericLoader(loader, url, callback) {
    return new Promise((resolve, reject) => {
      loader.load(url, (object) => {
        if (callback) {
          callback(object, resolve);
        } else {
          resolve(object);
        }
      }, (progress) => {
        console.log(progress);
      }, (error) => {
        reject(error);
      });
    });
  }

  static GetGLTF(url, callback) {
    let gltfLoader = new THREE.GLTFLoader();
    return this.GenericLoader(gltfLoader, url, callback);
  }


}

let vertexShader2 = '
uniform float time;
uniform vec3 materialColor;
uniform vec3 ambientLightColor;
uniform float ambientLightStrength;
uniform vec3 customPointLightPos;

varying vec3 vNormal;
varying vec3 lightVec;

void main() {
  // normal is an attribute passed in by threejs
  vNormal = normal;
  lightVec = normalize(customPointLightPos - position);
 

  gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
';

// import customFragmentShader from './shaders/fragmentShader1.glsl';
let fragmentShader1 = '
uniform float time;
uniform vec3 materialColor;
uniform vec3 ambientLightColor;
uniform float ambientLightStrength;
uniform vec3 customPointLightPos;

varying vec3 vNormal;
varying vec3 lightVec;

void main() {
  float dProd = max(0.0, dot(vNormal, lightVec));
  vec3 c = mix(materialColor * dProd, ambientLightColor, ambientLightStrength);
  gl_FragColor = vec4(c, 1.0);
}
';

let mouse = new THREE.Vector2();

window.addEventListener('mousemove', onDocumentMouseMove, false);

document.addEventListener('DOMContentLoaded', () => {
  let renderer,
    camera,
    scene = null;
  const container = document.getElementById('container');
  let controls;
  let startTime, time;
  let cube;
  let rotSpeed = new THREE.Vector3(0.05, 0.03, 0.0);
  let axesHelper;
  let uniforms;
  let customPointLight;

  initialize();

  // console.log('rotSpeed:  ', rotSpeed);
  // setupGUI();

  async function initialize() {
    scene = new THREE.Scene();
    renderer = new THREE.WebGLRenderer({
      antialias: true // to get smoother output
    });
    renderer.setClearColor(0x3b3b3b);
    renderer.setSize(window.innerWidth, window.innerHeight);
    container.appendChild(renderer.domElement);

    // create a camera in the scene
    camera = new THREE.PerspectiveCamera(
      35,
      window.innerWidth / window.innerHeight,
      1,
      10000
    );

    axesHelper = new THREE.AxesHelper(5);
    scene.add(axesHelper);

    addCube();

    addCustomPointLight();

    controls = new THREE.OrbitControls(camera);
    scene.add(camera);
    camera.position.z = 5;
    controls.update();

    // and then just look at it!
    camera.lookAt(scene.position);
    controls.update();

    animate();
  }

  function addCube() {
    // let geometry = new THREE.SphereGeometry(1, 32, 32);
    let geometry = new THREE.BoxGeometry(1,1,1);
    uniforms = {
      time: {
        type: 'f',
        value: 0
      },
      materialColor: {
        type: 'v3f',
        value: new THREE.Vector3(1.0, 0.0, 0.0)
      },
      ambientLightColor: {
        type: 'v3f',
        value: new THREE.Vector3(0.0, 0.0, 1.0)
      },
      ambientLightStrength: {
        type: 'f',
        value: 0.3
      },
      customPointLightPos: {
        type: 'v3f',
        value: new THREE.Vector3(2.0, 2.0, 2.0)
      }
    };
    const shaderMaterialParams = {
      uniforms: uniforms,
      vertexShader: vertexShader2,
      fragmentShader: fragmentShader1
    };
    const customMaterial = new THREE.ShaderMaterial(shaderMaterialParams);

    cube = new THREE.Mesh(geometry, customMaterial);
    scene.add(cube);
  }

  function addCustomPointLight() {
    let geo = new THREE.BoxGeometry(0.3, 0.3, 0.3);
    let mat = new THREE.MeshBasicMaterial();
    customPointLight = new THREE.Mesh(geo, mat);
    customPointLight.position.set(2, 2, 2);
    scene.add(customPointLight);
  }

  function normalize(x, fromMin, fromMax) {
    let totalRange;

    x = Math.abs(x);
    totalRange = Math.abs(fromMin) + Math.abs(fromMax);
    // now we can map out the range from 0 to the totalRange and get a normalized (0 - 1) value
    return x / totalRange;
  }

  function animate() {
    requestAnimationFrame(animate);

    time = performance.now() / 1000;

    cube.material.uniforms.time.value = time;

    cube.rotation.x += rotSpeed.x;
    cube.rotation.y += rotSpeed.y;

    render();
  }

  function render() {
    renderer.render(scene, camera);

    controls.update();
  }

  setupGUI(rotSpeed, uniforms, cube, customPointLight);
});

function setupGUI(rotSpeed, uniforms, cube, customPointLight) {
  let options = {
    velx: 0,
    vely: 0,
    rotSpeed: rotSpeed,
    materialColor: uniforms.materialColor.value.toArray(),
    ambientLightColor: uniforms.ambientLightColor.value.toArray(),
    ambientLightStrength: uniforms.ambientLightStrength.value,
    customPointLightPos: {
      x: 2,
      y: 2,
      z: 2
    }
  };
  let gui = new dat.GUI();
  let rotation = gui.addFolder('Rotation');
  rotation
    .add(options.rotSpeed, 'x', -0.02, 0.02)
    .name('X')
    .listen();
  rotation
    .add(options.rotSpeed, 'y', -0.02, 0.02)
    .name('Y')
    .listen();
  rotation.open();
  let uniformsGUI = gui.addFolder('Uniforms');
  uniformsGUI
    .addColor(options, 'materialColor')
    .onChange(function(value) {
      cube.material.uniforms.materialColor.value.x = value[0] / 255;
      cube.material.uniforms.materialColor.value.y = value[1] / 255;
      cube.material.uniforms.materialColor.value.z = value[2] / 255;
    })
    .name('materialColor')
    .listen();
  uniformsGUI.addColor(options, 'ambientLightColor').onChange(function(value) {
    cube.material.uniforms.ambientLightColor.value.x = value[0] / 255;
    cube.material.uniforms.ambientLightColor.value.y = value[1] / 255;
    cube.material.uniforms.ambientLightColor.value.z = value[2] / 255;
  });
  uniformsGUI
    .add(options, 'ambientLightStrength', 0.0, 1.0)
    .onChange(function(value) {
      cube.material.uniforms.ambientLightStrength.value = value;
    });
  uniformsGUI.open();

  let customPointLightGUI = gui.addFolder('Custom Point Light');
  customPointLightGUI
    .add(customPointLight.position, 'x', -5, 5)
    .onChange(function(value) {
      cube.material.uniforms.customPointLightPos.value.x = value;
    });
  customPointLightGUI
    .add(customPointLight.position, 'y', -5, 5)
    .onChange(function(value) {
      cube.material.uniforms.customPointLightPos.value.y = value;
    });
  customPointLightGUI
    .add(customPointLight.position, 'z', -5, 5)
    .onChange(function(value) {
      cube.material.uniforms.customPointLightPos.value.z = value;
    });
  customPointLightGUI.open();

  let box = gui.addFolder('Cube');
  box
    .add(cube.scale, 'x', 0, 3)
    .name('Width')
    .listen();
  box
    .add(cube.scale, 'y', 0, 3)
    .name('Height')
    .listen();
  box
    .add(cube.scale, 'z', 0, 3)
    .name('Length')
    .listen();
  box.add(cube.material, 'wireframe').listen();
  box.open();
}

function onDocumentMouseMove(event) {
  event.preventDefault();
  mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
  mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
html, body {
    height: 100%;
    width: 100%;
    font-size: 100%;
    font-family: 'Roboto', sans-serif;
    text-align: center;
    font-weight: lighter;
    background: grey;
    overflow-y: hidden;
}

a{
    color: inherit;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/98/three.js"></script>
<script src="https://unpkg.com/[email protected]/examples/js/loaders/GLTFLoader.js"></script>
<script src="https://unpkg.com/[email protected]/examples/js/controls/OrbitControls.js"></script>
<div id="container"></div>
<!--https://cdnjs.cloudflare.com/ajax/libs/three.js/98/three.js-->
Теги:
three.js
glsl

1 ответ

1
Лучший ответ

Проблема вызвана тем, что в фрагментном шейдере vNormal - это вектор в пространстве модели, а position - это точка в пространстве модели, а customPointLightPos - это позиция в мировом пространстве.

Вы должны преобразовать vNormal из пространства модели для просмотра пространства в вершинном шейдере. это может быть сделано normalMatrix, который предоставляется THREE.js.
Чтобы рассчитать вектор света, вы должны преобразовать position из пространства модели в пространство просмотра, что может быть сделано с помощью modelViewMatrix.
И вы должны преобразовать customPointLightPos из мирового пространства в пространство просмотра, что можно сделать с помощью viewMatrix:

vNormal           = normalMatrix * normal;
vec4 viewPos      = modelViewMatrix * vec4(position, 1.0);
vec4 viewLightPos = viewMatrix * vec4(customPointLightPos, 1.0);
lightVec          = normalize(viewLightPos.xyz - viewPos.xyz);

Это приводит к тому, что оба вектора связаны для одной и той же системы отсчета и могут сравниваться, соответственно, используемые для расчетов освещения.

Посмотрите, как я применил предложенные изменения к вашему исходному коду:

class PromisedLoad {

static GenericLoader(loader, url, callback) {
  return new Promise((resolve, reject) => {
    loader.load(url, (object) => {
      if (callback) {
        callback(object, resolve);
      } else {
        resolve(object);
      }
    }, (progress) => {
      console.log(progress);
    }, (error) => {
      reject(error);
    });
  });
}

static GetGLTF(url, callback) {
  let gltfLoader = new THREE.GLTFLoader();
  return this.GenericLoader(gltfLoader, url, callback);
}


}

let vertexShader2 = '
uniform float time;
uniform vec3 materialColor;
uniform vec3 ambientLightColor;
uniform float ambientLightStrength;
uniform vec3 customPointLightPos;

varying vec3 vNormal;
varying vec3 lightVec;

void main() {

    vNormal           = normalMatrix * normal;
    vec4 viewPos      = modelViewMatrix * vec4(position, 1.0);
    vec4 viewLightPos = viewMatrix * vec4(customPointLightPos, 1.0);
    lightVec          = normalize(viewLightPos.xyz - viewPos.xyz);

    gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
}
';

// import customFragmentShader from './shaders/fragmentShader1.glsl';
let fragmentShader1 = '
uniform float time;
uniform vec3 materialColor;
uniform vec3 ambientLightColor;
uniform float ambientLightStrength;
uniform vec3 customPointLightPos;

varying vec3 vNormal;
varying vec3 lightVec;

void main() {
float dProd = max(0.0, dot(vNormal, lightVec));
vec3 c = mix(materialColor * dProd, ambientLightColor, ambientLightStrength);
gl_FragColor = vec4(c, 1.0);
}
';

let mouse = new THREE.Vector2();

window.addEventListener('mousemove', onDocumentMouseMove, false);

document.addEventListener('DOMContentLoaded', () => {
let renderer,
  camera,
  scene = null;
const container = document.getElementById('container');
let controls;
let startTime, time;
let cube;
let rotSpeed = new THREE.Vector3(0.05, 0.03, 0.0);
let axesHelper;
let uniforms;
let customPointLight;

initialize();

// console.log('rotSpeed:  ', rotSpeed);
// setupGUI();

async function initialize() {
  scene = new THREE.Scene();
  renderer = new THREE.WebGLRenderer({
    antialias: true // to get smoother output
  });
  renderer.setClearColor(0x3b3b3b);
  renderer.setSize(window.innerWidth, window.innerHeight);
  container.appendChild(renderer.domElement);

  // create a camera in the scene
  camera = new THREE.PerspectiveCamera(
    35,
    window.innerWidth / window.innerHeight,
    1,
    10000
  );

  axesHelper = new THREE.AxesHelper(5);
  scene.add(axesHelper);

  addCube();

  addCustomPointLight();

  controls = new THREE.OrbitControls(camera);
  scene.add(camera);
  camera.position.z = 10;
  controls.update();

  // and then just look at it!
  camera.lookAt(scene.position);
  controls.update();
  window.onresize = resize;

  animate();
}

function resize() {
    
    var aspect = window.innerWidth / window.innerHeight;
    renderer.setSize(window.innerWidth, window.innerHeight);
    camera.aspect = aspect;
    camera.updateProjectionMatrix();
  }

function addCube() {
  // let geometry = new THREE.SphereGeometry(1, 32, 32);
  let geometry = new THREE.BoxGeometry(1,1,1);
  uniforms = {
    time: {
      type: 'f',
      value: 0
    },
    materialColor: {
      type: 'v3f',
      value: new THREE.Vector3(1.0, 0.0, 0.0)
    },
    ambientLightColor: {
      type: 'v3f',
      value: new THREE.Vector3(0.0, 0.0, 1.0)
    },
    ambientLightStrength: {
      type: 'f',
      value: 0.3
    },
    customPointLightPos: {
      type: 'v3f',
      value: new THREE.Vector3(2.0, 2.0, 2.0)
    }
  };
  const shaderMaterialParams = {
    uniforms: uniforms,
    vertexShader: vertexShader2,
    fragmentShader: fragmentShader1
  };
  const customMaterial = new THREE.ShaderMaterial(shaderMaterialParams);

  cube = new THREE.Mesh(geometry, customMaterial);
  scene.add(cube);
}

function addCustomPointLight() {
  let geo = new THREE.BoxGeometry(0.3, 0.3, 0.3);
  let mat = new THREE.MeshBasicMaterial();
  customPointLight = new THREE.Mesh(geo, mat);
  customPointLight.position.set(2, 2, 2);
  scene.add(customPointLight);
}

function normalize(x, fromMin, fromMax) {
  let totalRange;

  x = Math.abs(x);
  totalRange = Math.abs(fromMin) + Math.abs(fromMax);
  // now we can map out the range from 0 to the totalRange and get a normalized (0 - 1) value
  return x / totalRange;
}

function animate() {
  requestAnimationFrame(animate);

  time = performance.now() / 1000;

  cube.material.uniforms.time.value = time;

  cube.rotation.x += rotSpeed.x;
  cube.rotation.y += rotSpeed.y;

  render();
}

function render() {
  renderer.render(scene, camera);

  controls.update();
}

setupGUI(rotSpeed, uniforms, cube, customPointLight);
});

function setupGUI(rotSpeed, uniforms, cube, customPointLight) {
let options = {
  velx: 0,
  vely: 0,
  rotSpeed: rotSpeed,
  materialColor: uniforms.materialColor.value.toArray(),
  ambientLightColor: uniforms.ambientLightColor.value.toArray(),
  ambientLightStrength: uniforms.ambientLightStrength.value,
  customPointLightPos: {
    x: 2,
    y: 2,
    z: 2
  }
};
let gui = new dat.GUI();
let rotation = gui.addFolder('Rotation');
rotation
  .add(options.rotSpeed, 'x', -0.02, 0.02)
  .name('X')
  .listen();
rotation
  .add(options.rotSpeed, 'y', -0.02, 0.02)
  .name('Y')
  .listen();
rotation.open();
let uniformsGUI = gui.addFolder('Uniforms');
uniformsGUI
  .addColor(options, 'materialColor')
  .onChange(function(value) {
    cube.material.uniforms.materialColor.value.x = value[0] / 255;
    cube.material.uniforms.materialColor.value.y = value[1] / 255;
    cube.material.uniforms.materialColor.value.z = value[2] / 255;
  })
  .name('materialColor')
  .listen();
uniformsGUI.addColor(options, 'ambientLightColor').onChange(function(value) {
  cube.material.uniforms.ambientLightColor.value.x = value[0] / 255;
  cube.material.uniforms.ambientLightColor.value.y = value[1] / 255;
  cube.material.uniforms.ambientLightColor.value.z = value[2] / 255;
});
uniformsGUI
  .add(options, 'ambientLightStrength', 0.0, 1.0)
  .onChange(function(value) {
    cube.material.uniforms.ambientLightStrength.value = value;
  });
uniformsGUI.open();

let customPointLightGUI = gui.addFolder('Custom Point Light');
customPointLightGUI
  .add(customPointLight.position, 'x', -5, 5)
  .onChange(function(value) {
    cube.material.uniforms.customPointLightPos.value.x = value;
  });
customPointLightGUI
  .add(customPointLight.position, 'y', -5, 5)
  .onChange(function(value) {
    cube.material.uniforms.customPointLightPos.value.y = value;
  });
customPointLightGUI
  .add(customPointLight.position, 'z', -5, 5)
  .onChange(function(value) {
    cube.material.uniforms.customPointLightPos.value.z = value;
  });
customPointLightGUI.open();

let box = gui.addFolder('Cube');
box
  .add(cube.scale, 'x', 0, 3)
  .name('Width')
  .listen();
box
  .add(cube.scale, 'y', 0, 3)
  .name('Height')
  .listen();
box
  .add(cube.scale, 'z', 0, 3)
  .name('Length')
  .listen();
box.add(cube.material, 'wireframe').listen();
box.open();
}

function onDocumentMouseMove(event) {
event.preventDefault();
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
}
html, body {
    margin: 0;
    height: 100%;
    width: 100%;
    font-size: 100%;
    font-family: 'Roboto', sans-serif;
    text-align: center;
    font-weight: lighter;
    background: grey;
    overflow-y: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/99/three.min.js"></script>
<script src="https://threejs.org/examples/js/controls/OrbitControls.js"></script>
<script src="https://threejs.org/examples/js/loaders/GLTFLoader.js"></script>
<div id="container"></div>

Ещё вопросы

Сообщество Overcoder
Наверх
Меню