Власний движок WebGL. Стаття № 1. Полотно

Через серію статей спробую розібрати движок на webgl.
 
Основною вимогою буде мінімальний введення даних. Адже, грубо кажучи, движок — це модель, створена для спрощення завдання. Матеріал розрахований на початківець рівень, для тих, хто прочитав основи webgl і хоче спробувати почати працювати. Таких як я.
 
 
Перше. Опис завдання на пальцях
Необхідно створити класи об'єктів (примітиви), які з себе представляють набір точок. При цьому примітиви повинні бути незалежні один від одного. Кожен примітив можна переміщати, повертати навколо центру або навколо довільної точки.
Необхідно створити механізм окреслення цих об'єктів.
І наостанок необхідно створити щось на зразок карти на якій можна встановити наші об'єкти та за якою можна вільно переміщатися.
 
 
Друге. Механізм окреслення
Механізм окреслення, просто полотно і фарби для художника. Який необхідний мінімум повинен ввести в кінцевому підсумку користувач нашого движка, щоб у нього був готовий полотно? На мою, це просто посилання на полотно в DOMе. А потім вже можна встановити і колір, і розмір.
 
 
var scene = new Scene("webglID");
scene.setBackgroundColor([0.1,0.5,0.6,0.2]);
scene.setViewPort(300, 300);

 
Що може бути легше, якщо врахувати, що нам необхідна лише перший рядок?
«WebglID» — це id елемента canvas в якому відбуватиметься малювання.
Реалізація даного механізму поки теж не представляє з себе нічого складного, адже поки малювання не відбувається.
 
 
function Scene(canvasID) {
	this.backgroundColor = {red:1.0, green:1.0, blue:1.0, alpha:1.0};
        this.canvas = document.getElementById(canvasID);
        this.getContext();
}
Scene.prototype = {
    setViewPort: function(width,height){
        this.gl.viewportWidth = width;
        this.gl.viewportHeight = height;
    },
	setBackgroundColor: function(colorVec){
		if (colorVec){
			if (colorVec.length > 0) 
			{
				this.backgroundColor.red = colorVec[0];
			}
			if (colorVec.length > 1) 
			{
				this.backgroundColor.green = colorVec[1];
			}
			if (colorVec.length > 2) 
			{
				this.backgroundColor.blue = colorVec[2];
			}
			if (colorVec.alpha > 3) 
			{
				this.backgroundColor.red = colorVec[3];
			}
			
		}
	},	
	getContext:function(){
		var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
		this.gl = null;
		for (var ii = 0; ii < names.length; ++ii) {
			try {
				this.gl = this.canvas.getContext(names[ii]);
			} catch(e) {}
        if (this.gl) {
            break;
        }
		}	
	
	}
}

 
(Метод getContext узятий з статті . (До цього просто писав — this.gl = this.canvas.getContext («webgl») ;)
 
 
Полотно створений, залишилося придбати пензлі і фарби
Перед тим, як додати в наш движок можливість малювання, необхідно визначитися:
 
     
  1. Будемо ми малювати по вершинах або за індексами.
  2.  
  3. З яких фігур складатиметься наші примітиви.
  4.  
 
     
  • LINES
  •  
  • LINE_STRIP
  •  
  • LINE_LOOP
  •  
  • TRIANGLES
  •  
  • TRIANGLE_STRIP
  •  
  • TRIANGLE_FAN
  •  
  • POINTS
  •  
 
Для движка я вибрав малювання за індексами, як мені здається, це очевидно. А тип фігури — TRIANGLES. Тут вже менш очевидно, але спробую пояснити.
 
Ми будемо використовувати один буфер для всіх об'єктів і відповідно всіх вершин та індексів. Надалі, якщо буде можливість використовувати декілька буферів — тип фігури буде знаходиться в самому об'єкті примітиву. (Якщо така можливість вже є — напишіть, будь ласка, в коментарях.) При цьому об'єкти повинні бути незалежні один від одного, тому у нас залишається вибір серед — LINES, TRIANGLES і POINTS. Я вибрав TRIANGLES, як заповнюється зсередини фігура.
 
Сам процес малювання складатиметься з двох етапів — додаємо об'єкт (и) на сцену і малюємо всю сцену. Насправді, в подальшому це буде 3 етапу — виявлення об'єктів, які нам треба намалювати, додавання на сцену, ну і малювання всієї сцени.
 
 
var vertex = [
-50,50,50, 
50,50,50,
50,-50,50,
-50,-50,50    
];
var indices = [0,1,3,1,2,3];
var obj = new botuObject(vertex,indices);

scene.AddObject(obj);
scene.draw();


 
botuObject — це наш перший примітив. Не самий витончений, ну який є. Він просто містить в собі вершини і індекси, які в нього передали. За великим рахунком все примітиви будуть містити в собі вершини і індекси, тільки інші примітиви будуть ці вершини розраховувати самі, при ініціалізації. Про них буде докладно написано в наступних статтях.
 
Реалізація примітиву:
 
 
function botuObject(vertex,indices){
	this.vertex = vertex;
	this.indices = indices;
	this.vertex.size = 3;
}

 
Полотно, фінальна версія:
 

function Scene(canvasID) {
	this.backgroundColor = {red:1.0, green:1.0, blue:1.0, alpha:1.0};
    this.canvas = document.getElementById(canvasID);
    this.getContext();
    this.indicBuffer = "";
	this.vecVertex = [];
	this.vecIndices = [];
}

Scene.prototype = {
	clear: function(){
		this.indicBuffer = "";
		this.vecVertex = [];
		this.vecIndices = [];			
	},	
	getContext:function(){
		var names = ["webgl", "experimental-webgl", "webkit-3d", "moz-webgl"];
		this.gl = null;
		for (var ii = 0; ii < names.length; ++ii) {
			try {
				this.gl = this.canvas.getContext(names[ii]);
			} catch(e) {}
        if (this.gl) {
            break;
        }
		}		
	},	
    initBuffers: function (vertex, indices) {
		this.vertexBuffer = this.gl.createBuffer();
		this.vertexBuffer.size = vertex.size;
                this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.vertexBuffer);

		this.program.botuPositionAttr = this.gl.getAttribLocation(this.program, "botuPosition");
		this.gl.enableVertexAttribArray(this.program.botuPositionAttr);

		this.gl.bufferData(this.gl.ARRAY_BUFFER,new Float32Array(vertex), this.gl.STATIC_DRAW);
		
	
       if(indices)
        {
            this.indicBuffer = this.gl.createBuffer();
            this.indicBuffer.numberOfItems = indices.length;
            this.gl.bindBuffer(this.gl.ELEMENT_ARRAY_BUFFER, this.indicBuffer);
            this.gl.bufferData(this.gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), this.gl.STATIC_DRAW);
        }
    },
    initProgram: function (vxShaderDom, frShaderDom) {
        var vxShader = document.getElementById(vxShaderDom).textContent;
        var frShader = document.getElementById(frShaderDom).textContent;

        this.program = createProgram(this.gl,vxShader, frShader);
		this.gl.useProgram(this.program);
		
		this.program.botuPositionAttr = this.gl.getAttribLocation(this.program, "botuPosition");
		this.gl.enableVertexAttribArray(this.program.botuPositionAttr);
		
        function createProgram(context, vxs, frs) {
            var prg = context.createProgram();
            var VertexShader = createShader(context, context.VERTEX_SHADER, vxs);
            var FragmentShader = createShader(context, context.FRAGMENT_SHADER, frs);
            context.attachShader(prg,VertexShader);
            context.attachShader(prg,FragmentShader);
            context.linkProgram(prg);
            if (!context.getProgramParameter(prg, context.LINK_STATUS)) {
                alert(context.getProgramInfoLog(prg));
            }
            return prg;
        }
        function createShader(context,type,shader)
        {
            var sh = context.createShader(type);
            context.shaderSource(sh, shader);
            context.compileShader(sh);
            if (!context.getShaderParameter(sh, context.COMPILE_STATUS))
            {
                alert(context.getShaderInfoLog(sh));
            }
            return sh;            
        }
    },
    attributeSetup: function (attribName, attribSize) {
        var attrib = this.gl.getAttribLocation(this.program, attribName);
        this.gl.enableVertexAttribArray(attrib);
        this.gl.vertexAttribPointer(attrib, attribSize, this.gl.FLOAT, false, 0, 0);
        return attrib;
    },

    setViewPort: function(width,height){
        this.gl.viewportWidth = width;
        this.gl.viewportHeight = height;
    },
	setBackgroundColor: function(colorVec){
		if (colorVec){
			if (colorVec.length > 0) 
			{
				this.backgroundColor.red = colorVec[0];
			}
			if (colorVec.length > 1) 
			{
				this.backgroundColor.green = colorVec[1];
			}
			if (colorVec.length > 2) 
			{
				this.backgroundColor.blue = colorVec[2];
			}
			if (colorVec.alpha > 3) 
			{
				this.backgroundColor.red = colorVec[3];
			}
			
		}
	},	
	AddObject: function(botuObj){
		this.vecVertex.size = botuObj.vertex.size;
		var next = Math.max(this.vecVertex.length / this.vecVertex.size,0);
		this.vecVertex = this.vecVertex.concat(botuObj.vertex);
		this.vecIndices = this.vecIndices.concat(botuObj.indices.map(function(i){return i + next}));
		this.vecVertex.size = botuObj.vertex.size;
	},
    draw: function () {
		this.initProgram("vertexShader", "fragmentShader");
		this.initBuffers(this.vecVertex, this.vecIndices);
                this.gl.viewport(0, 0, this.gl.viewportWidth, this.gl.viewportHeight);        
		this.gl.clearColor(this.backgroundColor.red,this.backgroundColor.green,this.backgroundColor.blue,this.backgroundColor.alpha);
		this.gl.clear(this.gl.COLOR_BUFFER_BIT); 		        	
		this.gl.vertexAttribPointer(this.program.botuPositionAttr,this.vertexBuffer.size,this.gl.FLOAT,false,0,0);
		this.gl.enable(this.gl.DEPTH_TEST);
		this.gl.drawElements(this.gl.TRIANGLES, this.indicBuffer.numberOfItems, this.gl.UNSIGNED_SHORT, 0);		
    }
}

 
Читати чужий код — найбільш невдячна справа, за винятком тих випадків, коли це код майстра. Даний код на це не претендує, тому коротко про всі методи:
 
 
     
  • clear. Очищає вершини і індекси, яким буде заповнюватися буфер для малювання. Спочатку створений для анімації, надалі буде використовуватися при будь динаміки.
  •  
  • getContext Вже був згаданий. Узятий з статті . Ініціалізація контексту. Використовується тільки на самому початку для налаштування полотна.
  •  
  • initProgram. Створення програми і шейдеров. Тут варто зупинитися і обговорити невеликий недолік мого движка — шейдери поки храняться окремо і повинні мати стандуртную форму (як мінімум), потім недолік буде виправлений.
  •  
  • attributeSetup. Якщо будемо додавати новий атрибут в шейдери, даний метод буде ініціалізувати цей атрибут.
  •  
  • initBuffers. Створення та ініціалізація буферів. Використовуємо масиви завантажених об'єктів-примітивів.
  •  
  • setViewPort. Встановлюємо ширину і висоту полотна.
  •  
  • setBackgroundColor. Встановлюємо задній фон полотна.
  •  
  • AddObject. Додаємо примітив. До примітиву тільки 2 вимоги — наявності масиву вершин та індексів.
  •  
  • draw. Власне саме малювання.
  •  
 
Отже, перший явний «недолік» движка — шейдери. У мене вони встановлені таким чином:
 
 
<script type="x-shader" id="vertexShader">
	attribute vec3 botuPosition;
        varying vec4 colorPos;
        void main(){
        colorPos = vec4(max(botuPosition.x,(-1.0) * botuPosition.x) / 200.0, max(botuPosition.y,(-1.0) * botuPosition.y)  / 200.0, max(botuPosition.z,(-1.0) * botuPosition.z)  / 200.0,1.0);
        gl_Position = vec4(botuPosition,200);
        }
    </script>
    <script type="x-shader" id="fragmentShader">
        precision highp float;
        varying vec4 colorPos;
        void main(){
        gl_FragColor = colorPos;
        }
    </script>

 
Це тимчасовий варіант. При додаванні текстурирования — шейдери необхідно буде трохи підправити.
 
 

Що далі

У наступній статті — опис «самопісний» матриці. А також перший «нормальний» примітив — куб.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.