portaldacalheta.pt
  • Основен
  • Agile Talent
  • Дизайнерски Живот
  • Възходът На Дистанционното
  • Рентабилност И Ефективност
Технология

3D графика: Урок за WebGL



Светът на 3D графиките може да бъде много смущаващ за навлизане. Независимо дали просто искате да създадете интерактивно 3D лого или да проектирате пълноценна игра, ако не познавате принципите на 3D изобразяването, вие сте останали да използвате библиотека, която абстрахира много неща.

Използването на библиотека може да бъде точното средство и JavaScript има невероятен такъв с отворен код под формата на three.js . Има някои недостатъци при използването на предварително направени решения, въпреки че:



  • Те могат да имат много функции, които не планирате да използвате. Размерът на минифицираните базови функции three.js е около 500kB, а всякакви допълнителни функции (зареждането на действителни файлове с модели е една от тях) правят полезния товар още по-голям. Прехвърлянето на толкова много данни само за показване на въртящо се лого на вашия уебсайт би било загуба.
  • Допълнителен слой абстракция може да направи трудни за изпълнение иначе лесни модификации. Твоят творчески начин за засенчване на обект на екрана може да бъде или лесен за изпълнение, или да изисква десетки часове работа, за да се включи в абстракциите на библиотеката.
  • Въпреки че библиотеката е оптимизирана много добре в повечето сценарии, много камбани и свирки могат да бъдат изрязани за вашия случай на употреба. Визуализаторът може да накара някои процедури да стартират милиони пъти на графичната карта. Всяка инструкция, премахната от такава процедура, означава, че по-слаба графична карта може да се справи със съдържанието ви без проблеми.

Дори ако решите да използвате графична библиотека на високо ниво, наличието на основни познания за нещата под капака ви позволява да я използвате по-ефективно. Библиотеките също могат да имат разширени функции, като ShaderMaterial в three.js. Познаването на принципите на изобразяване на графики ви позволява да използвате такива функции.



Илюстрация на 3D лого на ApeeScape върху платно WebGL



Нашата цел е да дадем кратко представяне на всички ключови концепции за изобразяване на 3D графики и използване на WebGL за тяхното внедряване. Ще видите най-честото нещо, което се прави, което показва и премества 3D обекти в празно пространство.

The окончателен код е на разположение, за да се разклоните и да си поиграете.



Представяне на 3D модели

Първото нещо, което трябва да разберете, е как са представени 3D моделите. Модел е направен от мрежа от триъгълници. Всеки триъгълник е представен от три върха, за всеки от ъглите на триъгълника. Има три най-често срещани свойства, прикрепени към върховете.

Позиция на върха

Позицията е най-интуитивното свойство на върха. Това е позицията в 3D пространството, представена от 3D вектор на координати. Ако знаете точните координати на три точки в пространството, ще имате цялата необходима информация, за да нарисувате прост триъгълник между тях. За да изглеждат моделите всъщност добре, когато се изобразяват, има още няколко неща, които трябва да бъдат предоставени на визуализатора.



Vertex Normal

Сфери със същия каркас, които имат плоско и гладко засенчване

Помислете за двата модела по-горе. Те се състоят от едни и същи позиции на върховете, но изглеждат напълно различни, когато се визуализират. Как е възможно това?



Освен че казваме на визуализатора къде искаме да се намира връх, ние също можем да му дадем подсказка как повърхността е наклонена в точното положение. Подсказката е под формата на нормала на повърхността в тази конкретна точка на модела, представена с 3D вектор. Следващото изображение трябва да ви даде по-описателен поглед върху начина, по който се обработва.

Сравнение между нормалите за плоско и гладко засенчване



Лявата и дясната повърхност съответно съответстват на лявата и дясната топка в предишното изображение. Червените стрелки представляват нормали, посочени за връх, докато сините стрелки представляват изчисленията на визуализатора за това как нормалната трябва да търси всички точки между върховете. Изображението показва демонстрация за 2D пространство, но същият принцип се прилага и в 3D.

Нормалното е намек за това как светлините ще осветяват повърхността. Колкото по-близо е посоката на светлинния лъч до нормалната, толкова по-ярка е точката. Постепенните промени в нормалната посока причиняват градиенти на светлината, докато внезапните промени без промени между тях причиняват повърхности с постоянно осветяване върху тях и внезапни промени в осветеността между тях.



Координати на текстурата

Последното важно свойство са текстурните координати, обикновено наричани UV картиране. Имате модел и текстура, която искате да приложите върху него. Текстурата има различни области върху себе си, представляващи изображения, които искаме да приложим към различни части на модела. Трябва да има начин да маркирате кой триъгълник трябва да бъде представен с коя част от текстурата. Тук идва картографирането на текстурите.

За всеки връх маркираме две координати, U и V. Тези координати представляват позиция върху текстурата, като U представлява хоризонталната ос, а V вертикалната ос. Стойностите не са в пиксели, а в процентна позиция в изображението. Долният ляв ъгъл на изображението е представен с две нули, докато горният десен ъгъл е представен с две.

Триъгълникът просто се рисува, като се вземат UV координатите на всеки връх в триъгълника и се прилага изображението, което е заснето между тези координати върху текстурата.

Демонстрация на UV картиране, с подчертан един пластир и видими шевове на модела

Можете да видите демонстрация на UV картиране на изображението по-горе. Сферичният модел е взет и нарязан на части, които са достатъчно малки, за да бъдат изравнени върху 2D повърхност. Шевовете, където са направени разфасовките, са маркирани с по-дебели линии. Една от лепенките е подчертана, така че можете добре да видите как нещата съвпадат. Можете също така да видите как шев през средата на усмивката поставя части от устата в две различни петна.

Жичните рамки не са част от текстурата, а просто се наслагват върху изображението, за да можете да видите как нещата се съпоставят.

Зареждане на OBJ модел

Вярвате или не, това е всичко, което трябва да знаете, за да създадете свой собствен прост модел за зареждане. The OBJ файлов формат е достатъчно проста, за да внедри парсер в няколко реда код.

Файлът изброява позициите на върховете в v формат, с незадължителен четвърти плувка, който ние ще игнорираме, за да улесним нещата. Нормалите на върховете са представени по подобен начин с vn . И накрая, координатите на текстурата са представени с vt , с незадължителен трети поплавък, който ще игнорираме. И в трите случая плувките представляват съответните координати. Тези три свойства се натрупват в три масива.

Лицата са представени с групи върхове. Всеки връх е представен с индекса на всяко от свойствата, при което индексите започват от 1. Има различни начини, по които това се представя, но ние ще се придържаме към f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3 формат, изискващ да бъдат предоставени и трите свойства, и ограничаващ броя на върховете на лице до три. Всички тези ограничения се правят, за да се улесни максимално опростяването на товарача, тъй като всички останали опции изискват допълнителна тривиална обработка, преди да са във формат, който WebGL харесва.

Поставихме много изисквания за нашия файл за зареждане на файлове. Това може да звучи ограничаващо, но приложенията за 3D моделиране са склонни да ви дадат възможност да зададете тези ограничения при експортиране на модел като OBJ файл.

Следващият код анализира низ, представляващ OBJ файл, и създава модел под формата на масив от лица.

function Geometry (faces) [] // Parses an OBJ file, passed as a string Geometry.parseOBJ = function (src) { var POSITION = /^vs+([d.+-eE]+)s+([d.+-eE]+)s+([d.+-eE]+)/ var NORMAL = /^vns+([d.+-eE]+)s+([d.+-eE]+)s+([d.+-eE]+)/ var UV = /^vts+([d.+-eE]+)s+([d.+-eE]+)/ var FACE = /^fs+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)s+(-?d+)/(-?d+)/(-?d+)(?:s+(-?d+)/(-?d+)/(-?d+))?/ lines = src.split(' ') var positions = [] var uvs = [] var normals = [] var faces = [] lines.forEach(function (line) { // Match each line of the file against various RegEx-es var result if ((result = POSITION.exec(line)) != null) { // Add new vertex position positions.push(new Vector3(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))) } else if ((result = NORMAL.exec(line)) != null) { // Add new vertex normal normals.push(new Vector3(parseFloat(result[1]), parseFloat(result[2]), parseFloat(result[3]))) } else if ((result = UV.exec(line)) != null) { // Add new texture mapping point uvs.push(new Vector2(parseFloat(result[1]), 1 - parseFloat(result[2]))) } else if ((result = FACE.exec(line)) != null) { // Add new face var vertices = [] // Create three vertices from the passed one-indexed indices for (var i = 1; i <10; i += 3) { var part = result.slice(i, i + 3) var position = positions[parseInt(part[0]) - 1] var uv = uvs[parseInt(part[1]) - 1] var normal = normals[parseInt(part[2]) - 1] vertices.push(new Vertex(position, normal, uv)) } faces.push(new Face(vertices)) } }) return new Geometry(faces) } // Loads an OBJ file from the given URL, and returns it as a promise Geometry.loadOBJ = function (url) { return new Promise(function (resolve) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE) { resolve(Geometry.parseOBJ(xhr.responseText)) } } xhr.open('GET', url, true) xhr.send(null) }) } function Face (vertices) this.vertices = vertices function Vertex (position, normal, uv) new Vector3() this.normal = normal function Vector3 (x, y, z) function Vector2 (x, y) 0 this.y = Number(y)

Geometry Структурата съдържа точните данни, необходими за изпращане на модел към графичната карта за обработка. Преди да направите това обаче, вероятно бихте искали да имате възможност да премествате модела на екрана.

Извършване на пространствени трансформации

Всички точки в модела, който заредихме, са спрямо координатната му система. Ако искаме да преведем, завъртим и мащабираме модела, всичко, което трябва да направим, е да извършим тази операция върху неговата координатна система. Координатната система A, спрямо координатната система B, се дефинира от позицията на нейния център като вектор p_ab и вектора за всяка от нейните оси, x_ab, y_ab и z_ab, представляваща посоката на тази ос. Така че, ако една точка се движи с 10 на x ос на координатна система A, след това - в координатната система B - тя ще се движи в посока x_ab, умножена по 10.

Цялата тази информация се съхранява в следната матрична форма:

x_ab.x y_ab.x z_ab.x p_ab.x x_ab.y y_ab.y z_ab.y p_ab.y x_ab.z y_ab.z z_ab.z p_ab.z 0 0 0 1

Ако искаме да трансформираме 3D вектора q, просто трябва да умножим матрицата за трансформация с вектора:

q.x q.y q.z 1

Това кара точката да се движи с q.x по новия x ос, по q.y по новия y ос и от q.z по новия z ос. Накрая кара точката да се движи допълнително от p вектор, което е причината да използваме единица като краен елемент на умножението.

Голямото предимство на използването на тези матрици е фактът, че ако имаме множество трансформации, които да извършим върху върха, можем да ги обединим в една трансформация чрез умножаване на техните матрици, преди да трансформираме самия връх.

Има различни трансформации, които могат да бъдат извършени и ние ще разгледаме ключовите.

Без трансформация

Ако не се случат трансформации, тогава p вектор е нулев вектор, x вектор е [1, 0, 0], y е [0, 1, 0] и z е [0, 0, 1]. Отсега нататък ще наричаме тези стойности като стойности по подразбиране за тези вектори. Прилагането на тези стойности ни дава матрица за идентичност:

1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1

Това е добра отправна точка за обвързване на трансформации.

Превод

Трансформация на рамка за превод

Когато извършваме превод, тогава всички вектори с изключение на p вектор имат своите стойности по подразбиране. Това води до следната матрица:

1 0 0 p.x 0 1 0 p.y 0 0 1 p.z 0 0 0 1

Мащабиране

Трансформация на рамка за мащабиране

Мащабирането на модел означава намаляване на количеството, което всяка координата допринася за позицията на точка. Няма еднакво отместване, причинено от мащабиране, така че p vector запазва стойността си по подразбиране. Векторите на ос по подразбиране трябва да се умножат по съответните им мащабиращи коефициенти, което води до следната матрица:

s_x 0 0 0 0 s_y 0 0 0 0 s_z 0 0 0 0 1

Тук s_x, s_y и s_z представляват мащабирането, приложено към всяка ос.

Завъртане

Трансформация на рамка за въртене около оста Z

Горното изображение показва какво се случва, когато завъртим координатната рамка около оста Z.

Въртенето води до неравномерно отместване, така че p vector запазва стойността си по подразбиране. Сега нещата стават малко по-сложни. Завъртанията карат движението по определена ос в оригиналната координатна система да се движи в различна посока. Така че, ако завъртим координатна система на 45 градуса около оста Z, движейки се по x оста на оригиналната координатна система предизвиква движение в диагонална посока между x и y ос в новата координатна система.

За да улесним нещата, ние просто ще ви покажем как матриците за преобразуване търсят завъртания около основните оси.

Around X: 1 0 0 0 0 cos(phi) sin(phi) 0 0 -sin(phi) cos(phi) 0 0 0 0 1 Around Y: cos(phi) 0 sin(phi) 0 0 1 0 0 -sin(phi) 0 cos(phi) 0 0 0 0 1 Around Z: cos(phi) -sin(phi) 0 0 sin(phi) cos(phi) 0 0 0 0 1 0 0 0 0 1

Изпълнение

Всичко това може да бъде реализирано като клас, който съхранява 16 числа, съхраняващи матрици в a колона-основен ред .

function Transformation () { // Create an identity transformation this.fields = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] } // Multiply matrices, to chain transformations Transformation.prototype.mult = function (t) { var output = new Transformation() for (var row = 0; row <4; ++row) { for (var col = 0; col < 4; ++col) { var sum = 0 for (var k = 0; k < 4; ++k) { sum += this.fields[k * 4 + row] * t.fields[col * 4 + k] } output.fields[col * 4 + row] = sum } } return output } // Multiply by translation matrix Transformation.prototype.translate = function (x, y, z) // Multiply by scaling matrix Transformation.prototype.scale = function (x, y, z) // Multiply by rotation matrix around X axis Transformation.prototype.rotateX = function (angle) // Multiply by rotation matrix around Y axis Transformation.prototype.rotateY = function (angle) // Multiply by rotation matrix around Z axis Transformation.prototype.rotateZ = function (angle)

Гледайки през камера

Тук идва ключовата част от представянето на обекти на екрана: камерата. Има два ключови компонента на камерата; а именно позицията му и начина, по който прожектира наблюдавани обекти на екрана.

Позицията на камерата се обработва с един прост трик. Няма визуална разлика между преместването на камерата на метър напред и преместването на целия свят на метър назад. Така че, естествено, правим последното, като прилагаме обратното на матрицата като трансформация.

Вторият ключов компонент е начинът, по който наблюдаваните обекти се проектират върху обектива. В WebGL всичко видимо на екрана се намира в кутия. Кутията обхваща между -1 и 1 на всяка ос. Всичко видимо е в тази кутия. Можем да използваме същия подход на трансформационните матрици, за да създадем проекционна матрица.

Ортографска проекция

Правоъгълното пространство се трансформира в правилните размери на буфера на рамката с помощта на ортографска проекция

Най-простата проекция е правописна проекция . Взимате поле в пространството, обозначаващо ширината, височината и дълбочината, с предположението, че центърът му е в нулевата позиция. След това проекцията преоразмерява полето, за да го побере в описаното по-рано поле, в което WebGL наблюдава обекти. Тъй като искаме да преоразмерим всяко измерение на две, мащабираме всяка ос по 2/size, при което size е размерът на съответната ос. Малка забележка е фактът, че умножаваме оста Z с отрицателно. Това се прави, защото искаме да обърнем посоката на това измерение. Крайната матрица има тази форма:

2/width 0 0 0 0 2/height 0 0 0 0 -2/depth 0 0 0 0 1

Перспективна проекция

Frustum се трансформира в правилните размери на буфера на рамката, като се използва проекция в перспектива

Няма да разглеждаме подробностите за това как е проектирана тази проекция, а просто използваме окончателна формула , което вече е почти стандартно. Можем да го опростим, като поставим проекцията в нулевата позиция на оста x и y, като правим границите отдясно / ляво и отгоре / отдолу равни на width/2 и height/2 съответно. Параметрите n и f представляват near и far отрязващи равнини, които са най-малкото и най-голямото разстояние, което дадена точка може да бъде заснета от камерата. Те са представени от успоредните страни на frustum в горното изображение.

Перспективната проекция обикновено се представя с a полезрение (ще използваме вертикалния), съотношение , както и близките и далечните разстояния на самолета. Тази информация може да се използва за изчисляване width и height, а след това матрицата може да бъде създадена от следния шаблон:

2*n/width 0 0 0 0 2*n/height 0 0 0 0 (f+n)/(n-f) 2*f*n/(n-f) 0 0 -1 0

За изчисляване на ширината и височината могат да се използват следните формули:

height = 2 * near * Math.tan(fov * Math.PI / 360) width = aspectRatio * height

FOV (зрителното поле) представлява вертикалния ъгъл, който камерата улавя с обектива си. Съотношението на страните представлява съотношението между ширината и височината на изображението и се основава на размерите на екрана, към който правим визуализация.

Изпълнение

Сега можем да представим камерата като клас, който съхранява позицията на камерата и матрицата на проекция. Ние също трябва да знаем как да изчислим обратните трансформации. Решаването на общите матрични инверсии може да бъде проблематично, но има опростен подход за нашия специален случай.

function Camera () { this.position = new Transformation() this.projection = new Transformation() } Camera.prototype.setOrthographic = function (width, height, depth) { this.projection = new Transformation() this.projection.fields[0] = 2 / width this.projection.fields[5] = 2 / height this.projection.fields[10] = -2 / depth } Camera.prototype.setPerspective = function (verticalFov, aspectRatio, near, far) { var height_div_2n = Math.tan(verticalFov * Math.PI / 360) var width_div_2n = aspectRatio * height_div_2n this.projection = new Transformation() this.projection.fields[0] = 1 / height_div_2n this.projection.fields[5] = 1 / width_div_2n this.projection.fields[10] = (far + near) / (near - far) this.projection.fields[10] = -1 this.projection.fields[14] = 2 * far * near / (near - far) this.projection.fields[15] = 0 } Camera.prototype.getInversePosition = function () { var orig = this.position.fields var dest = new Transformation() var x = orig[12] var y = orig[13] var z = orig[14] // Transpose the rotation matrix for (var i = 0; i <3; ++i) { for (var j = 0; j < 3; ++j) { dest.fields[i * 4 + j] = orig[i + j * 4] } } // Translation by -p will apply R^T, which is equal to R^-1 return dest.translate(-x, -y, -z) }

Това е последното парче, от което се нуждаем, преди да започнем да рисуваме нещата на екрана.

Рисуване на обект с WebGL Graphics Pipeline

Най-простата повърхност, която можете да нарисувате, е триъгълник. Всъщност по-голямата част от нещата, които рисувате в 3D пространство, се състоят от голям брой триъгълници.

Основен поглед върху стъпките на графичния конвейер

Първото нещо, което трябва да разберете, е как екранът е представен в WebGL. Това е 3D пространство, обхващащо между -1 и 1 на х , Y. , и с ос. По подразбиране това с ос не се използва, но се интересувате от 3D графика, така че ще искате да я активирате веднага.

Имайки това предвид, следващите стъпки са три стъпки, необходими за изчертаване на триъгълник върху тази повърхност.

Можете да дефинирате три върха, които биха представлявали триъгълника, който искате да нарисувате. Сериализирате тези данни и ги изпращате на графичния процесор (графичен процесор). С наличен цял модел можете да го направите за всички триъгълници в модела. Позициите на върховете, които давате, са в локалното координатно пространство на модела, който сте заредили. Казано по-просто, позициите, които предоставяте, са точно тези от файла, а не тази, която получавате след извършване на матрични трансформации.

След като сте дали върховете на графичния процесор, вие кажете на графичния процесор каква логика да използва, когато поставяте върховете на екрана. Тази стъпка ще се използва за прилагане на нашите матрични трансформации. GPU е много добър в умножаването на много 4x4 матрици, така че ще използваме тази способност добре.

В последната стъпка GPU ще растеризира този триъгълник. Растеризацията е процес на вземане на векторни графики и определяне кои пиксели на екрана трябва да бъдат нарисувани, за да бъде показан този обект с векторна графика. В нашия случай GPU се опитва да определи кои пиксели са разположени във всеки триъгълник. За всеки пиксел графичният процесор ще ви попита с какъв цвят искате да бъде боядисан.

Това са четирите елемента, необходими за рисуване на всичко, което искате, и те са най-простият пример за a графичен конвейер . Следва поглед върху всеки от тях и просто изпълнение.

По подразбиране Framebuffer

Най-важният елемент за приложението WebGL е контекстът WebGL. Можете да получите достъп до него с gl = canvas.getContext('webgl') или да използвате 'experimental-webgl' като резервен вариант, в случай че използваният в момента браузър все още не поддържа всички функции на WebGL. canvas споменатият е DOM елементът на платното, върху който искаме да рисуваме. Контекстът съдържа много неща, сред които е рамков буфер по подразбиране.

Можете свободно да опишете рамков буфер като всеки буфер (обект), върху който можете да рисувате. По подразбиране рамковият буфер по подразбиране съхранява цвета за всеки пиксел на платното, към който е обвързан контекстът на WebGL. Както е описано в предишния раздел, когато черпим върху буфера на кадрите, всеки пиксел се намира между -1 и 1 на х и Y. ос. Нещо, което също споменахме, е фактът, че по подразбиране WebGL не използва с ос. Тази функционалност може да бъде активирана чрез стартиране gl.enable(gl.DEPTH_TEST) Страхотно, но какво е тест за дълбочина?

Активирането на теста за дълбочина позволява на пиксела да съхранява както цвета, така и дълбочината. Дълбочината е с координата на този пиксел. След като нарисувате до пиксел на определена дълбочина с , за да актуализирате цвета на този пиксел, трябва да нарисувате в с позиция, която е по-близо до камерата. В противен случай опитът за теглене ще бъде игнориран. Това позволява илюзията за 3D, тъй като изчертаването на обекти, които са зад други обекти, ще доведе до запушване на тези обекти от обекти пред тях.

Всички тиражи, които изпълнявате, остават на екрана, докато не им кажете да бъдат изчистени. За да направите това, трябва да се обадите gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT). Това изчиства както цвета, така и дълбочината на буфера. За да изберете цвета, на който са зададени изчистените пиксели, използвайте gl.clearColor(red, green, blue, alpha).

Нека създадем визуализатор, който използва платно и го изчиства при поискване:

function Renderer (canvas) Renderer.prototype.setClearColor = function (red, green, blue) { gl.clearColor(red / 255, green / 255, blue / 255, 1) } Renderer.prototype.getContext = function () { return this.gl } Renderer.prototype.render = function () gl.DEPTH_BUFFER_BIT) var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) loop() function loop () { renderer.render() requestAnimationFrame(loop) }

Прикачването на този скрипт към следния HTML ще ви даде ярко син правоъгълник на екрана

requestAnimationFrame

N call причинява повторното извикване на цикъла веднага щом завърши рендирането на предишния кадър и цялата обработка на събитията приключи.

Обекти на буфер на Vertex

Първото нещо, което трябва да направите, е да дефинирате върховете, които искате да нарисувате. Можете да направите това, като ги опишете чрез вектори в 3D пространство. След това искате да преместите тези данни в RAM на GPU, като създадете нова Обект на буфер на връх (FEB).

ДА СЕ Буферен обект като цяло е обект, който съхранява масив от парчета памет на графичния процесор. Това, че е VBO, просто означава за какво GPU може да използва паметта. През повечето време буферните обекти, които създавате, ще бъдат VBO.

Можете да попълните VBO, като вземете всички 3N върхове, които имаме, и създаване на масив от плувки с 2N елементи за позицията на върха и нормалните VBO на върха и Geometry за координатите на текстурата VBO. Всяка група от три поплавъка или два поплавъка за UV координати, представлява индивидуални координати на връх. След това предаваме тези масиви на GPU и нашите върхове са готови за останалата част от конвейера.

Тъй като данните вече са в RAM на GPU, можете да ги изтриете от RAM за общо предназначение. Това е, освен ако не искате по-късно да го модифицирате и да го качите отново. Всяка модификация трябва да бъде последвана от качване, тъй като модификациите в нашите JS масиви не се отнасят за VBO в действителната RAM на GPU.

По-долу е даден пример за код, който предоставя цялата описана функционалност. Важна забележка е фактът, че променливите, съхранявани на графичния процесор, не са събрани боклуци. Това означава, че трябва да ги изтрием ръчно, след като повече не искаме да ги използваме. Ние просто ще ви дадем пример за това как се прави тук и няма да се фокусираме върху тази концепция по-нататък. Изтриването на променливи от графичния процесор е необходимо само ако планирате да спрете да използвате определена геометрия в цялата програма.

Също така добавихме сериализация към нашите Geometry.prototype.vertexCount = function () { return this.faces.length * 3 } Geometry.prototype.positions = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.position answer.push(v.x, v.y, v.z) }) }) return answer } Geometry.prototype.normals = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.normal answer.push(v.x, v.y, v.z) }) }) return answer } Geometry.prototype.uvs = function () { var answer = [] this.faces.forEach(function (face) { face.vertices.forEach(function (vertex) { var v = vertex.uv answer.push(v.x, v.y) }) }) return answer } //////////////////////////////// function VBO (gl, data, count) { // Creates buffer object in GPU RAM where we can store anything var bufferObject = gl.createBuffer() // Tell which buffer object we want to operate on as a VBO gl.bindBuffer(gl.ARRAY_BUFFER, bufferObject) // Write the data, and set the flag to optimize // for rare changes to the data we're writing gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW) this.gl = gl this.size = data.length / count this.count = count this.data = bufferObject } VBO.prototype.destroy = function () { // Free memory that is occupied by our buffer object this.gl.deleteBuffer(this.data) } клас и елементи в него.

VBO

gl типът данни генерира VBO в предадения контекст на WebGL въз основа на масива, предаден като втори параметър.

Можете да видите три повиквания към createBuffer() контекст. The bindBuffer() call създава буфера. The ARRAY_BUFFER call казва на машината на състоянието WebGL да използва тази специфична памет като текущата VBO (bufferData()) за всички бъдещи операции, докато не се каже друго. След това задаваме стойността на текущия VBO на предоставените данни, с deleteBuffer() .

Ние също така предоставяме метод за унищожаване, който изтрива нашия буферен обект от GPU RAM, като използва function Mesh (gl, geometry) { var vertexCount = geometry.vertexCount() this.positions = new VBO(gl, geometry.positions(), vertexCount) this.normals = new VBO(gl, geometry.normals(), vertexCount) this.uvs = new VBO(gl, geometry.uvs(), vertexCount) this.vertexCount = vertexCount this.position = new Transformation() this.gl = gl } Mesh.prototype.destroy = function () { this.positions.destroy() this.normals.destroy() this.uvs.destroy() } .

Можете да използвате три VBO и трансформация, за да опишете всички свойства на мрежата, заедно с нейната позиция.

Geometry.loadOBJ('/assets/model.obj').then(function (geometry) { var mesh = new Mesh(gl, geometry) console.log(mesh) mesh.destroy() })

Като пример ето как можем да заредим модел, да съхраним свойствата му в мрежата и след това да го унищожим:

attribute

Шейдъри

Следва описаният по-рано двуетапен процес на преместване на точки в желани позиции и боядисване на всички отделни пиксели. За да направим това, ние пишем програма, която се изпълнява на графичната карта много пъти. Тази програма обикновено се състои от поне две части. Първата част е a Вертекс шейдър , който се изпълнява за всеки връх и извежда там, където трябва да поставим върха на екрана, наред с други неща. Втората част е Фрагмент Шейдър , който се изпълнява за всеки пиксел, който триъгълник покрива на екрана, и извежда цвета, в който трябва да бъде боядисан пикселът.

Вертекс шейдъри

Да предположим, че искате да имате модел, който се движи наляво и надясно по екрана. При наивен подход можете да актуализирате позицията на всеки връх и да го изпратите отново на графичния процесор. Този процес е скъп и бавен. Като алтернатива бихте дали програма за GPU, която да се изпълнява за всеки връх и да правите всички тези операции паралелно с процесор, който е създаден за извършване на точно тази работа. Това е ролята на а върхов шейдър .

vr срещу ar срещу mr

Вертексният шейдър е частта от рендъринг конвейера, която обработва отделни върхове. Извикването към шейдъра на върха получава един връх и извежда един връх, след като са приложени всички възможни трансформации към върха.

Шейдърите са написани на GLSL. В този език има много уникални елементи, но по-голямата част от синтаксиса е много подобен на C, така че трябва да е разбираем за повечето хора.

Има три типа променливи, които влизат и излизат от шейдър на върхове и всички те служат за специфична употреба:

  • uniform - Това са входове, които притежават специфични свойства на върха. По-рано описахме позицията на върха като атрибут под формата на триелементен вектор. Можете да разглеждате атрибутите като стойности, които описват един връх.
  • uniform - Това са входове, които са еднакви за всеки връх в рамките на едно и също извикване на рендиране. Да кажем, че искаме да можем да движим нашия модел, като дефинираме матрица за трансформация. Можете да използвате varying променлива, за да опише това. Можете да посочите и ресурси на графичния процесор, като текстури. Можете да разглеждате униформите като стойности, които описват модел или част от модел.
  • attribute vec3 position; attribute vec3 normal; attribute vec2 uv; uniform mat4 model; uniform mat4 view; uniform mat4 projection; varying vec3 vNormal; varying vec2 vUv; void main() { vUv = uv; vNormal = (model * vec4(normal, 0.)).xyz; gl_Position = projection * view * model * vec4(position, 1.); } - Това са изходи, които предаваме на шейдъра на фрагменти. Тъй като има потенциално хиляди пиксели за триъгълник от върхове, всеки пиксел ще получи интерполирана стойност за тази променлива в зависимост от позицията. Така че, ако един връх изпраща 500 като изход, а друг 100, пиксел, който е в средата между тях, ще получи 300 като вход за тази променлива. Можете да разглеждате вариациите като стойности, които описват повърхности между върховете.

И така, да предположим, че искате да създадете шейдър на връх, който получава координати на позиция, нормални и uv за всеки връх и позиция, изглед (обратна позиция на камерата) и матрица за проекция за всеки изобразен обект. Да предположим, че искате също да нарисувате отделни пиксели въз основа на техните UV координати и техните нормали. „Как би изглеждал този код?“ може да попитате.

main

Повечето от елементите тук трябва да се обясняват. Ключовото нещо, което трябва да забележите, е фактът, че в varying няма връщани стойности функция. Всички стойности, които бихме искали да върнем, са присвоени или на gl_Position променливи или към специални променливи. Тук присвояваме vec4, което е четириизмерен вектор, при което последното измерение винаги трябва да бъде зададено на едно. Друго странно нещо, което може да забележите, е начинът, по който изграждаме vec4 извън вектора на позицията. Можете да конструирате float чрез използване на четири vec2 s, две varying s или всяка друга комбинация, която води до четири елемента. Има много привидно странни кастинги, които са напълно логични, след като се запознаете с матриците за трансформация.

Можете също така да видите, че тук можем да извършваме матрични трансформации изключително лесно. GLSL е специално създаден за този вид работа. Изходната позиция се изчислява чрез умножаване на матрицата на проекцията, изгледа и модела и я прилага върху позицията. Нормалният изход просто се трансформира в световното пространство. По-късно ще обясним защо сме спрели там с нормалните трансформации.

Засега ще го улесним и ще преминем към рисуването на отделни пиксели.

Фрагментни шейдъри

ДА СЕ фрагментен шейдър е стъпката след растеризацията в графичния конвейер. Той генерира цвят, дълбочина и други данни за всеки пиксел на обекта, който се рисува.

Принципите, които стоят зад внедряването на фрагментни шейдъри, са много сходни с върховите шейдъри. Има обаче три основни разлики:

  • Няма повече attribute изходи и varying входовете са заменени с gl_FragColor входове. Току-що преминахме в нашия конвейер и нещата, които са изходни в вертексния шейдър, вече са входове в шейдъра на фрагменти.
  • Единственият ни изход сега е vec4, което е #ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec2 clampedUv = clamp(vUv, 0., 1.); gl_FragColor = vec4(clampedUv, 1., 1.); } . Елементите представляват съответно червено, зелено, синьо и алфа (RGBA), с променливи в диапазона от 0 до 1. Трябва да поддържате алфа на 1, освен ако не правите прозрачност. Прозрачността обаче е доста усъвършенствана концепция, така че ще се придържаме към непрозрачните обекти.
  • В началото на шейдъра на фрагменти трябва да зададете точността на поплавъка, което е важно за интерполациите. В почти всички случаи просто се придържайте към линиите от следващия шейдър.

Имайки предвид това, можете лесно да напишете шейдър, който рисува червения канал на базата на U позиция, зеления канал на база V позиция и задава синия канал на максимум.

clamp

Функцията function ShaderProgram (gl, vertSrc, fragSrc) { var vert = gl.createShader(gl.VERTEX_SHADER) gl.shaderSource(vert, vertSrc) gl.compileShader(vert) if (!gl.getShaderParameter(vert, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(vert)) throw new Error('Failed to compile shader') } var frag = gl.createShader(gl.FRAGMENT_SHADER) gl.shaderSource(frag, fragSrc) gl.compileShader(frag) if (!gl.getShaderParameter(frag, gl.COMPILE_STATUS)) { console.error(gl.getShaderInfoLog(frag)) throw new Error('Failed to compile shader') } var program = gl.createProgram() gl.attachShader(program, vert) gl.attachShader(program, frag) gl.linkProgram(program) if (!gl.getProgramParameter(program, gl.LINK_STATUS)) { console.error(gl.getProgramInfoLog(program)) throw new Error('Failed to link program') } this.gl = gl this.position = gl.getAttribLocation(program, 'position') this.normal = gl.getAttribLocation(program, 'normal') this.uv = gl.getAttribLocation(program, 'uv') this.model = gl.getUniformLocation(program, 'model') this.view = gl.getUniformLocation(program, 'view') this.projection = gl.getUniformLocation(program, 'projection') this.vert = vert this.frag = frag this.program = program } // Loads shader files from the given URLs, and returns a program as a promise ShaderProgram.load = function (gl, vertUrl, fragUrl) { return Promise.all([loadFile(vertUrl), loadFile(fragUrl)]).then(function (files) { return new ShaderProgram(gl, files[0], files[1]) }) function loadFile (url) { return new Promise(function (resolve) { var xhr = new XMLHttpRequest() xhr.onreadystatechange = function () { if (xhr.readyState == XMLHttpRequest.DONE) { resolve(xhr.responseText) } } xhr.open('GET', url, true) xhr.send(null) }) } } просто ограничава всички плувки в даден обект да бъдат в рамките на дадените граници. Останалата част от кода трябва да е доста ясна.

Имайки предвид всичко това, остава само да се внедри това в WebGL.

Комбиниране на шейдъри в програма

Следващата стъпка е да комбинирате шейдърите в програма:

ShaderProgram.prototype.use = function () { this.gl.useProgram(this.program) }

Няма какво много да се каже за случващото се тук. На всеки шейдър се присвоява низ като източник и се компилира, след което проверяваме дали е имало грешки при компилацията. След това създаваме програма, като свързваме тези два шейдъра. И накрая, ние съхраняваме указатели за всички подходящи атрибути и униформи за потомство.

Всъщност чертеж на модела

Не на последно място, но не на последно място, вие рисувате модела.

Първо изберете програмата за шейдър, която искате да използвате.

Transformation.prototype.sendToGpu = function (gl, uniform, transpose) gl.uniformMatrix4fv(uniform, transpose Camera.prototype.use = function (shaderProgram) { this.projection.sendToGpu(shaderProgram.gl, shaderProgram.projection) this.getInversePosition().sendToGpu(shaderProgram.gl, shaderProgram.view) }

След това изпращате всички униформи, свързани с камерата, на графичния процесор. Тези униформи се сменят само веднъж при смяна или движение на камерата.

VBO.prototype.bindToAttribute = function (attribute) { var gl = this.gl // Tell which buffer object we want to operate on as a VBO gl.bindBuffer(gl.ARRAY_BUFFER, this.data) // Enable this attribute in the shader gl.enableVertexAttribArray(attribute) // Define format of the attribute array. Must match parameters in shader gl.vertexAttribPointer(attribute, this.size, gl.FLOAT, false, 0, 0) }

Накрая вземате трансформациите и VBO и ги присвоявате съответно на униформи и атрибути. Тъй като това трябва да се направи с всеки VBO, можете да създадете неговото свързване на данни като метод.

drawArrays()

След това присвоявате масив от три плувки на униформата. Всеки унифициран тип има различен подпис, така че документация и още документация са вашите приятели тук. Накрая изчертавате триъгълния масив на екрана. Казвате обаждането на чертежа TRIANGLES от кой връх да започне и колко върха да нарисувате. Първият предаден параметър казва на WebGL как трябва да интерпретира масива от върхове. Използване на POINTS взема три по три върха и чертае триъгълник за всеки триплет. Използване на Mesh.prototype.draw = function (shaderProgram) { this.positions.bindToAttribute(shaderProgram.position) this.normals.bindToAttribute(shaderProgram.normal) this.uvs.bindToAttribute(shaderProgram.uv) this.position.sendToGpu(this.gl, shaderProgram.model) this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount) } просто ще нарисува точка за всеки преминат връх. Има много повече опции, но няма нужда да откривате всичко наведнъж. По-долу е даден кодът за рисуване на обект:

Renderer.prototype.setShader = function (shader) { this.shader = shader } Renderer.prototype.render = function (camera, objects) { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) var shader = this.shader if (!shader) { return } shader.use() camera.use(shader) objects.forEach(function (mesh) { mesh.draw(shader) }) }

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

var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) var gl = renderer.getContext() var objects = [] Geometry.loadOBJ('/assets/sphere.obj').then(function (data) { objects.push(new Mesh(gl, data)) }) ShaderProgram.load(gl, '/shaders/basic.vert', '/shaders/basic.frag') .then(function (shader) { renderer.setShader(shader) }) var camera = new Camera() camera.setOrthographic(16, 10, 10) loop() function loop () { renderer.render(camera, objects) requestAnimationFrame(loop) }

Можем да комбинираме всички елементи, които трябва да нарисуваме накрая нещо на екрана:

#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); gl_FragColor = vec4(brown, 1.); }

Обект, нарисуван върху платното, с цветове в зависимост от UV координатите

Това изглежда малко случайно, но можете да видите различните петна на сферата въз основа на това къде се намират на UV картата. Можете да промените шейдъра, за да боядисате обекта в кафяво. Просто задайте цвета за всеки пиксел да бъде RGBA за кафяво:

#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); vec3 sunlightDirection = vec3(-1., -1., -1.); float lightness = -clamp(dot(normalize(vNormal), normalize(sunlightDirection)), -1., 0.); gl_FragColor = vec4(brown * lightness, 1.); }

Кафяв предмет, нарисуван върху платното

Не изглежда много убедително. Изглежда, че сцената се нуждае от някои сенчести ефекти.

Добавяне на светлина

Светлините и сенките са инструментите, които ни позволяват да възприемаме формата на предметите. Светлините се предлагат в много форми и размери: прожектори, които греят в един конус, крушки, които разпространяват светлина във всички посоки, и най-интересното е слънцето, което е толкова далеч, че цялата светлина, която ни свети, излъчва, за всички намерения и цели, в същата посока.

Слънчевата светлина звучи така, сякаш е най-простата за изпълнение, тъй като всичко, което трябва да осигурите, е посоката, в която се разпространяват всички лъчи. За всеки пиксел, който нарисувате на екрана, проверявате ъгъла, под който светлината удря обекта. Тук влизат нормалите на повърхността.

Демонстрация на ъгли между светлинните лъчи и нормалите на повърхността, както за плоско, така и за гладко засенчване

Можете да видите всички светлинни лъчи да текат в една и съща посока и да удрят повърхността под различни ъгли, които се основават на ъгъла между светлинния лъч и повърхността нормално. Колкото повече съвпадат, толкова по-силна е светлината.

Ако извършите точково произведение между нормализираните вектори за светлинния лъч и повърхността нормално, ще получите -1, ако лъчът удари повърхността идеално перпендикулярно, 0 ако лъчът е успореден на повърхността и 1, ако го осветява от противоположната страна. Така че всичко между 0 и 1 не трябва да добавя никаква светлина, докато числата между 0 и -1 трябва постепенно да увеличават количеството светлина, удрящо обекта. Можете да тествате това, като добавите фиксирана светлина в кода на шейдъра.

#ifdef GL_ES precision highp float; #endif varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); vec3 sunlightDirection = vec3(-1., -1., -1.); float lightness = -clamp(dot(normalize(vNormal), normalize(sunlightDirection)), -1., 0.); float ambientLight = 0.3; lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(brown * lightness, 1.); }

Кафяв предмет със слънчева светлина

Зададохме слънцето да грее в посока напред-наляво-надолу. Можете да видите колко гладко е засенчването, въпреки че моделът е много назъбен. Можете също така да забележите колко тъмна е долната лява страна. Можем да добавим ниво на околна светлина, което ще направи зоната в сянката по-ярка.

#ifdef GL_ES precision highp float; #endif uniform vec3 lightDirection; uniform float ambientLight; varying vec3 vNormal; varying vec2 vUv; void main() { vec3 brown = vec3(.54, .27, .07); float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(brown * lightness, 1.); }

Кафяв обект със слънчева и околна светлина

Можете да постигнете същия ефект, като въведете светлинен клас, който съхранява посоката на светлината и интензивността на околната светлина. След това можете да промените фрагментния шейдър, за да приспособите това добавяне.

Сега шейдърът става:

function Light () { this.lightDirection = new Vector3(-1, -1, -1) this.ambientLight = 0.3 } Light.prototype.use = function (shaderProgram) { var dir = this.lightDirection var gl = shaderProgram.gl gl.uniform3f(shaderProgram.lightDirection, dir.x, dir.y, dir.z) gl.uniform1f(shaderProgram.ambientLight, this.ambientLight) }

След това можете да дефинирате светлината:

this.ambientLight = gl.getUniformLocation(program, 'ambientLight') this.lightDirection = gl.getUniformLocation(program, 'lightDirection')

В класа на програмата за шейдъри добавете необходимите униформи:

Renderer.prototype.render = function (camera, light, objects) { this.gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) var shader = this.shader if (!shader) { return } shader.use() light.use(shader) camera.use(shader) objects.forEach(function (mesh) { mesh.draw(shader) }) }

В програмата добавете повикване към новата светлина в визуализатора:

var light = new Light() loop() function loop () { renderer.render(camera, light, objects) requestAnimationFrame(loop) }

След това цикълът ще се промени леко:

sampler2D

Ако сте направили всичко правилно, тогава изобразеното изображение трябва да е същото като в последното изображение.

Последна стъпка, която трябва да обмислим, би била добавянето на действителна текстура към нашия модел. Нека направим това сега.

Добавяне на текстури

HTML5 има чудесна поддръжка за зареждане на изображения, така че няма нужда да правите луд синтактичен анализ. Изображенията се предават на GLSL като sampler2D като кажете на шейдъра коя от обвързаните текстури да вземете проба. Има ограничен брой текстури, които човек може да обвърже, а ограничението се основава на използвания хардуер. A #ifdef GL_ES precision highp float; #endif uniform vec3 lightDirection; uniform float ambientLight; uniform sampler2D diffuse; varying vec3 vNormal; varying vec2 vUv; void main() { float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(texture2D(diffuse, vUv).rgb * lightness, 1.); } могат да бъдат заявени за цветове на определени позиции. Тук влизат UV координатите. Ето един пример, при който сменихме кафявото с пробни цветове.

this.diffuse = gl.getUniformLocation(program, 'diffuse')

Новата униформа трябва да бъде добавена към списъка в програмата за шейдър:

function Texture (gl, image) { var texture = gl.createTexture() // Set the newly created texture context as active texture gl.bindTexture(gl.TEXTURE_2D, texture) // Set texture parameters, and pass the image that the texture is based on gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image) // Set filtering methods // Very often shaders will query the texture value between pixels, // and this is instructing how that value shall be calculated gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) this.data = texture this.gl = gl } Texture.prototype.use = function (uniform, binding) binding = Number(binding) Texture.load = function (gl, url) { return new Promise(function (resolve) { var image = new Image() image.onload = function () { resolve(new Texture(gl, image)) } image.src = url }) }

И накрая, ще приложим зареждане на текстура. Както бе казано по-рано, HTML5 предоставя възможности за зареждане на изображения. Всичко, което трябва да направим, е да изпратим изображението на графичния процесор:

sampler2D

Процесът не се различава много от процеса, използван за зареждане и свързване на VBO. Основната разлика е, че вече не сме обвързващи с атрибут, а по-скоро обвързваме индекса на текстурата с едно цяло число. Mesh type не е нищо повече от отместване на указател към текстура.

Сега всичко, което трябва да се направи, е да разширите function Mesh (gl, geometry, texture) { // added texture var vertexCount = geometry.vertexCount() this.positions = new VBO(gl, geometry.positions(), vertexCount) this.normals = new VBO(gl, geometry.normals(), vertexCount) this.uvs = new VBO(gl, geometry.uvs(), vertexCount) this.texture = texture // new this.vertexCount = vertexCount this.position = new Transformation() this.gl = gl } Mesh.prototype.destroy = function () { this.positions.destroy() this.normals.destroy() this.uvs.destroy() } Mesh.prototype.draw = function (shaderProgram) { this.positions.bindToAttribute(shaderProgram.position) this.normals.bindToAttribute(shaderProgram.normal) this.uvs.bindToAttribute(shaderProgram.uv) this.position.sendToGpu(this.gl, shaderProgram.model) this.texture.use(shaderProgram.diffuse, 0) // new this.gl.drawArrays(this.gl.TRIANGLES, 0, this.vertexCount) } Mesh.load = function (gl, modelUrl, textureUrl) { // new var geometry = Geometry.loadOBJ(modelUrl) var texture = Texture.load(gl, textureUrl) return Promise.all([geometry, texture]).then(function (params) { return new Mesh(gl, params[0], params[1]) }) } клас, за да се справя и с текстури:

var renderer = new Renderer(document.getElementById('webgl-canvas')) renderer.setClearColor(100, 149, 237) var gl = renderer.getContext() var objects = [] Mesh.load(gl, '/assets/sphere.obj', '/assets/diffuse.png') .then(function (mesh) { objects.push(mesh) }) ShaderProgram.load(gl, '/shaders/basic.vert', '/shaders/basic.frag') .then(function (shader) { renderer.setShader(shader) }) var camera = new Camera() camera.setOrthographic(16, 10, 10) var light = new Light() loop() function loop () { renderer.render(camera, light, objects) requestAnimationFrame(loop) }

И последният основен скрипт ще изглежда по следния начин:

function loop () { renderer.render(camera, light, objects) camera.position = camera.position.rotateY(Math.PI / 120) requestAnimationFrame(loop) }

Текстуриран обект със светлинни ефекти

Дори анимацията в този момент е лесна. Ако искате камерата да се върти около нашия обект, можете да го направите, като просто добавите един ред код:

void main() { float lightness = -clamp(dot(normalize(vNormal), normalize(lightDirection)), -1., 0.); lightness = lightness > 0.1 ? 1. : 0.; // new lightness = ambientLight + (1. - ambientLight) * lightness; gl_FragColor = vec4(texture2D(diffuse, vUv).rgb * lightness, 1.); }

Завъртена глава по време на анимация на камерата

Чувствайте се свободни да играете с шейдъри. Добавянето на един ред код ще превърне това реалистично осветление в нещо анимационно.

|_+_|

Това е толкова просто, колкото да кажете на осветлението да достигне крайностите си в зависимост от това дали е преминало зададен праг.

Глава с апликационно осветление

Къде да продължим

Има много източници на информация за изучаване на всички трикове и тънкости на WebGL. И най-хубавото е, че ако не можете да намерите отговор, който се отнася до WebGL, можете да го потърсите в OpenGL, тъй като WebGL се основава почти на подмножество на OpenGL, като някои имена се променят.

В никакъв конкретен ред, ето някои страхотни източници за по-подробна информация, както за WebGL, така и за OpenGL.

  • Основи на WebGL
  • Изучаване на WebGL
  • Много подробно Урок за OpenGL ще ви преведе през всички основни принципи, описани тук, по много бавен и подробен начин.
  • И има такива много , много други сайтове, посветени на това да ви научат на принципите на компютърната графика.
  • MDN документация за WebGL
  • Спецификация на Khronos WebGL 1.0 защото, ако се интересувате от разбирането на по-техническите подробности за това как трябва да работи API на WebGL във всички крайни случаи.

Разбиране на основите

Какво е WebGL?

WebGL е JavaScript API, който добавя естествена поддръжка за изобразяване на 3D графики в съвместими уеб браузъри, чрез API, подобен на OpenGL.

Как се представят 3D моделите в паметта?

Най-често срещаният начин за представяне на триизмерни модели е чрез масив от върхове, всеки от които има определено положение в пространството, нормално на повърхността, от която върхът трябва да бъде част, и координати върху текстура, използвана за рисуване на модела. След това тези върхове се поставят в групи от по трима, за да образуват триъгълници.

Какво е вертекс шейдър?

Вертексният шейдър е частта от рендъринг конвейера, която обработва отделни върхове. Извикването към шейдъра на върха получава един връх и извежда един връх, след като са приложени всички възможни трансформации към върха. Това позволява прилагане на движение и деформации върху цели обекти.

Какво представлява фрагментният шейдър?

Фрагментният шейдър е частта от конвейера за рендиране, която отнема пиксел на обект на екрана, заедно със свойствата на обекта в тази позиция на пиксела и може да генерира цвят, дълбочина и други данни за него.

Как се движат обектите в 3D сцена?

Всички позиции на върховете на даден обект са спрямо неговата локална координатна система, която е представена с матрица за идентичност 4x4. Ако преместим тази координатна система, като я умножим с матрици за преобразуване, върховете на обекта се движат заедно с нея.

Законни спортни залагания: Нова икономика, създадена по повод

Финансови Процеси

Законни спортни залагания: Нова икономика, създадена по повод
Изграждане на ъглов видео плейър с Videogular

Изграждане на ъглов видео плейър с Videogular

Back-End

Популярни Публикации
Широки срещу тесни набори от умения: Демистифицирани умения за софтуерно инженерство
Широки срещу тесни набори от умения: Демистифицирани умения за софтуерно инженерство
Талантът не е стока
Талантът не е стока
iOS 9 Betas и WatchOS 2 за разработчици
iOS 9 Betas и WatchOS 2 за разработчици
Ръководство за многообработващи мрежови сървърни модели
Ръководство за многообработващи мрежови сървърни модели
Как да създадете персонализирани шрифтове: 7 стъпки и 3 казуса
Как да създадете персонализирани шрифтове: 7 стъпки и 3 казуса
 
Настройка на производителността на базата данни на SQL за разработчици
Настройка на производителността на базата данни на SQL за разработчици
Събуждане на спяща индустрия: Нарушаване на индустрията на матраците
Събуждане на спяща индустрия: Нарушаване на индустрията на матраците
Figma срещу Sketch срещу Axure - Преглед, основан на задачи
Figma срещу Sketch срещу Axure - Преглед, основан на задачи
Разбиране на нюансите на класификацията на шрифтовете
Разбиране на нюансите на класификацията на шрифтовете
Възраст преди красотата - Ръководство за дизайн на интерфейси за възрастни възрастни
Възраст преди красотата - Ръководство за дизайн на интерфейси за възрастни възрастни
Популярни Публикации
  • какво измерва ценова еластичност на търсенето
  • обектен модел на страницата на уеб драйвер на селен
  • как да изготвим отчет за паричните потоци от баланса
  • как да хакна номера на нечия кредитна карта
  • сметкоплан на софтуерна фирма
  • Софтуер за примерен документ за проектиране на системата
Категории
  • Agile Talent
  • Дизайнерски Живот
  • Възходът На Дистанционното
  • Рентабилност И Ефективност
  • © 2022 | Всички Права Запазени

    portaldacalheta.pt