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
следующая статья на эту тему (если будет) обойдется одним кодом с комментариями. Так проще :-)
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.