Stage3d и Крутящийся Кубик

Немного поразбирался с новым 3d API из Flash Player 11 (то, что раньше называлось Molehill).

Все буду писать во FlashDevelop. Специально обновил версию до последней на текущий момент 4.0.0 RTM. Чего и всем желаю. В предыдущей версии, кроме выбора Flash Player 11 в опциях, надо было еще добавлять дополнительный параметр компилятора -swf-version=13. А в еще более предыдущих поддержки 11-ого флешплеера вообще не было.

Для компиляции примеров недостаточно SDK, нужна еще библиотека отсюда. Она маленькая, а нужен из нее всего один класс (AGAL mini assembler).

Документация на сайте Adobe по умолчанию не показывает классы Stage3D, Context3D и прочие, а заодно и их методы, пока не поменяешь фильтр с Flash Player 10.1 на Flash Player 11.1 (сверху окошка).

Сразу дам ссылку на отличную статью про написание шейдеров на AGAL: Пишем шейдер на AGAL.

Начать предлагаю с простого и короткого примера, который выведет на экран треугольник.
package
{
	import flash.display.*;
	import flash.display3D.*;
	import flash.events.*;
	import flash.geom.*;
	import flash.utils.*;
	import com.adobe.utils.*;
	
	[Frame(factoryClass="Preloader")]
	[SWF(width=640,height=480,backgroundColor="#FFFFFF")]
	public class Main extends Sprite
	{
		private var stage3D:Stage3D;
		private var ctx3d:Context3D;
		private var indices:IndexBuffer3D;
		
		private var asm:AGALMiniAssembler = new AGALMiniAssembler();
		
		public function Main()
		{
			if (stage)
				addedToStage(null)
			else
				addEventListener(Event.ADDED_TO_STAGE, addedToStage)
		}
		
		private function addedToStage(e:Event):void
		{
			removeEventListener(Event.ADDED_TO_STAGE, addedToStage);
			
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			
			stage3D = this.stage.stage3Ds[0];
			
			stage3D.addEventListener(Event.CONTEXT3D_CREATE, stage3D_context3dCreate);
			stage3D.addEventListener(ErrorEvent.ERROR, function(e:Event):void { trace(e); });
			stage3D.requestContext3D(Context3DRenderMode.AUTO);
		}
		
		private function stage3D_context3dCreate(e:Event):void
		{
			ctx3d = stage3D.context3D;
			trace("Driver: " + ctx3d.driverInfo);
			
			ctx3d.enableErrorChecking = true;
			ctx3d.configureBackBuffer(600, 400, 0, true);
			
			var vertices:VertexBuffer3D = ctx3d.createVertexBuffer(3, 3);
			vertices.uploadFromVector(new <Number>[0, 0, 0, 1, 0, 0, 0, 1, 0], 0, 3);
			ctx3d.setVertexBufferAt(0, vertices, 0, Context3DVertexBufferFormat.FLOAT_3);
			
			indices = ctx3d.createIndexBuffer(3);
			indices.uploadFromVector(new <uint>[0, 1, 2], 0, 3);
			
			asm.assemble(Context3DProgramType.VERTEX, "mov op, va0");
			var vertexCode:ByteArray = asm.agalcode;
			asm.assemble(Context3DProgramType.FRAGMENT, "mov oc, fc0");
			var fragmentCode:ByteArray = asm.agalcode;
			var program:Program3D = ctx3d.createProgram();
			program.upload(vertexCode, fragmentCode);
			ctx3d.setProgram(program);
			
			ctx3d.setProgramConstantsFromVector(Context3DProgramType.FRAGMENT, 0, new <Number>[1, 1, 1, 1]);
			
			addEventListener(Event.ENTER_FRAME, enterFrame);
		}
		
		private function enterFrame(e:Event):void
		{
			ctx3d.clear(0, 0, 0);
			ctx3d.drawTriangles(indices);
			ctx3d.present();
		}
	}
}


Разберемся что же он делает. Всяческую инициализацию пропускаю, думаю, тут всё понятно из кода.

ctx3d.configureBackBuffer(600, 400, 0, true);
600 и 400 — это размер выводимого изображения. Я специально сделал его размером меньше самой флешки, чтобы продемонстрировать это. Также можно менять stage3D.x и stage3D.y, чтобы двигать изображение по экрану. 0 — это уровень антиалисинга, попробуйте поставить его в 4 и присмотреться к выводимому изображению. А true — это включение буфера глубины и stencil-буфера, не буду пока в это вдаваться.

var vertices:VertexBuffer3D = ctx3d.createVertexBuffer(3, 3);
vertices.uploadFromVector(new <Number>[0, 0, 0, 1, 0, 0, 0, 1, 0], 0, 3);
ctx3d.setVertexBufferAt(0, vertices, 0, Context3DVertexBufferFormat.FLOAT_3);
Здесь создается вершинный буфер из трех вершин, в котором для каждой вершину хранится три числа (в нашем случае x, y и z). Т.е. у нас получились вершины {0,0,0}, {1,0,0} и {0,1,0}. Кроме того устанавливается, что эти три числа будут соответствовать переменной va0 в вершинном шейдере.

private var indices:IndexBuffer3D;
...
indices = ctx3d.createIndexBuffer(3);
indices.uploadFromVector(new <uint>[0, 1, 2], 0, 3);
А это создается индексный буфер, в нем хранятся номера (индексы) вершин в вершинном буфере. Каждые три индекса в этом буфере задают треугольник.

ctx3d.clear(0, 0, 0);
ctx3d.drawTriangles(indices);
ctx3d.present();
Тут, думаю, всё понятно: очистили экран, нарисовали заранее подготовленные треугольники, и вывели нарисованное на экран.

Еще из этого примера можно заметить, что координаты X и Y, независимо от размера окна, меняются от -1 до 1, также можно чуть-чуть поэкспериментировать и убедиться, что точки с координатой Z больше 1 или меньше 0 на экран не выводятся. Также можно понять, что оси направлены так: X — вправо, а Y — вверх. Куда направлена Z пока непонятно, но будем считать, что от нас.

Внесем небольшие изменения, чтобы двигаться дальше. Сделаем так, чтобы размер окна вывода соответствовал размеру окна флешки. Поменяем вызов configureBackBuffer на
stage.addEventListener(Event.RESIZE, stage_resize);
stage_resize(null);
...
private function stage_resize(e:Event):void 
{
	var w:int = Math.max(100, Math.min(2000, stage.stageWidth));
	var h:int = Math.max(100, Math.min(2000, stage.stageHeight));
	ctx3d.configureBackBuffer(w, h, 0, true);			
}


Теперь добавим матрицу проекции. (Все знают что такое матрица? А что такое проекция?) Чтобы не забивать себе голову, воспользуемся любезно предоставленным Adobe (в той же библиотеке, что и AGAL mini assembler) классом PerspectiveMatrix3D
private var projectionMatrix:PerspectiveMatrix3D = new PerspectiveMatrix3D;
...
asm.assemble(Context3DProgramType.VERTEX, "m44 op, va0, vc0");//поменяем вершинный шейдер
...
projectionMatrix.perspectiveLH(320, 320 * h / w, 10, 1000);//это в stage_resize
...
ctx3d.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, projectionMatrix, true);//а это в enterFrame
Итак мы поменяли вершинный шейдер так, что он умножает переданный ему вектор на матрицу записанную в регистр vc0, и поместили в этот регистр матрицу. Матрица с заданными параметрами делает преобразование координат такое, что объект шириной 320 будет размером ровно с ширину нашего окна, если он будет на расстоянии 10 от камеры, а на расстоянии 1000 — он будет в сто (1000/10) раз меньше, при этом объекты ближе 10 и дальше 1000 выводиться на экран не будут.

Поменяем наш треугольник на что-то более объемное, и заодно добавим ему цвета. Пусть это будет кубик с разноцветными гранями. Раз грани разноцветные, нам придется задать цвет для каждой вершины (или делать несколько вызовов drawTriangles меняя между ними константу, определяющую цвет, но так делать не следует.) Я был несколько ленив, чтобы вручную заполнять эти координаты поэтому написал немного кода, который заполнил вершинный и индексные шейдеры за меня, а я только задал координаты для одной стороны кубика и углы поворота. В вершинный буфер мы теперь записываем по 6 чисел на каждую вершину: первые 3 числа — координата, следующие 3 числа — цвет. Я сделал так, чтобы у вершин одной грани были немножко разные цвета.
var verticesVector:Vector.<Number> = new Vector.<Number>;
var indicesVector:Vector.<uint> = new Vector.<uint>;
const numbersPerVertex:int = 6;
var cubeSize:Number = 40;
var colors:Array = [0xFF0000, 0x00FF00, 0x0000FF, 0xFF8000, 0xFFFF00, 0x00FFFF];
var rotations:Array = [0, 90, 180, 270, 90, 270];
var rotAxis:Array = [Vector3D.Y_AXIS, Vector3D.Y_AXIS, Vector3D.Y_AXIS, Vector3D.Y_AXIS, Vector3D.X_AXIS, Vector3D.X_AXIS];
var planeVertices:Array = [new Vector3D(-cubeSize, -cubeSize, cubeSize),
	new Vector3D(cubeSize, -cubeSize, cubeSize),
	new Vector3D(cubeSize, cubeSize, cubeSize),
	new Vector3D( -cubeSize, cubeSize, cubeSize)];
var planeIndices:Array = [0, 1, 2, 0, 2, 3];
var mat:Matrix3D = new Matrix3D();
for (var i:int = 0; i < 6; ++i)
{
	var startIndex:int = verticesVector.length / numbersPerVertex;
	
	var red:Number = ((colors[i] >> 16) & 0xFF) / 255.0;
	var green :Number = ((colors[i] >> 8) & 0xFF) / 255.0;
	var blue:Number = (colors[i] & 0xFF) / 255.0;
	
	mat.identity();
	mat.appendRotation(rotations[i], rotAxis[i]);
	for each(var vec:Vector3D in planeVertices)
	{
		var transformedVec:Vector3D = mat.transformVector(vec);
		verticesVector.push(transformedVec.x, transformedVec.y, transformedVec.z, red, green, blue);
		red += 0.3;
		green += 0.3;
		blue += 0.3;
	}
	
	for each(var index:uint in planeIndices)
		indicesVector.push(index + startIndex);

}			
var vertices:VertexBuffer3D = ctx3d.createVertexBuffer(verticesVector.length / numbersPerVertex, numbersPerVertex);
vertices.uploadFromVector(verticesVector, 0, verticesVector.length / numbersPerVertex);
ctx3d.setVertexBufferAt(0, vertices, 0, Context3DVertexBufferFormat.FLOAT_3); //va0 = position
ctx3d.setVertexBufferAt(1, vertices, 3, Context3DVertexBufferFormat.FLOAT_3); //va1 = color
	
indices = ctx3d.createIndexBuffer(indicesVector.length);
indices.uploadFromVector(indicesVector, 0, indicesVector.length);
Кроме того надо поменять вершинный и фрагментный шейдеры, чтобы они брали цвет из вершины.
asm.assemble(Context3DProgramType.VERTEX, 
	"m44 op, va0, vc0\n" +
	"mov v0, va1"
);
var vertexCode:ByteArray = asm.agalcode;
asm.assemble(Context3DProgramType.FRAGMENT, "mov oc, v0");
var fragmentCode:ByteArray = asm.agalcode;
Если теперь запустить программу, она выведет на экран переднюю грань нашего кубика. Можно увидеть, что значения цвета заданные в вершинах между вершинами плавно перетекают друг в друга.

Но мы не видим никаких других частей нашего кубика. Надо заставить его вращаться. Для этого введем еще одну матрицу: матрицу задающую положение кубика.
private var modelMatrix:Matrix3D = new Matrix3D;
...
modelMatrix.appendRotation(1, Vector3D.Y_AXIS);
modelMatrix.appendRotation(10, Vector3D.Y_AXIS);

var mat:Matrix3D = modelMatrix.clone();
mat.append(projectionMatrix);
ctx3d.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, mat, true);
Если запустить программу сейчас, увидим какую-то ерунду — это из-за того, что мы периодически оказываемся внутри кубика. Чтобы увидеть кубик со стороны, зададим положение камеры. Для этого добавим еще и матрицу вида.
private var viewMatrix:Matrix3D = new Matrix3D;
...
viewMatrix.appendTranslation(0, 0, cubeSize * 3);
...
var mat:Matrix3D = modelMatrix.clone();
mat.append(viewMatrix);
mat.append(projectionMatrix);
Теперь кубик вращается как надо. Впрочем видно, что желательно подправить параметры проекции и положение кубика. Но это уже не важно…

Куда двигаться дальше? Зависит от целей. Я бы, может быть, попробовал использовать готовые движки, хоть это и не спортивно :-)

Вот окончательный код:
package
{
	import flash.display.*;
	import flash.display3D.*;
	import flash.events.*;
	import flash.geom.*;
	import flash.utils.*;
	import com.adobe.utils.*;
	
	[Frame(factoryClass="Preloader")]
	[SWF(width=640,height=480,backgroundColor="#FFFFFF")]
	public class Main extends Sprite
	{
		// stage3D - это поверхность, на которой рисуются всякие трехмерные штуки
		// Можно ее двигать, меняя stage3D.x и stage3D.y. Размер поверхности задается отдельно (см. stage_resize)
		private var stage3D:Stage3D;
		
		// context3D - (для краткости ctx3d) это просто ссылка на переменную stage3D.context3D.
		// т.к. непосредственно рисование осуществляется именно через нее, удобно иметь короткий способ к ней обратится
		private var ctx3d:Context3D;
		
		// индексный буфер. См. комментарии в коде. Вынесен в переменную класса, т.к. нужен на каждом шаге рисования
		private var indices:IndexBuffer3D;
		
		// Матрицы. Они вначале перемножаются друг на друга, а потом передаются в шейдер, который перемножает все вершины на получившуюся матрицу.
		
		// Матрица проекции задает проекцию :-)
		private var projectionMatrix:PerspectiveMatrix3D = new PerspectiveMatrix3D;
		
		// Матрица модели задает положение модели
		private var modelMatrix:Matrix3D = new Matrix3D;
		
		// Матрица вида задает положение камеры.
		// В том смысле, что она должна всё смещать так, чтобы камера оказалась в точке 0,0,0 и смотрела вдоль оси Z в сторону увеличения.
		private var viewMatrix:Matrix3D = new Matrix3D;
		
		// Конструктор класса. Всё стандартно.
		public function Main()
		{
			// Если stage уже есть, значит идем дальше, иначе ждем пока нас добавят на stage.
			if (stage)
				addedToStage(null)
			else
				addEventListener(Event.ADDED_TO_STAGE, addedToStage)
		}
		
		// Нас добавили на stage, можно двигаться дальше
		private function addedToStage(e:Event):void
		{
			// Очистим мусор
			removeEventListener(Event.ADDED_TO_STAGE, addedToStage);
			
			// Сделаем так, чтобы изменение размеров окна не вызывало масштабирования
			stage.scaleMode = StageScaleMode.NO_SCALE;
			stage.align = StageAlign.TOP_LEFT;
			
			// получим stage3D. Вообще их много, т.е. можно иметь много трехмерных картинок одновременно, но мы просто возьмем первый
			stage3D = this.stage.stage3Ds[0];
			
			// попробуем получить context3D, этот процесс может вызвать ошибки. Если они возникли просто сделаем trace, рисовать мы уже не сможем.
			// да и в дальнейшем при возниковении ошибок могут кидаться исключения...
			stage3D.addEventListener(Event.CONTEXT3D_CREATE, stage3D_context3dCreate);
			stage3D.addEventListener(ErrorEvent.ERROR, function(e:Event):void { trace(e); });
			stage3D.requestContext3D(Context3DRenderMode.AUTO);
		}
		
		// Получили context3D. Ура! Запускаемся дальше.
		private function stage3D_context3dCreate(e:Event):void
		{
			// сохраним для краткости context3D
			ctx3d = stage3D.context3D;
			
			// Трейсанем интересную инфу :-)
			trace("Driver: " + ctx3d.driverInfo);
			
			// Включим проверку ошибок. 
			// TODO не забыть выключить в релизе :-)
			ctx3d.enableErrorChecking = true;
			
			// Добавим обработчик изменения размера окна флешплеера. И сразу вызовем его, чтобы он все проинициализировал.
			stage.addEventListener(Event.RESIZE, stage_resize);
			stage_resize(null);
			
			// Этот кусок кода заполняет verticesVector и indicesVector вершинами и индексами для рисования кубика
			// Я решил что проще задать одну грань и потом програмно ее поворочивать, получив таким образом шесть граней
			var verticesVector:Vector.<Number> = new Vector.<Number>;
			var indicesVector:Vector.<uint> = new Vector.<uint>;
			// количество чисел на одну вершину: три координаты XYZ и три компонента цвета RGB
			const numbersPerVertex:int = 6; 
			// характерный размер кубика: половина длины ребра :-)
			var cubeSize:Number = 40; 
			// цвета граней
			var colors:Array = [0xFF0000, 0x00FF00, 0x0000FF, 0xFF8000, 0xFFFF00, 0x00FFFF]; 
			// углы поворота граней
			var rotations:Array = [0, 90, 180, 270, 90, 270]; 
			// оси, вокруг которых крутить грань
			var rotAxis:Array = [Vector3D.Y_AXIS, Vector3D.Y_AXIS, Vector3D.Y_AXIS, Vector3D.Y_AXIS, Vector3D.X_AXIS, Vector3D.X_AXIS]; 
			// четыре вершины задающие грань
			var planeVertices:Array = [new Vector3D(-cubeSize, -cubeSize, cubeSize),
				new Vector3D(cubeSize, -cubeSize, cubeSize),
				new Vector3D(cubeSize, cubeSize, cubeSize),
				new Vector3D( -cubeSize, cubeSize, cubeSize)]; 
			// список треугольников. Здесь два треугольника: из вершин с индексами 1, 2 и 3 в предыдущем массиве и из вершин с индексами 0, 2 и 3 
			var planeIndices:Array = [0, 1, 2, 0, 2, 3]; 
			// матрица, которую мы будем использовать для поворота нашей грани
			var mat:Matrix3D = new Matrix3D();
			for (var i:int = 0; i < 6; ++i)
			{
				// количество уже добавленных вершин в verticesVector
				var startIndex:int = verticesVector.length / numbersPerVertex;
				
				// компоненты цвета. Глупо, надо было их сразу числами задать.
				var red:Number = ((colors[i] >> 16) & 0xFF) / 255.0;
				var green :Number = ((colors[i] >> 8) & 0xFF) / 255.0;
				var blue:Number = (colors[i] & 0xFF) / 255.0;
				
				// сформируем матрицу поворота
				mat.identity();
				mat.appendRotation(rotations[i], rotAxis[i]);
				
				for each(var vec:Vector3D in planeVertices)
				{
					// умножаем вектор на матрицу
					var transformedVec:Vector3D = mat.transformVector(vec);
					// и записываем вершину и ее цвет в verticesVector
					verticesVector.push(transformedVec.x, transformedVec.y, transformedVec.z, red, green, blue);
					// немного изменям цвет, чтобы он чуть отличался у разных вершин одной грани для демонстрации интерполяции цветов
					red += 0.3;
					green += 0.3;
					blue += 0.3;
				}
				
				// записываем индексы в indicesVector. 
				// корректируем их, т.к. мы их должны указать на только что добавленные вершины, а они могут быть и не первыми в списке
				for each(var index:uint in planeIndices)
					indicesVector.push(index + startIndex);
			}
			// закончили заносить данные для кубика в verticesVector и indicesVector
			
			// создадим вершинный буфер и загрузим в него данные из verticesVector
			var vertices:VertexBuffer3D = ctx3d.createVertexBuffer(verticesVector.length / numbersPerVertex // количество вершин
				, numbersPerVertex); // количество чисел на вершину
			vertices.uploadFromVector(verticesVector, 0, verticesVector.length / numbersPerVertex);
			// установим какие вершинные шейеры будут соответствовать каким регистрам
			// va0 = position, а va1 = color
			ctx3d.setVertexBufferAt(0 // номер регистра, в данном случае 0, т.е. va0
				, vertices // вершинный буфер типа VertexBuffer3D
				, 0 // начиная с какого числа брать числа из данных вершины. В нашем случае данные вершины это [X, Y, Z, R, G, B]. Указываем 0 и берем [X, Y, Z]
				, Context3DVertexBufferFormat.FLOAT_3); // формат, определяет сколько и чего мы записали в вершинный буфер
			ctx3d.setVertexBufferAt(1, vertices, 3, Context3DVertexBufferFormat.FLOAT_3);  // здесь все аналогично, но регистр va1, а данные [R, G, B]
			
			// создадим индексный буфер и загрузим в него данные из indicesVector
			indices = ctx3d.createIndexBuffer(indicesVector.length);
			indices.uploadFromVector(indicesVector, 0, indicesVector.length);
			
			// откомпилируем шейдеры (вершинный и фрагментный), создадим программу (считай комбинацию двух шейдеров) и установим ее для использования при рендеринге
			// AGALMiniAssembler надо брать тут: https://github.com/flashplatformsdk/Flash-Platform-SDK
			// Про то, как писать шейдеры читать например тут: http://habrahabr.ru/blogs/Flash_Platform/130454/
			var asm:AGALMiniAssembler = new AGALMiniAssembler();
			// Программа вершинного шейдера вызывается для каждой вершины каждого треугольника.
			asm.assemble(Context3DProgramType.VERTEX, 
				"m44 op, va0, vc0\n" + // умножим вектор из va0 (данные вершины) на матрицу из vc0 (константа) и передадим результат дальше 
				"mov v0, va1" // также передадим дальше цвет
				);
			var vertexCode:ByteArray = asm.agalcode;
			// Программа фрагментного шейдера вызывается для каждого пикселя каждого треугольника
			// в нее передаются значения из вершинного шейдера в регистрах с названиями v0, v1 и т.д.
			// но поскольку вершин у трегольника всего три, а пикселей много, то эти значения равномерно размазываются по треугольнику (интерполируются)
			asm.assemble(Context3DProgramType.FRAGMENT, "mov oc, v0"); // просто передадим на выход полученный цвет
			var fragmentCode:ByteArray = asm.agalcode;
			// создадим программу
			var program:Program3D = ctx3d.createProgram(); 
			// и загрузим в нее откомпилированные коды шейдеров
			program.upload(vertexCode, fragmentCode);
			// установим программу в качестве текущей
			// в принципе можно иметь несколько программ и менять их, но у нас она одна
			ctx3d.setProgram(program);
			
			// Зададим матрицу вида, т.е. положение камеры.
			// Такая матрица переместит все предметы на 3 cubeSize "вперед", т.е. как бы камера переместится на 3 cubeSize "назад"
			viewMatrix.appendTranslation(0, 0, cubeSize * 3);
			
			// ЧТО ЭТО???
			addEventListener(Event.ENTER_FRAME, enterFrame);
		}
		
		// Обработчик изменения размера окна флешплеера, также вызывается при запуске программы
		private function stage_resize(e:Event):void 
		{
			// определим размеры нашего будущего окна
			// размер видимой области Stage3D не может быть меньше 50 пикселей по ширине или высоте (я взял 100, все равно 100х100 - очень мало)
			// и на всякий случай, я ограничил эти значения еще и сверху
			var w:int = Math.max(100, Math.min(2000, stage.stageWidth));
			var h:int = Math.max(100, Math.min(2000, stage.stageHeight));
			
			// эта функция задает размер окна в пикселях, в которое выводится изображение
			ctx3d.configureBackBuffer(w, h // размеh окна в пикселях
				, 0 // уровень антиалисинга. чем больше, тем тормознее, но красивее
				, true); // включать ли буфер глубины и stencil-буфер. Конечно, ДА!
				
			// меняем матрицу проекции, т.к. она должна учитывать соотношения сторон окна
			// такая матрица проекции означает, что объект размером 320 на расстоянии 10 будет размером с экран, и чем дальше, тем он будет меньше
			// причем объекты ближе 10 или дальше 1000 не будут выводится.
			// это не очень удачная матрица проекции :-)
			projectionMatrix.perspectiveLH(320, 320 * h / w, 10, 1000);
		}
		
		// вызывается каждый кадр
		private function enterFrame(e:Event):void
		{
			// крутим кубик
			modelMatrix.appendRotation(3, Vector3D.X_AXIS);
			modelMatrix.appendRotation(5, Vector3D.Y_AXIS);
			
			// формируем матрицу трансформации для шейдера, перемножая матрицы модели, вида и проекции
			// если моделей много, то надо это делать для каждой модели
			var mat:Matrix3D = modelMatrix.clone();
			mat.append(viewMatrix);
			mat.append(projectionMatrix);
			// записываем получившуюся матрицу в регистр vc0
			// кстати, по идее при смене шейдерной программы, надо бы почистить все ее регистры на всякий случай
			ctx3d.setProgramConstantsFromMatrix(Context3DProgramType.VERTEX, 0, mat, true);
			
			// очистка экрана
			ctx3d.clear(0, 0, 0);
			
			// отрисовка с помощью текущей шейдерной программы и заданных вершинных буферов и констант треугольников, заданных переданным индексным буфером
			ctx3d.drawTriangles(indices);
			
			// вывод на экран картинки
			ctx3d.present();
		}
	}
}


А вот ссылка на проект для FlashDevelop: скачать.
  • +6

Комментарии (21)

0
А конвертер моделей из 3Dmax во Flash, уже есть?
0
В 3D движках (Альтернатива к примеру) есть парсеры 3D моделей.
+1
Блин, вот и как на этом AGAL можно POM или SSAO написать?
Если меня от 2ух строчек в дрожь бросает.
0
та же фигня)
+1
Если бросает в дрожь, то надо просто себя преодолеть и разобраться, это чисто психологическая реакция. Страх неизвестности.
На AGAL оба эффекта не реализуемы, хотя за POM не скажу точно, не слишком себе представляю, как он работает.
0
Ну с чего бы это?
SSAO бегает уже давно :)
0
Где бегает? Возможно я ошибаюсь и всё возможно.
Насколько я понимаю, для того чтобы сделать SSAO надо уметь читать z-buffer, а flash этого вроде как не позволяет.
0
У меня бегает :)
Глубину надо писать в текстуру, а потом читать во втором проходе.
Так же и тени делаются, и все остальное.
0
Да, мог бы и сам догадаться…

А раз уж ты (сам реши, а я не нашел как тебя лучше умаслить), есть какие-то работающие техники для вывода прозрачных полигонов без сортировки. Я слышал про вариант с двойным буфером глубины, когда на каждом проходе рисуется то, что лежит дальше уже нарисованного, т.е. рендеринг многопроходный и от предыдущего прохода нужно отдельно сохранить zbuffer. Если использовать твою технику, то количество проходов увеличивается вдвое (отдельно сохрани z-buffer, отдельно картинку) Пихать z-buffer в альфу не очень хорошо, т.к. она нужна, поскольку полигоны-то прозрачные.
0
Нет, с прозрачностью так не получится. Двойной буфер даст тебе тебе только два слоя без сортировки, а потом дальше сортировать.
Можно сделать что-то вроде дизера, то есть для каждого пиксела писать 4 глубины в текстуру в два раза большую по размеру.
Но все равно, слоев слишком мало.
Сортировать проще.
0
Многопроходно же, сколько проходов — столько уровней. Копируем z-buffer от предыдущего прохода и кроме z-теста делаем еще и тест со значением из скопированного буфера, причем z должен оказаться не меньше, как обычно, а больше. Еще одна проблема тут в том, что получается обратный порядок рисования: от ближних к дальним. Но можно наверное и поменять z-тесты наоборот, тогда порядок правильный окажется (но очевидно проблема с количеством проходов), или блендингом это дело поправить. Не могу найти ссылки, где я это читал, давно было.

Сортировать очень не хочется, нудно это :-)

Другой вариант менять блендинг на такой, который не зависит от порядка. Тоже не все так просто.
0
Нашел ссылку, не ту, но про то же: www.slideshare.net/acbess/order-independent-transparency-presentation
+2
Здорово! Вообще, я конечно приверженец того что надо брать готовый фреймворк и работать с ним, но и с другой стороны, знать врага в лицо важная и посильная задача :).
0
Все это вполне посильно простому человеку, не квантовая механика. Но при условии, что уже есть готовые движки, написанные и оттестированные, заниматься этим не слишком продуктивно. Но интересно же. Я теперь примерно знаю, каково это писать шейдеры.
+1
Огромная просьба к автору: понапиши комментариев по коду, побольше, по всем ключевым моментам и по отдельным строкам. Это невероятно поможет разбирать код. Особенно тем, у кого паника =)
Спасибо за статью! Плюсую.
0
Вроде ж статья так и сделана? Все ключевые строчки прокомментированы. Или что имеется ввиду? Прямо в код это вписать в виде комментариев?
0
Прямо в код это вписать в виде комментариев?
Да. Я когда код читаю, по статье не бегаю — неудобно. Было бы здорово иметь каменты «на местах», и более подробные.
0
Ок. Будет.
+1
Уже есть.
+1
Спасибо, то что нужно!
+1
следующая статья на эту тему (если будет) обойдется одним кодом с комментариями. Так проще :-)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.