|
|
/**
* 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) 使用平行投影而非透视投影进行渲染
照明
在一个没有光线的房间中,所有的东西看上去都是黑的。那么前面的示例中没有光线,怎么还能看到东西呢?顶点颜色和后面即将介绍的材质是不需要光线的,它们永远显示为定义好的颜色。但光线会使它们发生一些变化,可增加景深。
光线的方向会根据对象的位置发生反射。如果您用手电筒垂直地照射您面前的镜子,那么光线会反射到您身上。如果镜子是倾斜的,则光线的入射角和反射角是完全相同的。总的来说,您需要一个与照射平面相垂直的方向向量。这一向量就称为法线向量 或简称为法线。M3G 会根据法线、光源位置和摄像机位置计算着色情况。
此外,法线是各顶点都具备的属性,各顶点之间的像素着色既可采用插值法(PolygonMode.SHADE_SMOOTH)也可从三角形的第三个顶点处选取(PolygonMode.SHADE_FLAT)。由于立方体有 8 个顶点,支持法线的方法之一就是指定从立方体中心指向各角的向量,如图 7a 所示。但这样做可能会导致立方体着色不当。有三个面的颜色可能会相同,其中有些边成为不可见状态,使立方体看上去缺乏棱角。这显然更适合球体,不太适合立方体。图 7b 展示了如何为每边使用 4 条法线 —— 共 24 条,从而创建棱角分明的边线。由于一个顶点只能有一条法线,所以还要复制顶点。
图 7.带有法线向量的立方体:a) 8 条法线;b) 24 条法线(每边 4 条)
可使用法线计算光线后,还需要告诉 M3G 您需要什么类型的光线。光线来源于不同形式:灯泡、太阳、手电筒等等。在 M3G 中的对应术语分别为全向光、定向光和聚光。
全向光是从一个点发出的,并平均地照射各个方向。没有灯罩的灯泡发出的就是这样的光。
定向光向一个方向发出平行的光线。太阳离我们的距离非常远,所以可以将其光线视为平行的。定向光没有位置,只有方向。
手电筒或剧场中使用的聚光灯发射出的光线就是聚光。其光线呈锥形,与圆锥相交的平面上的对象会被照亮。
在真实世界中,光线还会从对象上反射回来而将周围照亮。如果您打开卧室灯,就会发现即便没有能直接照射到床底下的光线,但床下仍会被照亮。Raytracer 通过追踪从摄像机到光源的路径而清晰真实地展示了图像,但需要很长时间。要获得交互式帧频,必须满足一个简单的模型:环境光。环境光以不变的频率从各方向照亮对象。您可以用环境光模拟前面的卧室场景,将所有对象都照亮到一定程度,从而提供了另外一个全向光源。
清单 8 描述了设置不同光线的方法。
清单 8. 设置光线模式
// Create light.
_light = new Light();
_lightMode = LIGHT_OMNI;
setLightMode(_light, _lightMode);
Transform lightTransform = new Transform();
lightTransform.postTranslate(0.0f, 0.0f, 3.0f);
_graphics3d.resetLights();
_graphics3d.addLight(_light, lightTransform);
/**
* Sets the light mode.
*
* @param light light to be modified.
* @param mode light mode.
*/
protected void setLightMode(Light light, int mode)
{
switch (mode)
{
case LIGHT_AMBIENT:
light.setMode(Light.AMBIENT);
light.setIntensity(2.0f);
break;
case LIGHT_DIRECTIONAL:
light.setMode(Light.DIRECTIONAL);
light.setIntensity(1.0f);
break;
case LIGHT_OMNI:
light.setMode(Light.OMNI);
light.setIntensity(2.0f);
break;
case LIGHT_SPOT:
light.setMode(Light.SPOT);
light.setSpotAngle(20.0f);
light.setIntensity(2.0f);
break;
// no default
}
}
在图 8 中,您可以看到各种光线模式的不同效果。分别以 4 种类型的光照射示例立方体。这里的光线均为白色,就在摄像机前面,朝向立方体的三个面。
图 8. 使用不同的光线照射立方体 a) 全向光;b) 聚光;c) 环境光;d) 定向光
全向光在面对光源的顶点处最亮,然后逐渐暗淡下来。另外,聚光在聚光圆锥的边缘处制造了强烈的明暗对比。如果定义了一个足够大的光锥,那么所得到的结果可能与全向光相同。环境光从各个方向照亮立方体,立方体看上去是平的,这是因为缺乏阴影。最后,定向光使每面都具有不同的颜色。每面内的颜色都相同,这是因为光线是平行的。
照明并不精确,否则,聚光照亮的圆锥体范围应该是圆形。这是因为光线计算比较复杂,手机的部件将简化这一计算。可以为立方体的各边添加更多的三角形,从而提高其显示质量。尽管三角形并不能定义一个可见的几何图形,但可使 M3G 拥有更多的控制点(要计算的数量也更多)。
材质
通过光线可实现不同的效果。一个闪闪发光的银色球反射光线的方式与一张纸显然不同。M3G 使用以下属性为这些材质的特征建立模型:
环境反射:由环境光源反射的光线。
漫反射:反射光均匀地分散到各个方向。
放射光:一个像炽热的物体那样发射光线的对象。
镜面反射:光线从有光亮平面的对象反射回来。
您可为各材质属性设置颜色。闪闪发光的银色球的漫反射光线应该是银色的,其镜面反射部分为白色。材质的颜色与光线的颜色相融合,从而得到最终的对象颜色。如果您用蓝光照射银色球,那么球看上去应该略带蓝色。
清单 9 展示了使用材质的方式:
清单 9. 设置材质
// Create appearance and the material.
_cubeAppearance = new Appearance();
_colorTarget = COLOR_DEFAULT;
setMaterial(_cubeAppearance, _colorTarget);
/**
* Sets the material according to the given target.
*
* @param appearance appearance to be modified.
* @param colorTarget target color.
*/
protected void setMaterial(Appearance appearance, int colorTarget)
{
Material material = new Material();
switch (colorTarget)
{
case COLOR_DEFAULT:
break;
case COLOR_AMBIENT:
material.setColor(Material.AMBIENT, 0x00FF0000);
break;
case COLOR_DIFFUSE:
material.setColor(Material.DIFFUSE, 0x00FF0000);
break;
case COLOR_EMISSIVE:
material.setColor(Material.EMISSIVE, 0x00FF0000);
break;
case COLOR_SPECULAR:
material.setColor(Material.SPECULAR, 0x00FF0000);
material.setShininess(2);
break;
// no default
}
appearance.setMaterial(material);
}
setMaterial() 创建了一个新的 Material 对象,通过使用各颜色组件标识符的 setColor() 设置颜色。Material 对象随后被指派给 Appearance 对象,该对象用于调用 Graphics3D.render()。尽管这里没有展示,但您还可以使用 Material.setVertexColorTrackingEnable() 为环境反射和漫反射使用顶点颜色,不必使用 Material.setColor()。LightingMaterialsSample.java 这一示例中实现了光线和材质。按其中的键可以将不同的颜色与材质综合,感受不同的效果。
在图 9 中,用全向光展示了不同的材质特征。各截图都将颜色组件设置为红色,以突出表现其效果。
图 9. 不同的颜色组件:a) 环境反射;b) 漫反射;c) 放射光;d) 镜面反射
环境反射仅对环境光起作用,因此,使用全向光是无效的。漫反射材质组件会造成一种不光滑的表面,而放射光组件则制造出一种发光效果。镜面反射颜色组件强调了发亮的效果。此外,您还可以通过使用更多的三角形改进明暗对比的着色质量。
纹理
至此,我已经介绍了更改立方体外观的两种方式:顶点颜色和材质。但经过这两种方式处理后的立方体看起来依然很不真实。在现实世界中,应该还有更多的细节。这就是纹理的效果。纹理是像包在礼物外面的包装纸那样环绕在 3D 对象外的图像。您必须为各种情况选择恰当的包装纸,并且决定如何排列。在 3D 编程中也必须作出相同的决策。
现在,您或许已经猜测到我将引入另外一种每个顶点都具备的属性。对于每个顶点而言,纹理坐标定义了使用纹理的位置。然后 M3G 会映射纹理以适应您的对象。可以这样设想,将一块有弹性的包装纸钉在礼物的各顶点上。这些坐标所引用的纹理像素就叫做 texel。图 10 展示了将 128 x 128 texel 的正方形纹理映射到立方体正面的效果。
图 10:将多边形坐标(x,y)映射为纹理坐标(s,t)
将纹理坐标命名为(s,t)是为了与用于表示顶点位置的(x,y)区分开来(从文字角度来讲,(u,v)更为常用)。坐标(s,t)定义为(0,0)的地方就是纹理的左上角,而(1,1)位于右下角。相应地,如果您需要将立方体正面的左下角映射到纹理的左下角,必须将纹理坐标(0,1)指派给顶点 0。
由于您定义了与纹理的角相关的纹理坐标,所以任意大小的图像都有相同的坐标。M3G 为最接近的 texel 插入 0 到 1 之间的值,例如,0.5 表示纹理的中点。如果纹理坐标超过 0~1 的范围,M3G 会提示您确认。坐标既可环绕(例如,1.5 与 0.5 的效果相同),也可采用加强方式,所谓加强,也就意味着任何小于 0 的值都按 0 使用,任何大于 1 的值都按 1 使用。纹理的宽和高可有所不同,但必须是 2 的幂,如图 1 中的 128。部件必须至少支持 256 的纹理大小,这是 M3G 的一个可选属性。
Graphics3D.getProperties() 返回一个 Hashtable,其中填充了特定于部件的属性,如最大纹理维度或支持的最大光源数。getProperties() 的文档包含一个属性及其最低需求的清单。在使用超过这些值的属性之前,应该检查设备的部件是否能提供支持。
清单 10 展示了纹理的使用。
清单 10. 使用纹理,第 1 部分:初始化
/** The cube's vertex positions (x, y, z). */
private static final byte[] VERTEX_POSITIONS = {
-1, -1, 1, 1, -1, 1, -1, 1, 1, 1, 1, 1, // front
1, -1, -1, -1, -1, -1, 1, 1, -1, -1, 1, -1, // back
1, -1, 1, 1, -1, -1, 1, 1, 1, 1, 1, -1, // right
-1, -1, -1, -1, -1, 1, -1, 1, -1, -1, 1, 1, // left
-1, 1, 1, 1, 1, 1, -1, 1, -1, 1, 1, -1, // top
-1, -1, -1, 1, -1, -1, -1, -1, 1, 1, -1, 1 // bottom
};
/** Indices that define how to connect the vertices to build
* triangles. */
private static final int[] TRIANGLE_INDICES = {
0, 1, 2, 3, // front
4, 5, 6, 7, // back
8, 9, 10, 11, // right
12, 13, 14, 15, // left
16, 17, 18, 19, // top
20, 21, 22, 23, // bottom
};
/** Lengths of triangle strips in TRIANGLE_INDICES. */
private static int[] TRIANGLE_LENGTHS = {
4, 4, 4, 4, 4, 4
};
/** File name of the texture. */
private static final String TEXTURE_FILE = "/texture.png";
/** The texture coordinates (s, t) that define how to map the
* texture to the cube. */
private static final byte[] VERTEX_TEXTURE_COORDINATES = {
0, 1, 1, 1, 0, 0, 1, 0, // front
0, 1, 1, 1, 0, 0, 1, 0, // back
0, 1, 1, 1, 0, 0, 1, 0, // right
0, 1, 1, 1, 0, 0, 1, 0, // left
0, 1, 1, 1, 0, 0, 1, 0, // top
0, 1, 1, 1, 0, 0, 1, 0, // bottom
};
/** First color for blending. */
private static final int COLOR_0 = 0x000000FF;
/** Second color for blending. */
private static final int COLOR_1 = 0x0000FF00;
/**
* 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);
VertexArray vertexTextureCoordinates =
new VertexArray(VERTEX_TEXTURE_COORDINATES.length/2, 2, 1);
vertexTextureCoordinates.set(0,
VERTEX_TEXTURE_COORDINATES.length/2, VERTEX_TEXTURE_COORDINATES);
_cubeVertexData.setTexCoords(0, vertexTextureCoordinates, 2.0f, null);
// Set default color for cube.
_cubeVertexData.setDefaultColor(COLOR_0);
// Create the triangles that define the cube; the indices point to
// vertices in VERTEX_POSITIONS.
_cubeTriangles = new TriangleStripArray(TRIANGLE_INDICES,
TRIANGLE_LENGTHS);
// Create a camera with perspective projection.
Camera camera = new Camera();
float aspect = (float) getWidth() / (float) getHeight();
camera.setPerspective(30.0f, aspect, 1.0f, 1000.0f);
Transform cameraTransform = new Transform();
cameraTransform.postTranslate(0.0f, 0.0f, 10.0f);
_graphics3d.setCamera(camera, cameraTransform);
// Rotate the cube so we can see three sides.
_cubeTransform = new Transform();
_cubeTransform.postRotate(20.0f, 1.0f, 0.0f, 0.0f);
_cubeTransform.postRotate(45.0f, 0.0f, 1.0f, 0.0f);
// Define an appearance object and set the polygon mode.
_cubeAppearance = new Appearance();
_polygonMode = new PolygonMode();
_isPerspectiveCorrectionEnabled = false;
_cubeAppearance.setPolygonMode(_polygonMode);
try
{
// Load image for texture and assign it to the appearance. The
// default values are: WRAP_REPEAT, FILTER_BASE_LEVEL/
// FILTER_NEAREST, and FUNC_MODULATE.
Image2D image2D = (Image2D) Loader.load(TEXTURE_FILE)[0];
_cubeTexture = new Texture2D(image2D);
_cubeTexture.setBlending(Texture2D.FUNC_DECAL);
// Index 0 is used because we have only one texture.
_cubeAppearance.setTexture(0, _cubeTexture);
}
catch (Exception e)
{
System.out.println("Error loading image " + TEXTURE_FILE);
e.printStackTrace();
}
}
在 init() 中,向 VertexBuffer 增加了定义为类的静态成员的纹理坐标。与在照明示例中的情况类似,我为立方体的每个面都使用了 4 个向量,以将各顶点映射到纹理的一个角。注意,我使用了比例 2.0 作为 _cubeVertexData.setTexCoords() 的第三个参数。这也就告诉 M3G 将所有纹理坐标都乘以此值。实际上,纹理仅使用了立方体面的四分之一。这样做的目的是展示 M3G 的加强和环绕特性。如果是加强,那么仅在左上角绘制;如果是环绕,那么纹理图案将填充满整个面。
纹理是用 Loader.load() 载入的,并指派给 Texture2D 对象。您还应使用 MIDP 的 Image.createImage(),但如果您想从 Java Archive(JAR)文件中读取纹理,那么 Loader 类是最快的方式。所得到的 Texture2D 对象随后被设置为立方体外观的纹理。
在进行纹理化处理时,您可能依然希望使用通过照明获得或直接指派给顶点的颜色。出于此方面的考虑,M3G 提供了各种混色功能,可通过调用 _cubeTexture.setBlending() 来设置。在 init() 中,我使用了 Texture2D.FUNC_DECAL,它将根据 α 值将纹理与基本顶点颜色相混合。图 10 中纹理图像的灰色位透明度为 60%。这里没有设置顶点颜色或使用照明,而是使用 _cubeVertexData.setDefaultColor() 为立方体设置了一个默认颜色,这也就意味着立方体中的所有三角形都将使用同样的颜色。通过混色,您也可以在各纹理上使用多重纹理,从而获得更丰富的效果。
我还内置了一个可选的 M3G 特性。如照明部分中所示,渲染的质量取决于您所使用的三角形数量 —— 顶点之间的距离越小,插值效果就越好。这对于纹理来说也是成立的。高质量对于纹理而言就意味着纹理在不失真的情况下映射。如图 10 所示的纹理是有缺陷的,因为其中包含直线,显然会发生失真的情况。MSG 提供了一种在处理能力方面代价低廉的方法来解决这一问题。可用 PolygonMode.setPerspectiveCorrectionEnable() 设置可选的透视修正标志,如清单 11 所示。
清单 11. 使用纹理,第 2 部分:交互式更改透视修正、环绕模式及混色
/**
* Checks whether perspective correction is supported.
*
* @return true if perspective correction is supported, false otherwise.
*/
protected boolean isPerspectiveCorrectionSupported()
{
Hashtable properties = Graphics3D.getProperties();
Boolean supportPerspectiveCorrection =
(Boolean) properties.get("supportPerspectiveCorrection");
return supportPerspectiveCorrection.booleanValue();
}
/**
* Handles key presses.
*
* @param keyCode key code.
*/
protected void keyPressed(int keyCode)
{
switch (getGameAction(keyCode))
{
case LEFT:
_cubeTransform.postRotate(-10.0f, 0.0f, 1.0f, 0.0f);
break;
case RIGHT:
_cubeTransform.postRotate(10.0f, 0.0f, 1.0f, 0.0f);
break;
case FIRE:
init();
break;
case GAME_A:
if (isPerspectiveCorrectionSupported())
{
_isPerspectiveCorrectionEnabled = !_isPerspectiveCorrectionEnabled;
_polygonMode.setPerspectiveCorrectionEnable(
_isPerspectiveCorrectionEnabled);
}
break;
case GAME_B:
if (_cubeTexture.getWrappingS() == Texture2D.WRAP_CLAMP)
{
_cubeTexture.setWrapping(Texture2D.WRAP_REPEAT,
Texture2D.WRAP_REPEAT);
}
else
{
_cubeTexture.setWrapping(Texture2D.WRAP_CLAMP,
Texture2D.WRAP_CLAMP);
}
break;
case GAME_C:
if (_cubeVertexData.getDefaultColor() == COLOR_0)
{
_cubeVertexData.setDefaultColor(COLOR_1);
}
else
{
_cubeVertexData.setDefaultColor(COLOR_0);
}
break;
// no default
}
repaint();
}
在示例中,isPerspectiveCorrectionSupported() 用于检查部件是否支持透视修正。如果支持,您可在 keyPressed() 中交互地切换标志的开关状态。这里还增加了一个更改纹理映射到立方体的方式(加强或重复)的选项以及一个更改混色的选项。对混色的更改示范了可以容易地将颜色与纹理相混合以获得更丰富的效果。在 TexturesSample.java 中可以看到完整的示例。
图 11 展示了使用不同选项的纹理映射效果。
图 11. 纹理:a) 无透视修正;b) 有透视修正;c) 加强而非平铺;d) 用绿色代替兰色进行混色
结束语
本文中介绍了大量基础知识,包括使用顶点数据创建立方体、使用摄像机为立方体照相、在立方体上应用光线和材质、利用纹理创建具有真实感的立方体的方法等详细信息。给出了许多立方体作为示例。
在论证概念时,立方体是一种极好的示例,但它并不是复杂的 3D 设计的里程碑。在介绍过程中,我从游戏的角度强调了 3D 图像。如果像示例那样通过手工组合顶点数据,那么设计一个复杂的游戏世界将成为一项令人望而却步的工作。您需要一种方法,通过建模工具来设计 3D 场景,并将数据导入程序。
导入模型后,必须再寻求一种组织数据的方法。如果使用 VertexBuffer 方法,您必须记住所有的转换以及对象之间的关系。比如说,上臂与下臂相连,而下臂应该与手相连。您必须对应地安置手臂与手。M3G 提供的一种场景图形 API —— 保留模式 —— 简化了此类任务,通过保留模式您可以为全部对象及其属性建模。在本系列的第 2 部分中将就此进行详细论述。
文章录入:mingjava 责任编辑:mingja
|
|