Java9-游戏开发高级教程-三-

97 阅读1小时+

Java9 游戏开发高级教程(三)

协议:CC BY-NC-SA 4.0

十、用户界面设计交互性:事件处理和图像效果

现在,您已经完成了闪屏和用户界面设计的场景图层次结构,让我们回到第十章中的 JavaFXGame 主应用类编码,并完成您已经准备好但实际上是“空的”事件处理框架的实现(除了几个 System.out.println 调用来测试您的按钮控件节点对象)。我们将在本章中概述的 Java 和 JavaFX 事件处理将实现用户界面,玩家将使用该界面来了解和启动您的 Java 9 游戏。在本书中,你将使用其他类型的事件处理(击键和鼠标),我们将在本章中讨论。您将添加 Java 游戏 UI 编程逻辑,它可以被视为游戏的交互引擎。有许多与游戏交互的方式,包括箭头键,被称为消费电子设备和现代遥控器的 pad 一个键盘;一只老鼠;轨迹球;游戏控制器;触摸屏;甚至是高级硬件,包括陀螺仪和加速度计。您将为您的 pro Java 9 游戏开发做出的一个重要选择是,您的玩家将如何使用他们玩游戏的硬件设备和游戏支持的硬件输入功能与您的 Java 游戏进行交互。

在本章中,您将学习不同类型的 JavaFX 事件类型,它们包含在javafx.event、javafx.scene.input 和 java.util 包中。您将覆盖 ActionEvent,因为您当前在您的用户界面设计中使用它,以及输入事件,如 MouseEvent 和 KeyEvent。

除了通过添加事件处理继续在 JavaFXGame Java 代码上工作之外,您将在本章中学习 JavaFX 特效,以确保我在本书中涵盖了 Java 中所有很酷的东西。这些 JavaFX 特效存储在 javafx.scene.effect 包中,为 javafx 和 Java 提供了与 GIMP 等数字图像合成软件包相同的特效优势。

事件处理:增加游戏的互动性

有人会说事件处理是游戏开发的基础。这是因为如果你没有一种方法来与游戏逻辑和游戏元素进行交互,你就真的没有一款游戏。在本章的这一节,我将介绍 JavaFX 事件处理类,您将实现 ActionEvent 处理结构,以便您的用户可以利用您在过去几章中设计的用户界面。在我们开始剖析 Java 和 JavaFX 包、类、接口和方法之前,我首先要说的是可以为 pro Java 游戏处理的不同类型的输入硬件事件。这些可以使用 iTV 遥控器或智能手机 DPAD 上的箭头键、键盘、鼠标或轨迹球以及智能手机、平板电脑或 iTV 电视机上的触摸屏来生成。还有自定义输入硬件,包括游戏机上的游戏控制器和现在的 iTV 电视机,智能手机和平板电脑中的陀螺仪和加速度计,以及自由形式的手势和动作控制器,如 Leap Motion,VR Glove 和 Razer Hydra Portal。

控制器的类型:我们应该处理什么类型的事件?

要考虑的一个关键问题是,支持游戏相关事件的最合理方法是什么,如箭头键、鼠标点击、触摸屏事件、游戏控制器按钮(A、B、C 和 D)以及更高级的控制器,如 Android、Kindle、Tizen、HTML5 OS 和 iOS 消费电子设备上可用的陀螺仪和加速度计。这个决定是由游戏运行的硬件设备决定的;如果一个游戏需要在任何地方运行,那么最终将需要处理不同事件类型的代码,甚至是不同的事件处理编程方法。在本章的这一节中,我们将进一步了解 Java 和 JavaFX 目前支持哪些输入事件,以便为您的游戏开发提供一个概览。

有趣的是,Java 和 JavaFX 应用已经可以在两个流行的嵌入式平台上运行,Android 和 iOS,我会在不久的将来的某个时候在开源平台(Opera,Tizen,Chrome,Ubuntu 和 Firefox)和当前支持 Java 8 或 9 技术的专有平台(Windows,Samsung Bada,RIM Blackberry,LG WebOS,OpenSolaris)上投入资金。Java 9 的未来是光明的,这要归功于 JavaFX、Java 平台几十年来的发展势头以及新的高级 i3D 硬件平台支持!

Java 和 JavaFX 事件包:java.util 和 javafx.event

正如您在事件处理结构的新 EventHandler 声明中看到的,javafx.event 包的 EventHandler 公共接口扩展了 java.util 包的 EventListener 接口,是创建和处理事件对象的方式,或者使用匿名内部类(Java 7)结构(我们正在使用它,因为它与 Android 兼容),或者使用 lambda 表达式(Java 8)。现在,您已经熟悉了如何编写这种类型的事件处理结构,在本书中,我将继续使用 Java 匿名内部类方法编写方法。也就是说,您可以将鼠标悬停在任何 Java 7 代码下的黄色波浪下划线高亮处,并让 NetBeans 9 将其转换为使用更简化的 Java 8 lambda 表达式。通过这种方式,您可以创建兼容 Java 7 (64 位 Android 5 和 6)、Java 8 (64 位 Android 7 和 8)和 Java 9(PC OS 和未来版本的 Android)游戏代码交付管道的游戏。在本节中,我们将查看 ActionEvent 和 InputEvent EventObject 子类类别,以便您了解 JavaFX 中的主要事件。这些来自 java.util.EventObject 超类,我们将看看它们如何应用于处理动作、击键、鼠标事件、触摸事件和类似的高级输入事件类型。

JavaFX ActionEvent 类:从 java.util 创建。EventObject 超类

到目前为止,您在本书中用于用户界面按钮控件事件处理的 ActionEvent 类(和对象)是 javafx.event 包的 Event 超类的子类,该超类本身是 java.util 包的 EventObject 超类的子类,该超类是 java.lang.Object 主类的子类。这个类还有一个已知的直接子类 MediaMarkerEvent 类。因此,类层次结构如下所示:

java.lang.Object
  > java.util.EventObject

    > javafx.event.Event
      > javafx.event.ActionEvent

ActionEvent 类与 EventHandler 公共接口一起包含在 javafx.event 包中。正如您可能已经猜到的,ActionEvent 对象是一个表示某种类型的动作的事件对象。这种类型的事件对象可以用来表示各种各样的事物。正如你所看到的,当一个按钮被触发时使用它,例如,当一个关键帧结束播放时,以及在其他类似的内部软件使用中也使用它。ActionEvent 是在 JavaFX 1.x 版本中引入的,在 JavaFX 1.x 版本(1.0 到 1.3)中不可用。它保留在 JavaFX 7 for Java 7 中(两者现在都已停止使用),保留在 JavaFX 8 for Java 8 中,现在保留在 JavaFX 9 for Java 9 中。

ActionEvent 对象有两个数据字段(属性)。第一个是静态 EventType 动作特征,这是 ActionEvent 的唯一有效 EventType。然而,还有一个 ActionEvent 对象的超类型,它采用静态 EventType ANY 的形式,为开发人员提供了一个能够表示所有动作事件类型的通用超类型。因此,如果希望 Java 代码处理任何 ActionEvent 对象,请使用此数据字段;如果希望 Java 代码处理特定的 ActionEvent 对象,请使用 ACTION 数据字段。

此 ActionEvent 类还支持两个构造函数方法。默认的空参数列表 ActionEvent()构造函数方法使用默认的事件类型 ACTION 创建一个新的 ActionEvent 对象。还有一个 ActionEvent(Object source,EventTarget)构造函数方法,它将使用指定的事件对象源和 EventTarget 目标创建新的 ActionEvent。

此 ActionEvent 类还支持两种方法。第一个是 ActionEvent copy for(Object new source,EventTarget newTarget)方法,用于使用指定的事件源和目标创建并返回事件的副本。第二个是 EventType extends ActionEvent> getEventType()方法,该方法将获取调用它的事件对象的事件类型。

我们将用于游戏的 i3D 组件的所有其他事件相关类都包含在 javafx.scene.input 包中。在本节的剩余部分,我将重点关注 javafx.scene.input 包,因为您已经学会了如何为 Java 7 编写新的 EventHandler { … }结构。如果您指示 NetBeans 9 将此转换为 Lambda 表达式,它将采用 Java 8 的(ActionEvent) -> { … }代码结构格式。

现在是时候学习如何在 Java 游戏开发工作流程中使用其他类型的事件,称为输入事件。让我们看看 javafx.scene.input 包及其 25 个输入事件相关的类。

JavaFX 输入事件类:javafx.scene.input 包

尽管 java.util 和 javafx.event 包包含核心的 eventObject、Event 和 EventHandler 类来“处理”您的事件,但在确保事件得到处理(处理)的基础级别上,还有另一个名为 javafx.scene.input 的 javafx 包,它包含您有兴趣用来处理(处理)您可能正在创建的不同类型游戏的玩家输入的类。这些事件被称为输入事件,它们不同于动作事件和脉冲事件,您已经了解了这些事件。

有趣的是,javafx.scene.input 包中支持的许多输入事件类型更适合智能手机和平板电脑等消费电子(行业术语是嵌入式)设备。这告诉我,JavaFX 正在被定位(设计)用于开源平台,如 Android OS、Firefox OS、Tizen OS、Bada OS、Opera OS、Ubuntu OS 或 Chrome OS。JavaFX 9 具有“专门化”事件,如 GestureEvent、SwipeEvent、TouchEvent 和 ZoomEvent,它们支持新的嵌入式设备市场中的特定功能。这些输入事件类支持高级触摸屏设备功能,如手势、页面滑动、触摸屏输入处理和多点触摸显示功能,如双指“捏”或“展开”触摸输入,例如分别用于放大和缩小屏幕上的内容。

我们将在本书中涵盖更多“通用”输入类型,这些类型在个人电脑(台式机、笔记本电脑、笔记本电脑、上网本和较新的“专业”平板电脑,如 Surface Pro 4)和嵌入式设备(包括智能手机、平板电脑、电子阅读器、iTV 电视机、游戏控制台、家庭媒体中心、机顶盒等)上都受支持。这些设备还将处理这些更广泛的(在它们的实现中)按键事件和鼠标事件类型的输入事件,因为鼠标事件和按键事件对于传统软件包总是受支持的。例如,触摸屏支持鼠标点击事件,但是定位设备(鼠标、轨迹球、控制器、DPAD 等)不支持触摸屏事件。).所以如果可以的话,用键盘鼠标事件!

有趣的是,触摸屏显示器将“处理”鼠标事件以及触摸事件,这对于确保您的游戏能够在尽可能多的不同平台上运行来说非常方便。我经常在我的 Android 书籍中使用这种使用鼠标事件处理的方法,以便用户可以使用触摸屏和 DPAD 中心(点击)按钮来生成鼠标点击事件,而不必专门使用触摸事件。对于触摸屏用户来说,尽可能使用鼠标(点击)事件的另一个好处是,如果您使用触摸事件,您将无法进行其他操作。也就是说,您的游戏应用只能在触摸屏设备上运行,而不能在具有某种鼠标硬件的设备(如 iTV、笔记本电脑、台式机、上网本等)上运行。

同样的原则也适用于按键事件,尤其是开发人员在游戏中使用的箭头键,因为这些键可以在键盘和遥控器的箭头小键盘上、游戏控制器上以及大多数智能手机的 DPAD 上找到。我还将向您展示如何包含备用键映射,以便您的玩家可以决定他们更喜欢使用哪种输入法来玩您的 pro Java 9 游戏。接下来让我们看看 KeyCode 和 KeyEvent 类。

KeyCode 类:使用枚举常量来定义玩家在游戏中使用的键

由于许多游戏使用箭头小键盘进行导航(通常是 A、S、D 和 W 键),并且有时使用这些键到游戏控制器的 GAME_A、GAME_B、GAME_C 和 GAME_D 按钮的替代映射,所以让我们先仔细看看 JavaFX KeyCode 类。此类是一个公共枚举类,它保存当按下或释放某个键时计算的键的枚举常数值。这个类是 KeyEvent 类获取 keycode 常量值的地方,keycode 常量值用于(处理)确定播放器在任何特定的键事件调用中使用了哪个键。KeyCode 类的 Java 和 JavaFX 类层次结构如下所示:

java.lang.Object
  > java.lang.Enum<KeyCode>
    > javafx.scene.input.KeyCode

keycode 类中包含的常量值使用大写字母,并以 KeyCode 支持的键命名。例如,A、S、W 和 d 键码是 A、S、W 和 d。箭头键盘键码是上、下、左和右,游戏控制器按钮键码是 GAME_A、GAME_B、GAME_C 和 GAME_D。

在本书的后面部分,您将在 EventHandler 对象中实现 KeyCode 常量和 KeyEvent 对象,因此我将在这里介绍这些用于输入事件处理的事件相关的包和类。正如您将看到的,这与设置 ActionEvent 的处理方式非常相似。您的 KeyEvents 也可以使用 Java 7 内部类方法或通过 Java 8 lambda 表达式进行编码。您的 KeyEvent 对象处理应该以模块化的方式完成,这样您的键码评估结构就可以为每个键码映射设置布尔标志变量。布尔标志将提供玩家在任何毫秒内按下或释放什么键的精确视图。

然后可以在其他游戏引擎类中使用 Java 游戏编程逻辑来读取和操作这些布尔值,这些游戏引擎类将实时处理这些关键事件,以便您的 pro Java 游戏运行良好,并且您的用户体验良好。接下来,让我们看看处理这些 KeyCode 对象的 KeyEvent 对象。

KeyEvent 类:使用 KeyEvent 对象保存键码常量

接下来,让我们仔细看看 KeyEvent 类。该类被指定为 public final KeyEvent,它扩展了 InputEvent 超类,后者用于创建 javafx.scene.input 包中的所有输入事件子类。使用 EventHandler 类将 KeyEvent 类设置为 motion,并处理 KeyCode 类常量值。该类的层次结构从 java.lang.Object 主类开始,经过 java.util.EventObject 事件超类,到 javafx.event.Event 类,后者用于创建 KeyEvent 类扩展的 javafx.scene.input.InputEvent 类(子类)。有趣的是,我们在这里跨越了四个不同的包。

KeyEvent 类的 Java 和 JavaFX 类层次结构从 java.lang 包跳转到 java.util 包、javafx.event 包、javafx.scene.input 包。键事件层次结构如下所示:

java.lang.Object
  > java.util.EventObject
    > javafx.event.Event
      > javafx.scene.input.InputEvent
        > javafx.scene.input.KeyEvent

EventHandler 对象生成的 KeyEvent 对象表示发生了击键。此键事件通常是使用场景图形节点对象之一生成的,如可编辑文本 UI 控件;但是,在您的游戏中,您可能要将场景图形节点对象层次级别之上的关键事件处理直接附加到名为 Scene 的场景对象。这将通过不向场景图中的任何节点对象附加任何关键事件处理来最小化场景图脉冲处理开销。在您的游戏中,这是包含 uiLayout StackPane 对象和 gameBoard 组对象的根组对象。

每当按下并按住、释放或键入(按下并立即释放)某个键时,就会生成一个 KeyEvent 对象。根据这个按键动作本身的性质,您的 KeyEvent 对象被传递到。onKeyPressed(),。onKeyTyped(),或者。onKeyReleased()方法,以便在. handle()方法中进行进一步处理。

游戏通常使用按键和按键释放事件,因为用户通常按住按键来移动游戏中的角色。另一方面,键入事件往往是“高级”事件,通常不依赖于操作系统平台或键盘布局。键入的按键事件(。onKeyTyped()方法调用)将在输入 Unicode 字符时生成。它们用于为 UI 控件(如文本字段)获取字符输入,并用于业务应用,如日历、计算器、电子邮件客户端和文字处理程序。

在一个简单的例子中,击键事件将通过使用一次击键及其立即释放来产生。此外,可以使用按键事件的组合来产生替代字符。例如,使用 Shift 键和“A”键类型(按下并立即释放)产生大写字母 A。

生成键类型的 KeyEvent 对象通常不需要按键释放。需要注意的是,在某些极端情况下,直到释放按键时才会生成按键类型的事件;这方面的一个很好的例子是使用老式的 Alt 键和数字键盘输入方法输入 ASCII 字符代码序列的过程,这种方法“过去”在 DOS 中使用,并保留到 Windows 操作系统中。值得注意的是,对于不生成 Unicode(可见、可打印)字符的计算机键盘键,如 Shift、Control (Ctrl)或 Alternate)键(通常称为修饰键),不会生成键类型的 KeyEvent 对象。

KeyEvent 类有一个字符变量(我倾向于称之为字符特征,但我不会这样做),对于键入键的事件,它总是包含一个有效的 Unicode 字符,对于按下键或释放键的事件,它总是包含 CHAR_UNDEFINED 字符。字符输入仅针对键入的事件进行报告,因为按键和按键释放事件不一定与字符输入相关联。因此,您的字符变量保证只对键类型的事件有意义。

对于按键和按键释放的 KeyEvent 对象,KeyEvent 类中的 code 变量将包含您的 KeyEvent 对象的 keycode,该 KeyCode 是使用您之前学习过的 KeyCode 类定义的。对于键入事件,这个代码变量总是包含常量 KeyCode.UNDEFINED。所以正如你所看到的,按键和按键释放被设计成不同于键入事件的用法,这就是为什么你可能会在游戏事件处理中使用按键和按键释放事件。按键和按键释放事件是低级事件,每当按下或释放给定的按键时都会生成。它们是“轮询”不产生字符输入的键的唯一方法。您的按键被按下或释放会使用 code 变量指示给操作系统,该变量包含一个虚拟键码。

添加键盘事件处理:使用 KeyEvents

我认为这些背景信息已经足够让你理解一个如何为 pro Java 游戏实现 KeyEvent 处理的基本例子了。这相当简单,所以我将向您简要介绍一下这里是如何完成的,因为我们正在讨论 KeyEvent 类。这在我的《Java 8 游戏开发入门》一书中也有涉及。您要做的第一件事是在 JavaFXGame 类的顶部添加一行代码,并使用一个复合声明语句声明四个布尔变量,分别命名为 up、down、left 和 right,如下所示:

boolean up, down, left, right;

由于布尔变量的默认值为 false,这将表示某个键未被按下,即当前释放或未使用的键。因为这也是键盘上按键的默认状态,所以这将是该应用的正确默认值。因为在 Java 中,布尔变量默认为 false,所以您不必显式初始化这些变量。

我通过使用。setOnKeyPressed()方法调用名为 Scene 的场景对象,我已经实例化了它。在这个方法调用中,我创建了新的 EventHandler ,就像对 ActionEvent 所做的那样。代码如下所示:

scene.setOnKeyPressed( new EventHandler<KeyEvent>() { a .handle() method will go in here } );

KeyEvent 对象处理恰好是实现高效 Java switch 语句的完美应用。您可以为想要处理的每个 JavaFX KeyCode 常量添加一个 case。它们包含在名为 Event 的 KeyEvent 对象中,该对象被传递到此。handle()方法通过它的参数区域。

然后,使用您的从 switch()评估区域内的 KeyEvent 对象中提取键码。事件 KeyEvent 对象的 getCode()方法调用。这可以通过使用下面的 Java 开关情况 Java 代码编程结构来完成:

scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
    @Override
    public void handle(KeyEvent event) {
        switch (event.getCode()) {
            case UP:    up    = true; break;
            case DOWN:  down  = true; break;
            case LEFT:  left  = true; break;
            case RIGHT: right = true; break; // No break; statement is needed here, if there
                                             // are no more case statements to be added later
        }
    }
});

这将为您提供基本的按键事件处理结构,您可以将它添加到您的 pro Java 游戏中,并使用您的代码轮询这四个布尔变量,以找出您的用户正在使用他们的箭头键做什么。如果您指示 NetBeans 9 将这个 Java 7 结构转换成 Java 8 lambda 表达式,您将得到一个紧凑的结构。在 lambda expression Java 8 代码结构中,许多东西,如公共 void handle()声明和新的 EventHandler ()声明,将被隐含或假定(但仍存在于编译器中)。使用 lambda 表达式将简化代码,将三层嵌套的代码块减少到只有两层嵌套的代码块,并将 11 行代码减少到 8 行。Lambda 表达式对于编写更紧凑的代码来说确实很优雅,但是它并没有向您展示正在使用的类(对象)、修饰符和返回类型的所有情况。这就是为什么我选择使用更明确的 Java 7(和更早版本)代码结构的原因,在 Java 5 到 7 中使用,在 Java 8 中引入 Lambda 表达式之前使用。Java 9 支持这两种方法。

您生成的 Java 8 lambda 表达式代码结构将类似于以下 Java 代码结构:

scene.setOnKeyPressed(KeyEvent event) -> {
    switch (event.getCode()) {
        case UP:    up    = true; break;
        case DOWN:  down  = true; break;
        case LEFT:  left  = true; break;
        case RIGHT: right = true; break;
    }
});

接下来你要做的是创建 OnKeyPressed 结构的对立面,从而创建一个 OnKeyReleased 结构。这将使用相同的代码结构,只是真值会变成假值,并且。setOnKeyPressed()方法调用将改为. setOnKeyReleased()方法调用。最简单的方法是选择。setOnKeyPressed()结构,并将其复制粘贴到自身下面。Java 代码如下所示:

scene.setOnKeyReleased(new EventHandler<KeyEvent>() {
    @Override
    public void handle(KeyEvent event) {
        switch (event.getCode()) {
            case UP:    up    = false; break;
            case DOWN:  down  = false; break;
            case LEFT:  left  = false; break;
            case RIGHT: right = false; break;
        }
    }
});

使用 lambda 表达式通过“隐式”声明和使用类(如本章实例中的 EventHandler 类)所做的一件有趣的事情是,它减少了类代码顶部的 import 语句的数量。这是因为,如果在代码中没有专门使用某个类(写下了它的名称),则该类的 import 语句不必与其他 import 语句一起放在代码的顶部。您还会注意到,如果您转换为 lambda 表达式,NetBeans 9 左边的代码折叠加号或减号图标将会消失。这是因为 lambda 表达式是一个基本的 Java 代码语句,而不是内部类或方法之类的构造或结构,在将其转换为 lambda 表达式之前是这样的。

既然你已经看了如何设置按键事件处理结构,让我们来看看添加一个备用按键映射到游戏中经常使用的 ASDW 按键是多么容易。这是通过为键盘上的 A、S、D 和 W 字符添加一些 case 语句,并将它们设置为我们已经设置好的 up、down、left 和 right 布尔等价物(UP、DOWN、LEFT 和 RIGHT 变量)来实现的。

例如,这将允许用户用左手使用 A 和 D 字符,用右手使用上下箭头来简化游戏。稍后,如果您想要使用游戏控制器及其对 KeyCode 类 GAME_A、GAME_B、GAME_C 和 GAME_D 常量的支持来为游戏添加更多功能,那么您将不得不为您的游戏添加这些新功能,只需为该类顶部的 up、down、left 和 right 变量添加另外四个布尔变量(A、B、C 和 D ),并添加另外四个 case 语句。

这四个 W(上)、S(下)、A(左)和 D(右)case 语句一旦添加到 switch 语句中,就会使您的 KeyEvent 对象及其事件处理 Java 代码看起来像下面 15 行 Java 代码:

scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
    @Override
    public void handle(KeyEvent event) {
        switch (event.getCode()) {
            case UP:    up    = true; break;
            case DOWN:  down  = true; break;
            case LEFT:  left  = true; break;
            case RIGHT: right = true; break;
            case W:     up    = true; break;
            case S:     down  = true; break;
            case A:     left  = true; break;
            case D:     right = true; break;
        }
    }
});

正如你所看到的,现在用户可以使用任意一组按键,或者同时使用两组按键来控制游戏。现在对。setOnKeyReleased()事件处理结构使用复制粘贴工作流程,并将值更改为 false。那个。setOnKeyReleased()事件处理 Java 代码如下所示:

scene.setOnKeyPressed(new EventHandler<KeyEvent>() {
    @Override
    public void handle(KeyEvent event) {
        switch (event.getCode()) {
            case UP:    up    = false; break;
            case DOWN:  down  = false; break;
            case LEFT:  left  = false; break;
            case RIGHT: right = false; break;
            case W:     up    = false; break;
            case S:     down  = false; break;
            case A:     left  = false; break;
            case D:     right = false; break;
        }
    }
});

接下来,让我们切换回 Java 编码模式,为您的用户界面设计实现事件处理。

完成用户界面设计:编写事件处理代码

让我们通过在。handle()方法。此方法清除文本对象,然后将文本对象添加到 infoOverlay TextFlow 对象中,并使用。setBackground()和。setImage()方法调用。正如你在图 10-1 中看到的,我总是首先使用。getChildren()。clear()方法调用,然后我使用. getChildren.addAll()方法调用将正确的文本对象添加到 TextFlow 对象中。然后我使用。setTranslateX()和。setTranslateY()方法调用 infoOverlay TextFlow 来定位该容器(和层)。之后,我使用。setBackground()方法调用来设置 uiLayout VBox 对象的背景图像(用于闪屏)或背景。其他四个按钮对象为空,这允许颜色。要显示的白色背景色。最后,我使用。setImage()方法调用,使用正确的图像对象设置 boardgameb 背板 ImageView 对象。在游戏规则(帮助)按钮的情况下,这是一个帮助者图像对象引用。对于第一个 helpButton 事件处理程序,此。handle()方法代码体将包括以下 Java 语句:

A336284_1_En_10_Fig1_HTML.jpg

图 10-1。

Implement the code in the handle() method to reconfigure your helpButton UI compositing layer objects

helpButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(helpText, cardText);
        infoOverlay.setTranslateX(130);
        infoOverlay.setTranslateY(360);
        uiLayout.setBackground(Background.EMPTY);
        boardGameBackPlate.setImage(helpLayer);
    }
} );

现在,当您使用您的 Run ➤项目工作流程时,游戏规则按钮 UI 控件将触发用户界面设计,该设计已优化为向您的游戏玩家显示您的说明(帮助)屏幕,如图 10-2 所示。在本章中,我们将使用 Java 代码对此设计做进一步的设计“调整”。

A336284_1_En_10_Fig2_HTML.jpg

图 10-2。

Test your UI Button event handling with the Start Game and Game Rules buttons, switching back and forth

此代码通过使用 Background.EMPTY 在 StackPane 对象中安装透明度来配置合成堆栈(Stage ➤根➤ StackPane ➤ ImageView ➤ TextFlow)中的不同对象,以显示操作系统的默认白色背景,即场景(和 Stage)对象。boardGameBackPlate ImageView 包含一个透明指令脚本字体投影 PNG32 图像,它允许白色背景颜色通过。TextFlow 和两个 Text 对象也支持透明度并添加了游戏说明,因此信息屏幕是一个漂亮、可读的白色,文本预设为 Color.GREEN。如果您单击 Start Game 按钮(我们接下来将编写该按钮,以将其自身重置为默认设置),您可以在闪屏和新的帮助文本之间切换,尽管在闪屏上有一些错误,因为 Game Button 事件处理程序需要重置特性,我们接下来将恢复白色文本、文本位置、闪屏图像和欢迎图像,因为该按钮更改了对象特性。

接下来,让我们将这些 Java 语句复制并粘贴到 gameButton 事件处理结构中,然后我们将使用正确的文本、背景、图像对象和像素位置值来配置您的方法调用参数区域。清除 TextFlow 对象,然后使用。addAll()方法。接下来,使用 240 整数值为您的设置 TextFlow 容器的 X,Y 像素位置(它在屏幕上的位置)。setTranslateX()方法调用,并为您的。setTranslateY()方法调用。使用 uiBackground 对象加载 uiLayout StackPane 对象的背景。setBackground()方法调用,然后使用。setImage()方法调用。这都是通过在。handle()方法,如图 10-3 中间高亮显示:

A336284_1_En_10_Fig3_HTML.jpg

图 10-3。

Implement the code in the handle() method to configure gameButton default UI compositing layer objects

gameButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(playText, moreText);
        infoOverlay.setTranslateX(240);
        infoOverlay.setTranslateY(420);
        uiLayout.setBackground(uiBackground);
        boardGameBackPlate.setImage(splashScreen);
    }
} );

请注意,在图 10-3 中,我使用代码编辑窗格左边的加号(+)图标在 NetBeans 9 IDE 中打开了 gameButton 和 helpButton 事件处理结构,这样您就可以从 Java 代码的角度看到您的。handle()方法设置所有合成管道对象特征,以便仅使用少量不同的变量和对象设置来控制每个不同按钮对象的屏幕设计。这是一个例子,说明当您以最佳方式设置 JavaFX 场景图时,Java 可以有多么强大。

使用“运行➤项目”工作流程,再次在“开始游戏”和“游戏规则”按钮 UI 控件之间切换。你会看到游戏规则按钮不再弄乱你的开始游戏屏幕,如图 10-4 所示。

A336284_1_En_10_Fig4_HTML.jpg

图 10-4。

Test your UI Button event handling with the Start Game and Game Rules, switching back and forth

接下来,让我们将 helpButton Java 语句复制并粘贴到 legalButton 事件处理结构中,然后通过使用正确的文本、背景、图像对象和像素位置值来配置这些方法调用参数区域。再次清除 TextFlow 对象,然后使用。addAll()方法。接下来,设置 TextFlow 容器的 X,Y 像素位置(它在屏幕上的位置)。setTranslateX()方法调用,并为。setTranslateY()方法调用。用背景加载你的 uiLayout 对象背景。使用您的。setBackground()方法调用,然后使用。setImage()方法调用。这都是通过在。handle()方法,如图 10-5 中间高亮显示:

A336284_1_En_10_Fig5_HTML.jpg

图 10-5。

Implement the code in the handle() method to reconfigure your legalButton UI compositing layer objects

legalButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(copyText, riteText);
        infoOverlay.setTranslateX(200);
        infoOverlay.setTranslateY(370);
        uiLayout.setBackground(Background.EMPTY);
        boardGameBackPlate.setImage(legalLayer);
    }
} );

接下来,使用您的运行➤项目工作流程,并确保您的免责声明按钮是在白色背景上以可读、有组织的格式配置您的文本对象。正如你将在图 10-6 中看到的,你的 UI 屏幕看起来不错,你可以继续通过再次使用复制和粘贴来创建一个游戏积分按钮对象事件处理结构。

A336284_1_En_10_Fig6_HTML.jpg

图 10-6。

Test UI Button event handling with the Start Game, Game Rules, and Disclaimers, switching back and forth

接下来,让我们将 helpButton Java 语句复制并粘贴到 legalButton 事件处理结构中,然后通过使用正确的文本、背景、图像对象和像素位置值来配置这些方法调用参数区域。再次清除 TextFlow 对象,然后使用。addAll()方法。接下来,设置 TextFlow 容器的 X,Y 像素位置(它在屏幕上的位置)。setTranslateX()方法调用,并为。setTranslateY()方法调用。用背景加载你的 uiLayout 对象背景。使用您的。setBackground()方法调用,然后使用。setImage()方法调用。这都是通过在。handle()方法,如图 10-7 中间高亮显示:

A336284_1_En_10_Fig7_HTML.jpg

图 10-7。

Implement the code in the handle() method to reconfigure your creditButton UI compositing layer objects

creditButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(credText, codeText);
        infoOverlay.setTranslateX(240);
        infoOverlay.setTranslateY(370);
        uiContainer.setBackground(Background.EMPTY);
        boardGameBackPlate.setImage(creditLayer);
    }
} );

接下来,使用您的“运行➤项目”工作流程,并确保您的 Credits TextFlow 对象以可读格式将其所有文本对象定位在屏幕上。如图 10-8 所示,你的 UI 屏幕看起来很棒。我们暂时不实现高分按钮,因为稍后我们将创建一个评分引擎和高分表。

A336284_1_En_10_Fig8_HTML.jpg

图 10-8。

Implement the code in the handle() method to reconfigure the creditButton UI compositing layer objects

特效:javafx.scene.effects 包

javafx.scene.effect 包包含所有 javafx 特效的基础超类。不奇怪,这叫效果类。Effect 类有 17 个已知的 2D 数字图像合成效果的直接子类,比如你会在 GIMP 2.10 中找到的,它们也包含在这个包中。其中包括 Blend、Bloom、BoxBlur、ColorAdjust、ColorInput、DisplacementMap、DropShadow、FloatMap、GaussianBlur、Glow、ImageInput、InnerShadow、Lighting、MotionBlur、PerspectiveTransform、Reflection、SepiaTone 和 Shadow 类。对于 2D,这个包还包含了轻超类和轻。遥远,轻盈。点,和光。Spot 子类,我们将在本书的 3D 部分使用。

让我们先来看看 JavaFX Effect 超类。该类是一个公共抽象类,扩展了 java.lang.Object 主类。这意味着它是由 JavaFX 开发团队从头开始创建的,专门用于在 JavaFX 中提供基于图像(基于像素)的特殊效果,并提供照明支持,可用于 2D 和 3D。提供的效果很像 GIMP 3 或 Photoshop 在各自的数字成像软件包中提供的效果。

因此,JavaFX Effect Java 类的层次结构如下所示:

java.lang.Object
  > javafx.scene.effect.Effect

Effect 类为 JavaFX 中所有特效实现的创建提供了一个抽象或“基础”类。JavaFX 中的效果对象(及其子类)将始终包含一个生成图像对象的像素图形算法。这将是对源图像对象中像素的算法修改,在 2D 和 3D 中都有效。

通过设置名为 Node.effect 的属性,也就是节点类(或从节点子类创建的对象)的效果属性,效果对象也可以与场景图形节点(而不是图像对象)相关联。

有些效果(如 ColorAdjust)会改变源像素的颜色特征(色调、亮度和饱和度),而其他效果(如 Blend)会通过算法(通过 Porter-Duff)将多个图像组合在一起。

DisplacementMap 和 PerspectiveTransform 特殊效果类将在 2D 空间中扭曲或移动源图像的像素,以模拟 3D 空间,通常称为“2.5D”或“等轴”空间光学效果。

所有 JavaFX 特效都至少定义了一个输入。此外,该输入可以设置为另一个效果对象,允许开发人员将效果对象链接在一起。这允许开发者组合效果结果,允许创建复合或混合特效。此输入也可以保持“未指定”,在这种情况下,效果会将其算法应用于使用. setEffect()方法调用附加到的节点对象的图形渲染(像素表示或渲染结果),或者应用于已提供的图像对象。

需要注意的是,特效处理是一个有条件的特性。条件功能。效果枚举类和常量将定义一组条件(支持的)特效功能。这些功能可能并非在所有操作系统或所有嵌入式平台上都可用,尽管“现代”消费电子设备通常可以使用其硬件 GPU 图形处理能力来支持效果处理以及 i3D 渲染。

如果您的专业 Java 游戏应用想要轮询硬件平台以确定是否有任何特定的效果功能可用,您可以使用 Platform.isSupported ()方法调用来查询效果支持。如果您在不支持条件功能的平台上使用任何条件功能,都不会导致异常。一般来说,条件特性将被忽略,这样您就不必编写任何特定的错误捕获或错误处理 Java 代码。

接下来,让我们看看如何在 UI 设计中实现一两个这样的特殊效果,并向 TextFlow 对象添加投影,以便使用增加的对比度使它显示的文本更具可读性。之后,我们将看看如何在可见光谱范围内改变数字图像的颜色。

创建特殊效果:添加 createSpecialEffects()方法

让我们跟随组织 Java 代码的趋势,创建一个名为。createSpecialEffects()。让 NetBeans 9 在 createTextAssets()方法调用之后,通过添加一行代码在 start()方法中调用它来创建一个空的private void createSpecialEffects() {...}基础架构,如图 10-9 中突出显示的。这里的逻辑是,我们将首先加载图像,然后定义效果,然后创建文本。

A336284_1_En_10_Fig9_HTML.jpg

图 10-9。

Add a createSpecialEffects() method call at the top of .start() so that NetBeans creates the method body

接下来,我们将用特效代码替换 createSpecialEffects()方法中的引导代码。

投影:向 TextFlow 对象添加投影

现在是时候将 Java 代码添加到空的 createSpecialEffects()方法中来设置投影效果了。稍后,您将通过使用。setEffect()方法调用。首先,您需要在类的顶部声明一个名为 DropShadow 的 dropShadow 对象,并使用 Alt+Enter 工作进程让 NetBeans 为您生成一个导入语句。接下来,在 createSpecialEffects()方法中,使用 Java new 关键字和 DropShadow()构造函数方法实例化该对象。接下来,使用。setRadius()方法调用 dropShadow 对象来设置一个 4.0 像素的阴影半径(它从源展开的程度)。接下来,使用。setOffsetX()和。setOffsetY()方法使用 3.0 像素的设置调用,以使阴影向右斜向偏移(使用负值表示相反方向)。最后,使用. setColor()方法调用来指定暗灰色颜色类常量。图 10-10 中突出显示的代码应如下所示:

A336284_1_En_10_Fig10_HTML.jpg

图 10-10。

Code your private void createSpecialEffects() method body to create and configure a DropShadow object

DropShadow dropShadow;
...
private void createSpecialEffects() {
    dropShadow = new DropShadow();
    dropShadow.setRadius(4.0);
    dropShadow.setOffsetX(3.0);
    dropShadow.setOffsetY(3.0);
    dropShadow.setColor(Color.DARKGRAY);
}

接下来,打开 createTextAssets()方法体,添加一个.setEffect(dropShadow)方法调用每个文本对象,将它们绑定到投影效果和你为对象设置的设置,如图 10-11 所示。

A336284_1_En_10_Fig11_HTML.jpg

图 10-11。

Add a .setEffect(dropShadow) method call to each of your Text objects in the createTextAssets() method

另一个流行且有用的特效是调整像素颜色值,这是你想在你的 pro Java 9 游戏开发中使用的。javafx.scene.effect 包中有一个强大的 ColorAdjust 特殊效果类,允许开发人员调整图像的数字成像属性,包括使用对比度。setContrast(),亮度使用。setBrightness(),饱和度使用。setSaturation()和 hue (color)使用。setHue()。接下来我们来了解一下这个。

颜色调整:调整色调、饱和度、对比度和亮度

让我们使用。setHue()方法调用 ColorAdjust 对象,以允许我们对 PNG32 透明徽标数字图像素材的色温进行“颜色转换”,以便它在视觉上与我们在本章中改进的每个按钮控件用户界面设计的所有其他屏幕设计元素颜色值的颜色相匹配。在类的顶部声明一个名为 ColorAdjust 的 colorAdjust 对象。在 createSpecialEffects()方法中,使用 ColorAdjust()构造函数实例化该对象,然后使用浮点 0.4 值调用该对象的. setHue()方法,将当前图像颜色值在色轮周围向前移动 40%。图 10-12 中间和底部突出显示的 Java 代码如下所示:

A336284_1_En_10_Fig12_HTML.jpg

图 10-12。

Add a colorAdjust object instantiation and use a .setHue (0.4) method call off of the object to configure it

DropShadow dropShadow;
ColorAdjust colorAdjust;
...
private void createSpecialEffects()  {
    dropShadow = new DropShadow();
    dropShadow.setRadius(4.0);
    dropShadow.setOffsetX(3.0);
    dropShadow.setOffsetY(3.0);
    dropShadow.setColor(Color.DARKGRAY);
    colorAdjust = new ColorAdjust();
    colorAdjust.setHue(0.4);        }

ColorAdjust 效果对象实现的下一步是添加。在 helpButton.setOnAction()事件处理程序中,对 logoLayer ImageView 对象调用 setEffect(colorAdjust)方法。这将使透明徽标 PNG32 图像中的棕色像素变为绿色,同时保持透明像素不变,因为它们具有零颜色值(和最大透明度值)。如果这些像素是使用部分颜色值和部分透明度值定义的,那么部分颜色值将向前移动 40%。

我加了这个。setEffect()方法调用紧接在你的boardGameBackPlate.setImage(helpLayer);方法调用之后,因为我们现在需要对徽标图像复合层进行颜色转换,正如你在图 10-13 中看到的高亮显示。logoLayer 对象的 Effect 对象被设置为 colorAdjust 对象,该对象随后被设置为色调值 0.4(40%)。

A336284_1_En_10_Fig13_HTML.jpg

图 10-13。

Use a Run ➤ Project work process to make sure that the drop shadow special effects are working well

你可能想知道为什么我已经在 createSpecialEffects()方法中设置了 40%的色调。原因是 createSpecialEffects()方法中的设置可以被视为“默认”设置,而我必须在 helpButton 事件处理程序代码中指定它(再次)的原因是其他按钮处理程序将设置不同的色调值。您的 helpButton.setOnAction()事件处理代码现在应该如下所示:

helpButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(helpText, cardText);
        infoOverlay.setTranslateX(130);
        infoOverlay.setTranslateY(360);
        uiLayout.setBackground(Background.EMPTY);
        boardGameBackPlate.setImage(helpLayer);
        logoLayer.setEffect(colorAdjust);
        colorAdjust.setHue(0.4);
    }
});

现在是时候使用“运行➤项目”工作流程,并确保 TextFlow 对象上的投影效果使您的文本对象更具可读性,并与屏幕字幕图像上的投影相匹配。您可以在图 10-14 中看到,Java 代码中有些地方出了问题,因为 JavaFX 应用运行了,但没有对文本进行阴影处理。让我们检查一下 Java 代码执行语句的顺序,看看是否有什么地方出了问题!

A336284_1_En_10_Fig14_HTML.jpg

图 10-14。

Declare and instantiate a ColorAdjust object named colorAdjust and use .setHue() to shift the color 40 percent

由于 Java 代码在方法中的顺序是正确的,我怀疑调用方法的顺序很可能是这个问题的根源。让我们看看 start()方法内部的方法调用顺序,如图 10-9 顶部所示。请注意,createSpecialEffects()是在 createTextAssets()之后调用的,但是我们使用的是。setEffect(dropShadow)方法调用在 createTextAssets()方法内部,所以我们必须将 createSpecialEffects()方法调用移到 createTextAssets()方法调用之上,如图 10-13 所示,这样你的效果在使用之前就已经设置好了。如果你在你正在做的事情的过程中追踪这种逻辑,那么 Java 代码是非常合乎逻辑的!

正如你在图 10-16 中看到的,这解决了问题,你的投影效果渲染正确。

下一步需要修改的是您的 legalButton.setOnAction()事件处理结构,使屏幕上的所有内容都变成紫色。这可以通过将你的 logo 的色调改变 40%来实现,这次是在色轮的负方向。使用浮点数,色轮的右正 180 度范围从 0.0 到 1.0,左负 180 度范围从 0.0 到-1.0。

在图 10-15 的底部,高亮显示了您的 legalButton 事件处理 Java 语句的 Java 代码。它应该类似于下面的 Java 代码:

A336284_1_En_10_Fig16_HTML.jpg

图 10-16。

Use the Run ➤ Project work process and make sure that the drop shadow effect is rendering correctly

A336284_1_En_10_Fig15_HTML.jpg

图 10-15。

Add a .setEffect (colorAdjust) method call off logoLayer and call .setHue(-0.4) to change the color shift

legalButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(copyText, riteText);
        infoOverlay.setTranslateY(200);
        infoOverlay.setTranslateY(370);
        uiLayout.setBackground(Background.EMPTY);
        boardGameBackPlate.setImage(legalLayer);
        logoLayer.setEffect(colorAdjust);
        colorAdjust.setHue(-0.4);
    }
} );

图 10-17 显示了我的 Run ➤项目按钮处理程序测试工作流程,显示了投影效果和免责声明按钮控件中的色调(颜色)移动,进一步完善了设计。我在所有不同的按钮元素之间来回点击,以确保所有的属性都没有以不可取的方式重置任何其他按钮屏幕设计属性,这就是我将所有正确的变量放在所有事件处理代码体中的原因,以便没有方法调用忽略设置(未指定/传递)。

A336284_1_En_10_Fig17_HTML.jpg

图 10-17。

Use the Run ➤ Project work process and make sure that the color hue shift matches the rest of the design

您需要做的最后一项修改是对 creditButton.setOnAction()事件处理结构进行修改,以使屏幕上的所有内容呈现出漂亮的蓝色。这可以通过围绕一个色轮将你的标志色调向负方向偏移 90%来实现。图 10-18 中间突出显示了处理 Java 语句的 creditButton 事件的代码,如下所示:

A336284_1_En_10_Fig18_HTML.jpg

图 10-18。

Copy and paste the colorAdjust.setHue(-0.9) and logoLayer.setEffect(colorAdjust) Java code in creditButton

creditButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event)    {
        infoOverlay.getChildren().clear();
        infoOverlay.getChildren().addAll(credText, codeText);
        infoOverlay.setTranslateY(240);
        infoOverlay.setTranslateY(370);
        uiLayout.setBackground(Background.EMPTY);
        boardGameBackPlate.setImage(creditLayer);
        logoLayer.setEffect(colorAdjust);
        colorAdjust.setHue(-0.9);
    }
});

使用“运行➤”项目的工作流程,确保色轮周围这 90%的负偏移现在将徽标变成鲜艳的蓝色,这与用户界面设计的其余部分看起来很好。正如你在图 10-19 中看到的,演职员表按钮控制屏幕现在在颜色和阴影效果上都匹配了。

A336284_1_En_10_Fig19_HTML.jpg

图 10-19。

Use the Run ➤ Project work process and make sure that the color hue shift matches the rest of the design

我们将编写 scoreButton.setOnAction()事件处理程序的内部代码,当我们在本书后面介绍实现评分引擎和高分 UI 设计时,您将有一个可以就地评分的棋盘游戏。

在您的 pro Java 9 游戏开发中实现许多其他特殊效果的工作过程也可以用同样的方式来实现——为您想要使用的效果声明一个类,在 createSpecialEffects()方法中实例化它,使用该类中的方法设置效果配置的参数,最后使用。setEffect(effectClassNameHere)方法调用对象名称。

您会发现 JavaFX 特效包和类在这种实现方法中特别灵活,因为您可以将大多数软件包中常见的所有特效应用于 JavaFX 9 中的几乎任何对象或场景图形层次结构,通常使用大约十几行代码,有时甚至更少。

一旦你知道了如何在 JavaFX 中创建和应用这些特效,你的 pro Java 9 游戏开发创造力就会提升一个数量级。这是因为这些效果可以应用于场景图形层次结构中的任何地方,以及 2D、成像和 3D 渲染管道中的任何地方。

在本书的后面部分,我会尝试使用更多的 JavaFX 9 Effect 子类,因为我会给每一章增加更多的复杂性,并且随着我们在本书中的进展。

摘要

在第十章中,我们使用 ActionEvent 处理结构将交互性添加到用户界面设计中,了解了 InputEvent 对象和 MouseEvent 及 KeyEvent 对象处理,并了解了如何应用 javafx.scene.effects 包中包含的利用 JavaFX Effect 超类的特殊效果。

接下来,通过使用 java.util 和 javafx.event 包及其 eventObject、Event、ActionEvent 和 InputEvent 类,您了解了 Java 9 和 JavaFX 中如何处理允许交互的事件。我们讨论了不同类型的 InputEvent 对象,如 MouseEvents、TouchEvents 和 KeyEvents,然后您实现了 ActionEvent 处理,以使用户界面的(中间三个)说明、法律免责声明和 Production Credits 部分(按钮对象)具有交互性。

最后,您了解了 javafx.scene.effects 包以及 javafx 为开发人员提供的许多特殊效果。我们查看了 Effect 超类,并讨论了如何实现 DropShadow 类(和对象)和 ColorAdjust 类(和对象),以便您可以通过向 TextFlow 对象添加阴影来美化用户界面,提高可读性(对比度),并对顶部徽标数字图像素材进行颜色转换,以匹配每个按钮控件对象用户界面设计的颜色方案。

在第十一章中,我们将看看如何配置您的 JavaFX 游戏来利用 3D 资源。这涉及 Camera 超类及其 ParallelCamera 和 PerspectiveCamera 子类。我们还将学习如何在你的 3D 场景中创建光线,以便相机物体可以“看见”我们将看看 LightBase 超类及其 AmbientLight 和 PointLight 子类,它们是专门为 3D 场景应用中的照明设计而提供的。

十一、3D 场景配置:使用透视相机和点光源

现在,您已经完成了闪屏和用户界面设计的 2D 场景图层次,让我们回到第十一章中的 JavaFXGame 主要应用类编码,并开始设计 3D 游戏棋盘场景基础设施,这将是棋盘游戏及其游戏性的渲染和照明的基础。我们将了解基本的 3D 场景组件,您会发现这些组件预装在 3D 软件包(如 Blender 或 Autodesk 3D Studio Max)中(适用于所有默认或空场景)。之后,我们可以在第十二章进入 JavaFX 图元(盒子、平面、圆柱体、圆盘、球体和药丸)以及在第十三章使用材质和纹理贴图进行着色。

在本章中,您将了解 JavaFX 9 Camera和 LightBase 子类的不同类型,这些子类包含在核心 javafx.scene 包中,而 Java FX . scene 包又包含在 javafx.graphics 模块中(从 Java 9 开始)。我们将讨论 PerspectiveCamera,因为您将在本章中创建的基本 3D 场景基础架构中使用它,以及 ParallelCamera,另一个更适合您的 2D 或 2.5D 游戏开发管道的相机子类。Camera 是一个抽象超类,不能直接使用。我们还将了解 public LightBase 抽象超类及其两个核心照明子类 AmbientLight 和 PointLight。

我们还将通过向 JavaFX SceneGraph 添加 3D 渲染、相机和照明来继续处理您的 JavaFXGame Java 代码,以便您可以开始向您的 3D 游戏添加 3D 元素,这将在我们讲述 JavaFX Shape3D 类及其图元子类(在第十二章)并使用着色器以及将材质和纹理贴图应用于该 3D 几何图形(在第十三章)之后进行。

关于 JavaFX 如何能够在 3D 场景中可视化(渲染)游戏的 3D 几何资源及其纹理贴图,我们还有很多需要了解的,所以让我们开始了解场景相机对象。

使用 3D 相机:给 3D 游戏添加视角

任何 3D 渲染管道的顶层都是场景摄像机,因为这是处理 3D 场景中所有事情的设备,然后将数据交给渲染引擎。在这种情况下,它是 PRISM 软件渲染器(在没有 GPU 的情况下),或者它可能是您正在玩 3D 游戏的消费电子设备(PC、手机、平板电脑、iTV 电视机、笔记本电脑、游戏控制台、机顶盒)上的 OpenGL 硬件渲染引擎。如果您仍在使用 Windows,它可能还包括 DirectX 3D 渲染。相机对象(在我们的例子中,这将是一个 PerpectiveCamera 对象)专门用于 3D 场景渲染;我们将在本章的这一节讨论它。它是 JavaFX 9 SceneGraph 不可或缺的一部分,因此它有自己的 Scene.setCamera(Camera)方法调用。此方法调用用于将相机对象添加到场景图根,以确保它位于场景图渲染层次的最顶端(根)。它不使用。getChildren()。add()方法链,因此,它将被设置在 createSceneGraphNodes()方法中,在本章的这一节,当我们为您的 pro Java 9 游戏设置这个 Camera 对象时,您将会看到这一点。我们还将介绍 ParallelCamera,它更适合 2D 游戏。

JavaFX Camera 类:定义相机的抽象超类

公共 JavaFX Camera 超类是一个抽象类,仅用于创建不同类型的相机。目前有一个正交或平行摄影机子类(对象)或透视摄影机子类(对象)。您的应用不应该试图直接扩展这个抽象的 Camera 类;如果您尝试这样做,Java 将抛出一个 UnsupportedOperationException,您的 pro Java 9 游戏将无法编译或运行。Camera 类保存在核心 javafx.scene 包的 javafx.graphics 模块中,是 Node 的子类,因为它最终是 SceneGraph 顶部的一个节点。Camera 类实现了 Styleable 接口,因此它可以被样式化,并且它包含 EventTarget 接口,因此它可以处理事件。JavaFX Camera 类的 Java 9 类层次结构如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.Camera

camera 类是用于渲染场景的任何 Camera 子类的基类。相机用于定义场景的坐标空间如何呈现在用户正在观看的 2D 窗口(舞台)上。默认摄影机(如果您没有专门创建一个,我们将在本节稍后进行)将被定位在场景中,这样它在场景坐标空间中的投影平面在 Z=0 处(正好在中间),并且在正 Z 方向上看向屏幕。出于这个原因,我们将在代码中使我们的相机远离屏幕中心(-1000)1000 个单位,因为 i3D 游戏板将位于“中心舞台”并位于 0,0,0 (X,Y,Z)。

从摄影机到投影平面的距离(以 Z 为单位)可以由摄影机所附着的场景(也是生成的投影平面)的宽度和高度以及摄影机对象的 fieldOfView 参数来确定。相机对象的 nearClip 和 farClip 属性是在这个抽象类中定义的仅有的两个属性或特征,并且是在 JavaFX 所谓的眼睛坐标空间中指定的。该空间由观察者的眼睛在相机对象的原点处定义,并且投影平面在眼睛前方的正 Z 方向上是一个单位。任何相机子类(如 PerspectiveCamera)的 nearClip 和 farClip 属性都可以使用。setNearClip()和。setFarClip()方法调用。这是两个 PerspectiveCamera 类(object)方法调用,我们将在本章的后半部分使用它们来配置 SceneGraph camera 对象。

JavaFX PerspectiveCamera 类:您的 3D 透视相机

JavaFX PerspectiveCamera 类扩展了 Camera 类,用于创建 PerspectiveCamera(对象),该对象用于渲染 i3D 场景。PerspectiveCamera 类也保存在核心 javafx.scene 包的 javafx.graphics 模块中;它是 Node 的子类,是 JavaFX 场景图顶部的一个节点。PerspectiveCamera 类还实现了 Styleable 接口,以便可以对其进行样式化,并实现了 EventTarget 接口,以便可以处理事件。JavaFX PerspectiveCamera 类的 Java 9 类层次结构如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.Camera
      > javafx.scene.PerspectiveCamera

PerspectiveCamera 对象定义透视投影的观察体积。想象一个截断的面向右侧的金字塔,因为大多数相机都是在 Blender 或 3D Studio Max 等 3D 软件中可视化表示的。

这个类有两个(重载的)构造函数方法。一个有一个空的参数区域,像这样:

camera = new PerspectiveCamera();

第二个使用布尔值 fixedEyeAtCameraZero 属性(或参数或特征),这是我们将在摄像机对象声明、实例化和配置 Java 代码中使用的属性,如下所示:

camera = new PerspectiveCamera(true);

当然我们也会在类的顶部声明一个 PerspectiveCamera camera,用 Alt+Enter 让 NetBeans 9 为我们写一个这个类的 import 语句。PerspectiveCamera 有一个 fieldOfView 值,可用于更改相机投影的视野(FOV)角度,以度为单位。我将保留 FOV 的默认值,并假设这个默认的 FOV 给出了最佳的视觉效果,这是由 JavaFX 开发团队决定的。

我在游戏和模拟中使用 i3D 的倾向是“推拉”,或沿着 Z(场景内外)变换轴移动相机,而不是使用 FOV 值变化,因为即使在现实生活中,改变相机镜头(如从 24 毫米到 105 毫米)往往会更剧烈地改变视角。根据我的经验,使用不同的 3D 虚拟相机,这种视角的变化在虚拟 3D 中比使用真实相机时更加剧烈。

默认情况下,在创建(实例化)时,透视摄影机位于场景的中心,并沿着正 z 轴观察(指向下方)。如果使用 PerspectiveCamera(false)构造 PerspectiveCamera,那么由该相机定义的坐标系的 0,0 原点将位于面板的左上角,y 轴指向下方,z 轴指向远离查看者的方向(进入屏幕)。如果将 PerspectiveCamera 节点添加到场景图中,则转换后的相机位置和方向将定义相机的位置和相机正在观察的方向。在默认相机中,fixedEyeAtCameraZero 为 false,眼睛位置 Z 值在 Z 中进行调整,以便使用指定的视场生成的投影矩阵将使用设备无关的像素在 Z = 0 处(在投影平面上)生成单位。这符合你的平行摄像机的特点。调整场景大小时,投影平面(Z = 0)上场景中的对象将保持相同的大小,但场景中或多或少的内容是可见的,这比 3D 相机更适合 2D 相机和 2D 滚动条的使用,在 3D 相机中,调整相机大小将会缩放场景。这就是为什么 PerspectiveCamera 通常使用 PerspectiveCamera(true)进行实例化的原因,我们将在本章的这一节稍后进行。

当 fixedEyeAtCameraZero 设置为 true 时,眼睛位置在相机的局部坐标中固定在(0,0,0)。将使用默认的(或指定的)fieldOfView 属性生成投影矩阵,并且将在窗口(视口或舞台对象)上映射投影体积,使得它将在投影平面的点处的或多或少的设备无关像素上被“拉伸”(缩放)。当场景大小属性更改时,场景中的对象将按比例缩小或增大,但内容的可见范围(边界)将保持不变。

如果您计划变换(移动或推拉)相机对象,JavaFX 开发团队建议将此 fixedEyeAtCameraZero 设置为 true。当 fixedEyeAtCameraZero 设置为 false 时变换相机可能会导致最终用户感觉不直观的结果。

请注意,PerspectiveCamera 是一个有条件的 3D 特征。您可以轮询条件功能。确定给定用户的设备是否支持该特性的布尔变量(在本例中,支持 i3D)。这将使用以下 Java 代码结构来完成,该结构设置一个布尔变量来反映系统对 3D 渲染的支持:

boolean supportFor3D = Platform.isSupported(ConditionalFeature.SCENE3D);

最后,该类有一个名为 verticalFieldOfView 的布尔属性,用于定义 FieldOfView 属性是否将应用于投影的垂直维度。这在逻辑上意味着,如果这是假的,增加或减少 FOV 将改变投影的宽度,但不改变(垂直)高度,如果这是真的,它将改变(缩放)相机投影的水平(宽度)和垂直(高度)维度,这将表面上比仅改变相机投影平面的一个维度更好地保持纵横比。

接下来,让我们看一下 ParallelCamera 类,我们将在 Camera 子类的覆盖范围中包括它,以保持一致性,尽管这个相机更适合用于 2D 游戏和可能的正交 3D 应用。

JavaFX ParallelCamera 类:你的 2D 空间平行相机

JavaFX ParallelCamera 类也扩展了 Camera 类,并用于创建 ParallelCamera(对象),该对象用于渲染 i2D 场景。这个 ParallelCamera 类也保存在核心 javafx.scene 包的 javafx.graphics 模块中;它是 Node 的子类,是 JavaFX 场景图顶部的一个节点。ParallelCamera 类还实现了 Styleable 接口,以便可以对其进行样式化,并实现了 EventTarget 接口,以便可以处理事件。因此,JavaFX ParallelCamera 类的 Java 类层次结构如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.Camera
      > javafx.scene.ParallelCamera

JavaFX 9 创建的默认相机将始终是一个 ParallelCamera,这就是为什么我们在本章中编写特定相机和 LightBase 对象创建的原因。例如,如果您只是创建了一个球体对象,而没有创建任何 Camera 子类对象或任何 LightBase 子类对象,JavaFX 运行时将自动创建一个 ParallelCamera 对象和一个 AmbientLight 对象,以便 Shape3D 子类(球体)对渲染器可见。

如果一个场景只包含 2D 变换,那么它不需要透视相机,因此会使用平行相机,这不会渲染 3D 对象的所有特征。ParallelCamera 更适合 Java 8 游戏开发入门(Apress,2014)中所涉及的内容。这种相机定义了平行的观察体积,在 3D 行业中也称为正交投影。实质上,正投影相当于一个矩形平面。

ParallelCamera 始终位于窗口的中心,并将沿着 z 轴的正方向看。ParallelCamera(相对于 PerspectiveCamera)的不同之处在于,该相机定义的场景坐标系的原点位于屏幕的左上角,y 轴位于屏幕的左侧,x 轴位于屏幕顶部的右侧,z 轴指向远离观察者的方向(屏幕表示中的距离)。

ParallelCamera 对象中使用的单位使用像素坐标表示,因此这就像 2D 数字成像软件包和我们用于 2D UI 设计的 2D StackPane 图像合成层对象一样,它也引用屏幕左上角 0,0 (X,Y)处的坐标。这是另一个事实,这是一个更符合逻辑的相机子类,用于 i2D 游戏,而不是 i3D 游戏。

这个类只有一个构造函数方法,它使用一个空的参数区域,如下所示:

Camera2D = new ParallelCamera();

接下来,让我们看看如何创建 PerspectiveCamera 对象,我们将在 pro Java 9 游戏中使用它。我们将了解如何对其进行初始配置,以及如何将其添加到 JavaFX SceneGraph 的根目录中。

向场景添加透视相机:使用。setCamera()

我们需要添加到 JavaFXGame 类顶部的第一件事是使用 PerspectiveCamera 摄像机声明 PerspectiveCamera 对象;Java 语句,它将在 PerspectiveCamera 对象下显示一个红色波浪下划线指示器(类用法)。使用 Alt+Enter 快捷键让 NetBeans 9 为您编写 import 语句,然后打开 createSceneGraphNodes()方法,以便您可以将该照相机对象添加到场景图的顶部(根)。使用 camera = new perspective camera(true)在根组实例化下实例化这个 camera 对象;构造函数语句。然后,在下一行中,使用-1000 值调用. setTranslateZ()方法,将相机从 3D 场景的 0,0,0 中心移动 1,000 个单位。

使用. setNearClip()方法调用摄影机对象,将 nearClip 摄影机对象属性设置为 0.1,并使用。setFarClip()方法调用。最后,使用。setCamera()方法调用了场景对象,并在.setCamera( camera )方法调用中使用相机对象作为参数传递相机对象。将场景对象的背景值设置为颜色。通过使用。setFill()方法调用,这样您的 3D 对象会很突出,使用这些 Java 语句:

PerspectiveCamera camera;
...
createSceneGraphNodes() {
    camera = new PerspectiveCamera(true);
    camera.setTranslateZ(-1000);
    camera.setNearClip(0.1);
    camera.setFarClip(5000.0);
...
    scene.setFill(Color.BLACK);
    scene.setCamera(camera);

正如你在图 11-1 中看到的,你的代码是没有错误的,摄像机现在已经设置好并连接到你的 3D 场景,我们现在已经把它转换成一个 3D 场景。它现在是一个 3D 场景,因为它在其渲染管道的顶部(即在其根处)使用了 PerspectiveCamera,所以它下面的所有对象现在都将使用 3D 透视图。

A336284_1_En_11_Fig1_HTML.jpg

图 11-1。

Add a PerspectiveCamera object declaration at the top of the class and then instantiate it and configure it

接下来,让我们使用运行➤项目工作流程来看看您通过添加 PerspectiveCamera 创建的新 3D 场景如何影响您现有的 2D UI 设计;现在,这是一个“混合”2D 和 3D 应用,是 JavaFX 应用的最高级类型。这是真的,因为我们需要在一个无缝合成环境中结合 2D 和 3D 素材,这是一个极其复杂的任务。两个最先进的电影和特效合成软件包 Fusion 和 Nuke 完成了 2D 与 3D 的融合。事实上,如果你想了解更多关于将 2D 和 3D 素材结合到一个管道中的信息,请查看 VFX 基本面(Apress,2016)。正如你在图 11-2 中看到的,你的场景图根的 StackPane 分支,以及它下面的所有东西,都被它(正确地)携带着,并且现在从层次顶部的 PerspectiveCamera 和它的屏幕的 0,0,0 中心(视觉上)引用,你在这一章的前面已经学过。

A336284_1_En_11_Fig2_HTML.jpg

图 11-2。

Run the Project and notice that the StackPane is now located at the PerspectiveCamera 0,0,0 center origin

我尝试的第一件事是通过使用。settranslate(-640)和。setTranslateY(-320),这在一定程度上起了作用,因为结果看起来像图 11-2;但是,它在左上角,整个 StackPane 布局可见,并且缩小了 200%(四倍,或四分之一屏幕)。

这告诉我,StackPane 是一个 2D 对象,从技术上讲是一个“平面”,它与相机投影平面“完全平行”,面向相机对象的 z 轴。相比之下,现在 StackPlane 是 3D 渲染管道的一部分,因为它是 PerspectiveCamera 的子级(在渲染器处理管道之下)。

这意味着 StackPane 及其所有子元素(VBox、ImageView 和 TextFlow)都是通过 PerspectiveCamera 对象进行处理的。这包括它的所有算法和坐标系统(以及类似的“交战规则”,如果你愿意的话),所有这些都改变了它将被渲染到屏幕(场景对象)的方式和位置。

我尝试的下一件事是使用。setTranslateX)和。setTranslateY(0)方法调用。这是通过添加以下两个 Java 语句来实现的,将 StackPane 重定位到。start()方法位于 uiLayout StackPane 对象实例化 Java 语句之后。

这里显示了这些 Java 代码,并且在图 11-3 的中间附近用蓝色突出显示:

A336284_1_En_11_Fig3_HTML.jpg

图 11-3。

Add the .setTranslateX() and .setTranslateY() method calls off the uiLayout StackPane object, both set to zero

uiLayout = new StackPane();
uiLayout.setTranslateX(0);
uiLayout.setTranslateY(0);

注意图 11-3 中您正在使用。setTranslateX()和。setTranslateY()在 logoLayer ImageView 上,以及在 infoOverlay TextFlow 上,每个都保持它们相对于 uiLayout StackPane 的位置。

这种相对定位的保留是因为您已经在 SceneGraph 层次中建立了父子关系,这就是为什么这是一个强大的场景构造工具,适用于任何类型的场景,无论是 i2D、i3D 还是混合场景。这也将是非常重要的,因为我们在本书中开发了你的 pro Java 9 游戏的 i3D 部分,因为我们将需要在你的游戏的 3D 部分进行更多的整体转换,而不是简单地将你的 UI 控制面板放在相机前面,这样它就会阻挡 3D 游戏的视图(至少现在是这样;随着我们不断完善 Java 代码和游戏设计,我们可能会在以后更改这个 UI 设计)。这正是游戏设计和编码在现实生活中发生的方式;游戏开发是一个旅程,而不是目的地。

使用运行➤项目工作流程,看看我们是否更接近同步您的 2D UI 覆盖图及其背后的 3D 场景。正如你在图 11-4 中看到的,UI 面板现在在你屏幕的中央,尽管缩小了。因此,我们将继续细化我们的对象属性。接下来,我们将使用相机对象的 Z 平移变量来使相机更接近 3D 场景,以实现我们想要的最终结果。

A336284_1_En_11_Fig4_HTML.jpg

图 11-4。

Run the project; your StackPane is now centered, but your camera object Z translation is too far out

我认为这是因为相机的 Z 平移距离这个新的 3D 场景的中心 1000 个单位。因此,接下来我将尝试将 camera.setTranslateZ()方法调用参数从-1000 减少到-500,以查看 3D 合成中 2D 的最终变化。

完成这一修改的 Java 代码应该如下所示,并在图 11-5 的顶部以蓝色突出显示:

A336284_1_En_11_Fig5_HTML.jpg

图 11-5。

Move the camera object 50 percent closer to the 3D scene projection plane by setting .setTranslateZ() to -500

camera.setTranslateZ(-500);

再次使用“运行➤项目”工作流程。正如你在图 11-6 中看到的,你的 UI 屏幕现在大了 50 %,所以我们需要缩小我们的。将 TranslateZ()设置为零,使 StackPane 与 3D 场景投影平面同步。

A336284_1_En_11_Fig6_HTML.jpg

图 11-6。

Run the project to see that the StackPane is still centered, but the camera Z translate value is still too far out

完成此任务的 Java 代码如下所示,在图 11-7 中可以看到用蓝色突出显示的代码:

A336284_1_En_11_Fig7_HTML.jpg

图 11-7。

Set the camera.setTranslateZ() method call to zero to synchronize the StackPane and projection plane

camera.setTranslateZ(0);

接下来,使用“运行➤项目”工作流程来查看您是否已经实现了将 StackPane 节点分支及其子节点与 3D 相机对象的投影平面同步的视觉目标。正如你在图 11-8 中看到的,你的 UI 屏幕看起来很棒,按钮也工作正常。

A336284_1_En_11_Fig8_HTML.jpg

图 11-8。

Use Run ➤ Project to see that your StackPane is perfectly synchronized (visually) with the camera projection plane

在我们开始学习 LightBase(超类)对象以及 AmbientLight 和 PointLight 子类之前,让我们确保我们之前的所有其他 2D UI 代码仍在工作,并做我们希望它做的事情。当向 Java 9 代码添加主要特性或更改时,花时间做这件事总是很重要的。

StackPane UI 测试:确保其他一切仍然工作

点击游戏规则按钮,如图 11-8 所示,并确保游戏说明的用户界面屏幕在外观上仍然可读和专业,尽管我们将在游戏发布前进一步完善该用户界面。从图 11-9 中可以看到,指令屏幕确实仍然可读;然而,颜色。白色背景颜色已被替换为彩色。黑色因为我们设置了新的 3D 场景对象来使用这个为其填充颜色值,如图 11-1 所示,使用了一个scene. setFill (Color. BLACK );的 Java 语句。这意味着我们现在需要将 StackPane 的背景颜色值设置为 Color。白色在场景合成(现在是渲染)管道中用白色填充我们的 UI 屏幕。由于 StackPane 位于场景之上,VBox、ImageView 和 TextFlow 之下,因此这是要设置颜色的逻辑对象。白色背景填充颜色。这将涉及到在位于。start()方法,而不是更改许多与设置文本对象颜色和 DropShadow 属性相关的 Java 语句,更不用说使用。setLightness()方法调用来使标题图像的文本元素变亮。这也让我有机会向您展示如何避开 StackPane 对象(类)没有. setFill()方法的限制,这意味着我们必须创建一个复杂的方法链,其中包含两个嵌套的“方法内部的对象实例化”Java 构造,我们在. setBackground(Background)方法调用中创建一个新的 Background 对象和一个新的 BackgroundFill 对象,并将 BackgroundFill 配置为白色。

A336284_1_En_11_Fig9_HTML.jpg

图 11-9。

Click the Game Rules Button control to see whether the Instructions section is rendering correctly. It is now black!

这个方法调用的基本 Java 9 编程语句结构(包含两个嵌套的对象实例化)将如下所示(最初;接下来我们会进一步配置)Java 9 编程结构:

uiLayout.setBackground( new Background( new BackgroundFill(Color.WHITE) ) );

这显示在图 11-10 中,尽管在 BackgroundFill 下有红色波浪下划线,因为我们需要使用 Alt+Enter 击键组合让 NetBeans 从 javafx.scene.layout (package)导入 BackgroundFill 类。这在图中也以蓝色突出显示为“Add import for Java FX . scene . layout . background fill”,双击它可以让 NetBeans 9 为您编写此导入语句。

A336284_1_En_11_Fig10_HTML.jpg

图 11-10。

Code your private void createSpecialEffects() method body to create and configure a DropShadow object

NetBeans 从最基本的(无导入语句)开始评估问题。因此,一旦为 BackgroundFill 对象准备好了 import 语句,NetBeans 就会继续评估该语句,从内部(BackgroundFill)到外部(到新的背景对象,再到。setBackground()方法调用)。

原来 BaclgroundFill 类的构造函数方法需要几个参数,而不仅仅是颜色。白色填充规格。这是因为 BackgroundFill 类将创建圆角并支持 Insets 对象规范,因此 BackgroundFill 构造函数的正确构造函数方法格式应该如下所示:

backgroundFill = new BackgroundFill(Paint, CornerRadii, Insets);

因此,对于我们的用法,一个完整的白色背景填充构造函数方法将使用 EMPTY 常量,没有任何填充或圆角边缘,因此看起来像下面的 Java 实例化:

new BackgroundFill( Color.WHITE, CornerRadii.EMPTY, Insets.EMPTY );

一旦导入了 BackgroundFill 类,NetBeans 就会向我们显示出new BackgroundFill(Color.WHITE)对象实例化有问题,这通过整个代码结构下面的红色波浪下划线来表示,如图 11-11 所示。我将鼠标放在我正在构建的 Java 语句部分,NetBeans 9 会弹出对该问题的解释,它显示在浅黄色框中,周围有黑色轮廓。

A336284_1_En_11_Fig11_HTML.jpg

图 11-11。

Set uiLayout StackPane background to white using .setBackground(new Background(new BackgroundFill(Color.WHITE)));

我想要白色填充,所以我用了圆角半径。空和 Insets。按照构造函数方法参数的要求,最后两个参数按此顺序为空。最终的方法调用是这样的,如图 11-12 所示:

A336284_1_En_11_Fig12_HTML.jpg

图 11-12。

Add BackgroundFill constructor method parameters (Paint, CornerRadii, and Insets) to the method call

.setBackground(new Background(new BackgroundFill(Color.White,CornerRadii.EMPTY,Insets.EMPTY) ));

最后,使用“运行➤项目”工作流程来查看您是否已经完成了白色背景色填充的目标。正如您在图 11-13 中看到的,您的 UI 屏幕看起来又很棒,UI 按钮也工作正常。

A336284_1_En_11_Fig13_HTML.jpg

图 11-13。

The StackPane now has a Color.WHITE background fill, preventing scene Color.BLACK from showing

让我们在 Legal 和 Credits 按钮事件处理结构中实现这一修复,并将我们的应用恢复到 100%的工作状态。如图 11-14 所示,我已经将这个 uiLayout StackPane 对象后台属性配置 Java 9 代码结构复制并粘贴到您的 legalButton 和 creditButton 事件处理基础设施中,代码编译无误。

A336284_1_En_11_Fig14_HTML.jpg

图 11-14。

Copy and paste the uiLayout.setBackground() construct from helpButton to legalButton and creditButton

如果您使用您的运行➤项目工作流程并测试这三个按钮 UI 元素,您将会看到,由于我们使用了 StackPane 的背板(背景对象)来保存设置为 Color.WHITE 的颜色辅助类常量的 Paint 对象,您在前面章节中所做的所有艰苦设计工作都已完全恢复

实现开始游戏按钮:隐藏你的用户界面

我们要做的下一件事是注释掉 gameButton 事件处理程序代码中的所有代码(这样我们可以在以后恢复它们,如果我们想的话),然后添加一些新的语句,这些语句将隐藏(将 visibility 设置为 false)SceneGraph 的 StackPane 分支;我们还将把 camera.setTranslateZ()方法调用设置为我们最初想要使用的-1000 值。当我们构建游戏时,我们将添加额外的配置和控制语句到这个关于 i3D 游戏的按钮中,正如你现在看到的,它将“活”在 StackPane UI 控制面板后面。

正如你在图 11-15 中看到的,我已经注释掉了与你的 StackPane UI 合成管道相关的代码;我添加了关于从视图中移除 UI 控制面板以及将 3D 场景相机对象设置到游戏开始时我们想要的位置的语句。新的代码语句看起来像下面的 Java 代码,并在图 11-15 中间高亮显示:

A336284_1_En_11_Fig15_HTML.jpg

图 11-15。

Add a .setVisible(false) method call off of uiLayout and a .setTranslateZ(-1000) method call off the camera

uiLayout.setVisible(false);
camera.setTranslateZ(-1000);

现在,当您使用“运行➤项目”工作流程并单击“开始游戏”按钮时,您的 StackPane 将会消失,空的(黑色)3D 场景将会显示出来。

现在是时候使用 JavaFX 9 LightBase 超类及其 AmbientLight 和 PointLight 子类来学习 3D 场景照明了。在我们的 JavaFXGame 类中实现它们之前,在我们结束关于核心 3D 场景元素(相机和灯光基础)的这一章之前,我们将详细介绍这些元素,这些元素需要在我们的 i3D pro Java 9 游戏设计和开发管道中作为 SceneGraph 的根。开始兴奋了吗?灯光,摄像机…动作事件!

使用 3D 照明:为 3D 游戏添加照明

JavaFX 9 中有两组不同的照明 API。一个是用于 3D 场景的,包含在 javafx.scene 包中,提供了一个抽象的 LightBase 超类和“concrete”(可在代码中作为可构造的对象使用)子类 AmbientLight 和 PointLight。另一个是抽象的 Light 超类,包含在 javafx.scene.effect 包中;这个包包含 2D 数字成像效果,正如我们在本书前面提到的。对于 3D 的使用,我们将重点放在灯光基础,环境光线和点光源类上,最初使用点光源类,因为我们可以使用该类获得最生动和真实的效果。

JavaFX LightBase 类:定义光的抽象超类

公共 JavaFX LightBase 超类是一个抽象类,仅用于创建不同类型的灯光。目前,3D 场景有一个普通或“环境”级别的照明,由模拟灯泡属性的 AmbientLight 子类(对象)或 PointLight 子类(对象)提供。您的应用不应试图直接扩展抽象的 LightBase 类;如果您尝试这样做,Java 将抛出一个 UnsupportedOperationException,您的 pro Java 9 游戏将无法编译或运行。LightBase 类保存在核心 javafx.scene 包的 javafx.graphics 模块中,是 Node 的子类,因为它最终是 SceneGraph 顶部的一个节点。LightBase 类实现了一个 Styleable 接口,这样它就可以被样式化,还实现了一个 EventTarget 接口,这样它就可以处理事件。因此,JavaFX LightBase 类的 Java 9 类层次结构如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.LightBase

LightBase 类为构造对象的子类提供了通用属性的定义,这些对象用于在 3D 场景中表示(“投射”)某种形式的光。这些 LightBase 对象属性应该包括光源的初始颜色,以及光源最初是打开(启用)还是关闭(禁用)。请务必注意,由于这是一个 3D 特征,因此它是一个有条件的特征。参考我在本章的 PerspectiveCamera 一节中给出的例子,了解如何设置代码来检测 ConditionalFeature。SCENE3D 标志。

LightBase 子类有两个属性(或者属性或特征,如果你喜欢这些术语的话);一个是 color 或 ObjectProperty ,它指定从光源发出的光的颜色,第二个是一个名为 lightOn 的 BooleanProperty,它允许打开和关闭光。

LightBase 抽象类有两个重载的受保护构造函数方法。一个没有参数,创建默认颜色。白光源,使用此构造函数方法调用格式:

protected LightBase()

第二个重载的受保护构造函数方法允许子类使用下面的构造函数方法调用格式为灯光指定颜色值:

protected LightBase(Color color)

LightBase 类有七个方法,每个 LightBase 子类都可以使用(继承)这些方法,包括 AmbientLight 和 PointLight 子类,所以在这里要注意这些方法,因为我只介绍一次。

colorProperty()方法指定光源的 ObjectProperty ,而 getColor()方法获取光源的颜色值属性。getScope()方法将获得一个 ObservableList ,其中包含一个节点列表,该列表指定了 LightBase 子类(object)的层次结构范围。

isLightOn()方法调用返回光源的布尔值 On(真)或 off(假),lightOnProperty()方法调用将设置光源 BooleanProperty lightOn 的布尔数据值。

最后,void setColor(Color value)方法将设置 light color 属性的数据值,void setLightOn(boolean value)方法将设置 LightBase 子对象 lightOn boolean value 属性的数据值。

接下来,让我们分别仔细看看 AmbientLight 和 PointLight 具体类。

JavaFX AmbientLight 类:均匀地照亮 3D 场景

公共 JavaFX AmbientLight 类是一个具体的类,用于为 3D 场景创建常规或“环境”级别的照明。对于给定的 3D 场景实例,通常只定义一个 AmbientLight 实例。AmbientLight 类保存在核心 javafx.scene 包的 javafx.graphics 模块中,是 LightBase 的子类,light base 是 Node 子类,因为它最终是 SceneGraph 顶部的节点。AmbientLight 类还实现了一个 Styleable 接口,以便可以对其进行样式化,还实现了一个 EventTarget 接口,以便可以处理事件。因此,JavaFX AmbientLight 类的 Java 类层次结构如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.LightBase
      > javafx.scene.AmbientLight

如果需要,AmbientLight 类为 3D 场景定义环境光源对象。环境光可以被定义为来自看起来从每个方向进入场景的看不见的光源对一个区域的整体或一般的照明量。所有 AmbientLight 对象属性都继承自 LightBase 超类,并且应该包括光源的初始颜色以及光源最初是打开(启用)还是关闭(禁用)。同样需要注意的是,由于这是一个 3D 特征,因此它是一个有条件的特征。

AmbientLight 有两个重载的构造函数方法;第一个使用(默认)颜色创建一个未配置的 AmbientLight 对象类。白色光源,使用以下 Java 实例化编程格式:

AmbientLight ambient = new AmbientLight();

第二个重载构造函数方法使用非 color 的指定颜色创建一个新的 PointLight 实例。白色,使用以下 Java 实例化编程格式:

AmbientLight ambientaqua = new AmbientLight(Color.AQUA);

接下来,让我们详细了解一下点光源的具体类,然后我们可以在本章结束之前将点光源对象添加到您的 3D 场景中,在本书的剩余部分中,我们可以将 3D 对象放入 3D 渲染场景环境中。

JavaFX PointLight 类:戏剧性地照亮你的 3D 场景

公共 JavaFX PointLight 类是一个具体的类,用于为 3D 场景创建局部或“点源”照明实例。3D 场景中通常有多个点光源实例,以允许艺术家实现模拟真实世界光源的复杂照明模型。PointLight 类保存在核心 javafx.scene 包的 javafx.graphics 模块中,是 LightBase 的子类,light base 是 Node 子类,因为它最终是 SceneGraph 顶部的节点。PointLight 类还实现了一个 Styleable 接口,以便可以对其进行样式化,还实现了一个 EventTarget 接口,以便可以处理事件。因此,JavaFX PointLight 类的 Java 类层次结构如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.LightBase
      > javafx.scene.PointLight

PointLight 类根据需要为 3D 场景定义点光源(比如灯泡)对象。尝试使用尽可能少的点光源对象,因为它们的渲染成本很高(计算或处理它们的算法)。点光源被定义为局部光发射点,可以制作动画来创建各种特殊效果。所有点光源对象属性都继承自 LightBase 超类,并且应该包括光源的初始颜色以及光源最初是打开(启用)还是关闭(禁用)。同样需要注意的是,由于这是一个 3D 特征,因此它也是一个条件特征。

聚光灯有两个重载的构造方法。第一个使用默认颜色创建一个未配置的点光源对象类。白色光源,使用以下 Java 实例化编程格式:

PointLight light = new PointLight();

第二个重载构造函数方法使用非 color 的指定颜色创建一个新的 PointLight 实例。白色,使用以下 Java 实例化编程格式:

PointLight aqualight = new PointLight(Color.AQUA);

接下来,让我们仔细看看添加一个 PointLight 对象作为 JavaFXGame 类基础设施的光源的工作过程。

为游戏的 3D 场景添加灯光:使用点光源对象

接下来,让我们在 JavaFXGame 代码中添加一个点光源,这样我们就可以在下一章学习 3D 图元了。在 JavaFXGame 类顶部声明一个名为 Light 的 PointLight 对象;然后使用 Alt+Enter 组合键弹出帮助器弹出窗口,选择“为 javafx.scene.PointLight 添加导入”选项,如图 11-16 底部用黄色和蓝色突出显示。

A336284_1_En_11_Fig16_HTML.jpg

图 11-16。

Declare a PointLight named light at the top of your JavaFXGame class and hit Alt+Enter and Add Import.

因为你还需要灯光照明,在你的点光源声明后添加一个球体声明,这样我们就有东西来测试我们的代码了,如图 11-17 顶部用黄色突出显示的。接下来,在 scene.setCamera(相机)之后实例化你的点光源;方法调用。我使用了第二个更显式的构造函数方法,但给了它默认的颜色。白色,在我们看了材质以及它们如何与光色值相互作用之后,我们可能会改变它。使用light.setTranslateZ(-25);方法调用将灯光向下移动一点,使其不在球体内部(在 0,0,0 处)。接下来,使用一个light.getScope().add(sphere);方法链,将球体对象添加到点光源对象“看到”的范围内请注意,这允许您让不同的灯光对象影响 3D 场景中的不同 3D 对象,这是一个非常强大的功能。图 11-17 底部突出显示了点光源和球体对象声明、实例化和配置 Java 语句的 Java 代码,应该类似于以下 Java 代码:

A336284_1_En_11_Fig17_HTML.jpg

图 11-17。

Declare a sphere and light object, and in createBoardGameNodes instantiate and configure them for use.

PointLight light;
Sphere sphere;
...
private void createBoardGameNodes() {
    ...
    light = new PointLight(Color.WHITE);
    light.setTranslateY(-25);
    light.getScope().add(sphere); // "Wire" the Sphere and Light together via .getScope().add()

    sphere = new Sphere(100);
    ...
}

你需要做的最后一件事是将你的点光源和球体连接到彼此,连接到相机,连接到场景图的游戏板 3D 分支。getChildren()。在您的。addNodesToSceneGraph()方法。

图 11-18 中间突出显示了 addNodesToSceneGraph()方法 Java 语句的代码,看起来应该是这样的 Java 方法体:

A336284_1_En_11_Fig18_HTML.jpg

图 11-18。

Add the sphere to the gameBoard branch of the root Node so the primitive is added to your scenegraph.

private void addNodesToSceneGraph() {
    root.getChildren().addAll(gameBoard, uiLayout);
    gameBoard.getChildren().add(sphere);
    uiLayout.getChildren().addAll(boardGameBackPlate, logoLayer, infoOverlay, uiContainer);
    ...
}

使用您的“运行➤项目”工作流程,确保我们在本章中完成的所有代码升级和添加都正常工作,并为您提供在 pro Java 9 游戏开发的早期阶段应该预期的最终结果。

确保三个(中间)游戏规则、免责声明和游戏积分按钮已经恢复到最大工作容量(现在再次填充白色背景)。此外,在 NetBeans 9 中,“高分”按钮仍然应该向输出控制台打印出一条文本消息,并且“开始游戏”现在应该移除 StackPane uiLayout 覆盖面板并显示 3D 场景。

3D 场景中应该有一个 3D 球体对象图元,我们将在下一章学习 3D 场景中的 3D 对象,在 3D 行业中称为模型、几何体、网格和图元。因为 3D 图元还没有纹理映射或颜色值,并且因为点光源对象被设置为颜色。白色,这应该是一个被白光照亮的浅灰色球体。

正如你在图 11-19 中看到的,开始游戏按钮控件现在隐藏了闪屏、UI 设计、按钮控件、文本流、文本元素和格式的整个 2D 合成管道,只需要一个简单的 Java 语句uiLayout.setVisible(false);,因为我们已经在本书中设置了 StackPane 父节点和 VBox、ImageView 和 TextFlow 子层次结构。一旦管道从视图中隐藏,我们就可以看到 3D 场景,为了测试 PerspectiveCamera 和 PointLight 对象,我们临时添加了一个球体对象原语。

A336284_1_En_11_Fig19_HTML.jpg

图 11-19。

Use your Run ➤ Project work process and test the 3D Scene infrastructure that you have put into place

我们现在能够使用 JavaFX APIs 进行 3D 建模和 3D 纹理映射。

摘要

在第十一章中,我们通过添加 PerspectiveCamera 对象为JavaFXGame.java添加了 3D 场景功能,该对象允许使用 X、Y 和 Z 维度以及场景对象的 3D 透视图来渲染 3D 素材。我们还添加了一个点光源对象来模拟灯泡光源来照亮这些 3D 素材,以及一个球体对象(“基本体”)来测试我们的基本 3D 场景设置。

您学习了抽象的 Camera 超类及其 ParallelCamera(用于 2D 或正交 3D 场景)和 PerspectiveCamera,我们将使用它们进行最有效的 3D 或 i3D 场景渲染。然后,我们学习了如何在 JavaFXGame 中声明、实例化和配置 PerspectiveCamera,改变它的操作方式。

然后,我们测试了我们的 2D UI 元素和层次结构,并观察到它们现在位于 3D 空间中的 2D“平面”上。我们修改了 Java 代码来补偿坐标空间的变化,将用户界面恢复为全屏。

然后,我们测试了所有的 UI 按钮对象,发现我们新的 3D 场景黑色背景颜色影响了我们的信息屏幕,并且非常巧妙地使用了一个复杂的嵌套 Java 语句来创建和插入颜色。白色背景将对象填充到 StackPane 对象的背景对象中。这解决了这个问题,用白色填充替换了一个合成层的透明度,并在我们现在的混合 3D 和 2D 合成管道中添加了另一个不透明层。这个问题解决后,我们改变了 gameButton 事件处理程序中的逻辑,允许最终用户通过隐藏 UI 覆盖来启动游戏,并显示正确点亮的测试球体原语。

在下一章中,我们将了解 JavaFX Shape3D 超类及其子类,继续学习创建 pro Java 9 游戏的 i3D 部分所需的基础知识。

十二、3D 模型设计和原语:使用 JavaFX 9 Shape3D

现在,您已经通过将相机对象添加到场景根和专门设计用于 3D 资源的点光源对象完成了基本(空)3D 场景的设置,让我们开始了解一些关于 3D 资源本身的基础知识。这些素材以预定义的基本 3D 形状(称为图元)以及更多定制 3D 几何素材(业内通常称为网格或线框 3D 素材)的形式出现。JavaFX 9 在 javafx.graphics 模块的javafx.scene.shape包中提供了七个类,专门为您创建 3D 几何图形(图元或网格),我们将在本章中了解它们。在第十二章中,我们还将回到 JavaFXGame 主应用类编码,并开始向场景图的游戏板组节点添加 3D 图元,以练习向 JavaFXGame 应用添加 3D 素材。虽然我们可以在 Blender 等 3D 软件包中完成这项工作,但棋盘游戏非常简单(正方形、球形、圆柱形),我们完全可以在 JavaFX 代码中完成这项工作,这意味着我们不需要导入(和分发)3D 模型,而是可以编写代码来“凭空”模拟您的 i3D 游戏。这也将教会您更多关于 Java 9 和 JavaFX 9 中的 3D APIs,因为您可以学习如何仅使用最新的 Java 和 JavaFX APIs 来建模复杂的对象(例如您的棋盘游戏的游戏板)。

在本章中,您将了解 javafx.scene.shape 包中包含的不同类型的 JavaFX 3D 类。我们将讨论球体,它可以用来创建一个球体,你已经在第十一章中使用它来测试你的 3D 场景设置。我们还将看看另外两个基本类,Box 和 Cylinder,它们可以用来创建平面和圆盘基本体。这些原语基于 Shape3D 超类,我们将首先研究这个超类。我们还将了解更高级的 TriangleMesh 类,它允许您构建基于多边形的网格对象,最后是 Mesh 和 MeshView 类层次,它允许您渲染在外部 3D 建模和渲染软件包(如 Blender 2.8(开源)或 Autodesk 3D Studio Max(付费软件包))中创建的 3D 网格对象。

JavaFX Shape3D 超类:图元或网格视图

公共抽象 Shape3D 超类用于创建四个主要的 3D 类:长方体、球体、圆柱体和网格视图。您将使用这些类为您的 pro Java 9 游戏开发创建和显示 3D 素材。其中三个子类创建图元,这些图元是通过算法创建的预定义 3D 对象,MeshView 子类允许在 3D 场景中渲染基于多边形几何体的更详细的复杂 3D 模型。需要注意的是,还有一个 javafx.scene.shape.Shape 超类与 javafx.scene.shape.Shape3D 不相关(类层次结构方面);它用于 2D 形状,如 SVG 2D 数字插图语言中常见的形状,这在 Java 8 游戏开发入门(Apress,2014)和数字插图基础(Apress,2016)中有所介绍。

Shape3D 超类是 Node 的子类,正如我们将在 JavaFXGame 代码中使用的大多数具体类一样。像 Camera 和 LightBase 超类一样,这个 Shape3D 超类实现了 Styleable 和 EventTarget 接口,这样它的子类(对象)就可以被样式化和处理事件(可以是交互式的)。因此,Java 9 类层次结构跨越了 Java 和 JavaFX APIs,如下所示:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.shape.Shape3D

创建 Shape3D 基类(抽象的或未直接实例化的)是为了给表示 3D 几何形状的 3D 对象提供公共属性的定义。三个主要的 3D 属性包括“材质”(或着色器和纹理贴图),应用于形状的可填充内部或形状的轮廓,我们将在第十三章中讨论;“绘制模型”属性,定义 JavaFX 9 渲染引擎如何向查看者呈现几何图形(作为实体或线框模型);以及定义要剔除哪些面的“面剔除”属性。面剔除是一种优化,渲染引擎将通过不渲染场景中模型中的所有多边形来获得更好的性能(更快的 FPS)。由于渲染器正在从相机拍摄 3D 场景并渲染 2D 视图,因此这种“背面剔除”不会渲染模型背向相机(对相机不可见)的部分上的任何面(多边形)。正面剔除将进行相反的操作,只渲染背面的多边形,这基本上是渲染多边形的内部,模型正面(多边形)变得隐藏或不可见。还有 CullFace。关闭面剔除优化算法的 NONE 常量。卡夫斯。“后退”是默认设置,也是您通常想要使用的设置,除非您使用的是 CullFace。前面得到一些特殊的内部体绘制效果,这一章结束后,你会知道如何实验,如果你愿意的话。

如你所知,3D 渲染,以及 Shape3D 的任何子类,都是一个条件特性,你可以在你的代码中检查,就像我们在上一章中提到的那样。让我们深入了解任何 Shape3D 子类对象的三个对象设置(属性、特性、特征),它们定义了 3D 渲染引擎将如何渲染它。

cullFace 对象属性将定义哪个 CullFace 优化算法(前、后或无)将用于此 Shape3D 对象。这很可能会影响专业 Java 9 3D 游戏的性能。

drawMode ObjectProperty 将定义用于呈现 Shape3D 对象的绘制模式。您的两个选项包括 DrawMode。实心 3D 对象的填充和绘制模式。线框表示的线。

材质对象属性定义了 Shape3D 对象将用作“皮肤”的材质我们将在第十三章中学习所有关于着色算法、材质和纹理贴图的知识,这一章涵盖了材质。

抽象 Shape3D 超类的受保护(不可直接使用)构造函数如下所示:

protected Shape3D()

现在让我们进入将成为所有 Shape3D 子类的一部分的方法。这很方便,因为我们可以在一个地方涵盖所有这些方法。这些可以用在任何原始三维形状或网格视图。

那个。cullFaceProperty()方法为 Shape3D 对象定义 ObjectProperty ,而。getCullFace()方法允许您轮询 Shape3D 对象的当前 CullFace 常量设置。还有就是。setCullFace(CullFace value)方法,该方法允许您更改 Shape3D 对象的 CullFace 常量设置。

的。drawModeProperty()方法为 Shape3D 对象定义 ObjectProperty ,而。getDrawMode()方法允许您轮询 Shape3D 对象的当前 DrawMode 常量设置。还有就是。setDrawMode(DrawMode 值)方法,该方法允许您更改 Shape3D 对象的 DrawMode 常量设置。

那个。materialProperty()方法为 Shape3D 对象定义 ObjectProperty ,而您的。getMaterial()方法允许您轮询 Shape3D 对象的当前材质对象设置。还有就是。setMaterial(Material value)方法,该方法允许您更改 Shape3D 对象的材质对象设置。

接下来,让我们单独看看 Shape3D 子类,因为我们将在 JavaFXGame 中利用它们。

JavaFX 球体:为 3D 游戏创建球体图元

因为我们已经在前一章中创建了一个名为 Sphere 的球体对象来测试 PerspectiveCamera 和 PointLight 3D 场景设置 Java 代码,所以让我们先来看看 Shape3D 子类。该类保存在 javafx.scene.shape 包中,是 Shape3D 的子类,因此它具有以下 Java 类层次结构:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.shape.Shape3D
      > javafx.scene.shape.Sphere

Sphere 类定义了一个具有指定大小的三维球体。球体是使用程序员输入的半径尺寸(大小)通过算法创建的 3D 几何图元。该球体最初总是以 3D 原点 0,0,0 为中心。因此,球体对象有一个定义球体半径的 radius DoubleProperty 以及三个从 javafx.scene.shape.Shape3D 继承的 cullFace、drawMode 和 material 属性。

Sphere 类包含三个重载的构造函数方法,其中一个没有参数,它创建一个半径为 1.0 的球体实例。这看起来像下面的 Java 9 Sphere 实例化:

sphere = new Sphere();

第二个构造方法,也就是我们在第十一章中使用的方法,允许你使用双数值指定半径。这类似于下面的球体实例化 Java 代码:

sphere = new Sphere(100);

第三个构造函数允许您通过多个分段参数指定半径和网格密度,类似于下面的 Java 语句,该语句创建了一个半径为 100 单位、分段数为 24 的球体:

sphere = new Sphere(100, 24)

除了从 Shape3D 类继承的方法之外,Sphere 类还有一些自己独特的方法,包括。getDivisions()方法,该方法轮询球体对象以查看它使用了多少个分区;那个。radiusProperty()方法,它定义了球体对象的半径;那个。getRadius()方法,获取当前半径的值;还有。setRadius(double value)方法,该方法将半径值设置为不同的值。

JavaFX 圆柱体:为游戏创建圆柱体或磁盘图元

接下来,让我们来看看公共的 Cylinder Shape3D 子类,它可以用来创建圆柱形 3D 对象,因为它是一个具体的(可用的)类,也实现了 Styleable 和 EventTarget 接口。该类保存在 javafx.scene.shape 包中,是 Shape3D 的子类,因此它将具有以下 Java 类层次结构:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.shape.Shape3D
      > javafx.scene.shape.Cylinder

Cylinder 类用于定义具有指定半径和高度的三维圆柱体。圆柱体是一种 3D 几何图元算法,采用半径(double)属性和高度(double)属性。它最初以 0,0,0 原点为中心,半径使用 z 轴方向,高度使用 y 轴方向。

除了半径和高度属性,它还将继承 Shape3D cullFace、drawMode 和 material 属性。它有三个重载的构造函数方法,一个是默认的(空),一个是半径和高度,第三个是半径、高度和等份。

第一个空构造函数方法创建一个半径为 1.0、高度为 2.0 的圆柱体对象的新实例。它具有以下 Java 语句格式:

cylinder = new Cylinder();

第二个构造函数方法使用开发人员指定的半径和高度创建一个圆柱体对象的新实例。它具有以下 Java 语句格式:

cylinder = new Cylinder(50, 250);

第三个构造函数方法使用开发人员指定的半径、高度和分辨率(确定平滑度的分割数)创建一个圆柱体对象的新实例。它具有以下 Java 语句格式:

cylinder = new Cylinder(50, 250, 24);

半径有三种方法,高度有三种方法,还有一种。getDivisions()方法用于轮询 Divisions 属性,该属性必须使用第三个构造函数方法格式进行设置,因为没有。setDivisions()方法调用或 divisionsProperty()方法调用。

双份。getHeight()方法将轮询(获取)圆柱体对象的 Height 属性的值。DoubleProperty heightProperty()方法定义圆柱体对象的高度属性或 Y 维度。最后,void setHeight(double value)方法允许开发人员设置圆柱体对象的 Height 属性值。

double getRadius()方法将轮询(获取)圆柱体对象的 Radius 属性的值。DoubleProperty radiusProperty()方法定义圆柱体对象的半径属性或 Z 维度。最后,void setRadius(double value)方法允许开发人员为圆柱体对象设置 Radius 属性的值。

最后,让我们来看一个 Box 原语类,它允许创建各种有用的形状。

JavaFX 盒子:为 3D 游戏创建盒子、柱子和平面

接下来,让我们来看看 public Box Shape3D 子类,它可以用来创建正方形、矩形和平面 3D 对象,因为它是一个具体的(可用的)类,也实现了 Styleable 和 EventTarget 接口。该类保存在 javafx.scene.shape 包中,是 Shape3D 的子类,因此它将具有以下 Java 类层次结构:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.shape.Shape3D
      > javafx.scene.shape.Box

Box 类定义了一个三维盒子,通常称为立方体图元,具有指定的大小。Box 对象是一个 3D 几何基本体,除了三个继承的 cullFace、drawMode 和 material Shape3D 属性之外,还具有三个 double 属性(深度、宽度和高度)。在实例化时,它最初以原点为中心。

Box 类有两个重载的构造方法。一个创建默认的 2,2,2 立方体,看起来像下面的 Java 代码:

box = new Box();

第二种构造函数方法允许您指定多维数据集的维度,如下所示:

box = new Box(10, 200, 10); // Creates a Post (or Tall Rectangle) Primitive
box = new Box(10, 0.1, 10); // Creates a Plane (or a Flat Surface) Primitive

正如您可能已经猜到的,Box 类中有九个可用的方法,每个属性三个。这是我们将用来创建大部分游戏板基础设施的类,所以我们可能会经常使用它们。

DoubleProperty depthProperty()方法用于定义盒子的深度或 Z 维度。double getDepth()方法可用于从 Box 对象获取(轮询)深度属性的值。void setDepth(double value)方法可用于为 Box 对象的 Depth 属性设置或指定新值。

DoubleProperty heightProperty()方法用于定义框的高度或 Y 维度。double getHeight()方法可用于从 Box 对象获取(轮询)Height 属性的值。void setHeight(double value)方法可用于设置或指定 Box 对象的 Height 属性的新值。

DoubleProperty widthProperty()方法用于定义宽度,即盒子的 X 维度。double getWidth()方法可用于从 Box 对象获取(轮询)Width 属性的值。void setWidth(double value)方法可用于设置或指定 Box 对象的 Width 属性的新值。

接下来,让我们看看在 JavaFXGame 代码中实际实现不同的原语需要做些什么!

使用原语:向 JavaFXGame 类添加原语

让我们将另外两个基本对象 Box 和 Cylinder 添加到 JavaFXGame 类中,这样我们就可以了解面剔除和绘制模式。我们将为它自己的章节 13 保存材料,因为着色器和纹理贴图值得它们自己的章节和重点讨论。在类的顶部声明一个名为 box 的 Box 对象,并使用 Alt+Enter 让 NetBeans 9 帮助您编写导入语句。正如您在图 12-1 中所看到的,将正确的类添加到您的 Java 9 游戏中是非常重要的,因为还有一个 javax.swing.Box 类(在弹出助手下拉列表中排在第二位)用于 2D UI 设计,并且在列表的顶部是(NetBeans 最佳猜测)用作 3D 原语的 javafx.scene.shape.Box!双击第一个(正确的)类,让 NetBeans 为您编写导入语句。

A336284_1_En_12_Fig1_HTML.jpg

图 12-1。

Declare a Box object at the top of the class; use Alt+Enter, and select Add import for javafx.scene.shape.Box

使用第二个构造函数在 createBoardGameNodes()方法中实例化 box 对象,如图 12-2 所示。请记住,您需要将这个 box 节点添加到。addNodesToSceneGraph()方法。

A336284_1_En_12_Fig2_HTML.jpg

图 12-2。

Instantiate the Box in createBoardGameNodes, and set the depth, height, and width to 100, 100, 100

这可以通过将您当前的gameBoard.getChildren().add(sphere); Java 语句修改为gameBoard .getChildren(). addAll (sphere, box );来轻松实现,如下图所示和图 12-3 :

A336284_1_En_12_Fig3_HTML.jpg

图 12-3。

Use the .addAll() method to add a box object to the SceneGraph in the addNodesToSceneGraph() method

box = new Box(100, 100, 100);                   // in .createBoardGameNodes() method

gameBoard.getChildren().addAll(sphere, box);   //  in .addNodesToSceneGraph() method

声明 Box 对象后,在 createBoardGameNodes()方法中实例化该对象,使用与球体相同的 100 单位值。您将能够看到大小之间的关系,因为它们都是在 0,0,0 处创建的。对于 Box 构造函数方法,这需要三个(double)值,它们都应该是 100。

接下来,在类的顶部声明一个名为 pole 的圆柱体,并在。createBoardGameNodes()方法,使用 50 的宽度、250 的高度和 24 的用于网格(线)绘制表示的部分或分割的数量。

这应该看起来像下面的 Java 代码,在图 12-4 中用黄色和蓝色突出显示:

A336284_1_En_12_Fig4_HTML.jpg

图 12-4。

Create a Cylinder object named pole and instantiate it with a radius of 50, a height of 250, and 24 divisions

Cylinder pole;                        // Declare object for use at the top of your class
...
pole = new Cylinder(50, 250, 24);     // in .createBoardGameNodes() method

如果您在此时使用 Run ➤项目工作流程,您将看不到 pole 对象,因为您还没有将它添加到 JavaFX 场景图中。打开 addNodesToSceneGraph()方法,将 pole 对象添加到参数区域(parens)内包含的 Java 列表的末尾。这都是通过在。handle()方法,如图 12-5 中间高亮显示:

A336284_1_En_12_Fig5_HTML.jpg

图 12-5。

Add a pole Cylinder object to SceneGraph, at the end of the gameBoard.getChildren().addAll() method call

gameBoard.getChildren().addAll(sphere, box, pole);    // in addNodesToSceneGraph() method

正如您将看到的,当我们在 3D 场景中渲染此代码时,您在 3D 合成中将对象添加到场景图的顺序与 2D 合成堆栈中的 2D 素材层顺序类似,因为 3D 图元将显示为“在彼此前面”越晚将对象添加到场景图中的游戏板组,越晚将它们渲染到屏幕上。因此,添加到场景图的最后一个图元将被渲染在它之前的所有其他图元之上,而添加到场景图的第一个图元将首先被渲染(即,在所有其他 3D 图元的下面或后面)。

在大多数 3D 软件包中,位于 0,0,0(场景中心)的三个图元将在彼此内部进行渲染。这告诉我们一些关于 JavaFX 的非常重要的 3D 艺术家的事情,那就是你不能使用 JavaFX 原语执行建设性的实体几何(CSG)建模。CSG 是 3D 建模的早期形式之一,涉及使用基本的 3D 图元结合布尔运算来创建更复杂的 3D 模型。

让我们使用您的运行➤项目工作流程,看看 JavaFX 是如何渲染这三个位于 0,0,0 的原语的。如图 12-6 所示,圆柱体对象在一个盒子对象的前面,盒子对象在一个球体对象的前面。大多数 3D 软件包会将其渲染为球体内部的一个盒子,可能盒子的角穿过球体(取决于比例),圆柱体的末端会从球体的顶部和底部出来。我按照这个特定的顺序做了这个练习,因为对于开发人员来说,在构建 Java 9 游戏时,意识到他们能做什么和不能做什么是至关重要的。您可以在 JavaFX 中实现这种布尔效果,方法是使用从三维建模器(如 MOI3D、SILO 或 Blender)导入的网格对象,其中布尔操作已在 JavaFX 9 外部完成。

A336284_1_En_12_Fig6_HTML.jpg

图 12-6。

Use the Run ➤ Project to see these three primitives in the Z-order that you added them to the SceneGraph

接下来,让我们使用一些 3D 图元修改(移动和旋转)方法调用,将它们从中心场景移开,并旋转立方体,使其看起来不像 2D 对象。这都可以通过使用。setTranslateX()和 setRotate()方法调用 box 和 pole 对象,如图 12-7 底部所示:

A336284_1_En_12_Fig7_HTML.jpg

图 12-7。

Use setTranslateX(250) to move primitives 250 units apart and use setRotate(45) to rotate the box 45 degrees

box.setTranslateX(500);
box.setRotate(45);
pole.setTranslateX(250);

接下来,使用“运行➤项目”工作流程来分别查看基本数据。如图 12-8 所示。setRotate()方法使用 z 轴进行旋转,因此您的 3D 对象仍然呈现为 2D 对象。让我们解决这个问题!

A336284_1_En_12_Fig8_HTML.jpg

图 12-8。

All three primitives are now spaced apart; the box still looks 2D

要更改您的。setRotate()方法用来配置它的旋转算法,还有第二个。setRotationAxis()方法,可用于更改默认旋转。旋转的 z 轴设置。x 轴常量,正如你在 Rotate 类中看到的点符号。

显然,正如您现在所了解的那样。setRotationAxis()方法调用必须在。setRotate(45)方法调用,以便在实际使用旋转算法之前改变旋转轴。

在 box . settranslate(500)之后添加一个. setRotateAxis()方法调用 off 您的 box 对象;方法调用,使用旋转。用于配置旋转算法的 X_AXIS 常量。Java 语句序列应该类似于下面的 Java 代码,可以在图 12-9 的底部看到:

A336284_1_En_12_Fig9_HTML.jpg

图 12-9。

Add a .setRotationAxis() method call off box after the box.setTranslateX(500); and set it to Rotate.X_AXIS

box.setTranslateX(500);
box.setRotationAxis(Rotate.X_AXIS);
box.setRotate(45);
pole = new Cylinder(50, 250, 24);
pole.setTranslateX(250);

接下来,使用运行➤项目工作流程,再次单独查看您的原语。如图所示,在图 12-10 中。setRotate()方法现在使用 z 轴进行旋转,因此您的 3D 对象现在呈现为 3D 对象,您可以看到阴影(不同面上的颜色或亮度差异)。

A336284_1_En_12_Fig10_HTML.jpg

图 12-10。

Now all primitives are oriented in such a way that their default light gray shading is visible in the renderer

随着本书的深入,我们将了解 JavaFX 中旋转 3D 对象的更复杂的方法,因为旋转在 3D 中是一个非常复杂的主题,似乎不是“在表面上”(没有双关语)。旋转在它的算法中使用了比转换更复杂的数学体系,其中一些复杂性会渗透到表面,因此必须由所有专业 Java 9 3D 游戏开发人员来处理和理解。

现在我们已经将 JavaFX 中提供的三个基本图元分开,并以一种在渲染视图中向您显示更多面和边的方式面对,我们将进一步了解面剔除和绘制模式对几何图形的影响。我们将在第十三章中为自己保存实物的创建和应用;材质对象创建是一个核心的 3D 主题(纹理映射),应该作为它自己的主题来对待,因为 3D 对象的着色决定了它的视觉质量。

接下来,让我们看看绘制模式(在大多数 3D 软件包中称为渲染模式),以便您在开发 pro Java 9 游戏时可以查看对象的 3D 线框表示。

Shape3D 绘制模式属性:实体几何图形和线框

现在我们已经在屏幕上排列了三个主要的 JavaFX 原语,让我们看看 Shape3D 超类的 drawMode 属性,它由这些原语中的每一个继承。这个属性使用了 DrawMode 类中的一个常量,您可能已经猜到了,目前可用的两个常量是 DrawMode。FILL 和 draw mode . LINE。FILL 常量为您提供实体模型几何图形表示,LINE 常量为您提供线框模型几何图形表示。我们将使用。本节中的 setDrawMode(drawMode)方法调用将我们的三个图元从实体模型更改为线框模型,以便我们可以更改线框的分辨率或分割,并查看这样做的效果,以便我们可以围绕 X 维度旋转球体,查看其线框构造的外观以及分割属性如何更改它在 3D 场景中的外观(渲染)。然而,首先,我有点厌倦了在 3D 场景的左上角看这些图元,所以我们将使用。setTranslateZ(-500)将相机对象拉近 100%(或将基本体放大 100%),并使用。setTranslateY(300)方法将图元置于视图的水平中心。稍后我们将使用。settranslate(-300)方法调用使图元在视图的垂直中心居中。

打开你的。start()方法和 gameButton 事件处理代码块,并更改。setTranslateZ()方法调用值从-1000 到-500。然后添加一个. setTranslateY()方法调用关闭 camera 对象并传递给它一个-300 场景单位的数据值,如图 12-11 以及下面的 Java 代码语句所示:

A336284_1_En_12_Fig11_HTML.jpg

图 12-11。

Zoom the camera object in 100 percent using .setTranslateZ(-500), and move it down with .setTranslateY(-300)

camera.setTranslateZ(-500);
camera.setTranslateY(-300);

接下来,让我们打开 createBoardGameNodes()方法,并为每个图元添加一个. setDrawMode (DrawMode. LINE )方法调用,将它们的渲染模式从立体几何设置为线框几何,这样我们就可以看到它们的底层结构。在图 12-12 中用黄色突出显示的 Java 语句应该如下所示:

A336284_1_En_12_Fig12_HTML.jpg

图 12-12。

Set the drawMode property to LINE for all primitives with a .setDrawMode(DrawMode.LINE) method call

sphere.setDrawMode(DrawMode.LINE);
box.setDrawMode(DrawMode.LINE);
pole.setDrawMode(DrawMode.LINE);

接下来,使用运行➤项目工作流程,再次查看您的原语。正如你所看到的,在图 12-13 中,你的图元正在使用线框表示法进行渲染,并在 3D 场景中的 Y 维度上居中。

A336284_1_En_12_Fig13_HTML.jpg

图 12-13。

All three primitives are now rendered in wireframe mode and are centered vertically

使用将相机置于 X 维度的中心。使用下面的代码 setTranslateX(),如图 12-14 所示:

A336284_1_En_12_Fig14_HTML.jpg

图 12-14。

Add a .setTranslateX(-300) to move your primitives to the vertical (X dimension) center of your 3D Scene

camera.setTranslateX(-300);

请注意,当您移动相机对象时,它保持直视前方,而在 3D 软件包中,有一个相机“目标”保持锁定在场景的中心或场景中的 3D 对象上。在 JavaFX 中,相机对象是用一条直线(称为光线或向量)固定的,这条直线从相机的后面穿过前面,沿着相机对象指向的方向延伸到无穷远处。所以,在 3D 软件中,如果你向上移动相机,它的视野向下旋转,相机和它的主体之间有一个链接(或线)。

如果您想要 JavaFX 中的这种行为,您必须手动旋转相机,因为 JavaFX 相机超类当前没有指定目标属性(目标功能)。随着本书的进展,我们将关注 PerspectiveCamera 对象以及如何在 3D 场景中以更高级的方式利用它,因为相机是 i3D 场景的一个重要方面,也是 pro Java 9 i3D 游戏开发过程中使用的一个重要工具。

在我们再次渲染 3D 场景之前,因为我们从代码中知道它现在将会很好地居中,足以让我们查看分割和面剔除等属性,并了解这些属性如何影响组成 3D 图元的多边形,所以让我们使用重载的(第二个)球体(大小,分割)构造函数方法格式,并降低球体对象的网格分辨率,以优化保存该 3D 对象所需的内存量。您还将向前旋转它,以便可以看到球体构造的顶部,同时将圆柱体的分辨率降低 100%,从 24 等份减少到 12 等份。我总是使用可被 4 整除的除法值(90 度乘以 4 是 360),如果打开面剔除,一半的除法甚至都没有渲染。这些都可以通过使用以下 Java 语句来完成,这些语句在图 12-15 中(和底部)突出显示:

A336284_1_En_12_Fig15_HTML.jpg

图 12-15。

Construct your Sphere with 12 divisions, X rotate it 90 degrees, and reduce your Cylinder to 12 divisions

sphere = new Sphere(100, 12);
sphere.setRotationAxis(Rotate.X_AXIS);
sphere.setRotate(90);
sphere.setDrawMode(DrawMode.LINE);

现在是时候使用运行➤项目的工作过程,并渲染我们的场景。正如你在图 12-16 中看到的,我们的 3D 图元更接近 3D 场景的中心,更容易查看,并且使用更少的数据来构建。正如你在图 12-13 中看到的,这个球体使用了 48 个分区来构建。这使用了几百个多边形,可以计算为 48×48×2 = 192;192 个多边形需要大量内存来处理(存储和渲染),因为每个多边形都有大量数据来定义它(模型中的位置、大小、方向、颜色、法线方向、平滑组)。

A336284_1_En_12_Fig16_HTML.jpg

图 12-16。

Your primitives are now centered in the 3D Scene Camera view, and you can see the Sphere construction

当我们在下一节面剔除中渲染这些图元时,您将看到立方体和圆柱体在外观上并没有真正改变,因此圆柱体的分区减少了 100 %( 24 到 12 ),这是一次成功的优化。球体缩小了 200 %(从 48 到 12 ),这是一个很大的变化,平滑的错觉在球体周围有点分散,尤其是从顶部渲染时,这就是为什么我把它向前旋转了 90 度。

接下来,让我们看看使用背面剔除的渲染算法的优化,以及在使用实体模式渲染 3D 图元时,较低的分辨率(较少的划分)如何影响它们的视觉质量。

Shape3D 面剔除属性:优化渲染管道

Shape3D cullFace 属性和 cullFace 类用于控制 3D 场景的面和多边形渲染优化。默认为 CullFace。没有,所以你需要使用代码来打开这个优化,我将在本章的这一节向你展示如何做。我认为模型在剔除人脸后看起来更好(对比度更高),如果你对你的 pro Java 9 游戏进行了足够好的优化,它应该可以在所有平台和设备上很好地运行,而不必剔除模型中一半的人脸。也就是说,一旦你知道如何做到这一点,你应该很容易在测试阶段进行实验,看看它如何影响游戏的视觉质量和流畅度。

让我们继续向 createBoardGameNodes()方法添加代码,为基本对象设置背面剔除。首先,我们需要改变你的基本体的 drawMode 属性,在你的球体,盒子和柱子上使用. setDrawMode (DrawMode. FILL )来填充实体模型。在对每个基本对象调用这个方法之后,添加一个. setCullFace(CullFace。BACK)方法调用。如果您在 NetBeans 9 中使用弹出助手工作进程,您将会看到它使用默认的 CullFace 编写您的代码。无设置,因此您必须将它更改为 CullFace。返回以开启此渲染管道优化算法。

背面剔除语句的 Java 代码在图 12-17 的底部突出显示,看起来应该像下面的 Java 代码:

A336284_1_En_12_Fig17_HTML.jpg

图 12-17。

Add method calls to .setCullFace() with the value CullFace.BACK off of all of the primitives in your Scene

sphere.setDrawMode(DrawMode.FILL);
sphere.setCullFace(CullFace.BACK);
box.setDrawMode(DrawMode.FILL);
box.setCullFace(CullFace.BACK);
pole.setDrawMode(DrawMode.FILL);
pole.setCullFace(CullFace.BACK);

图 12-18 显示了一个运行➤项目 Java 代码测试的工作流程,显示了背面剔除算法的安装和对 3D 场景图元的操作。请注意,在您的球体上,降低的几何体分辨率(更少的划分)会导致网格上的一些平滑问题,其中网格拓扑通过平滑算法显示。我会将球体划分增加到 24 以减轻这种情况,这仍然是对默认设置的 100%优化。

A336284_1_En_12_Fig18_HTML.jpg

图 12-18。

The renderer is now rendering half as much 3D data, and your lower resolution can be seen on the Sphere

另请注意,当背面剔除打开时,立方体(长方体)基本体上的明暗对比的对比度(面之间明暗颜色的差异)要小得多。你做的自定义纹理映射越多,这个问题就越小(不太明显)(在下一章及以后的章节中讨论),但这可能是为什么默认为 FaceCull 类和这个方法调用的 NONE,因为面剔除优化可能以某种方式影响当前算法代码中的对比度(质量)。我以这种方式设置这一章,以便您可以看到这一点,因为最基本的图元之一是使用默认的中灰色着色器颜色显示面部之间的对比度明显降低,如果您比较图 12-10 和图 12-18 ,您可以看到对比度从高到几乎为零。

接下来,让我们看看三个与网格相关的类,mesh、TriangleMesh 和 MeshView,看看它们做什么,以及它们如何相互关联,因为它们将允许您渲染使用 3D 软件创建的复杂网格对象。

JavaFX Mesh 超类:构造一个三角形网格

理解抽象 Mesh 超类及其与 TriangleMesh 子类的关系很重要,triangle Mesh 子类可用于“手动编码”复杂的网格对象,以及它与 MeshView 类的关系,Mesh view 类实际上是 Shape3D 的子类,而不是 Mesh 的!这是为了使 MeshView 可以继承(扩展)Shape3D 的 cullFace、drawMode 和 material 属性,当然,这些属性对于使网格对象变得逼真是至关重要的(尤其是 material 属性和 Material 类)。正如您将看到的,MeshView 构造函数接受一个网格对象。这是复杂 3D 对象所基于的核心类(算法),因此 Mesh 和 MeshView 是用于 pro Java 9 游戏开发的最关键的类。如果出于某种原因,您想要编码复杂的多边形几何体,也称为“三角形网格”,这不是一个最佳的工作流程,您可以使用 triangle mesh,我们将详细介绍它。

更好的工作流程是使用外部 3D 软件包,将 3D 对象直接导入网格对象,然后由 MeshView 对象引用。这是一种更快的方式来让一个先进的 i3D 游戏快速有效地运行起来,也是一种将专业技术人员引入 i3D 游戏开发工作流程的方式。

JavaFX Mesh 超类:您的原始 3D 模型数据容器

公共抽象 Mesh 超类看似简单,只有一个 Mesh()构造函数方法和一个 TriangleMesh 子类(用于使用 Java 代码加载网格数据),因此我们将在这里首先介绍它。它本质上是一个用于包含 3D 数据的对象,与其他以 3D 模型为中心的类一起包含在 javafx.scene.shape 包中。Java 类的层次结构如下所示,因为 Mesh 类被临时编码为保存 3D 网格表示的 Java 类:

java.lang.Object

  > javafx.scene.shape.Mesh

这是一个基类,用于表示不是 JavaFX Shape3D 图元的复杂 3D 几何表面。请注意,这显然是一个有条件的功能,因为复杂的 3D 几何图形将需要一个 3D 渲染管道来对您的 pro Java 9 游戏开发有用。轮询 ConditionalFeature。场景 3D 将是必要的。

如前所述,构造函数方法非常简单,类似于下面的 Java 代码:

protected Mesh() // Protected Code Cannot Be Used Directly (but can be used by a subclass)

接下来,让我们看看 MeshView 类,它将引用、保存在内存中,并使用渲染引擎在 3D 场景中显示该网格对象。这个类是网格引擎和 Shape3D 之间的“桥梁”。

JavaFX MeshView 类:格式化并显示 3D 网格数据

公共 MeshView 类几乎和 Mesh 类一样简单,只有两个重载的 MeshView()构造函数方法,没有子类,所以接下来我将在这里介绍它。它是 Shape3D 的子类,存储在 javafx.scene.shape 包中。它实现了 Styleable 和 EventTarget 接口,就像三个原语类一样。它用于使用网格对象中保存的原始 3D 模型数据来定义 3D 表面。MeshView 类的 Java 类层次结构如下所示,因为 MeshView 类需要继承所有这些关键的 Shape3D 渲染特征:

java.lang.Object
  > javafx.scene.Node
    > javafx.scene.shape.Shape3D

      > javafx.scene.shape.MeshView

MeshView 对象有一个 ObjectProperty mesh 属性,它指定 MeshView 的 3D 网格数据,该数据是从第二个重载的构造函数方法参数或使用. getMesh(mesh)方法调用获得的。此类(对象)还从 javafx.scene.shape.Shape3D 类继承核心 Shape3D 属性,这些属性您已经介绍过(除了 material ),它们是 cullFace、drawMode 和 material。

有两种重载的构造函数方法。创建一个空的网格视图,以便在将来加载网格对象(3D 数据),这当然会利用以下 Java 语句格式:

meshView = new MeshView();

第二个重载构造函数方法调用使用以下对象实例化 Java 语句格式实例化 MeshView 对象,并同时用网格对象(3D 几何数据)加载它:

meshView = new MeshView(yourMeshNameHere);

MeshView 类有三个用于处理网格对象的方法调用,包括获取属性 Mesh 的网格对象值的 getMesh(Mesh)方法调用,为调用此方法的任何 MeshView 指定 3D 网格(网格对象)数据的 ObjectProperty meshProperty()方法调用,以及为 MeshView 属性 Mesh 设置网格对象值的 void setMesh(Mesh value)方法调用。

在我们介绍 TriangleMesh 类之前,让我们先看一下 VertexFormat 类,它将通过指定用于给定 3D 模型(即网格对象及其 3D 模型数据)的顶点数据格式来定义顶点数据。

JavaFX VertexFormat 类:定义你的 3D 顶点数据格式

public final VertexNormal 类还扩展了 Java Object master 类,这意味着该类是临时编码的,以定义数据点数组、其纹理坐标及其法线的格式,如果外部 3D 模型以 JavaFX 导入/导出软件支持的各种数据格式导出的话。该类是 Mesh、TriangleMesh 和 MeshView 类的实用程序类,这可以从它的最终修饰符看出,这意味着它不能被子类化。与我们介绍的其他六个类一样,它保存在 javafx.graphics 模块的 javafx.scene.shape 包中,其类层次结构如下所示:

java.lang.Object
  > javafx.scene.shape.VertexFormat

VertexFormat 类(对象)定义了两个不同的数据格式常量,它们反映了 3D 网格对象中每个顶点所包含的 3D 数据的类型。静态顶点格式 POINT_NORMAL_TEXCOORD 字段将指定包含点坐标、法线和纹理坐标数据的顶点格式。静态 VertexFormat POINT_TEXCOORD 字段将指定包含点坐标和纹理坐标数据的顶点格式。我建议使用支持法线的格式,因为可以用来定义 3D 模型的数据越多,渲染器就可以更准确地渲染它们,因此也更专业。

该类中有五种方法用于处理顶点及其法线、点和纹理坐标数据组件。的。getVertexIndexSize()方法将返回表示顶点索引的整数个组件索引。的。getNormalIndexOffset()方法将返回给定顶点内法线组件的面数组的整数索引偏移量。的。getPointIndexOffset()方法将返回给定顶点内点组件的面数组中的整数索引偏移量。的。getTexCoordIndexOffset()方法将返回顶点内纹理坐标组件的面数组中的索引偏移量。String toString()方法将返回 VertexFormat 的字符串(文本)数据,允许您以可读格式查看顶点数据。

接下来,我们来看看 TriangleMesh 对象,它是最复杂的;它允许您使用 Java 代码创建 3D 模型。在本章中,我们不会看到这样的例子,因为这不是获得快速、专业的 i3D 游戏开发 3D 模型创建结果的最有效方式。

这是因为使用专业的 3D 建模、纹理、渲染和动画软件包,如开源的 Blender.org、Autodesk 3D Studio Max、Maya 或 NewTek Lightwave,是创建专业 3D 模型的最合理的工作流程。

由于大量的 3D 数据导入文件格式,可以更快地创建 3D 模型,然后 pro Java 9 游戏开发人员可以使用 JavaFX 9 importer 格式之一将高质量的 3D 数据作为网格对象引入 JavaFX。

我们将在第十三章中查看纹理映射后,在第十四章中查看这一工作过程,以便我们更好地理解什么是纹理映射,因为它也用于第三方 3D 建模软件包中。使用第三方开发工具,如 Fusion、Blender、Audacity、Gimp 和 Inkscape,通常会产生更好的结果。

JavaFX TriangleMesh 类:创建一个 3D 多边形网格对象

public TriangleMesh 类是 Mesh 超类的一个子类,它不实现任何接口,因为它用于创建要存储在 Mesh 对象内部的 3D 数据,很像使用许多流行的 3D 文件格式导入程序导入 JavaFX 的 3D 模型,我们将在第十四章中介绍这些导入程序。TriangleMesh 存储在 javafx.graphics 模块的 javafx.scene.shape 包中,其 Java 类层次结构如下所示:

java.lang.Object
  > javafx.scene.shape.Mesh

    > javafx.scene.shape.TriangleMesh

TriangleMesh 对象用于定义 3D 多边形网格。该对象将使用两个 VertexFormat 常量之一,并包括一组包含顶点组件的独立数据数组对象,这些组件包括点、法线、纹理坐标和定义网格的各个三角形的面数组。正如我在本章中不止一次提到的,通过使用支持建模的外部 3D 软件包,如 Blender、Hexagon、Lightwave、Maya 或 3D Studio Max,可以完全避免这种低层次的复杂性,并加速过去。

请注意,JavaFX 术语点等同于 3D 软件术语顶点。JavaFX 9 使用 vertex 来指代顶点(点)及其所有关联的属性,包括其法线位置和关联的 UV 纹理贴图坐标。因此,在三角形网格方法名称和方法描述中提到的点(我们将在本章的后半部分介绍)实际上是指 3D 空间中的 3D 点(x,y,z)位置数据,代表一个顶点的空间位置。

类似地,术语点(或点的集合)用于表示表示多个顶点的 3D 点集。术语法线用于表示 3D 空间中的 3D 向量(nx,ny,nz ),其表示单个顶点的方向,该方向告诉渲染引擎面面向哪个方向,因此它可以在面的正确侧渲染纹理。术语法线(或法线数据的集合)用于表示多个顶点的 3D 向量集。

术语“纹理坐标”用于表示单个顶点的一对 2D 纹理坐标(u,v ),而术语“纹理坐标”(纹理坐标的集合)用于表示跨多个顶点的纹理坐标组。

最后,术语“面”用于表示一组三个交错点、法线(这些是可选的,取决于指定的相关 VertexFormat 字段类型)和纹理坐标,它们一起表示一个三角形的几何拓扑。术语面(面的集合)用于表示一组三角形(每个用一个面表示),这通常是 3D 多边形模型的组成部分。困惑了吗?正如我所说的,使用导入/导出工作流并让高级 3D 建模软件用户界面完成所有工作是获得令人难以置信的结果的更好方法,而不是试图使用 Java 将点、法线和 UV 坐标放置到 3D 空间中。我在本书中尝试做的是向您展示创建混合 2D 和 3D 游戏的最快、最简单和最优化的方法,以便您可以创建市场上任何游戏玩家从未体验过的 pro Java 9 游戏。

这个 TriangleMesh 类(object)有一个 ObjectProperty vertexFormat 属性,该属性将用于使用 vertexFormat 实用程序类指定这个 TriangleMesh 的顶点格式,因此这将是 vertex format。POINT_TEXCOORD 或 VertexFormat。点 _ 法线 _TEXCOORD。

TriangleMesh 类有两个重载的构造函数方法。第一个(空的)使用默认的 VertexFormat 创建一个 TriangleMesh 类的实例。POINT_TEXCOORD 格式类型,如下所示:

triangleMesh = new TriangleMesh(); // Creates Points & Texture Map Only Polygonal Mesh Object

第二个构造函数方法使用在方法调用的参数区域中指定的 VertexFormat 创建 TriangleMesh 的新实例。这类似于下面的 Java 实例化语句:

normalTriangleMesh = new TriangleMesh(VertexFormat.POINT_NORMAL_TEXCOORD) // Includes Normals

有十几种方法用于处理 TriangleMesh 对象构造;接下来让我们来看看它们。

那个。getFaceElementSize()方法将返回表示给定人脸的元素数量。使用此方法确定任何给定面使用的数据(点、法线、纹理贴图)。

ObservableFaceArray getFaces()方法将获取 TriangleMesh 对象中的整个面数组,包括点的索引、法线(仅当 VertexFormat。POINT_NORMAL_TEXCOORD 是为网格指定的),以及 TEXCOORD 数组。使用它从你的 TriangleMesh 对象中提取多边形数据。

ObservableIntegerArray getface smoothinggroups()方法将从 TriangleMesh 对象中获取一个 faceSmoothingGroups 数据数组。平滑组定义渲染的 3D 对象的表面着色(平滑)中接缝出现的位置。我们在本书前面的第三章中讨论过这个话题。

那个。getNormalElementSize()方法将返回表示 TriangleMesh 对象中法线的元素数量。这告诉你有多少法线被用来表示表面方向。

ObservableFloatArray getNormals()方法将获取三角形网格对象的法线数组。

的。getPointElementSize()方法将返回表示 TriangleMesh 对象中 XYZ 点的元素数量。这将告诉你有多少顶点(顶点计数)在你的三角网格的三维模型。

ObservableFloatArray getPoints()方法用于获取 TriangleMesh 的点数据数组。

那个。getTexCoordElementSize()方法将返回许多表示 TextureMesh 对象中纹理坐标的数据元素。使用它来确定模型中 UV 贴图坐标的数量。

ObservableFloatArray getTexCoords()方法将获取 TriangleMesh 对象的 tex coords 数组。使用此选项从 TextureMesh 3D 多边形对象中提取纹理坐标数据(仅限)。

VertexFormat getVertexFormat()方法将从 TriangleMesh 对象内部获取 VertexFormat 属性的值。使用它来确定该 3D 模型数据是否支持法线。

虚空。setVertexFormat(VertexFormat value)方法用于设置 TriangleMesh 对象的 VertexFormat 属性值。确保对象内的数据数组与此设置正确匹配。

object propertyvertexFormatProperty()方法可用于指定三角形网格的顶点格式;它可以是 VertexFormat。POINT_TEXCOORD 或 VertexFormat。点 _ 法线 _TEXCOORD。

在下一章我们学习了更多关于着色器、纹理和贴图的知识后,我们将进入 3D 软件并学习导入工作流,该工作流允许我们将强大的 3D 软件连接到 JavaFX 9 游戏引擎。

摘要

在第十二章中,我们学习了 javafx.scene.shape 包中允许您使用 3D 模型的类,包括使用 Box、Sphere 和 Cylinder 类的基本体以及使用 MeshView、VertexFormat 和 TriangleMesh 类的多边形对象。这些类都基于抽象网格和 Shape3D 超类。

您学习了如何创建 3D 基本体以及如何设置它们的属性,学习了面剔除和线框,并且观察了在 3D 场景中移动(平移)相机对象时它是如何工作的。

您了解了算法(代码)生成的图元和更高级的多边形网格对象之间的区别,以及为 pro Java 9 游戏设计和开发管道创建 3D 模型的不同工作流程,我们将在接下来的几章中继续学习。

在下一章中,我们将了解使用抽象材质超类及其 PhongMaterial 子类的 JavaFX 纹理映射,并了解更多关于着色器、纹理、纹理映射以及环境、漫射、镜面反射和自发光属性等相关主题的信息。

十三、3D 模型着色器创建:使用 JavaFX 9 PhongMaterial

现在,您已经了解了 JavaFX API 中包含的 3D 资源(称为图元),让我们开始了解一些关于如何使用 2D 图像资源“装扮”这些 3D 资源的基础知识,我们将使用着色器将这些资源转化为可应用于 3D 表面的材质。JavaFX 支持 Phong 着色器,它包含几个通道,这些通道接受称为纹理贴图的特殊图像,这些纹理贴图应用不同的效果,如着色、照明、曲面凹凸、曲面光泽等。JavaFX 在 javafx.graphics 模块的javafx.scene.paint包中提供了两个核心着色器类,专门为您“着色”或表面 3D 几何体(基本体或网格),我们将在本章中了解它们。我们还将看看如何使用 GIMP 2.8.22 基于像素和数学快速准确地创建纹理贴图,从而提供准确的纹理贴图结果。我们还将回到我们的 JavaFXGame 主要应用类编码,并开始将 Phong 着色器材质添加到 3D 图元中以获得一些实践。您可以在像 Blender 这样的 3D 软件包中完成这项工作,但是棋盘游戏非常简单(正方形、球形、圆柱形),我们只需使用 JavaFX 代码就可以完成这项工作。这意味着我们不需要导入(和分发)3D 模型,而是可以编写代码来“凭空”为你的 i3D 游戏建模这也将教会您更多关于 Java 9 和 JavaFX 中的 3D APIs,因为您将学习如何仅使用 Java 9 及其 JavaFX APIs 来建模复杂的 3D 对象。

在本章中,您将了解 JavaFX 3D 着色器类层次,它包含在 javafx.scene.paint 包中。在 Java 9 和 Android 8 中,Paint 类将像素颜色和属性应用于画布,在本例中是 3D 图元的表面。paint 包包含与此“蒙皮”或纹理映射目标相关的类。您将涵盖 Material,一个保存顶级着色器定义的超类,以及 PhongMaterial 类,后者可用于为 3D 图元创建纹理贴图或“皮肤”(在第十二章中介绍)。

JavaFX 材质超类:i3D 着色器属性

public abstract Material 超类用于创建 PhongMaterial 类,您将使用该类为在 pro Java 9 游戏设计和开发中使用的 i3D 原语创建由 Shape3D 子类使用的 Material 属性。从外部 3D 软件包导入的高级模型在 3D 软件生产环境中已经应用了材质(有时称为着色器)和纹理贴图,导入后,它们将位于使用 MeshView 对象显示的网格对象中,因此在大多数实际应用中,您不会总是在此低级直接使用 PhongMaterial 类来着色高级 3D 对象。Material 超类比 Mesh 更像一个空壳,因为它只有一个空的构造函数,没有属性或方法!Material 类是 javafx.scene.paint 包的一部分,具有以下 Java 类层次结构:

java.lang.Object
  > javafx.scene.paint.Material

一个空的构造函数方法是受保护的,这意味着它不会被直接实例化。然而,这个构造函数方法功能是在 PhongMaterial 子类中实现的,如 PhongMaterial(),我们将在本章的下一节中介绍。

protected Material()

接下来,让我们看看 PhongMaterial 子类,它代表 Phong 着色器渲染算法。这就是我们将在本章中直接使用(和学习)的内容,为我们在第十二章中创建的 3D 图元上色。

JavaFX PhongMaterial: Phong 着色算法和属性

公共 PhongMaterial 类扩展了 Material 类,为 JavaFX 3D 场景定义 Phong(算法)着色器材质、其颜色设置及其纹理贴图。该类保存在 javafx.graphics 模块的 javafx.scene.paint 包中,并且是 Material 的子类,因此您将拥有以下 Java 类层次结构:

java.lang.Object
  > javafx.scene.paint.Material
    > javafx.scene.paint.PhongMaterial

JavaFX 9 中的 Phong 着色(材质和纹理渲染)算法描述了点光源对象和环境光对象(如果存在)与 PhoneMaterial 对象所应用到的 3D 图元表面之间的交互。PhongMaterial 对象在应用漫反射和镜面反射颜色着色时反射光线,就像真实生活中的光线一样。当光线被有色物体反射时,光线本身也变得有颜色。PhongMaterial 算法支持 AmbientLight 对象设置(如果存在),并支持自发光或“辉光”贴图,以便您可以应用特殊效果来进一步增强着色器的真实感。

根据 JavaFX 9 PhongMaterial 文档,几何表面上任何给定点的着色都是以下四个部分的数学函数:环境光、漫反射、镜面反射和自发光贴图。这些对象的子组件(算法输入)包括环境光(对象),点光源(对象),漫反射颜色(设置),漫反射颜色贴图(图像对象),高光颜色(设置),高光功率(设置),高光贴图(图像对象),自发光或辉光贴图(图像对象)。

如果有多个 AmbientLight 对象,则 AmbientLight 光源的最终颜色(在这种情况下,它们的值将被简单地相加(这就是我建议使用一个对象的原因),将使用以下等式进行计算:

For each AmbientLight (Object) Source [i]: { ambient += AmbientLightColor[i] } // Color Summed

点光源算法计算要高级得多,这就是为什么我建议在 Pro Java 9 3D 游戏中使用点光源,因为它允许对 PhongMaterial 对象的表现进行微调控制,并添加更具戏剧性的照明(衰减、阴影、更高的对比度等)。)添加到您的 3D 场景中,使其更加逼真。值得注意的是,这些等式中使用的周期指的是点积数学运算。

For each PointLight (Object) Source [i]:
{    diffuse  += (SurfaceToLightVector . Normal) * PointLightSourceColor[i]

     specular += ( (NormalizedReflectionVector . NormalizedViewVector)
               ^ (specularPower * intensity(specularMap)) )
               * PointLightSourceColor[i]
}

渲染结果中的颜色值将使用以下输入组件算法进行计算:

color = ((ambient + diffuse) * diffuseColor * diffuseMap
      + specular * specularColor * specularMap
      + selfIlluminationMap

这里列出这些是为了完整性,因为它们在 PhongMaterial 文档中有概述,而不是因为你需要成为一名高级着色数学家才能开发 pro Java 9 游戏。也就是说,这将使您了解我们将在本章中探索的着色器输入组件如何在 Phong 着色器算法中相互交互,以及如何通过足够的贴图和参数调整,微调这些输入中的任何一个可以让您获得任何您想要的专业表面渲染结果!

PhongMaterial 类中有七个属性,告诉您可以使用哪些类型的纹理贴图和颜色规范来绘制 3D 图元。这些也可以在所有标准 3D 包中获得,因此在 JavaFX 9 外部创建和纹理化的模型也可以访问这些(实际上还有更多)。

ObjectProperty bumpMap 是一个图像对象,用于模拟 3D 模型上的凸起或表面高度的微小变化。这可用于向 3D 模型添加精细的表面细节,这些细节实际上不是模型的几何表面拓扑的一部分,但是凹凸贴图会使其看起来像是模型的物理拓扑的一部分。凹凸贴图有时会被错误地称为法线贴图,如 JavaFX 9 文档中所示。文档说“PhongMaterial 的凹凸贴图是以 RGB 图像形式存储的法线贴图”,所以我写信给 Oracle,询问他们 bump map 属性是凹凸贴图还是更高级的法线贴图!我所希望的是,它最初是一个凹凸贴图算法,随着时间的推移,它被升级为支持更复杂的法线贴图算法,同时保留属性名称 bump map,以便不破坏现有代码。法线贴图可以创建更好的表面效果。

ObjectProperty diffuseColor 表示材质的漫射或基础表面颜色。通过使用漫反射颜色贴图或漫反射贴图,可以更改对象表面的颜色。如果您的 3D 软件具有比导入 JavaFX 更高级的着色贴图类型,可以使用一种称为烘焙的技术,其中 3D 渲染器的着色器管线和纹理贴图结果可以渲染到漫反射贴图图像中,然后导出(作为 TIFF、BMP、PNG 或 TGA 24 位 RGB 图像)并用作 JavaFX 中的漫反射贴图图像对象。让我们接下来看一看,事实上,因为我们已经基本上涵盖了它!

ObjectProperty diffuseMap 属性引用一个图像对象,该图像对象的数据定义了一个将使用 UV 纹理坐标映射到使用 PhongMaterial 的 3D 图元表面上的漫射贴图。

object propertyself illumination map 属性引用一个图像对象,该图像对象的数据定义了一个发光或照明贴图(使用灰度图像对象表示照明强度),该贴图将使用 PhongMaterial 使用 UV 纹理坐标映射到 3D 图元的表面上。

object propertyspecular color 属性指定 PhongMaterial 的镜面反射颜色。这是镜面高光的颜色(见图 13-5 ),它改善了 3D 原始表面的视觉特性。

ObjectProperty specularMap 属性引用一个图像对象,该图像对象的像素数据定义了 3D 图元表面上的一个区域,该区域将使用镜面反射贴图响应镜面反射颜色(一个灰度图像对象,表示将应用或不应用镜面反射颜色)。这应该使用 UV 纹理坐标映射到一个基本体上,并且将影响基本体表面的反射映射区域的亮度。

DoubleProperty specularPower 属性用于指定镜面高光的功率(我喜欢把它看作焦点)。该属性在球体和圆柱体(弯曲的)基本体上特别明显,如图 13-8 所示,其具有应用于 phongMaterial 的高(紧密或聚焦)镜面高光功率值 100。

PhongMaterial 类有三个重载的构造函数方法。第一个使用默认颜色创建 PhongMaterial 对象的新实例。白色扩散颜色属性。这将使用以下 Java 代码:

phongMaterial = new PhongMaterial();

第二个构造函数将使用指定的 diffuseColor 属性创建 PhongMaterial 对象的新实例。这将使用下面的 Java 代码和颜色类 GOLD 常量,我们将在后面的代码中用到:

phongMaterial = new PhongMaterial(Color.GOLD);

第三个构造函数允许您指定漫射颜色和四种不同类型的效果图。这是最方便的构造方法,一旦我们进入游戏设计和开发的更高级阶段,我们就会用到它。这个高级构造函数方法将采用以下 Java 代码语句格式:

phongMaterial = new PhongMaterial(Color diffuseColor, Image diffuseMap, Image specularMap,
                                                      Image bumpMap, Image selfIlluminationMap)

最后,让我们来看看 22 种方法,让您可以处理所有这些声音材料组件。这些允许您使用 Java 代码动态地或交互地更改 PhongMaterial。这将允许你在你的 3D 和 i3D 游戏属性上创建一些非常令人印象深刻的效果,正如你将在本书中看到的。

object propertydiffuseColor property()方法调用将返回调用它的 PhongMaterial 的 diffuse color 属性。这是一个颜色值,用于设置图元的基本(或基础)颜色。

object propertyspecularColorProperty()方法调用为调用它的 PhongMaterial 返回一个 specularColor 属性。这是一个颜色值,用于设置图元的镜面反射(或高光)颜色。

double property specularPowerProperty()方法调用为调用它的 PhongMaterial 返回 double specularpower 属性。这是一个双精度值,用于设置图元的镜面反射(或高光)能力。

object propertybupmapproperty()方法调用将返回调用它的 PhongMaterial 的 bumpMap 属性。这是存储为 RGB 图像对象的法线贴图。

object propertydiffuseMap property()方法调用将返回调用它的 PhongMaterial 的 diffuse map 属性。这是一个作为 RGB 图像对象存储的漫反射颜色贴图。

object propertyselfIlluminationMap property()方法调用将返回调用它的 PhongMaterial 的 self illumination map 属性。该自发光贴图存储为 RGB 图像对象。

object propertyspecularMapproperty()方法调用返回调用它的 PhongMaterial 的 spectrurmap 属性。该高光颜色贴图存储为 RGB 图像对象。

getBumpMap()方法调用获取 PhongMaterial 属性 BumpMap 的图像对象。

getDiffuseColor()方法调用获取 PhongMaterial 属性 DiffuseColor 的颜色值。

getDiffuseMap()方法调用获取 PhongMaterial 属性 DiffuseMap 的图像对象。

getSelfIlluminationMap()方法调用获取 selfIlluminationMap 属性的图像对象。

getSpecularColor()方法调用获取 PhongMaterial 属性 SpecularColor 的颜色值。

getSpecularMap()方法调用获取 PhongMaterial 属性 specularMap 的 Image 对象。

getSpecularPower()方法调用为 PhongMaterial 属性 specularPower 获取一个双精度值。

void setBumpMap(Image image)方法调用为属性 BumpMap 设置图像引用。

void setDiffuseColor(Color color)方法调用为属性 DiffuseColor 设置颜色值。

void setDiffuseMap(Image image)方法调用为属性 DiffuseMap 设置图像引用。

void setselfIlluminationMap(Image)方法调用为 selflightionmap 属性设置图像。

void setspecularColor(Color Color)方法调用设置 spectrorcolor 属性的颜色值。

void 镜象映射(Image image)方法调用为属性镜象映射设置图像对象。

void setspecularPower(double value)方法调用设置属性 spectrorpower 的值。

toString()方法调用转换非文本(二进制、数字等)形式的任何数据。)格式化成文本格式。

接下来,让我们在 JavaFXGame 类中实现一些核心颜色属性,看看它们是如何工作的。

实现 PhongMaterial:指定颜色和功率值

既然我们已经熟读了声材料课,那就言归正传吧。让我们在 JavaFXGame 类的顶部声明一个 PhongMaterial 对象,并将其命名为 phongMaterial。在 light 对象代码后的 createBoardGameNodes()方法中,使用第二个重载的构造函数方法添加一个 PhongMaterial 实例化,并将漫反射颜色设置为 color。金色,如图 13-1 以及以下 Java 代码语句中突出显示的:

A336284_1_En_13_Fig1_HTML.jpg

图 13-1。

Declare and instantiate your phongMaterial object and configure its diffuse color value to be Color.GOLD

PhongMaterial phongMaterial;                    // Declared at the top of the JavaFXGame class
...

phongMaterial = new PhongMaterial(Color.GOLD);  // In the createBoardGameNodes() method body

如您所知,您的 PhongMaterial 对象可以配置颜色值并加载酷炫效果纹理贴图(图像对象),但除非您使用 Shape3D 类 setMaterial(Material)方法调用(您在上一章中了解到)将 3D 基本体和 Phong 着色器定义连接在一起,否则您将看不到应用到 3D 对象表面的着色器。

球体对象实例化后,使用点标记法添加一个对球体对象的 setMaterial(phongMaterial)方法调用,如图 13-2 中黄色突出显示。将这个相同的方法调用添加到 pole Cylinder 对象和 box Box 对象中。在截图之前,我在 NetBeans 9 中用黄色单击了 phongMaterial 着色器对象,以突出显示它的所有用法,从声明到实例化再到用法。您添加的语句的 Java 代码应该如下所示:

A336284_1_En_13_Fig2_HTML.jpg

图 13-2。

Wire the phongMaterial to the three primitives, using a setMaterial(phongMaterial) method call off each

sphere.setMaterial(phongMaterial);
box.setMaterial(phongMaterial);
pole.setMaterial(phongMaterial);

使用您的 Run ➤项目工作流程,查看 phongMaterial 渲染,如图 13-3 所示。

A336284_1_En_13_Fig3_HTML.jpg

图 13-3。

Showing the phongMaterial object with the diffuseColor property set to a Color.GOLD value

接下来,让我们使用 setSpecularColor()方法和颜色将高光颜色添加到 phongMaterial shader 对象中。黄色常数。在 phongMaterial 对象实例化之后添加一行代码,然后键入 phongMaterial 对象名称。点击句点键,从弹出的帮助选择器中选择 setSpecularColor(颜色值)选项,双击它将其插入到 Java 语句中。在参数区域内键入 Color,然后键入句点键,并通过向下滚动或键入 Y 跳转到 Y 颜色常量来选择黄色常量。

您生成的 Java 语句应该看起来像下面的 Java 代码,在图 13-4 的中间用黄色和浅蓝色突出显示:

A336284_1_En_13_Fig4_HTML.jpg

图 13-4。

Call the setSpecularColor() method off of the phongMaterial object, passing the Color.YELLOW constant

phongMaterial.setSpecularColor(Color.YELLOW);

如果您使用您的运行➤项目工作流程,在这一点上,您将看到您的图元表面的外观发生了巨大的变化,图元的边缘变得更加圆滑。事实上,如果你比较图 13-3 和图 13-5 ,你会发现长方体基本体根本不受高光颜色的影响,除非你设置它的动画,在这种情况下,当它平行于点光源时,偶然的面会被高光颜色着色。

A336284_1_En_13_Fig5_HTML.jpg

图 13-5。

Run your project to see the PhongShader object configured to use a Color.YELLOW specularColor property

但是,随着使用“镜面反射颜色”属性添加镜面反射高光,圆柱体和球体类(对象)基本体的外观发生了巨大变化。我用黄色给它一个金属的外观,但如果你用白色(默认),它会看起来更正常。请注意,点光源可以设置为白色,并且您可以在点光源照射到原始曲面之前对其进行调节(添加颜色过滤器)。

因此,如果你正在寻找真实感,确保你的点光源和镜面颜色值匹配!

PhongMaterial 类(object)的 specularPower 属性(attribute)控制曲面的亮度,至少在弯曲的对象上是如此。如图 13-3 所示,镜面高光为零,产生了所谓的无光泽表面。需要注意的是,调用 setspectrorpower(0)不会移除镜面高光。事实上,那样会适得其反,给你一个巨大的“爆炸”高光,看起来很糟糕。接下来让我们研究一下这个属性,然后我们可以继续研究所有其他属性。其余的属性涉及地图和它们的图像对象,这将涉及数字成像软件,在我们的例子中是 GIMP 2.10(或者 3.0,如果已经发布的话)。

让我们使用双精度数据值为 12 的 setSpecularPower()方法调用,将 specularPower 属性设置添加到 phongMaterial shader 对象。从技术上讲,这在 Java 代码中被标注为“12.0d”。但是,由于一个整数(只有 12)数据值符合 Double 规范,所以您可以只使用 12,Java 构建和编译过程将理解您正在做什么,并确保它被配置为 Double 值(在运行时)。

在 PhongMaterial 对象实例化之后添加一行代码,并键入 phongMaterial 对象名称。点击句点键,从弹出的助手选择器中选择 setspecrorpower(Double value)选项,双击它将其插入到 Java 语句中。在参数区域内键入 12 或 12.0d。

您生成的 Java 代码将看起来像下面两个 Java 语句中的一个,如图 13-6 底部的三分之一所示:

A336284_1_En_13_Fig6_HTML.jpg

图 13-6。

Call the setSpecularPower() method off of the phongMaterial object, passing the double value of 12

sphere.setSpecularPower(12);    // If you use Integer (simpler) format Java will convert for you
sphere.setSpecularPower(12.0d); // You can also use the 12.0d (double) required numeric format

我愚弄了这个值,改变它,并通过运行➤项目渲染。如图 13-5 所示,默认值似乎在 20 左右,即 20.0d。改变该值会产生非常细微的变化;较低的数值将用于扩大镜面高光(尝试零设置,但不要在游戏中使用它,除非是为了特殊效果),而较高的数值将限制它在任何曲面上的精确位置。平坦的表面不会受到太大的影响,如果有的话。

使用“运行➤项目”工作过程来查看 12 的“镜面反射功率”设置将如何扩展镜面反射高光。这可以从图 13-7 中看出。

A336284_1_En_13_Fig7_HTML.jpg

图 13-7。

A specularPower property set to 12 will expand the specular highlight on the surface

接下来,将 setspecularpower()方法调用值更改为 100(或 100.0d),然后使用“运行➤项目”工作流程来查看具有更高镜面反射能力的图元,这将使它们更闪亮,或更“有光泽”,如图 13-8 所示。

A336284_1_En_13_Fig8_HTML.jpg

图 13-8。

A specularPower property set at 100 will actually contract or reduce the specular highlight on the surface

既然我们已经在本章的第一部分介绍了基本的漫反射颜色、镜面反射颜色和镜面反射能量属性,让我们更进一步,开始应用在 GIMP 中创建的图像,学习使用四个纹理贴图效果(凹凸/法线、漫反射、镜面反射和发光或自发光)通道的高级纹理贴图。

使用外部图像资源:创建纹理贴图

PhongMaterial 类及其算法最强大的功能是支持的四个纹理贴图属性。这为您提供了四个着色器通道来影响您的曲面颜色(漫反射贴图):光泽(镜面贴图)、照明(自照明贴图)和高度(凹凸贴图或法线贴图)。想象一下这种类似于数字图像层合成的情况,其中这四个通道将由 Phong 着色器渲染算法进行组合,然后将镜面反射颜色和能量应用到曲面(由 specularMap 属性图像对象引导,如果 PhongMaterial 着色器管道中存在)。

使用外部第三方软件:使用 GIMP 创建地图

Java 9 和 JavaFX 的设计足够灵活,允许您使用高级(专业)第三方软件,如 GIMP(数字图像合成)、Blender (3D 建模)、Fusion(特效)、Inkscape (SVG 内容)或 Audacity(数字音频编辑)。纹理贴图通常在专业的像素编辑和图层合成软件中制作和细化得最好,如免费的开源 GIMP 2.8(即将成为 GIMP 3.0),它非常强大。

www.gimp.org 下载 GIMP 并安装。然后启动它,这样你就可以和我一起创建一些纹理贴图,这些贴图将恰当地展示你在本章前几页学到的四种不同类型的纹理贴图通道。使用文件➤新建菜单序列并访问创建新图像对话框,如图 13-9 红色 1 所示,并将宽度和高度字段设置为 2 的幂。渲染器最适用于二进制或 2 的幂的数字,包括 2、4、8、16、32、64、128、256 等。大多数游戏使用 256 像素的纹理贴图,所以我在这里使用这个尺寸。将色彩空间下拉列表设置为 RGB,并将背景色填充下拉列表设置为白色。使用图层➤新建图层菜单系列创建一个新图层,或者右键单击图层面板中的背景图层(红色 3)并选择新建图层,这将打开如图 13-9 中红色 2 所示的新建图层对话框。将图层名称设置为灰度贴图,将图层填充类型设置为透明,然后单击确定按钮创建图层。使用相同的工作过程来创建第二层,称为彩色地图,如红色 3 所示。选择灰度地图层以显示 GIMP 在哪里应用你的下一个图像创建“移动”(操作),并选择一个矩形选择工具,在图 13-9 中右上方中间显示为按下。一个矩形选择工具选项(红色 4)将出现在图的右下角,在这里您可以精确地(像素精确地)设置选择的位置和大小设置。

A336284_1_En_13_Fig9_HTML.jpg

图 13-9。

Create a 256-pixel image, add layers to hold your color and grayscale maps, and create eight striped areas

接下来,在 GIMP 画布上画出任意大小的矩形选择,如图 13-10 右侧所示。在位置字段中,设置 0,0,在大小字段中,设置 32,256。这将把选区放在八分之一跨度和画布的左侧。单击 GIMP 工具图标下前景/背景颜色样本旁边的小黑底白字图标,将 FG 颜色设置为黑色,将 BG 颜色设置为白色;然后使用你的编辑➤填充 FG 颜色菜单序列,用黑色填充四个条纹的第一个。由于该层是透明的,背景是白色的,合成的结果将是一个黑白纹理贴图(最终是四个黑白相间的条纹)。接下来,将选区向右拖动,放置第二个条纹填充;然后编辑位置字段以设置 64,0,并将大小字段设置为 32,256。再次使用编辑➤填充 FG 颜色,并将选择拖动到位置(或将位置字段设置为)128,0,选择填充 FG 颜色,并将选择拖动到位置(或将位置字段设置为)192,0。最后,最后一次选择用 FG 颜色填充,完成黑白效果(凹凸,高光)应用纹理贴图。在图 13-10 中的第二层灰度贴图中可以看到黑白(或透明)纹理贴图。

A336284_1_En_13_Fig10_HTML.jpg

图 13-10。

Create a beach ball texture in the Color Map layer and an on/off (black/white) grayscale striped texture

现在,我们已经创建了(更容易的)镜面反射或凹凸贴图效果图像资源,让我们创建一个颜色来显示漫反射颜色贴图将如何工作。稍后,我们将把它们结合起来使用(在不同的着色器通道中),并试验这些 PhongMaterial 属性能为我们的 pro Java 9 游戏开发做些什么。

为了确保你的颜色数据与你的效果(灰度)数据是分开的,选择颜色贴图层,它会变成蓝色表示它被选中,如图 13-10 左侧所示。如果愿意,可以通过单击图层左侧的眼睛图标来关闭灰度地图图层的可见性。在位置字段中设置 0,0,在大小字段中设置 32,256。这将再次把选区放在八分之一跨度和画布的左侧。点击 FG/BG 色样(颜色选择器)上的黑色方块,弹出颜色选择器对话框,设置绿色,如图 13-10 所示。一旦你点击确定,这将设置 FG 颜色为绿色,BG 颜色将保持白色。使用编辑➤填充 FG 颜色菜单序列,用绿色填充四个条纹中的第一个。由于该层是透明的,背景是白色的,因此合成的结果将是一个绿色和白色的纹理贴图(最终是四个交替的彩色和白色条纹)。接下来,将选区向右拖动 64 个像素,将其定位为第二个条纹填充;然后编辑位置字段以设置 64,0,并将大小字段设置为 32,256。使用拾色器设置蓝色,并再次使用编辑➤填充 FG 颜色创建第二个蓝色条纹。接下来,将 64 个像素向右拖动到位置 128(或将位置字段设置为 128,0),使用拾色器选择黄色前景(FG)颜色,并使用 FG 颜色填充第三个条纹。最后,将选择拖动到位置 192,0(或使用位置字段设置),使用拾色器选择红色前景(FG)颜色,然后最后一次使用“编辑➤填充 FG 颜色”菜单序列来完成沙滩球颜色(漫射,发光)应用纹理贴图创建。图 13-10 显示了 GIMP 中的最终结果。

我还将创建一个纹理贴图,交替使用 25%的灰色和 50%的灰色条纹来显示不同效果的应用,如镜面和自发光,以及如何通过使用不同的灰色阴影来控制效果应用的强度或大小。您可以创建第三个贴图,作为重新创建我们之前用于彩色和黑白纹理贴图的工作流程的“练习回合”。要导出你在 GIMP 2.10 中创建的任何纹理贴图,你可以使用文件➤导出图像作为菜单序列,这将打开导出图像对话框,如图 13-11 所示。

A336284_1_En_13_Fig11_HTML.jpg

图 13-11。

Export to C:\Users\Name\Documents\NetBeansProjects\JavaFXGame\src

如图 13-11 所示,您可以使用该对话框顶部的文件导航部分来定位您的 NetBeansProjects 文件夹。我用文件名中的描述、颜色数和像素数来命名文件。确保使用您的 JavaFXGame 文件夹和\src\子文件夹,其中保存了游戏的源代码,就像我们在本书中一直做的那样。一旦文件位于正确的文件夹中,它们将对 NetBeans 9 可见,并且我们可以在代码中将它们用作图像对象素材。接下来,让我们回到 PhongMaterial 对象编码,并进一步探索着色器管线的创建,因为这是让您的 Pro Java 9 i3D 游戏看起来非常壮观的一种方式。

在 PhongMaterial 中使用纹理贴图:着色器特殊效果

在 JavaFX 中使用 Image 对象的第一步是将 Image 对象的名称添加到类顶部的 Image 对象复合声明语句中。我将图像对象命名为与它们将被使用的属性相同的名称。接下来,由于我们有一个 loadImageAssets()方法,我们将添加四个引用包含纹理映射数据的 PNG 文件的图像实例化语句。如图 13-12 所示的 Java 代码应该如下所示:

A336284_1_En_13_Fig12_HTML.jpg

图 13-12。

Declare and instantiate Image objects to hold texture map data for a diffuse, specular, glow, or bump map

Image diffuseMap, specularMap, glowMap, bumpMap // plus the other Image objects already in use
...
diffuseMap = new Image("/beachball5color256px", 256, 256, true, true, true);
specularMap = new Image("/beachball3grayscale256px", 256, 256, true, true, true);
glowMap = new Image("/beachball2grayscale256px", 256, 256, true, true, true);
bumpMap = new Image("/beachball3grayscale256px", 256, 256, true, true, true);

接下来,进入 createBoardGameNodes()方法,将漫反射和镜面反射颜色设置更改为一种颜色。白色值,并添加对 phongMaterial 对象的 setDiffuseMap(diffuseMap)方法调用。在图 13-13 中用蓝色突出显示的漫反射颜色纹理贴图语句的 Java 代码应该如下所示:

A336284_1_En_13_Fig13_HTML.jpg

图 13-13。

Add a diffuseMap to the shader pipeline to add some surface color and set the specular and diffuse colors to white

phongMaterial = new PhongMaterial(Color.WHITE);
phongMaterial.setSpecularColor(Color.WHITE);
phongMaterial.setSpecularPower(20);
phongMaterial.setDiffuseMap(diffuseMap);

接下来,使用运行➤项目工作流程,再次查看您的原语。正如你所看到的,在图 13-14 中,你的基本体的表面现在正在使用一个漫射贴图来控制它们的表面着色,并且球体 3D 基本体现在看起来像一个沙滩球。

A336284_1_En_13_Fig14_HTML.jpg

图 13-14。

A diffuse color texture map is now painting the surface of the primitive, making a sphere into a beach ball

接下来,让我们将你的球体旋转 25 度,以便黄色和白色条纹之间的描绘发生在镜面高光中,我们在前面的代码中将其扩展回默认设置 20。

我们将使用 beachball3grayscale256px.png 形象素材;它有八个条纹,其中四个是 100%开(白色),两个是 75%开(25%灰色),两个是 50%开(半功率,或 50%灰色)。这将“静音”或减少沙滩球白色部分的镜面眩光,因为镜面贴图定义了镜面效果的强度或数量(闪亮度)。

我们将保留调用 phongMaterial 的 setDiffuseMap(diffuseMap)方法,因为我们试图在本章中构建一个高级着色器渲染管道,以将 PhongMaterial 类推到专业着色器效果创建管道的极限,就像我们在 3D 软件中一样,但只使用 JavaFX API 和 Java 9 语句。

因此,在 setDiffuseMap()方法调用之后,我们将添加一行 Java 代码,调用 phongMaterial 对象的 setSpecularMap(),然后传入在 loadImageAssets()方法中已经设置为 beachball3grayscale256px.png 图像素材的 specularMap 图像对象,如图 13-12 所示。这将通过使用以下 Java 语句来完成,这些语句在图 13-15 的底部突出显示:

A336284_1_En_13_Fig15_HTML.jpg

图 13-15。

Add a SpecularMap Image reference to the shader pipeline to control the specular highlight intensity

phongMaterial = new PhongMaterial(Color.WHITE);
phongMaterial.setSpecularColor(Color.WHITE);
phongMaterial.setSpecularPower(20);
phongMaterial.setDiffuseMap(diffuseMap);
phongMaterial.setSpecularMap(specularMap);
sphere = new Sphere(100, 24);
sphere.setRotationAxis(Rotate.Y_AXIS);
sphere.setRotate(25);
sphere.setMaterial(phongMaterial);

现在是时候再次使用运行➤项目的工作过程,并把这个着色器管线渲染到您的 3D 场景中。正如你在图 13-16 中看到的,球体上的高光似乎被黄色和白色之间的线切断了。这是由纹理贴图的交替区域的高光贴图(降低高光强度)造成的。这也可以在圆柱体原语上看到。现在你可能已经注意到了,为了放大视图,我减少了相机与场景中心的距离,从 250 个单位减少到 100 个单位,并且我增加了 3D 图元的大小,这样我们可以更清楚地看到纹理映射效果。

A336284_1_En_13_Fig16_HTML.jpg

图 13-16。

The specular highlight on the curved surface sphere and pole objects is now brighter on the colored area

接下来,让我们把你的球体旋转回 5 度,这样你的黄色部分就在高光的中心,白色条纹在两边。这将更准确地向您展示自发光贴图的威力。

我们将使用 beachball2grayscale256px.png 图像素材,它有八个条纹。其中四个是 100%打开(白色),四个是 100%关闭(黑色),就效果处理纹理贴图而言,这是最极端的情况,因为这等同于完全应用(白色或全部打开 255 值)或不应用(黑色或零)。

这种自发光贴图(在 3D 软件中通常称为发光贴图)将像光源一样打开用白色贴图的 3D 图元部分,而黑色区域将不会被照亮,并将使用现有的纹理贴图管道。更多的灰色将增加更多的光,因此 25%的灰色将模拟 25%的照明(25%的光强度)。我们将保留调用 phongMaterial 的 setDiffuseMap()和 setSpecularMap()方法,因为我们试图构建一个高级着色器渲染管道,并将 PhongMaterial 类推到专业着色器效果创建管道的极限,就像在 3D 软件中一样,但只使用 JavaFX API 和 Java 9 语句。

因此,在 setSpecularMap()方法调用之后,我们将调用 phongMaterial 对象的 setselflightionmap(glowMap)方法,并传入 glow map 图像对象,设置为在 loadImageAssets()方法中实例化的 beachball2grayscale256px.png 图像素材,如图 13-12 所示。这将通过使用以下 Java 语句来完成,这些语句在图 13-17 的底部用黄色和浅蓝色突出显示:

A336284_1_En_13_Fig17_HTML.jpg

图 13-17。

Add the SelfIlluminationMap Image reference to the shader pipeline to control self-illumination intensity

phongMaterial = new PhongMaterial(Color.WHITE);
phongMaterial.setSpecularColor(Color.WHITE);
phongMaterial.setSpecularPower(20);
phongMaterial.setDiffuseMap(diffuseMap);
phongMaterial.setSpecularMap(specularMap);
phongMaterial.setSelfIlluminationMap(glowMap);
sphere = new Sphere(100, 24);
sphere.setRotationAxis(Rotate.Y_AXIS);
sphere.setRotate(5);
sphere.setMaterial(phongMaterial);

图 13-18 显示了运行➤项目 Java 代码测试工作流程,在所有三个原语上都有自发光映射。白色区域已转化为光源,彩色区域仍显示漫反射和镜面反射贴图特征。selfIlluminationMap 属性代码的抗锯齿算法部分似乎有一点问题,正如您在球体图元的周界边缘上看到的那样。

A336284_1_En_13_Fig18_HTML.jpg

图 13-18。

The self-illumination map turns white area on 3D primitives into a light source, leaving color areas alone

接下来,让我们看看如何使用我们到目前为止所学的知识,并在 GIMP 2.8.22 中创建一些着色器的纹理贴图组件,以便在我们开始使用 Java 9 中的 JavaFX 9 APIs 构建 i3D 游戏时,在第十四章中创建的游戏板组节点层次中使用。

游戏板纹理:创建游戏板方块

理解抽象网格超类及其与 TriangleMesh 子类(可用于“手工编码”复杂网格对象)的关系,以及它与 MeshView 类(实际上是 Shape3D 的子类,而不是 Mesh 的子类)的关系非常重要!这是为了使 MeshView 可以继承(扩展)Shape3D 的 cullFace、drawMode 和 material 属性,当然,这些属性对于使网格对象变得逼真是至关重要的(尤其是 material 属性和 Material 类)。正如您将看到的,MeshView 构造函数采用一个网格对象,因此这是复杂 3D 对象所基于的核心类(算法);因此,Mesh 和 MeshView 是用于 pro Java 9 游戏开发的关键类。如果出于某种原因,您想要编码复杂的多边形几何图形,也称为“三角形网格”(这不是一个最佳的工作流程),您可以使用 Triangle Mesh,我们将详细介绍它。

更好的工作流程是使用外部 3D 软件包,将 3D 对象直接“导入”到网格对象中,然后由 MeshView 对象引用。这是一个工作流程,我们将用整整一章来讲述如何使用这些 JavaFX 类来“建模”3D 游戏,这样您就不必导入任何“数据繁重”的网格对象。导入 3D 资源可以更快地快速高效地启动和运行高级 i3D 游戏,也是将专业人员引入 i3D 游戏开发工作流程的一种方式。

准备创建游戏板:代码重新配置

让我们为下一章将要做的事情(构建我们的 i3D 游戏板)做好准备,并为我们的 gameButton 事件处理程序、createBoardGameNodes()方法、addNodesToSceneGraph()方法和 loadImageAssets()方法重新配置 Java 代码体。让我们从相机对象推拉切换,设置相机 Z = 0,而不是使用 FOV 来放大和缩小场景。由于我们现在要删除球体和圆柱体基本体,我们将 X 和 Y translate 属性设置为-500,并将相机绕 X 轴旋转 45 度,以便它向下看游戏板。进行这些摄像机调整的 Java 代码如图 13-19 所示,如下所示:

A336284_1_En_13_Fig19_HTML.jpg

图 13-19。

Reconfigure your camera object to dolly to Z = 0, rotate 45 degrees, and zoom in with FOV = 1

camera.setTranslateZ(0);
camera.setTranslateY(-500);
camera.setTranslateX(-500);
camera.setRotationAxis(Rotate.X_AXIS);
camera.setRotate(-45);
camera.setFieldOfView(1);

接下来,让我们删除球体球体和极柱圆柱体实例化和配置语句;如果你愿意,你可以把声明放在类的顶部,因为我们以后会用到它们。

要制作一个游戏板正方形,它将在游戏板的周边使用,并且将是 150 个单位的正方形和 5 个单位的薄(高),我们将留下 Box box 对象并用 Box(150,5,150)方法调用来构造它。我现在也将它旋转 45 度,使点(角)面向相机对象。我们可以保留 PhongMaterial 代码,因为一旦我们在 GIMP 中创建了 diffuseMap,我们所要做的就是在 loadImageAssets()方法中更改文件名,这将在我们创建游戏棋盘正方形纹理贴图之后进行。不要忘记,如果你忘记删除我们已经从 SceneGraph 节点中删除的对象,你将在编译期间得到一个致命的错误。

如前所述,一个盒子构造器方法是非常基本的,看起来像图 13-20 中的 Java 代码:

A336284_1_En_13_Fig20_HTML.jpg

图 13-20。

Remove sphere and pole instantiations and configurations and change the box dimensions to 150, 5, 150

box = new Box(150, 5, 150);
box.setRotationAxis(Rotate.Y_AXIS);
box.setRotate(45);
box.setMaterial(phongMaterial);

接下来,让我们从游戏板组对象中移除这些(当前)未使用的极点和球体原语,这将把我们的 addAll()方法调用改回 add()方法调用。如果您忘记这样做,并试图选择运行➤项目,它将不会编译。产生的 Java 语句如图 13-21 所示,如下所示:

A336284_1_En_13_Fig21_HTML.jpg

图 13-21。

Remove the pole and sphere objects from your gameBoard.getChildren().addAll() method call for now

gameBoard.getChildren().add(box);

现在让我们回到 GIMP,给我们的纹理贴图合成添加一层,创建一个游戏棋盘方块。

创建你的游戏板正方形漫射纹理:使用 GIMP

让我们在本章中完成一些游戏棋盘方块的设计,所以在下一章中,我们要做的就是设计棋盘的中心,创建四周的方块,并对图像进行颜色转换,以创建方块之间的轮廓。我们将在 GIMP 中使用我们在本章前面使用的相同方法来完成此操作,使用相同的 32x256 条纹,只是这一次四个条纹将位于游戏棋盘正方形的周边。我们将使用 RGB 255,0,0(纯红色),这样我们就可以用 GIMP 中的算法对这个值进行颜色转换。

打开您的多层 GIMP XCF 文件,右键单击顶层,并使用新的➤图层菜单项创建一个空的透明层。关闭除白色背景图层之外的其他图层中的所有可见性(眼睛)图标。将层名称设置为 GameBoardTile。确保选择这一层,使它变成蓝色,以显示 GIMP 在哪里应用您的下一个图像创建“移动”(操作)。

选择你的矩形选择工具,在图 13-22 的中上方显示为按下。矩形选择工具选项将出现在工具图标的下面,如图的底部中间所示,您可以(再次)精确地(精确到像素)设置您的选择的位置和大小设置。

A336284_1_En_13_Fig22_HTML.jpg

图 13-22。

Use the same Rectangle Select technique we used earlier in the chapter to create a game board square

接下来,在 GIMP 画布上画出任意大小的矩形选区,如图 13-22 右侧所示。在位置字段中,设置 0,0,在大小字段中,设置 32,256。这将把选区放在八分之一跨度和画布的左侧。单击顶部颜色上位于 GIMP 工具图标下的大前景色/背景色样本,并将 FG 颜色设置为红色。然后使用你的编辑➤填充 FG 颜色菜单序列,用红色填充前四个条纹。由于这一层是透明的,背景是白色的,合成的结果将是一个红白纹理贴图(最终四个重叠的红色条纹)。

接下来,将选区向右拖动,放置在第二个条纹填充的位置。然后编辑位置字段,将其设置为 0,0,并反转大小字段,将其设置为 256,32,如图 13-22 所示。再次选择编辑➤填充 FG 颜色。一半的游戏板广场漫射颜色纹理地图已创建在短短几个步骤!

让我们通过再次拖动选择到 256 像素纹理贴图画布右侧的位置(或将位置字段设置为)224,0 来完成其他两个周界条纹。请确保将您的大小数据字段设置回 32,256(宽度、高度),然后再次使用“编辑➤填充 FG 颜色”将右边的周界条纹填充为红色(也是 JavaFX 中的颜色类常量)。最后,拖动选择位置(或将位置字段设置为)0,224,然后最后一次使用编辑➤填充 FG 颜色,以完成黑白效果(凹凸,镜面反射)应用纹理贴图。

除了能够为您的漫反射颜色纹理贴图对周边颜色进行颜色转换,以创建几十个独特的游戏棋盘方块,因为内部颜色是白色,不会受到影响(白色、黑色和灰色没有颜色值可以进行颜色转换)。

使用我们在本章中学到的其他概念和代码技术,我们将能够创建其他 PhongMaterial 类着色器对象,当游戏棋子落在特定的游戏棋盘方格上时,这些对象将高亮显示、发光或给当前活动的游戏棋盘方格涂上不同于所有其他方格的颜色。

值得注意的是,这将只使用一个单一的漫反射颜色纹理贴图(680 字节或 1 千字节数据/内存的三分之二)来完成,从而交互式地为您的游戏提供更专业的用户体验。我还将使用黑色、白色和灰色创建一个效果纹理图(可能是两个或三个),这将与红白一个像素一个像素地匹配,为我提供游戏代码中最具过程性(外科手术)的效果应用。白色周边(和黑色内部)将允许我只隔离彩色区域以获得特殊效果,而黑色周边(和白色内部)允许我隔离游戏棋盘方块的内部以获得特殊效果。我们将把这几个纹理与数字成像(第二章)和漫射和镜面颜色控制结合起来。

最后,确保使用 GIMP 的文件导出为工作流程,如图 13-11 所示,将完成的游戏棋盘正方形漫反射纹理贴图数据保存在 NetBeansProject 文件夹和 JavaFXGame 子文件夹中正确的 source assets 文件夹下的一个名为 gameboardsquare.png 的文件中。现在,我们所要做的就是将这个文件名引用交换到 loadImageAssets()方法体内的 diffuseMap 图像对象实例中,并且我们可以使用 box()构造器方法在我们之前创建的新的 box Box 对象配置中利用它(参见之前的图 13-20 )。

打开 loadImageAssets()方法体,编辑 diffuseMap 图像对象实例化,使其引用从 GIMP 导出到NetBeansProject\JavaFXGame\src\文件夹的 gameboardsquare.png 文件。新图像实例化的 Java 语句应该如下所示,在图 13-23 中用黄色和浅蓝色突出显示:

A336284_1_En_13_Fig23_HTML.jpg

图 13-23。

Change your diffuseMap Image object instantiation statement to reference your gameboardsquare.png file

diffuseMap = new Image("/gameboardsquare.png", 256, 256, true, true, true);

图 13-24 显示了运行➤项目 Java 代码测试的工作流程;您可以看到新的游戏棋盘方形盒子盒子对象映射了新的漫反射颜色纹理贴图,这是您刚刚使用 GIMP 2.8.22(或更高版本)创建的。

A336284_1_En_13_Fig24_HTML.jpg

图 13-24。

We now have a game board square, which will be duplicated around the perimeter (in the next chapter)

边缘上有一点白色(JavaFX 目前不允许每边框对象映射),我们将在以后的章节中通过调整camera.setRotate()方法调用值来最小化它,直到它变得不那么明显。

在结束本章的着色器管线创建和纹理贴图之前,我还想说明最后一点,即如何使用 JavaFX 为 3D 图元设置皮肤。你可能想知道为什么我对这个纹理使用 PNG24 (24 位)图像格式,而不是更优化的 PNG8 格式。嗯,这个 PNG24 编解码器在将 256 × 256 × 3 (196,608)字节压缩为 680 字节方面做得非常好,数据减少了 290 : 1 或 99.67%!

从更技术性的角度来看,Java 将在内存中使用 24 位 RGB 颜色表示,因此,如果我们使用了 8 位索引彩色图像,那么当它被加载到内存中时,就会被简单地转换回 24 位颜色值图像。因此,我倾向于尽可能使用 PNG24 和 PNG32 图像,特别是对于 3D 纹理贴图,无论如何,对于 pro Java 9 游戏设计和开发应用,这些贴图主要是 32x32、64x64、128x128、256x256 和 512x512。对于照片图像,您也可以使用 JPEG。

摘要

在第十三章中,我们学习了 javafx.scene.paint 包中允许您使用 3D 着色器、纹理贴图和材质的类,包括基于抽象材质超类的 PhongMaterial 类。我们了解到 Material 类基本上是一个“空”类或一个“外壳”来容纳一个“材质”对象(Shape3D 类中的属性),而 heavy lifting(算法)在 PhongMaterial 子类中。我们相当详细地研究了这个类中的属性、构造函数和方法调用,以便您知道 PhongMaterial 对象可以做什么,然后我们研究了如何用 Java 代码实现这些(bumpMap 除外,它在我正在使用的当前 JavaFX 9 代码库中不起作用,因此我们将在本书的后面部分再次讨论)。

您了解了如何使用 GIMP 创建纹理贴图资源(目前本书的版本为 2.8.22,但我预计 2.10 将于 2017 年推出,3.0 将于 2018 年推出),以及如何通过在最佳工作流程中使用 GIMP 的工具来创建平衡的、像素精度的 2 次方纹理贴图,这些贴图针对专业 3D 游戏开发进行了优化。

然后,我们看了看如何在四个当前的纹理贴图“通道”中实现这些纹理贴图素材,这些通道目前通过 JavaFX 9 PhongMaterial 类提供给我们。我们看到了这些纹理贴图通道如何允许我们微调材质属性的渲染方式,从而允许我们为 Java 9 游戏创建更加专业的外观。

最后,我们为我们的游戏板方块创建了 diffuseColor 属性纹理贴图,将 box Box 对象转化为这些游戏板方块中的一个,并将新的纹理贴图应用到新的 3D 原始“plane”对象,为我们在下一章将要做的事情(在场景图中创建我们的游戏板分支)做准备,以便它看起来像我们正在创建的游戏板。如您所知,我建议以这样一种方式进行您的 pro Java 9 游戏开发,即在您编写 Java 9 代码、创建新媒体素材以及将您的 pro Java 9 游戏内容和交付内容“变形”成您最终想要的样子时,您可以看到 JavaFX 9 将要做什么。Pro Java 9 游戏开发是一个精炼的过程,所以这就是我写这本书的方式。我将向您展示我实际上是如何使用 NetBeans 9 IDE 以及 Java 9 和 JavaFX 9 APIs“凭空蒸发一个 3D 棋盘游戏应用”的。

在第十四章中,我们将进一步完善我们的 Java 代码组织,创建新的方法并重组一些现有的方法,以创建并整合我们的 i3D 棋盘游戏的核心 gameboard。我们将在游戏板组节点(分支)下创建一个嵌套的组 3D 层次结构,并查看 3D 图元 X、Y、Z 定位和相关概念,这些概念适用于无缝布局 3D 游戏板,以便未来的 Java 代码可以以逻辑、最佳的方式访问和引用其组件和子组件。就像数据库设计一样,你如何设计你的 SceneGraph 极大地影响了你的 Pro Java 9 3D 游戏在未来如何运行。我们保持设计、层次结构和 3D 对象命名模式越简单、越直接,我们在为交互性、动画、运动、碰撞检测等编写未来代码时就会处于更好的状态。此时,您应该开始对 Java 9 和 JavaFX 9 为您提供的可能性感到兴奋了。