Компьютерная графика c применением Threejs

Макс Пшеничкин | 21.12.2016

LogoThreeJs

Не так давно гуляя по просторам интернета, я наткнулся на интересную штуку под названием Threejs. Она представляет из себя кроссбраузерную библиотеку и позволяет создавать ускоренную на GPU 3D графику, используя язык JavaScript как часть сайта без подключения проприетарных плагинов для браузера. Это возможно благодаря использованию технологии WebGL.  Не буду на ней останавливаться, лишь только скажу, что она основывается на использования низкоуровневых средств поддержки OpenGL, т.е.  часть кода на WebGL может выполняться непосредственно на видеокартах.  Программы, исполняемые на графических процессорах, называются шейдерами.

Шейдер – небольшая программа, выполняемая на стороне видеокарты или другого аппаратного, или программного устройства рендеринга, которая позволяет производить отдельные элементы цикла рендеринга объекта особым, отличным от стандартного, образом. Иначе говоря, шейдер – программа, выполняющая некоторую часть цикла рендеринга.

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

В каждый момент выполнения программы активна лишь одна пара, состоящая из вершинного и пиксельного шейдера. В случае, если программист не установил активной пары шейдеров, работает стандартный шейдер, который обеспечивает всю стандартную функциональность графической библиотеки (OpenGL или DirectX). Обычно пару вершинных и фрагментных шейдеров называют просто шейдером.

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

Геометрический шейдер, в отличие от вершинного, способен обработать не только одну вершину, но и целый примитив.

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

Хватит теории. Окунёмся в моё прошлое. Первый опыт работы с компьютерной графикой я получил на лабораторных работах по КГ в институте. Это были одни из самых интереснейший лабораторных работ. Всё началось с WinApi, и результатом была такая программка. Буквы прыгали и крутились.

WinApi_gr

После прекрасного домика я стал познавать азы OpenGL.

OpenGL_fig1

А это просто шедевр=) Вращающаяся и переливающаяся фигура.

OpenGL_fig2

Была даже работа с визуализацией части солнечной системы.

OpenGL_planet

Всеми любимая змейка стала даже темой моей курсовой. Реализована она была также на OpenGL.

OpenGl_snake

Достал, наверное, уже со своим OpenGL. Все же пользуются DirectX. Что же тогда держите следующую игру. Машина, которая движется по дороге и прыгает через вращающиеся шарики и чайники.

DirectX_game

Плавно переходим к шейдерам. Первой задачей было реализовать флаг Бенина.  В результате вышло как-то так.

Shader_flag

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

Shader_board

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

Shader_micro

Здесь использовалась программа для GLSL шейдеров Render Monkey. На картинке же продемонстрирован 3D объект (куб) с микрорельефом.

Также удалось поработать с параллельные вычисления на GPU. Использовался OpenCL и опять всё те же шейдеры. На картинки продемонстрирован эффект акварели.

Shader_aqwa

Сам шейдер был такой:

__kernel void kmain(__global uint* data,__global uint* out, int w,int h)
{
   int pix = get_global_id(0);

   if (pix>=w*h)
      return;

   int y=pix/w;
   int x=pix-(y*w);
   float r_m[9];
   float g_m[9];
   float b_m[9];
   float M[9] = {1,2,1,2,4,2,1,2,1};
   float R1[9] = {-0.5,-0.5,-0.5,-0.5,5,-0.5,-0.5,-0.5,-0.5};
   int k=0;
   int r=0,g=0,b=0;
   float sum_r=0,sum_g=0,sum_b=0;
   uint color;

   for (int j=0;j<3;j++)
      for (int i=0;i<3;i++)

      {
         color = data[(x+i)+(y+j)*w];
         r_m[k] = (color&0xFF);
         g_m[k] = ((color>>8)&0xFF);
         b_m[k] = ((color>>16)&0xFF);
         k++;
      }

   for (int i=0;i<9;i++)
   {
       r_m[i]=((r_m[i]*M[i])/16)*R1[i];
       g_m[i]=((g_m[i]*M[i])/16)*R1[i];
       b_m[i]=((b_m[i]*M[i])/16)*R1[i];
       sum_r+=r_m[i];
       sum_b+=b_m[i];
       sum_g+=g_m[i];
    }

   r=sum_r;
   g=sum_g;
   b=sum_b;

   if (r>255) {r=255;}
   if (g>255) {g=255;}
   if (b>255) {b=255;}
   if (r<0) {r=0;}
   if (g<0) {g=0;}
   if (b<0) {b=0;}
   out[pix] = 0xff000000 | r | g<<8 | b<<16;
}

Всё хватит истории. Пора вернуться к текущему моменту времени и к основной теме статьи.

Налив чашечку кофе с молочком, вспомнив былое я начал разбираться в только что скаченной библиотеки Threejs. Закинул её на локальный сервер, иначе некоторые функции могут не работать.

Приступим.

Создаём html файл примерно с таким кодом.

<!DOCTYPE html>

<html lang="ru">
<head>
   <title>three.js</title>
   <meta charset="utf-8">
   <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">

   <style>
     body {
        color: #ffffff;
       font-family:Monospace;
        font-size:13px;
        text-align:center;
        font-weight: bold;
        background-color: #000000;
        margin: 0px;
        overflow: hidden;
     }
     #info {
        color: #fff;
        position: absolute;
        top: 0px; width: 100%;
        padding: 5px;
        z-index:100;
     }
   </style>
</head>

<body>
   <div id="container"></div>
</body>

</html>

Подключим дополнительные библиотеки.

<script src="js/Detector.js"></script> <!-- Проверяет, поддерживает ли браузер WebGL. -->

<script src="js/libs/stats.min.js"></script> <!-- вспомогательная библиотека, дает информацию о частоте кадров, с которой работает анимация -->

<script src="../build/three.js"></script>

<script src="js/controls/OrbitControls.js"></script> <!-- Работа с поворотами и движением на мышку и клавиши клавиатуры. -->

<script src="js/SkyShader.js"></script><!—Позволит сделать окружающее пространство-->

<script src="js/libs/dat.gui.min.js"></script><!—Меню для управления параметрами объектов в реальном времени-->

Теперь сформируем два шейдера

    //вершинный шейдер
<script type="x-shader/x-vertex" id="vertexshader">
   uniform float amplitude;
   attribute vec3 displacement;
   attribute vec3 customColor;
   varying vec3 vColor;

   void main() {
      vec3 newPosition = position + amplitude * displacement;
      vColor = customColor;
      gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );
   }
</script>
 
//фрагментный(пиксельный) шейдер

<script type="x-shader/x-fragment" id="fragmentshader">
   uniform vec3 color;
   uniform float opacity;
   varying vec3 vColor;

   void main() {
     gl_FragColor = vec4( vColor * color, opacity );
   }
</script>

Определим переменные и создадим необходимые функции

var renderer, scene, camera, stats;

var object, uniforms;
//инициализация компонентов
function init( font ) {
}
//ресайз при изменении окна браузера
function onWindowResize() {
}

//анимация
function animate() {
}

//отрисовка
function render() {
}

Функция animate() и onWindowResize() содержат стандартный код. Если захочешь ищи в гугле).

Подключаем необходимый шрифт в формате json. Можно найти в папке со скаченной библиотекой по следующему пути examples\fonts.

var loader = new THREE.FontLoader();
   loader.load( 'fonts/optimer_bold.typeface.json', function ( font ) {
      init( font );
      animate();
   } 
);

Теперь перейдём к функции init(). Фрагменты кода представлены ниже.

//создаем камеру

camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 100, 2000000 );
camera.position.set( 0, 0, 250 );

//создаем сцену
scene = new THREE.Scene();

Описываем материалы для объектов и параметры для надписи, которую будем выводить на экран.

//значение для всех вершин шейдера
uniforms = {
   amplitude: { value: 5.0 },
   opacity:   { value: 0.3 },
   color:     { value: new THREE.Color( 0x663366 ) }//цвет элементов
};

/*Материал визуализации с помощью пользовательских шейдеров (GLSL)*/
var shaderMaterial = new THREE.ShaderMaterial( {
   uniforms:       uniforms, //значение для всех вершин
   vertexShader:   document.getElementById( 'vertexshader' ).textContent,//вершинный шейдер
   fragmentShader: document.getElementById( 'fragmentshader' ).textContent,//фрагментный шейдер
   blending:       THREE.AdditiveBlending,//смешивание
   depthTest:      false,//глубина
   transparent:    true //прозрачность
});

var geometry = new THREE.TextGeometry( 'sawtech', {
   font: font,
   size: 50,//размер
   height: 15,
   //параметры объемных объектов
   curveSegments: 20,//сегменты
   bevelThickness: 20, //толщина
   bevelSize: 1.5,
   bevelEnabled: true,
   bevelSegments: 10,
   steps: 30
} );

Здесь параметры завышать особо не стоит, ибо FPS может сильно просесть. Всё зависит от мощности GPU.

Идем далее. Работаем собственно с шейдерами и добавляем созданный текст на сцену.

//устанавливаем начальные параметры
geometry.center();//центрирует  наш текст на сцене
var vertices = geometry.vertices; //вершины нашего объекта
var buffergeometry = new THREE.BufferGeometry();
var position = new THREE.Float32BufferAttribute( vertices.length * 3 , 3 ).copyVector3sArray( vertices );
buffergeometry.addAttribute( 'position', position );
var displacement = new THREE.Float32BufferAttribute( vertices.length * 3, 3 );
buffergeometry.addAttribute( 'displacement', displacement );
var customColor = new THREE.Float32BufferAttribute( vertices.length * 3, 3 );
buffergeometry.addAttribute( 'customColor', customColor );
var color = new THREE.Color( 0xcccccc );

for( var i = 0; i < customColor.count; i ++ ) {
   color.setHSL( i / customColor.count, 0.2, 0.2 );         //разброс цветов
   color.toArray( customColor.array, i * customColor.itemSize );
}

object = new THREE.Line( buffergeometry, shaderMaterial );
object.rotation.x = 0.0;//начальный поворот объекта
scene.add( object );//добавляем наш объект на сцену

//привязываемся к dom
renderer = new THREE.WebGLRenderer( { antialias: true } );
renderer.setClearColor( 0x050505 );
renderer.setPixelRatio( window.devicePixelRatio );
renderer.setSize( window.innerWidth, window.innerHeight );
var container = document.getElementById( 'container' );
container.appendChild( renderer.domElement );
window.addEventListener( 'resize', onWindowResize, false ); //формируем нормальные параметры для объекта при ресайзе окна браузера

Осталось немного в функцию рендера вставляем следующую строку

renderer.render( scene, camera );

Смотрим результат в браузере

threehs_res1

Это ещё не конец. Добавим управление мышью. Не зря же мы библиотеку подключали (OrbitControls.js).

Пишем в init()

controls = new THREE.OrbitControls( camera, renderer.domElement );
controls.enableDamping = true;
controls.dampingFactor = 0.25;
controls.enableZoom = false;

Удерживая левую кнопку мыши у нас будет вращение камеры, а правую изменение её местоположения.

threehs_res2

Что-то фон мрачненький. Сделаем, пожалуй, Skybox(SkySphere).

threehs_res3sky

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

threehs_res4conf

Автоматически библиотекой dat.gui.min.js к клавише H привязано скрытие/отображение меню с параметрами. Мне показалось это не очень удобным для применения технологии в проектах. Поэтому в данной библиотеке я переназначил событие на менее используемую клавишу (F9).

Ну и в завершении я добавил автоматическое вращение текста и эффект распада.

threehs_res5

В завершении хочу сказать, что данная библиотека реально очень классная и полезная.

Оставлю несколько интересный ссылок:

Небольшая галерея с результатами прилагается.