Пример водяного знака поверх 3D модели
Пример водяного знака поверх 3D модели

В данном уроке напишем собственный шейдер, который будет накладывать текстуру поверх 3D сцены с прозрачностью и сохранением пропорций изображения. Для этого будем использовать post-processing.

Под нашу задачу уже есть частичное решение в виде класса TexturePass, который работает на основе базового шейдера CopyShader. Данный шейдер может разместить поверх сцены текстуру с прозрачностью, но без сохранения пропорций. Как раз эту проблему мы и будем решать в нашем собственном шейдере.

Для начала добавим в TexturePass возможность передавать свой шейдер.

class TexturePass extends Pass
{
  constructor(map, opacity, shader = undefined)
  {
    super();
    if (shader === undefined && CopyShader === undefined)
    {
      console.error('TexturePass relies on CopyShader');
    }
    ...
  }
  ...
}

Далее создадим класс TexturePassExtend и унаследуем его от TexturePass.

class TexturePassExtend extends TexturePass
{
  constructor(map, opacity)
  {
    super(map, opacity, TexturePassExtendShader);
  }
 
  setRenderSize(size)
  {
    if (this.map === undefined)
      return;
    
    const mapWidth = this.map.image.width;
    const mapHeight = this.map.image.height;
    const mapRatio = mapWidth / mapHeight;
    
    const scaleX = mapWidth / size.x;
    const scaleY = (mapWidth / mapRatio) / size.y;
    
    this.uniforms['scaleX'].value = scaleX;
    this.uniforms['scaleY'].value = scaleY;
  }
}

Здесь мы передаем в TexturePass наш шейдер TexturePassExtendShader и описываем метод setRenderSize для корректировки поведения шейдера при смене размеров холста сцены.

Для написания собственного шейдера за основу возьмем CopyShader. Нас в первую очередь будет интересовать вершинный шейдер.

var TexturePassExtendShader = {
 
  uniforms:
    {
      "tDiffuse": {value: null},
      "opacity": {value: 1.0},
      "scaleX": {value: 1},
      "scaleY": {value: 1}
    },
 
  vertexShader:
    [
      "uniform float scaleX;",
      "uniform float scaleY;",
      "varying vec2 vUv;",
      "void main() {",
      "  vUv = uv;",
      "  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);",
      "  gl_Position.x *= scaleX;",
      "  gl_Position.y *= scaleY;",
      "}"
    ].join("\n"),
 
  fragmentShader:
    [
      "uniform sampler2D tDiffuse;",
      "uniform float opacity;",
      "varying vec2 vUv;",
      "void main() {",
      "  gl_FragColor = texture2D(tDiffuse, vUv);",
      "  gl_FragColor.a *= opacity;",
      "}"
    ].join("\n")
};

В вершинном шейдере мы корректируем размеры текстуры по ширине и высоте через переменные scaleX и scaleY, которые мы и корректируем в методе setRenderSize с целью сохранения пропорций нашей текстуры.

Надеюсь, что моя статья сподвигнет многих разработчиков пробовать писать собственные решения на языке шейдеров GLSL.

Пример реализации данного шейдера можно посмотреть по ссылке.