Собственный движок WebGL. Статья №1. Холст

в 8:48, , рубрики: javascript, WebGL, движок, учимся вместе, метки: , , ,

Через серию статей попробую разобрать движок на 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. Из каких фигур будет состоять наши примитивы.

  • 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>

Это временный вариант. При добавлении текстурирования — шейдеры необходимо будет немного подправить.

Что дальше

В следующей статье — описание «самописной» матрицы. А также первый «нормальный» примитив — куб.

Автор: Botu

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js