Pull to refresh

Собственный движок WebGL. Статья №2. Матрица

Reading time9 min
Views8K
В продолжении статьи.

Матрица


Когда только начал разрабатывать матрицу, даже не предполагал — на сколько она в дальнейшем нам упростит жизнь. У матрицы много свойств, но в нашей задаче я бы их все свел к одному — «отделение мух от котлет», то есть массива точек от общего массива координат. С точки зрения нашего кода — это будет выделение массива строк, каждая из которых является точкой и массива столбцов, массив одной из координат x,y,z или w. У меня упрощенная модель, поэтому «w» использовать не буду.

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

При описании класса матрицы, нам достаточно знать массив из которого мы получим матрицу и размерность точек. Я использую размерность равную трем — x,y,z.

Итак, сам код.

function botuMatrix (source,columns)
{
	this.source = source;
	this.columnNumbers = columns;
	this.rowNumbers = source.length / columns;
	this.rows = [];
	this.minval = [];
	this.maxval = [];
	this.radius = [];
	this.center = [];
	this.column = [];

		
	if (source.length > 0)
	{
		var count = 0;

		while(count < source.length)
		{
			var currentRow = this.source.slice(count,count + this.columnNumbers);		
		    this.rows.push(currentRow);
			
			var columnCount = 0;
			while(columnCount <= this.columnNumbers)
			{
				if (!this.column[columnCount]) 
				{
					this.column[columnCount] = [];
				}
				this.column[columnCount].push(currentRow[columnCount]);
				columnCount += 1;
			}
			
			count = count + this.columnNumbers; 
		}	
		this.rowNumbers = this.rows.length; 
		
		if (this.rows.length > 0)
		{
			count = 0;
			while(count < this.rows.length)
			{
				var tempRow = this.rows[count].slice(0);
				if (count == 0 )
				{
					this.minval = tempRow.slice(0);
					this.maxval = tempRow.slice(0);
					this.radius = tempRow.slice(0);
					this.center = tempRow.slice(0);
				}
			
				if (count > 0)
				{
					var rowcount = 0;
					while(rowcount < tempRow.length)
					{
						this.minval.splice(rowcount,1,Math.min(this.minval[rowcount],tempRow[rowcount]));
					
						this.maxval.splice(rowcount,1,Math.max(this.maxval[rowcount],tempRow[rowcount]));
						this.radius.splice(rowcount,1,(this.maxval[rowcount] - this.minval[rowcount]) / 2);
						this.center.splice(rowcount,1,this.maxval[rowcount] - this.radius[rowcount]);
						rowcount = rowcount + 1; 
					}
				}
				tempRow = null;
				count = count + 1;
			}
			tempRow = null;
		}
	
	}

}


Здесь я сначала определил массивы строк и колонок, это обязательная часть.
Потом некие характеристики матрицы — центр, радиус и максимальные(минимальные) значения всех координат, данный цикл возможно имеет смысл вынести в отдельный метод (функцию) или если они вам не нужны — убрать. «Центр» нам понадобится в дальнейшем при повороте матрицы.

Операции с матрицей

Вот сейчас и проявляется вся прелесть матрицы.

Перемещение

move: function(value,xyzw){
	this.column[xyzw] = this.column[xyzw].map(function(i){return i+value;})	
	this.updateByColumn();
}


При перемещении мы должны у каждой точки объекта изменить нужные координаты на необходимое значение. xyzw — индекс координаты, value — значение. То есть, если нам надо сместить объект вправо на 10 единиц, достаточно к матрице объекта применить следующий метод: move(10,0);
После преобразования мы обновляем всю матрицу — updateByColumn.

Перемещение к определенной точки

toPoint:function(point){
    if (point)
    {
        if(point.length == this.columnNumbers)
	{
	    this.rows = this.rows.map(function(rowArray)
                                            {
				                return rowArray.map(function(rowElement,index)
			                             {
						         return rowElement + point[index];
					             })
		                            });
            this.updateByRow();
	}		
	}
}


Это перемещение сразу по всем координатам. Очень полезный метод, в этой статье мы его будем использовать для поворота вокруг определенной точки, в следующей статье он нам также пригодиться при построении сложных, «составных» примитивов.


Поворот матрицы

	rotate:function(angle,point,xyzType){
		function multPointByValue(point,value){
			return point.map(function(val){return value * val});
		}
		this.toPoint(multPointByValue(point,-1));
		var rotateSource = [];
		var radians = angle * Math.PI / 180.0;

		
		switch(xyzType){
			case "byX":
				rotateSource = [1,0,0,
								0,Math.cos(radians),Math.sin(radians),
								0,-1 * Math.sin(radians),Math.cos(radians)
				];
				break;
			case "byY":
				rotateSource = [Math.cos(radians),0,-1 * Math.sin(radians),
								0,1,0,
								Math.sin(radians),0,Math.cos(radians)
								];
				break;
			case "byZ":
				rotateSource = [Math.cos(radians),Math.sin(radians),0,
								-1 * Math.sin(radians),Math.cos(radians),0,
								0,0,1];
				break;			

		}
		
		var rotateMatrix = new botuMatrix(rotateSource,3);
		this.rows = this.rows.map(function(irow){
			return vectorByMatrix(irow,rotateMatrix);
		});
		rotateMatrix = null;
		rotateSource = null;
		this.updateByRow();
		this.toPoint(point);
	}


Поворот вокруг определенной точки point на определенный угол angle, по определенной оси xyzType. Вначале перемещаю матрицу к той точки, вокруг которой будет вращение, потом формируем матрицу поворота в зависимости от оси xyzType, вокруг которой будет поворот. Разворачиваю каждую точку (строку) нашего объекта, после этого перемещаю развернутую матрицу в исходную точку.

Обновление матрицы

У нашей матрицы 3 основных переменных. Весь массив, массив точек-строк (rows), массив координат-колонок (columns). При изменении одного из этих массивов, другие массивы требуется обновить, для этого и используются 2 метода

updateByColumn:function(){
	var columnCount = 0;
	while(columnCount < this.columnNumbers)
	{
		var rowCount = 0;
		while(rowCount < this.rowNumbers)
		{
			this.rows[rowCount][columnCount] = this.column[columnCount][rowCount];
			this.source[columnCount + rowCount * this.columnNumbers] = this.column[columnCount][rowCount]; 
			rowCount++;
		}			
		columnCount++;
	}	
},

updateByRow:function(){
       var rowCount = 0;
	while(rowCount < this.rowNumbers)
	{
		var columnCount = 0;
		while(columnCount < this.columnNumbers)
		{
			this.column[columnCount][rowCount] = this.rows[rowCount][columnCount];
			this.source[columnCount + rowCount * this.columnNumbers] = this.column[columnCount][rowCount]; 
			columnCount++;
		}
		columnCount = null;	
		rowCount++;
	}	
	columnCount = null;
	rowCount = null;
},	


Функция vectorByMatrix — умножение вектора на матрицу, мы её использовали при повороте матрицы, вынесена за пределы класса матрицы, я данную функцию рассматривал как статическую. Если б мне пришлось отдельно делать класс для вектора, то данная функция была бы в прототипе вектора.

function vectorByMatrix(vector,matrix)
{
	//alert(vector);
	var resultVector = [];
	if (vector.length == matrix.rowNumbers)
	{
		var columnCount = 0;
		while(columnCount < matrix.columnNumbers){
			var rowCount = 0;
			var value = 0;
			while(rowCount < matrix.rowNumbers)
			{

				value += vector[rowCount] * matrix.column[columnCount][rowCount];	
				rowCount++;
			}
			//alert(value);
			resultVector.push(value);
			columnCount++;
		}
	}

	return resultVector;
}


Полный код:

/*умножение вектора vector на матрицу matrix. Возвращает вектор - resultVector*/
function vectorByMatrix(vector,matrix)
{
    var resultVector = [];
    if (vector.length == matrix.rowNumbers)
    {
        var columnCount = 0;
        while(columnCount < matrix.columnNumbers){
            var rowCount = 0;
            var value = 0;
            while(rowCount < matrix.rowNumbers)
            {

                value += vector[rowCount] * matrix.column[columnCount][rowCount];	
                rowCount++;
            }
            resultVector.push(value);
            columnCount++;
        }
    }

    return resultVector;
}

/*описание матрицы. Входящий параметр, массив source и кол-во колонок, на который данный массив требуется разбить - columns*/
function botuMatrix (source,columns)
{
    this.source = source;  // исходные данные
    this.columnNumbers = columns; // количество колонок. Размерность точки
    this.rowNumbers = source.length / columns; //количество строк. Количество точек
    this.rows = []; //массив строк. Массив точек. 
    this.minval = []; // точка с минимальными координатами по всем осям
    this.maxval = []; // точка с максимальными координатами по всем осям
    this.radius = []; // расстояние от центра до самой отдаленной точки
    this.center = []; // точка-центр.
    this.column = []; // массив колонок - координат. this.column[0] - это массив координат x по всем точкам.

    /*Если есть хотя бы одно значение в входящем векторе.*/    
    if (source.length > 0)
    {
        var count = 0;
	/*заполняем массив строк - rows и массив колонок column*/
        while(count < source.length)
        {
	    /*текущая строка*/
            var currentRow = this.source.slice(count,count + this.columnNumbers);		
            /*добавляем текущую строку в массив строк*/
            this.rows.push(currentRow);
            
            var columnCount = 0;
            /*формируем массив колонок, добавляя в каждую колонку - соответствующее значение координаты текущей строки.
           */
            while(columnCount <= this.columnNumbers)
            {
                /*Вначале инициализируем каждую колонку*/
                if (!this.column[columnCount]) 
                {
                    this.column[columnCount] = [];
                }
                /*Для каждой колонки добавляем значение из текущей строки.*/
                this.column[columnCount].push(currentRow[columnCount]);
                columnCount += 1;
            }
            /*переходим к следующей строке*/    
            count = count + this.columnNumbers; 
        }	
        this.rowNumbers = this.rows.length; 
        /*нахождение полезных переменных - центр, точка максимума, точка минимума*/        
        if (this.rows.length > 0)
        {
            count = 0;
            /*проходим по всем строкам - точкам*/
            while(count < this.rows.length)
            {
                /*Изначально все точки равняются первой попавшейся точки*/
                var tempRow = this.rows[count].slice(0);
                if (count == 0 )
                {
                    this.minval = tempRow.slice(0);
                    this.maxval = tempRow.slice(0);
                    this.radius = tempRow.slice(0);
                    this.center = tempRow.slice(0);
                }
            
                /*Затем после сравнения рассчитываем новые значение точек.*/
                if (count > 0)
                {
                    var rowcount = 0;
                    while(rowcount < tempRow.length)
                    {
                        this.minval.splice(rowcount,1,Math.min(this.minval[rowcount],tempRow[rowcount]));
                    
                        this.maxval.splice(rowcount,1,Math.max(this.maxval[rowcount],tempRow[rowcount]));
                        this.radius.splice(rowcount,1,(this.maxval[rowcount] - this.minval[rowcount]) / 2);
                        this.center.splice(rowcount,1,this.maxval[rowcount] - this.radius[rowcount]);
                        rowcount = rowcount + 1; 
                    }
                }
                /*На всякий случай удаляем ссылку на первую точку*/
                tempRow = undefined;
                count = count + 1;
            }
            /*очищаем память.*/ 
            tempRow = undefined;
        }
    
    }

}

/*Операции с матрицей*/
botuMatrix.prototype = {
    /*перемещаем матрицу*/
    move: function(value,xyzw){
        /*У всех точек изменяем значение соответствующих координат*/
        this.column[xyzw] = this.column[xyzw].map(function(i){return i+value;})	
        /*Обновляем матрицу по новому значению колонок*/
        this.updateByColumn();
    },
    /*обновление матрицы по обновленным колонкам*/
    updateByColumn:function(){
        var columnCount = 0;
        /*проходим по всем колонкам*/
        while(columnCount < this.columnNumbers)
        {
            var rowCount = 0;
         /*по каждой строке обновляем значение исходя из нового значения колонки, также обновляем значение у вектора*/
            while(rowCount < this.rowNumbers)
            {
                this.rows[rowCount][columnCount] = this.column[columnCount][rowCount];
                this.source[columnCount + rowCount * this.columnNumbers] = this.column[columnCount][rowCount]; 
                rowCount++;
            }
            
            columnCount++;
        }	
    },
    /*обновление матрицы по обновленным строкам*/
    updateByRow:function(){
        var rowCount = 0;
        /*проходим по каждой строке*/
        while(rowCount < this.rowNumbers)
        {
            var columnCount = 0;
/*по каждой колонке обновляем значение исходя из нового значения строки, также обновляем значение у вектора*/      
      while(columnCount < this.columnNumbers)
            {
                this.column[columnCount][rowCount] = this.rows[rowCount][columnCount];
                this.source[columnCount + rowCount * this.columnNumbers] = this.column[columnCount][rowCount]; 
                columnCount++;
            }
            columnCount = undefined;
            
            rowCount++;
        }	
        columnCount = undefined;
        rowCount = undefined;
},	
/*перемещение матрицы к определенной точки - point*/
    toPoint:function(point){
        if (point)
        {
            if(point.length == this.columnNumbers)
            {
      /*каждую строку-точку изменяем на значение из точки - point. */   
             this.rows = this.rows.map(function(rowArray){
                    return rowArray.map(function(rowElement,index)
                    {
                        return rowElement + point[index];
                    }
                    )
                });
/*Обновляем матрицу по новому значению строк*/				
                this.updateByRow();
            }		
        }
    },
       
    /*разворот матрицы this на угол angle, вокруг точки point по оси - xyzType*/    
    rotate:function(angle,point,xyzType){
		/*умножение вектора на число*/
        function multPointByValue(point,value){
            return point.map(function(val){return value * val});
        }
		/*смещаем матрицу так, чтобы точка point, вокруг которой идет разворот сместилась в точку [0,0,0]*/
        this.toPoint(multPointByValue(point,-1));
        //массив для матрицы разворота
		var rotateSource = [];
        // перевод градусы в радианы
		var radians = angle * Math.PI / 180.0;

        //выбор оси вокруг которой происходит разворот
        switch(xyzType){
            case "byX":
			    //формируем массив для матрицы разворота, вокруг оси X
                rotateSource = [1,0,0,
                                0,Math.cos(radians),Math.sin(radians),
                                0,-1 * Math.sin(radians),Math.cos(radians)
                ];
                break;
            case "byY":
			//формируем массив для матрицы разворота, вокруг оси y
                rotateSource = [Math.cos(radians),0,-1 * Math.sin(radians),
                                0,1,0,
                                Math.sin(radians),0,Math.cos(radians)
                                ];
                break;
            case "byZ":
			//формируем массив для матрицы разворота, вокруг оси Z
                rotateSource = [Math.cos(radians),Math.sin(radians),0,
                                -1 * Math.sin(radians),Math.cos(radians),0,
                                0,0,1];
                break;			

        }
        
		//создаем матрицу разворота
        var rotateMatrix = new botuMatrix(rotateSource,3);
        //разворачиваем текущую матрицу, умножив каждую точку - строки rows[i] на матрицу разворота
		this.rows = this.rows.map(function(irow){
            return vectorByMatrix(irow,rotateMatrix);
        });
		//обнуляем матрицу разворота и массив для матрицы разворота. Данные операции автомат. произведутся сборщиком мусора. На всякий случай очищаем явно.
        rotateMatrix = null;
		//обнуляем массив для матрицы разворота
        rotateSource = null;
		/*обновление матрицы по обновленным строкам-точкам, которые были развернуты*/
        this.updateByRow();
		//перемещение матрицы в исходное положение.
        this.toPoint(point);
    }

    
}


Заключение

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

В следующей статье будут рассмотрены примитивы — куб, шар, плоская поверхность и так далее.
Tags:
Hubs:
Total votes 18: ↑10 and ↓8+2
Comments9

Articles