Использование Flash 3D API

На этой странице будет рассказано, как последнее нововведение Flash-платформы использовать в Haxe.

Установка

Скачайте следующее:

  • Flash Player 11 "Incubator" Public Beta c Adobe Labs
  • Обновите Haxe до версии 2.07 Release
  • Обновите Haxe из SVN до ревизии r3817 и выше, скачав автоматическую сборку и переписав ее файлами установленную версию Haxe (к примеру C:/Motion-Twin/haxe в Windows)
  • Скачайте и распакуйте примеры Haxe molehill.zip
  • Установите библиотеку format из haxelib:
    haxelib install format

Пример 0 - Куб

Мы начнем с примера маленького цветного кубика.

Можно посмотреть этот пример, выполнив в консоли haxe test.hxml или просто открыв готовую test.swf в браузере.
Вам понадобится установленный Flash Player 11!

molehill_1.png

Давайте рассмотрим на Test.hx подробнее :

В конструкторе (new), мы создаем новую 3D сцену (stage), которая станет местом, где будет происходить рендер:

var s : flash.display.Stage3D;
//...
var stage = flash.Lib.current.stage;
s = stage.stage3Ds[0];
s.viewPort = new flash.geom.Rectangle(0, 0, 
            stage.stageWidth,stage.stageHeight);
s.addEventListener( flash.events.Event.CONTEXT3D_CREATE, onReady );
s.requestContext3D();

Когда 3D контекст готов, мы настраиваем его, затем создаем новый шейдер (Shader), камеру (Camera), а также данные полигонов трехмерного кубика:

var c : flash.display3D.Context3D;
var shader : Shader;
var pol : Polygon;
var camera : Camera;
// ...
function onReady( _ ) {
    c = s.context3D;
    c.enableErrorChecking = true;
    var w = Std.int(s.viewPort.width);
    var h = Std.int(s.viewPort.height);
    c.configureBackBuffer( w, h, 0, true );

    shader = new Shader(c);
    camera = new Camera();

    pol = new Cube();
    pol.alloc(c);
}

Затем, в методе update, мы задаем свойства контекста:

c.clear(0, 0, 0, 1);
c.setDepthTest( true, flash.display3D.Context3DCompareMode.LESS_EQUAL );
c.setCulling(flash.display3D.Context3DTriangleFace.BACK);

Проверяем состояние клавиш (стрелок и +/- цифровой клавиатуры) и двигаем камеру:

if( keys[K.UP] )
    camera.moveAxis(0,-0.1);
if( keys[K.DOWN] )
    camera.moveAxis(0,0.1);
if( keys[K.LEFT] )
    camera.moveAxis(-0.1,0);
if( keys[K.RIGHT] )
    camera.moveAxis(0.1, 0);
if( keys[109] )
    camera.zoom /= 1.05;
if( keys[107] )
    camera.zoom *= 1.05;
camera.update();

После этого, мы можем обратиться к матрице проекции камеры и создать матрицу трансформации для нашего кубика, вращающегося вокруг собственной оси Z:

var project = camera.m.toMatrix();
var mpos = new flash.geom.Matrix3D();
mpos.appendRotation(t * 10, flash.geom.Vector3D.Z_AXIS);

Осталось настроить шейдер, нарисовать полигон, потом отобразить 3D контекст на экране:

shader.init(
    { mpos : mpos, mproj : project },
    {}
);
shader.bind(pol.vbuf);
c.drawTriangles(pol.ibuf);
c.present();

Готово! У нас на экране вращающийся куб.
Обратите внимание, что цвета в шейдере заданы как позиция на кубе, поэтому переходят от черного (0,0,0) к белому (1,1,1).

Шейдеры

Мы не рассказали про то, как, собственно, кубик трансформируется и раскрашивается. Это делается шейдером. В отличие от AS3, где вы пишете шейдеры на ассемблере, в Haxe шейдеры используют HxSL - язык шейдеров Haxe и написаны на самом Haxe, в чем вы можете убедиться, посмотрев в начало Test.hx:

@:shader({
    var input : {
        pos : Float3,
    };
    var color : Float3;
    function vertex( mpos : M44, mproj : M44 ) {
        out = pos.xyzw * mpos * mproj;
        color = pos;
    }
    function fragment() {
        out = color.xyzw;
    }
}) class Shader extends format.hxsl.Shader {
}

Секция @:shader содержит Haxe метаданные, которые компилируются одновременно с проектом (используя макросы).

Подробная документация по шейдерам находится в документации по hxsl.

Пример 1 - Освещение

В этом примере, мы подсветим наш 3D кубик с помощью простого алгоритма Гуро. Для этого нужно добавить нормали к данным о геометрии кубика, мы сделаем это автоматически:

pol.addNormals();

Нормали будут рассчитаны для каждой вершины. Так как все точки куба являются общими для поверхностей (faces) треугольников, нормали получатся гладкими.

Теперь, когда у нас есть нормали, создадим вращающийся источник освещения и передадим его в параметры шейдера :

var light = new flash.geom.Vector3D(
    Math.cos(t * 10) * 1,
    Math.sin(t * 5) * 2,
    Math.sin(t) * Math.cos(t) * 2
);
light.normalize();
shader.init(
    { mpos : mpos, mproj : project, light : light },
    {}
);

Готово. Теперь нам надо просто изменить код шейдера. Чтобы посчитать силу света, нужно взять скалярное произведение трансформированной нормали на направление света. Убедимся, что это число положительно и нормируем его в пределах 0-1, чтобы получить цвет вершины:

var tnorm = (norm * mpos).normalize();
var lpow = light.dot(tnorm).max(0);
color = pos * lpow;

molehill_2.png

Хочется более резких границ? Можно сделать нормали перпендикулярными, сделав уникальную нормаль для каждой из вершин. Добавьте следующую строчку перед расчетом нормалей:

pol.unindex();

Пример 2 - Текстурирование

В этом примере мы добавим текстуру. Для этого нам понадобятся текстурные координаты в данных геометрии кубика:

pol.addTCoords();

Обратите внимание, что расчет текстурных координаты требует, чтобы точки куба были неиндексированными (unindex).

Теперь загрузим текстуру из файла и разместим (allocate) ее в 3D контексте :

var l = new flash.display.Loader();
var me = this;
l.contentLoaderInfo.addEventListener(flash.events.Event.COMPLETE, 
    function(_) {
        var b : flash.display.Bitmap = cast l.content;
        me.initTexture(b);
    });
l.load(new flash.net.URLRequest("hxlogo.png"));
//...    
function initTexture( b : flash.display.Bitmap ) {
    var data = b.bitmapData;
    texture = c.createTexture(data.width, data.height, 
            flash.display3D.Context3DTextureFormat.BGRA, false);
    texture.uploadFromBitmapData(data);
}

Передадим текстуру в шейдер:

shader.init(
    { mpos : mpos, mproj : project, light : light },
    { tex : texture }
);

Теперь можно читать цвет из текстуры в шейдере, используя текстурные координаты из данных о геометрии:

var input : {
    pos : Float3,
    norm : Float3,
    uv : Float2,
};
var tuv : Float2;
var lpow : Float;
function vertex( mpos : M44, mproj : M44, light : Float3 ) {
    out = pos.xyzw * mpos * mproj;
    var tnorm = (norm * mpos).normalize();
    lpow = light.dot(tnorm).max(0);
    tuv = uv;
}
function fragment( tex : Texture ) {
    out = tex.get(tuv) * (lpow * 0.8 + 0.2);
}

molehill_3.png

Обратите внимание, что мы изменили расчет цвета, добавив 20% света окружения (ambient), чтобы текстура не была полностью черной, когда грань в тени.

version #10573, modified 2011-05-19 13:59:40 by Scythian