站内搜索: 请输入搜索关键词

当前页面: 开发资料首页J2ME 专题Java 移动设备的 3D 图形3

Java 移动设备的 3D 图形3

摘要: Java 移动设备的 3D 图形3
转换

在本文开始处,我曾经使用了一个 Transform 对象将摄像机向后移动,以便查看整个立方体。通过同样的方式可以转换任意 3D 对象。

您可以通过数学方式将转换表示为矩阵操作。一个向量 —— 例如,摄像机位置 —— 乘以恰当的平移矩阵从而得到相应移动的向量。Transform 对象就表示了这样的一个矩阵。对于绝大多数普通转换来说,M3G 提供了 3 种便于使用的接口,隐藏了底层的数学计算:

Transform.postScale(float sx, float sy, float sz):在 x、y、z 方向伸缩 3D 对象。大于 1 的值将按照给定因数扩大对象;0 和 1 之间的值将缩小对象。负值则同时执行伸缩和镜像操作。
Transform.postTranslate(float tx, float ty, float tz):通过为 x、y 和 z 坐标增加指定值移动 3D 对象。负值则表示向负轴方向移动对象。
Transform.postRotate(float angle, float ax, float ay, float az):按给定角度绕穿过(0, 0, 0)和(ax, ay, az)的轴旋转对象。角度为正值,则表示若您顺着正旋转轴方向观察,对象是按顺时针旋转的。例如,postRotate(30, 1, 0, 0) 将绕 x 轴将对象旋转 30 度。
所有操作名都是以 "post" 开头的,表示当前 Transform 对象是从右边与给定转换矩阵相乘的 —— 矩阵操作的顺序是非常重要的。如果您向右旋转 90 度,然后走两步,这时您所处的位置显然与先走两步再转身不同。您可以在各步行指令之后调用两个 post 方法 postRotate() 和 postTranslate(),从而获得上面的步行指令。调用顺序决定了所获得的步行指令。由于使用的是后乘,所以您最后使用的转换会首先应用。

M3G 有一个 Transform 类和一个 Transformable 接口。所有快速模式的 API 均可接受 Transform 对象作为参数,用于修改其关联的 3D 对象。另外,在保留模式下使用 Transformable 接口来转换作为 3D 世界一部分的节点。在本系列的第 2 部分中将就此详细讨论。

清单 6 的示例展示了转换。


清单 6. 转换


/**
* Renders the sample on the screen.
*
* @param graphics the graphics object to draw on.
*/
protected void paint(Graphics graphics)
{
_graphics3d.bindTarget(graphics);
_graphics3d.clear(null);
_graphics3d.render(_cubeVertexData, _cubeTriangles,
new Appearance(), _cubeTransform);
_graphics3d.releaseTarget();

drawMenu(graphics);
}

/**
* Handles key presses.
*
* @param keyCode key code.
*/
protected void keyPressed(int keyCode)
{
switch (getGameAction(keyCode))
{
case UP:
transform(_transformation, TRANSFORMATION_X_AXIS, false);
break;

case DOWN:
transform(_transformation, TRANSFORMATION_X_AXIS, true);
break;

case LEFT:
transform(_transformation, TRANSFORMATION_Y_AXIS, false);
break;

case RIGHT:
transform(_transformation, TRANSFORMATION_Y_AXIS, true);
break;

case GAME_A:
transform(_transformation, TRANSFORMATION_Z_AXIS, false);
break;

case GAME_B:
transform(_transformation, TRANSFORMATION_Z_AXIS, true);
break;

case FIRE:
init();
break;

case GAME_C:
_transformation++;
_transformation %= 3;
break;

// no default
}

repaint();
}

/**
* Transforms the cube with the given parameters.
*
* @param transformation transformation (rotate, translate, scale)
* @param axis axis of translation (x, y, z)
* @param positiveDirection true for increase, false for decreasing
* value.
*/
protected void transform(int transformation, int axis,
boolean positiveDirection)
{
if (transformation == TRANSFORMATION_ROTATE)
{
float amount = 10.0f * (positiveDirection ? 1 : -1);

switch (axis)
{
case TRANSFORMATION_X_AXIS:
_cubeTransform.postRotate(amount, 1.0f, 0.0f, 0.0f);
break;

case TRANSFORMATION_Y_AXIS:
_cubeTransform.postRotate(amount, 0.0f, 1.0f, 0.0f);
break;

case TRANSFORMATION_Z_AXIS:
_cubeTransform.postRotate(amount, 0.0f, 0.0f, 1.0f);
break;

// no default
}
}
else if (transformation == TRANSFORMATION_SCALE)
{
float amount = positiveDirection ? 1.2f : 0.8f;

switch (axis)
{
case TRANSFORMATION_X_AXIS:
_cubeTransform.postScale(amount, 1.0f, 1.0f);
break;

case TRANSFORMATION_Y_AXIS:
_cubeTransform.postScale(1.0f, amount, 1.0f);
break;

case TRANSFORMATION_Z_AXIS:
_cubeTransform.postScale(1.0f, 1.0f, amount);
break;

// no default
}
}
else if (transformation == TRANSFORMATION_TRANSLATE)
{
float amount = 0.2f * (positiveDirection ? 1 : -1);

switch (axis)
{
case TRANSFORMATION_X_AXIS:
_cubeTransform.postTranslate(amount, 0.0f, 0.0f);
break;

case TRANSFORMATION_Y_AXIS:
_cubeTransform.postTranslate(0.0f, amount, 0.0f);
break;

case TRANSFORMATION_Z_AXIS:
_cubeTransform.postTranslate(0.0f, 0.0f, amount);
break;

// no default
}
}
}


paint() 方法现有一个 Transform 对象 _cubeTransform,该对象是 _graphics3d.render() 调用的第 4 个参数。改进的 keyPressed() 方法中包含使用 transform() 交互地更改转换的代码。GAME_C 键在旋转、平移和缩放立方体之间切换。UP/DOWN 键更改当前转换的 x 轴,LEFT/RIGHT 更改 y 轴,GAME_A/GAME_B 更改 z 轴。按 FIRE 可将立方体重新设置为初始位置。您可以在 TransformationsSample.java 中找到完整的源代码。


图 5. 示例立方体:a) 旋转;b) 平移;c) 缩放











深度缓冲和投影

这里我想介绍两个在使用转换时已用到但未说明过的概念:投影,定义了将 3D 对象映射到 2D 屏幕的方法;深度缓冲,是根据对象与摄像机之间的距离正确渲染对象的一种方法。

要从摄像机的观察点观察渲染后的图像,您必须考虑摄像机的位置和方位,将 3D 世界转换为摄像机空间。在前面的示例代码中,我用 Camera 和 Transform 对象调用了 Graphics3D.setCamera()。可将后者视为摄像机转换或告诉 MSG 如何从世界坐标转换为摄像机坐标的指令 —— 两种定义都是正确的。最后,三维对象被显示在二维屏幕上。到这里,Camera.setPerspective() 告诉了 M3G 在将 3D 转换为 2D 空间时实现透视投影。

透视投影与真实世界中的情况比较类似:当您俯视一条又长又直的道路时,道路两边看上去似乎在地平线处交汇了。距离摄像机越远,路旁的对象看起来也就越小。您也可以忽略透视,以相同大小绘制所有对象,不管它们离得多远。这对于某些应用程序,如 CAD 程序来说是很有意义的,因为没有透视可更容易地将精力集中在绘图上。要禁用透视投影,可用 Camera.setParallel() 替换 Camera.setPerspective()。

在摄像机空间中,对象的 z 坐标表示其与摄像机之间的距离。如果渲染一些具有不同 z 坐标的 3D 对象,那么您当然希望距离摄像机较近的对象比远处的对象清晰。通过使用深度缓冲,对象可得到正确的渲染。深度缓冲与屏幕有着相同的宽和高,但用 z 坐标取代颜色值。它存储着绘制在屏幕上的所有像素与摄像机之间的距离。然而,M3G 仅在一个像素比现有同一位置上的像素距离摄像机近时,才将其绘制出来。通过将进入的像素的 z 坐标与深度缓冲中的值相比较,就可以验证这一点。因此,启用深度缓冲可根据对象的 3D 位置渲染对象,而不受 Graphics3D.render() 命令顺序的影响。反之,如果您禁用了深度缓冲,那么必须在绘制 3D 对象的顺序上付出一定精力。在将目标图像绑定到 Graphics3D 时,可启用深度缓冲,也可不启用。在使用接受一个参数的 bindTarget() 重载版本时,默认为启用深度缓冲。在使用带有三个参数的 bindTarget() 时,您可以通过作为第二个参数的布尔值显式切换深度缓冲的开关状态。

您可以更改两个属性:深度缓冲与投影,如清单 7 所示:


清单 7. 深度缓冲与投影


/**
* Initializes the sample.
*/
protected void init()
{
// Get the singleton for 3D rendering.
_graphics3d = Graphics3D.getInstance();

// Create vertex data.
_cubeVertexData = new VertexBuffer();

VertexArray vertexPositions =
new VertexArray(VERTEX_POSITIONS.length/3, 3, 1);
vertexPositions.set(0, VERTEX_POSITIONS.length/3, VERTEX_POSITIONS);
_cubeVertexData.setPositions(vertexPositions, 1.0f, null);

// Create the triangles that define the cube; the indices point to
// vertices in VERTEX_POSITIONS.
_cubeTriangles = new TriangleStripArray(TRIANGLE_INDICES,
new int[] {TRIANGLE_INDICES.length});

// Create parallel and perspective cameras.
_cameraPerspective = new Camera();

float aspect = (float) getWidth() / (float) getHeight();
_cameraPerspective.setPerspective(30.0f, aspect, 1.0f, 1000.0f);
_cameraTransform = new Transform();
_cameraTransform.postTranslate(0.0f, 0.0f, 10.0f);

_cameraParallel = new Camera();
_cameraParallel.setParallel(5.0f, aspect, 1.0f, 1000.0f);

_graphics3d.setCamera(_cameraPerspective, _cameraTransform);
_isPerspective = true;

// Enable depth buffer.
_isDepthBufferEnabled = true;
}


/**
* Renders the sample on the screen.
*
* @param graphics the graphics object to draw on.
*/
protected void paint(Graphics graphics)
{
// Create transformation objects for the cubes.
Transform origin = new Transform();
Transform behindOrigin = new Transform(origin);
behindOrigin.postTranslate(-1.0f, 0.0f, -1.0f);
Transform inFrontOfOrigin = new Transform(origin);
inFrontOfOrigin.postTranslate(1.0f, 0.0f, 1.0f);

// Disable or enable depth buffering when target is bound.
_graphics3d.bindTarget(graphics, _isDepthBufferEnabled, 0);
_graphics3d.clear(null);

// Draw cubes front to back. If the depth buffer is enabled,
// they will be drawn according to their z coordinate. Otherwise,
// according to the order of rendering.
_cubeVertexData.setDefaultColor(0x00FF0000);
_graphics3d.render(_cubeVertexData, _cubeTriangles,
new Appearance(), inFrontOfOrigin);
_cubeVertexData.setDefaultColor(0x0000FF00);
_graphics3d.render(_cubeVertexData, _cubeTriangles,
new Appearance(), origin);
_cubeVertexData.setDefaultColor(0x000000FF);
_graphics3d.render(_cubeVertexData, _cubeTriangles,
new Appearance(), behindOrigin);

_graphics3d.releaseTarget();

drawMenu(graphics);
}

/**
* Handles key presses.
*
* @param keyCode key code.
*/
protected void keyPressed(int keyCode)
{
switch (getGameAction(keyCode))
{
case GAME_A:
_isPerspective = !_isPerspective;
if (_isPerspective)
{

_graphics3d.setCamera(_cameraPerspective, _cameraTransform);
}
else
{
_graphics3d.setCamera(_cameraParallel, _cameraTransform);
}
break;

case GAME_B:
_isDepthBufferEnabled = !_isDepthBufferEnabled;
break;

case FIRE:
init();
break;

// no default
}

repaint();
}


使用 GAME_A 键可在透视投影与平行投影之间切换。GAME_B 可启用或禁用深度缓冲。完整的源代码包含在 DepthBufferProjectionSample.java 中。图 6 展示了不同设置下的效果。


图 6. 立方体:a) 启用深度缓冲,根据与摄像机之间的距离进行渲染;b) 禁用深度缓冲,根据绘图操作的顺序进行渲染;c) 使用平行投影而非透视投影进行渲染







↑返回目录
前一篇: Java 移动设备的 3D 图形4
后一篇: Java 移动设备的 3D 图形2