2007年6月12日星期二

Papervision3D 教學 part 1 -- 初探

這算是我第一篇的 PaperVision3D(PV3D) 初探與教學,看完這篇,大部分的人應該就能自己寫一個 3D 特效了!

首先,有些基本的框架程式要寫:

Step1.as


package {
import flash.display.Sprite;
import flash.display.Bitmap;
import flash.events.Event;

import org.papervision3d.cameras.Camera3D;
import org.papervision3d.scenes.Scene3D;
import org.papervision3d.objects.*
import org.papervision3d.materials.*

[SWF(backgroundColor="#000000", frameRate="60", width="400", height="400")]

public class Step1 extends Sprite
{
private var container:Sprite;
private var scene:Scene3D;
private var camera:Camera3D;
private var rootNode:DisplayObject3D;

public function Step1()
{
// initialize the objects
init3D();

// add a listener for the 3D loop
addEventListener(Event.ENTER_FRAME, loop3D);
}

private function init3D():void{

container = new Sprite();
addChild(container);

container.x = 200;
container.y = 200;

scene = new Scene3D(container);

camera = new Camera3D();
camera.z = -3000;
camera.zoom = 10;
camera.focus = 100;

rootNode = scene.addChild(new DisplayObject3D("rootNode"));
}

private function loop3D(event:Event):void{
scene.renderCamera(camera);
}
}
}


在建構子 Step1() 時,會做兩部份的工作,一個是初始化 init3D(),另一個是設定當 onEnterFrame() 時,執行 loop3D()。

init3D() 的部份,我們首先要準備一個 Sprite 給 PV3D engine 使用,PV3D 會將所有圖像內容繪製在這個容器中,所以我們習慣上都會命名 container,並將這個身為 container 的 Sprite 放在整個 Flash 檔案的中央,在此我的 swf 大小為 400x400,所以我的 container 放在座標 (200, 200) 的位置。

將我們的 container 指定給 PV3D engine 的方法,在於建立一個 Scene3D 時,將這個身為 container 的 Sprite 指派給他即可。然後,我們需要先準備一個 Camera3D 物件,這個 Camera3D 物件會在 onEnterFrame() 時,被 Scene3D 拿來以他作為輸出畫面的依據,輸出由 Carema3D 物件所在三度空間中的座標所看出去的畫面。

最後,在 Scene3D 中,加入一個根節點,之後所有的 DisplayObject3D 物件都是加入這個 rootNode 根節點之下,你也可以想像它就是我們平時的 _root 的感覺,實際上當然他並不是 _root。

以上,測試時,並不會看到任何畫面!因為只是準備了一個空殼,接下來才是要開始建構基本的模型。

下列程式碼,我在 rootNode 之下,增加了一個 DisplayObject3D 物件,是一個 Cube 類別的物件名叫 myCube1。

Step2.as


package {
import flash.display.Sprite;
import flash.display.Bitmap;
import flash.events.Event;

import org.papervision3d.cameras.Camera3D;
import org.papervision3d.scenes.Scene3D;
import org.papervision3d.objects.*
import org.papervision3d.materials.*

[SWF(backgroundColor="#000000", frameRate="60", width="400", height="400")]

public class Step2 extends Sprite
{
private var container:Sprite;
private var scene:Scene3D;
private var camera:Camera3D;
private var rootNode:DisplayObject3D;

public function Step2()
{
// initialize the objects
init3D();

// add a listener for the 3D loop
addEventListener(Event.ENTER_FRAME, loop3D);
}

private function init3D():void{

container = new Sprite();
addChild(container);

container.x = 200;
container.y = 200;

scene = new Scene3D(container);

camera = new Camera3D();
camera.z = -3000;
camera.zoom = 10;
camera.focus = 100;

rootNode = scene.addChild(new DisplayObject3D("rootNode"));

var myCube1:DisplayObject3D = rootNode.addChild(new Cube(new ColorMaterial(0xffcc00), 100, 100, 100, 1, 1, 1), "myCube1");
}

private function loop3D(event:Event):void{
scene.renderCamera(camera);
}
}
}


測試畫面如下:



這是一個 Cube 物件,用 ColorMaterial 材質填色,表示我用了 0xffcc00 的純色來填,寬 深 高 各為 100px,三邊每邊被切割的數量都為 1,如此一來在貼圖時,需要切成小碎片貼圖的數量將會減少,CPU 運算減少,效能較好,然而,若是未來有需要在有弧面的物體上貼圖的話,則可以考慮將切割數增加,比較能貼出畫面誤差較小的感覺。

在建立 myCube1 這個 Cube 後,我們可以對他做些移動,從以下程式碼中,可以看到


myCube1.moveLeft(500);
myCube1.moveRight(500);
myCube1.moveUp(500);
myCube1.moveDown(500);
myCube1.moveForward(500);
myCube1.moveBackward(2949.9);


這些控制移動的程式碼,你們可以自行一一解開我的注解,分別看看這些移動各式朝哪些方向移動,上下左右應該很容易懂,至於 moveForward() 是表示往前,也就是更往螢幕裏面,會有變得更小、更遠的感覺,而 moveBackward() 則就是往靠近我們的方向,要跑出螢幕的感覺,會變得更大、更近的感覺。

為何我要特別嘗試 myCube1.moveBackward(2949.9); 呢?這是為了要測試到底它會靠近我們到多少距離才會消失,測試結果發現,當 DisplayObject3D 物件的內部中心點 與 Camera3D 的座標相同時,我們就會看不到它了。以上面的範例中,carema 現在的座標在 -3000 的地方,也就是說我們是在 -3000 的地方,看著遠在 螢幕裏面 3000 的距離外的 原點座標,所以當 myCube1 向後退 2950 時,這個大小為 100x100x100 的 Cube 的中心點,會正好通過 camera,於是我們就看不到它了。然而,只要他還在 carema 前面,就還可看得到,以我的測試來說,myCube1 在 carema 前方 0.1 的位置,這樣就可以看到了。

Step3.as


package {
import flash.display.Sprite;
import flash.display.Bitmap;
import flash.events.Event;

import org.papervision3d.cameras.Camera3D;
import org.papervision3d.scenes.Scene3D;
import org.papervision3d.objects.*
import org.papervision3d.materials.*

[SWF(backgroundColor="#000000", frameRate="60", width="400", height="400")]

public class Step3 extends Sprite
{
private var container:Sprite;
private var scene:Scene3D;
private var camera:Camera3D;
private var rootNode:DisplayObject3D;

public function Step3()
{
// initialize the objects
init3D();

// add a listener for the 3D loop
addEventListener(Event.ENTER_FRAME, loop3D);
}

private function init3D():void{

container = new Sprite();
addChild(container);

container.x = 200;
container.y = 200;

scene = new Scene3D(container);

camera = new Camera3D();
camera.z = -3000;
camera.zoom = 10;
camera.focus = 100;

rootNode = scene.addChild(new DisplayObject3D("rootNode"));

var myCube1:DisplayObject3D = rootNode.addChild(new Cube(new ColorMaterial(0xffcc00), 100, 100, 100, 1, 1, 1), "myCube1");

//myCube1.moveLeft(500);
//myCube1.moveRight(500);
//myCube1.moveUp(500);
//myCube1.moveDown(500);
//myCube1.moveForward(500);
myCube1.moveBackward(2949.9);
}

private function loop3D(event:Event):void{
scene.renderCamera(camera);
}
}
}


所以,測試畫面會如下圖,camera 會非常的接近 myCube1,然而因為 myCube1 是用黃色純色填色,所以畫面上幾乎都是黃色!



使用純色填色,實在看不出狀態,看不出我們到底有多靠近 myCube1,也看不出這個 myCube1 已經變得多大了,所以,以下是教如何使用 BitmapMaterial 材質貼圖。

Step4.as


package {
import flash.display.Sprite;
import flash.display.Bitmap;
import flash.events.Event;

import org.papervision3d.cameras.Camera3D;
import org.papervision3d.scenes.Scene3D;
import org.papervision3d.objects.*
import org.papervision3d.materials.*

[SWF(backgroundColor="#000000", frameRate="60", width="400", height="400")]

public class Step4 extends Sprite
{
[Embed(source="../assets/IMG_1054.JPG")]
private var CubeTexture:Class;

private var container:Sprite;
private var scene:Scene3D;
private var camera:Camera3D;
private var rootNode:DisplayObject3D;

public function Step4()
{
// initialize the objects
init3D();

// add a listener for the 3D loop
addEventListener(Event.ENTER_FRAME, loop3D);
}

private function init3D():void{

container = new Sprite();
addChild(container);

container.x = 200;
container.y = 200;

scene = new Scene3D(container);

camera = new Camera3D();
camera.z = -3000;
camera.zoom = 10;
camera.focus = 100;

rootNode = scene.addChild(new DisplayObject3D("rootNode"));

//var myCube1:DisplayObject3D = rootNode.addChild(new Cube(new ColorMaterial(0xffcc00), 100, 100, 100, 1, 1, 1), "myCube1");

var cubeTexture:Bitmap = new CubeTexture() as Bitmap;
var myCube1:DisplayObject3D = rootNode.addChild(new Cube(new BitmapMaterial(cubeTexture.bitmapData), 100, 100, 100, 1, 1, 1), "myCube1");

//myCube1.moveLeft(500);
//myCube1.moveRight(500);
//myCube1.moveUp(500);
//myCube1.moveDown(500);
//myCube1.moveForward(500);
myCube1.moveBackward(2949.9);
}

private function loop3D(event:Event):void{
scene.renderCamera(camera);
}
}
}


上面程式中:


[Embed(source="../assets/IMG_1054.JPG")]
private var CubeTexture:Class;


這是 ActionScript3 使用 metadata tag 的機制,目的是用來幫助編譯器瞭解一些編譯時所知道的資訊,這些 tag 不會增加多餘的程式碼,但,他不一定不會增加輸出 swf 的檔案大小,譬如這裡就是要將外部的 jpg 圖檔一起編繹到 swf 中,有點像是匯入圖檔放到 library 的感覺。不過,接下來的程式碼,我就不太會解釋了,這有點像是我們要 embed 的這個圖檔,我們將它變成是一種類別,未來用這個類別所建立出來的物件,就直接是等於這張圖,這部份的觀念我還ㄉㄟ要去學學他的意義為何!

當圖檔已經被 embed 進來一起輸出時,我們就可以利用剛剛建立的類別來產生物件,而產生出來的物件,我們要將他轉型成為 Bitmap 類別,var cubeTexture:Bitmap = new CubeTexture() as Bitmap;最後,在建立 rootNode 下的 myCube1 這個 DisplayObject3D 物件時,就可以以 BitmapMaterial 材質進行貼圖,而 BitmapMaterial 材質正是以剛剛拿到的 Bitmap 的 bitmapData 資料填入。

解釋得不是很好,但是至少應該用途與寫法大致可以了解!

測試畫面如下,myCube1 已經變成某張相片了,而且我們也可以知道我們與這個 Cube 有多麼的靠近了。



接著,我們可能會希望,myCube1 最靠近我們的距離時,他的畫面不要那麼大,那麼我們可以藉由修改之前設定 carema 的 zoom 做調整,carema.zoom 的意思就是當物件最靠近 carema 時的大小倍數,之前的範例我用 10 倍,以下,我改為 5 倍。

Step5.as


package {
import flash.display.Sprite;
import flash.display.Bitmap;
import flash.events.Event;

import org.papervision3d.cameras.Camera3D;
import org.papervision3d.scenes.Scene3D;
import org.papervision3d.objects.*
import org.papervision3d.materials.*

[SWF(backgroundColor="#000000", frameRate="60", width="400", height="400")]

public class Step5 extends Sprite
{
[Embed(source="../assets/IMG_1054.JPG")]
private var CubeTexture:Class;

private var container:Sprite;
private var scene:Scene3D;
private var camera:Camera3D;
private var rootNode:DisplayObject3D;

public function Step5()
{
// initialize the objects
init3D();

// add a listener for the 3D loop
addEventListener(Event.ENTER_FRAME, loop3D);
}

private function init3D():void{

container = new Sprite();
addChild(container);

container.x = 200;
container.y = 200;

scene = new Scene3D(container);

camera = new Camera3D();
camera.z = -3000;
camera.zoom = 5;
camera.focus = 100;

rootNode = scene.addChild(new DisplayObject3D("rootNode"));

//var myCube1:DisplayObject3D = rootNode.addChild(new Cube(new ColorMaterial(0xffcc00), 100, 100, 100, 1, 1, 1), "myCube1");

var cubeTexture:Bitmap = new CubeTexture() as Bitmap;
var myCube1:DisplayObject3D = rootNode.addChild(new Cube(new BitmapMaterial(cubeTexture.bitmapData), 100, 100, 100, 1, 1, 1), "myCube1");

//myCube1.moveLeft(500);
//myCube1.moveRight(500);
//myCube1.moveUp(500);
//myCube1.moveDown(500);
//myCube1.moveForward(500);
myCube1.moveBackward(2949.9);
}

private function loop3D(event:Event):void{
scene.renderCamera(camera);
}
}
}


測試畫面如下,可以看到這是 myCube1 最靠近 carema 時的大小,這已經是他自己五倍的大小了,只要 myCube1 再往後 myCube1.moveBackward(1); 再靠近我們一點點,他就會消失了!我想,這個 carema.zoom 的倍數因人而異,對我來說,只要物體最靠近我們時,在要消失前一刻,不要還讓我能看到他的全部整體物體大小,就好了,因為那就不像是穿透我的眼睛到我的背後去,而比較像是突然不見了,對吧!



Step6.as

貼圖完,也了解基本三度空間,以及基本移動後,下列程式碼就要來玩點比較有意義的,在 onEnterFrame() 時,讓 myCube1 不斷旋轉。


package {
import flash.display.Sprite;
import flash.display.Bitmap;
import flash.events.Event;

import org.papervision3d.cameras.Camera3D;
import org.papervision3d.scenes.Scene3D;
import org.papervision3d.objects.*
import org.papervision3d.materials.*

[SWF(backgroundColor="#000000", frameRate="60", width="400", height="400")]

public class Step6 extends Sprite
{
[Embed(source="../assets/IMG_1054.JPG")]
private var CubeTexture:Class;

private var container:Sprite;
private var scene:Scene3D;
private var camera:Camera3D;
private var rootNode:DisplayObject3D;

public function Step6()
{
// initialize the objects
init3D();

// add a listener for the 3D loop
addEventListener(Event.ENTER_FRAME, loop3D);
}

private function init3D():void{

container = new Sprite();
addChild(container);

container.x = 200;
container.y = 200;

scene = new Scene3D(container);

camera = new Camera3D();
camera.z = -3000;
camera.zoom = 10;
camera.focus = 100;

rootNode = scene.addChild(new DisplayObject3D("rootNode"));

//var myCube1:DisplayObject3D = rootNode.addChild(new Cube(new ColorMaterial(0xffcc00), 100, 100, 100, 1, 1, 1), "myCube1");

var cubeTexture:Bitmap = new CubeTexture() as Bitmap;
var myCube1:DisplayObject3D = rootNode.addChild(new Cube(new BitmapMaterial(cubeTexture.bitmapData), 100, 100, 100, 1, 1, 1), "myCube1");

//myCube1.moveLeft(500);
//myCube1.moveRight(500);
//myCube1.moveUp(500);
//myCube1.moveDown(500);
//myCube1.moveForward(500);
//myCube1.moveBackward(2949.9);
}

private function loop3D(event:Event):void{

var myCube1:DisplayObject3D = rootNode.getChildByName("myCube1");

if(myCube1!=null){
myCube1.rotationX++;
myCube1.rotationY++;
myCube1.rotationZ++;
}

scene.renderCamera(camera);
}
}
}


透過 rootNode.getChildByName("myCube1"); 可以取得指定名稱的 DisplayObject3D 物件,然後可以藉由改變他的 rotationX, rotationY, rotationZ,並不斷的重新 render 圖,就可以做出動畫效果。

看效果!

最後,只是延伸範例,多產生一些 Cube,用亂數改變她們的位置,並且設定不同的旋轉速度,程式碼如下:

Step7.as


package {
import flash.display.Sprite;
import flash.display.Bitmap;
import flash.events.Event;

import org.papervision3d.cameras.Camera3D;
import org.papervision3d.scenes.Scene3D;
import org.papervision3d.objects.*
import org.papervision3d.materials.*

[SWF(backgroundColor="#000000", frameRate="60", width="400", height="400")]

public class Step7 extends Sprite
{
[Embed(source="../assets/IMG_1054.JPG")]
private var CubeTexture:Class;

private var container:Sprite;
private var scene:Scene3D;
private var camera:Camera3D;
private var rootNode:DisplayObject3D;

private var AMOUNT:int = 30;
private var rotationSettingArray:Array = new Array();

public function Step7()
{
// initialize the objects
init3D();

// add a listener for the 3D loop
addEventListener(Event.ENTER_FRAME, loop3D);
}

private function init3D():void{

container = new Sprite();
addChild(container);

container.x = 200;
container.y = 200;

scene = new Scene3D(container);

camera = new Camera3D();
camera.z = -3000;
camera.zoom = 10;
camera.focus = 100;

rootNode = scene.addChild(new DisplayObject3D("rootNode"));

var cubeTexture:Bitmap = new CubeTexture() as Bitmap;

for(var i:int=0; i<AMOUNT; i++){
var myCube:DisplayObject3D = rootNode.addChild(new Cube(new BitmapMaterial(cubeTexture.bitmapData), 100, 100, 100, 1, 1, 1), "myCube" + i);

myCube.moveBackward(Math.random() * (3000-100/2+1));

if(Math.random()*2>1)
myCube.moveLeft(Math.random()*500);
else
myCube.moveRight(Math.random()*500);

if(Math.random()*2>1)
myCube.moveUp(Math.random()*500);
else
myCube.moveDown(Math.random()*500);

rotationSettingArray.push({rX:(Math.random()-Math.random())*2
, rY:(Math.random()-Math.random())*2
, rZ:(Math.random()-Math.random())*2});
}
}

private function loop3D(event:Event):void{

for(var i:int=0; i<AMOUNT; i++){
var myCube:DisplayObject3D = rootNode.getChildByName("myCube" + i);

if(myCube!=null){
myCube.rotationX+=rotationSettingArray[i].rX;
myCube.rotationY+=rotationSettingArray[i].rY;
myCube.rotationZ+=rotationSettingArray[i].rZ;
}
}

scene.renderCamera(camera);
}
}
}


看效果!

17 意見:

Sam Wang 提到...

Eclipse 內出現以下錯誤:
com.blitzagency.xray.logger.XrayLog 找不到...
papervision 3D 資料夾內只有 org 的 package 阿!邦大你會這樣嗎?

邦邦 提到...



用 google 查一下,你會找到他原來也是一個 open source 的專案,我猜測這是一個用來做 debug 用的工具。你可以在 osflash 找到!

Sam Wang 提到...

恩~ 我後來用 svn 下載完之後 把 com 這個 package ,程式內碼連結都正常但是出現 source-path 錯誤 "A file found in a source-path must have an externally visible definition. If a definition in the file is meant to be externally visible, please put the definition in a package." 是因為路徑的關係嗎? 但路徑如果錯誤 org 內應該抓不到才對阿

fJish 提到...

邦邦你好....

我在step1-3都可以成功跑出(我是用FLASH CS3做的)
但是到了step4的時候卻出現以下的錯誤,
不知道是什麼原因呢??
TypeError: Error #1007: 嘗試個體化非建構函式。
at Step4/::init3D()
at Step4$iinit()

Ben Chang 提到...

哈!考倒我了,光看錯誤訊息,我會猜測你用了一個不是建構函式的function來當作建構函式使用,用它來new一個類別的物件,說不定是打錯字?

可能還是上 google 查查看別的遇到相同問題的人,看看問題出在哪裡吧!對於類別的使用方面!

fJish 提到...

嗯嗯~~~~我有找到一些資訊......
ActionScript 3.0 的 Document Class 在正常情況下是無法建立額外的實體
意思就是除了 SWF 載入時自動建立的實體
完全不能對 Document Class 呼叫 "new"
否則會出現 Error 訊息

TypeError: Error #1007: 嘗試個體化非建構函式。假如該 Document Class 是由 Loader 從外部載入
甚至還會造成 Flash Player Crash

不過可以在 static 初始階段產生額外 Document Class 實體

花生一劍小師 提到...

嗨~邦大您好,小弟嘗試將您的Step6.as編譯,結果跑出這樣的錯誤:

..\src\Step3.as(46): col: 63 Error: org.papervision3d.materials:ColorMaterial 類型值以隱含方式強制轉型成不相關的類型 org.papervision3d.materials:MaterialsList。

var myCube1:DisplayObject3D = rootNode.addChild(new Cube(new ColorMaterial(0xffcc00), 100, 100, 100, 1, 1, 1), "myCube1");
(出問題的箭頭停在"new ColorMaterial..."底下)


@_@
我看doc試了很久,改用MaterialsList當參數代替new ColorMaterial仍是編譯不過(變成其他語法的問題),看著doc覺得Cube、Materials和MaterialsList的關係有點搞不太清楚,不得已要來求救邦大了,希望邦大有空的話,能幫小弟看看問題出在哪 ?

我用的PV3D是當下最新的版本:1.7

謝謝~

花生一劍小師 提到...

噗~~我懂了XD ,原來是語法變成這樣,用MaterialsList 的方式定義cube不同面的材質。

var materials:MaterialsList = new MaterialsList({
all: new ColorMaterial(0xffcc00) // This is the default material
});

var myCube1:DisplayObject3D = rootNode.addChild(new Cube(materials, 100, 100, 100, 1, 1, 1), "myCube1");

Young 提到...

邦大

您好 , 先謝謝您的教學

讓初學者也能很快就上手

我遇到了一個貼圖的問題 ,

當我使用 bitmap 的貼圖方式時

在物件朝 Y axis 旋轉時

會產生貼圖拉扯的狀況

這是上網搜尋後發現一樣的狀況

但沒找到解答

http://fiems.com/pv3d/test.swf



由於 blog 上好像無法貼檔案

不知道是否可以寄我的檔案給您看看 ,

謝謝

Young 提到...

邦大

您好 , 我觀察了一下

覺得可能是 2d cordinate(uv space)

project 到 camera space 時出的問題

, 就範例看起來 checker 的中心點像

是在對角兩 頂點 (triangle) 的

中點 , 所以我猜也許有別的 command

是用不同的方式做 texture samlping

但是沒找到 , 另再問一個 效果

就是 texture antialias

不知在 PV3D 有沒有這樣的指令呢

謝謝您

Young




Ben Chang 提到...

這篇文章 John Iacoviello - Papervision3D Optimization Tips 中,或許可以找到一些用得上的資訊:

http://interactivehug.com/2008/04/21/john-iacoviello-papervision3d-optimization-tips/

像是 material.smooth = true;

jk127780 提到...

我想請問,假如我想做一個畫面(1個用3D max建出來的模型,然後旁邊環繞著立體方塊選單),請問3DMAX如何匯入到FLASH?

Ben Chang 提到...

你可以用 google 查一下 collada 或 *.dae 的檔案格式,先找找有沒有 3d max 可以輸出這種格式的 plug-in,然後再來學 pv3d 如何使用 *.dae 的檔案即可。

mookio 提到...
此文章已被作者刪除。
mookio 提到...

邦大你好~
感謝你花時間寫出一系列的PaperVision3D教學.
不知道你是否有在接這一方面的案子呢?
因為數學展示有需要,要展示可以720度控制旋轉的多方體、方椎或圓柱體。

如果你有時間也願意的話,請mail到我的信箱詳談
mookio@gmail.com

謝謝你 ^_^~

阿默

Ben Chang 提到...

已經超級久沒碰 PV3D 了,現在還有在接觸的有 奶綠,你知道他嗎?建議你可以問問他有沒有接案意願。

mookio 提到...

感謝邦大~ 那我去問問奶綠願不願意接點外快!謝謝你 ^_^~

關於我






* ben {dot} chang {at} ben {dot} idv {dot} tw
* FriendFeed

贊助我1元美金:

Plurk

標籤雲