Библиотека Java 3D API состоит из почти двухсот взаимодействующих между собой специализированных классов. Чтобы разобраться во всех этих связях, нужно изучить их основные свойства и методы. Сама библиотека условно подразделяется на базовую часть, в которую входят javax.media.j3d и javax.vecmath, и вспомогательную, состоящую из com.sun.j3d.audioengines, com.sun.j3d.loaders и com.sun.j3d.utils. Первая служит фундаментом Java 3D API, определяет ее технические возможности и задает механизм взаимодействия объектов. Вторая представляет собой надстройку, реализованную с помощью базовых классов, облегчающую применение наиболее часто употребляемых операций и расширяющую возможности разработчика.
Начнем рассматривать библиотеку с фундамента. Ключевое понятие в Java 3D API — граф сцены. Несмотря на привычное название, он имеет очень мало общего с графом в его классическом понимании. Это иерархическое дерево с расположенным вверху основанием. Узлы такого дерева соответствуют экземплярам классов библиотеки (рис. 1). Под ветвью графа понимается совокупность узлов, объединенных связями и имеющих общего предка.
Рис. 1 |
Граф предназначен для того, чтобы задавать структуру сцены, изменять ее в реальном времени и реализовывать механизм наследования. Последнее действие воплощается через так называемый механизм группировки, позволяющий распространять изменения свойств одного узла на все лежащие ниже. Но принципы соподчинения элементов графа сцены значительно отличаются от правил иерархии классов в Java, поэтому не стоит путать эти понятия.
Большая часть классов библиотеки спроектирована с учетом взаимодействия элементов графа сцены. Их общий предок — абстрактный класс SceneGraphObject, определяющий основные возможности элементов, включаемых в граф сцены. Его дополняют еще два класса: абстрактный Node, вводящий основные механизмы построения графа сцены, и NodeComponent — базовый для всех, не участвующих в ее создании, но являющихся компонентами тех объектов, которые работают непосредственно с ним.
Свойства класса Node также переходят к двум потомкам — Group (группирующий узел) и Leaf (лист — конечный элемент, не имеющий подчиненных узлов и несущий информацию о визуальном восприятии сцены). Первый из них служит базовым классом для всех объектов, выполняющих работу по построению и модификации сцены. В нем предусмотрены методы для присоединения, вставки и удаления дочерних объектов. Таким образом, группирующий элемент способен управлять структурой лежащих ниже ветвей, манипулируя подчиненными узлами. Это основное отличие группирующих элементов от листьев, представляющих собой, как правило, данные об объектах визуализируемой сцены (источники света, 3D-модели, классы, описывающие «поведение», и пр.). Исключением считается только класс Link. Хотя он и относится к листам, все же предназначен для формирования структуры графа. В режиме реального времени с его помощью можно легко переключать ветви SharedGroup на другие ветви, реализуя механизм множественного наследования для некоторых конечных элементов.
Из ветви Group на практике обычно бывают полезны классы BranchGroup, OrderedGroup, SharedGroup, Switch и TransformGroup. Класс BranchGroup чаще всего применяется при конструировании графа сцены. Как правило, вначале к нему присоединяются все данные, относящиеся к конкретному визуализируемому объекту сцены, и только после этого он добавляется к графу сцены:
/* некий объект, к которому необходимо присоединить визуализируемую модель */ BranchGroup PointToGraph = new BranchGroup(); BranchGroup NewModel = new BranchGroup(); /* присоединение безымянного визуализируемого примитива к своему узлу управления, а потом к графу */ NewModel.addChild(new Box(x, y, z, App)); PointToGraph.addChild(NewModel);
Класс OrderedGroup выполняет такие же функции, но дает возможность выстраивать лежащие ниже элементы в порядке их рендеринга. Для этого каждому элементу присваивается индекс, значение которого и будет определять очередность. SharedGroup работает аналогично BranchGroup, но используется для его «разделения» между несколькими лежащими выше элементами. Именно он обеспечивает соединение в один узел нескольких ветвей, но вниз по иерархии (рис. 2).
Рис. 2 |
Класс SwitchGroup управляет рендерингом лежащих ниже конечных элементов. Он дает возможность определять, какие именно объекты должны быть обработаны, а какие скрыты. Еще один класс группы, TransformGroup, встречается наиболее часто, поскольку он способен определять все трансформации объектов, присоединенных к узлу. Ниже приведена схема его применения:
BranchGroup branch = new BranchGroup(); TransformGroup trans = new TransformGroup(); Trans.addChild(new Box()); Branch.addChild(trans);
Но все же основная роль этого класса не построение структуры графа сцены, а трансформация конечных элементов. И это особенно актуально потому, что существует еще несколько важнейших классов управления трансформациями, взаимодействующих с узлами посредством TransformGroup. Сам же TransformGroup содержит программный компонент Transform3D, обеспечивающий около сотни операций с матрицами. Воздействуя на него, можно задавать такие трансформации, как перемещение, вращение и масштабирование. Чтобы непосредственно применять данный компонент, его нужно сначала создать и вызвать методы модификации внутренних данных, а потом вставить в объект типа TransformGroup. Если объект TransformGroup уже имеет измененные координаты, то Transform3D нужно просто извлечь, модифицировать и вставить обратно:
BranchGroup branch = new BranchGroup(); TransformGroup trans = new TransformGroup(); Transform3D tr3d = new Transform3D(); /* один из множества способов задать значение компоненту Transform3D. В данном случае - масштабирование, которому передается в качестве параметров простейший безымянный объект типа Vector3d (три значения типа double) из библиотеки vecmath */ tr3d.set(scale, new Vector3d(xpos, ypos, zpos)); trans.setTransform(tr3d); branch.addChild(trans);
Также очень важным элементом считается объект-лист Behaviour, обеспечивающий автоматическое управление трансформациями. Принцип его работы заключается в следующем: после возникновения некоего условного события (завершение рендеринга текущего кадра, прохождение указанного интервала времени, воздействие пользователя и др.) вызывается метод processStimulus. В теле этого метода обрабатывается компонент типа Transform3D, вставляющийся в объект типа TransformGroup. Класс Behaviour — абстрактный, поэтому разработчик сам должен реализовать его методы, хотя в библиотеке имеется несколько созданных объектов такого типа, решающих большинство повседневных задач. Схема подключения любого объекта типа Behaviour выглядит так:
TransformGroup transgroup = new TransformGroup(); BoundingSphere bounds = new BoundingSphere(x, y, z, radius); MouseRotate behavior1 = new MouseRotate(transgroup); transgroup.addChild(behavior1); behavior1.setSchedulingBounds(bounds);
Здесь MouseRotate — Behaviour-объект из вспомогательной библиотеки, BoundingSphere — объект, определяющий зону, на которую распространяется воздействие MouseRotate. Причем в данном случае вовсе не требуется оказывать непосредственное влияние на компонент Transform3D.
И наконец, рассмотрим еще два объекта — Shape3D и Appearance. Первый включает класс Geometry — все геометрические данные визуализируемого объекта, и их может быть целый набор в случае реализации сложной графики, состоящей из жестко скрепленных элементов. Во втором объекте собраны параметры, необходимые для отображения поверхности объекта: цвет, режимы рендеринга, текстуры и пр. Класс Geometry — абстрактный, поэтому на практике используются такие его реализации, как PointArray, LineArray, IndexedLineArray и др., содержащие массивы вершин, отрезков, треугольников или других элементов, с помощью которых создается объемная модель. Множество способов представления позволяет подобрать оптимальный вариант для каждой конкретной модели.
Для работы с такими массивами подходят методы, наследованные от абстрактных классов Geometry и GeometryArray. Это удобно, но на обработку больших массивов требуется много времени. Можно повысить эффективность работы, обращаясь к массивам геометрии напрямую, минуя содержащие их классы. При этом наиболее ресурсоемкие операции обработки выполняются с помощью native-методов, реализованных, например, на Си++ для конкретной аппаратной платформы. Поэтому самая сложная операция модификации поверхности модели, морфинг, происходит в Java 3D с вполне приличной скоростью.
Чтобы использовать объект типа Shape3D, его нужно просто присоединить к графу с помощью метода addChild. Помимо Geometry объект Shape3D включает и Appearance, содержащий информацию о поверхности геометрической фигуры. Значит, прежде чем вставлять геометрический объект, следует создать и подготовить объект типа Appearance, также представляющий собой довольно сложную структуру. Однако для этого достаточно заполнить только некоторые поля, и кроме того, возможен даже вариант создания и применения с установками по умолчанию.
Практика
Наиболее интересным и полезным применением Java Media API является создание приложений для подготовки трехмерных сцен. Особенно это актуально в операционной системе Linux, для которой существует только один развитый и свободно доступный 3D-редактор Blender.
А прежде чем браться за создание приложения, следует переписать с сайта www.javasoft.com последние версии Java SDK и Java 3D SDK. В качестве рабочей среды можно выбрать свободно распространяемый JBuilder компании Borland (www.borland.com) или весьма удобный пакет xEmacs, имеющийся в любом дистрибутиве Linux. Это не просто текстовый редактор, это среда, обладающая скромными, но вполне достаточными средствами для написания программ на Java. Так, загрузив Java-программу в xEmacs, можно не только запускать программы и аплеты, но и отлаживать их, просматривая индикацию работы в окне вывода. Причем такого набора ПО вполне достаточно. Средства же визуального проектирования JBuilder лучше не использовать, поскольку сгенерированный исходный текст все равно придется переделывать.
Давайте построим скелет полноценного приложения, позволяющего создать маленькую виртуальную интерактивную сцену, в которую можно вставлять стандартные примитивы из Java 3D API. В соответствии с принципами объектно-ориентированного программирования разделим приложение на несколько небольших классов, каждый из которых будет выполнять определенный круг задач. Подобная структура облегчит повторное использование этой программы, так как можно будет легко разбить ее на компоненты JavaBean.
В примере, рассматриваемом ниже, для экономии места были удалены все операторы import и операции управления допустимостью (capability), его полный текст можно посмотреть на сайте www.kofestudio.h1.ru.
Итак, назовем первый класс MainKofeStudio и поместим его в одноименный файл с расширением java. Этот формальный класс предназначен для «содержания» метода main и служит своего рода входом в программу.
package kofestudio; public class MainKofeStudio { JFrame frame; public MainKofeStudio() { frame = new mainFrame(); frame.setVisible(true); } public static void main(String[] args) { new MainKofeStudio(); } }
Здесь описан класс, выполняющий функцию создания главного окна из пользовательского класса MainFrame, и затем создан его экземпляр в функции main. Теперь поговорим о достаточно простом классе MainFrame:
package kofestudio; public class MainFrame extends JFrame { Bar br = new Bar(); static SimpleCanvas3D can = new SimpleCanvas3D(); ModifyScenePanel mods = new ModifyScenePanel(); ModeCanvas mc = new ModeCanvas(); public MainFrame() { getContentPane().add(br, BorderLayout.WEST); getContentPane().add(can, BorderLayout.CENTER); getContentPane().add(mods, BorderLayout.SOUTH); getContentPane().add(mc, BorderLayout.EAST); pack(); } }
Сначала создадим в его теле основные компоненты. Затем в конструкторе добавим их в главное окно приложения, задав место расположения в параметрах метода add. Поскольку при этом берутся компоненты библиотеки Swing, то, чтобы вставить их, нужно указать имя «подпанели», вызвав метод getContentPane().
Основными элементами главного окна будут классы, условно называемые пользовательскими. Они расширяют базовый класс JPanel. Элемент br типа Bar станет главным меню, выдержанным в стиле LightWave; can типа SimpleCanvas3D предназначается для хранения компонента Canvas3D, отображающего виртуальную сцену; mods типа ModifyScenePanel обеспечит редактирование анимации, которую мы сегодня рассматривать не будем, и mc типа ModeCanvas — вспомогательная панель регулировки режимов работы виртуальной сцены.
Наиболее важный класс — SimpleCanvas3D. Он реализуется для того, чтобы продемонстрировать основные принципы работы, поэтому в дальнейшем рекомендуется выносить вспомогательные операторы в другие, новые классы.
package kofestudio; public class SimpleCanvas3D extends JPanel { /* Создадим экземпляр объекта для хранения информации о графической среде, вызвав статический метод, определяющий конфигурацию. Затем сделаем экземпляр объекта Canvas3D */ static private GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration(); static public Canvas3D bcan = new Canvas3D(config); /* Создадим вершину графа сцены */ private SimpleUniverse u = new SimpleUniverse(bcan); /* Выполним все элементы класса. Поскольку часть из них используется в статических методах обработки сообщений, они объявляются с модификатором static */ static BranchGroup rt; static TransformGroup viewTrans = new TransformGroup(); static BranchGroup shBranch = new BranchGroup(); static TransformGroup unviewTrans = new TransformGroup(); static Bounds bounds = new BoundingSphere(new Point3d (0.0,0.0,0.0), 100.0); static Transform3D tr3d = new Transform3D(); /* Этот метод демонстрирует способ подготовки графа сцены, имеющего в качестве вершины объект типа BranchGroup */ private BranchGroup createSceneGraph(){ /* Метод отделяет громоздкую инициализацию свойств объектов */ initGroups(); /* Сделаем узел, под которым можно организовывать работу с графом сцены */ BranchGroup objRoot = new BranchGroup(); /* Реализуем источники света по умолчанию */ createBaseLights(objRoot); /* Выполним базовый элемент в сцене. Его можно применять для представления основания сцены (ground) */ BranchGroup t1 = new BranchGroup(); t1.addChild(new com.sun.j3d.utils.geometry.Box (1f,0.01f,1f, new Appearance())); InsertBranch(t1); /* Вызовем метод, в котором реализована поддержка интерактивного взаимодействия со сценой, чтобы ее было более удобно просматривать */ makeTrans(viewTrans); /* Присоединим к нетрансформируемому узлу другой узел, содержащий все рабочие элементы сцены. Его отделяют от узла интерактивного взаимодействия тогда, когда требуется, чтобы могли обрабатываться события, поступающие непосредственно к элементам сцены, а она сама оставалась неподвижной */ unviewTrans.addChild(shBranch); objRoot.addChild(viewTrans); objRoot.addChild(unviewTrans); return objRoot; } /* Создадим класс SimpleCanvas3D */ SimpleCanvas3D(){ /* Установим цвета фона */ bcan.setBackground(new Color(0, 0, 0)); /* Зададим размер Canvas3D для экранного разрешения 1024x768 точек */ bcan.setSize(900, 670); /* Присоединим Canvas3D к панели */ add(bcan); /* Сделаем граф сцены с присоединением пользовательского узла */ rt = createSceneGraph(); u.getViewingPlatform() .setNominalViewingTransform(); u.addBranchGraph(rt); } /* Оба метода выполняют переключение разделяемого узла shBranch от трансформируемого узла к нетрансформируемому и обратно */ void offTransformScene(){ viewTrans.removeChild(3); viewTrans.getTransform(tr3d); unviewTrans.setTransform(tr3d); unviewTrans.addChild(shBranch); } void onTransformScene(){ unviewTrans.removeChild(0); viewTrans.addChild(shBranch); viewTrans.setTransform(tr3d); } /* Теперь вполне понятно, как с помощью объектов типа Behaviour происходит управление элементами сцены. В данном случае управляющие классы взяты из вспомогательной библиотеки */ void makeTrans(TransformGroup vt){ MouseRotate behavior1 = new MouseRotate(vt); vt.addChild(behavior1); behavior1.setSchedulingBounds(bounds); MouseZoom behavior2 = new MouseZoom(vt); vt.addChild(behavior2); behavior2.setSchedulingBounds(bounds); MouseTranslate behavior3 = new MouseTranslate(vt); vt.addChild(behavior3); behavior3.setSchedulingBounds(bounds); } /* Чтобы программа могла изменять структуру графа сцены во время выполнения, модифицируемым узлам следует явно задать необходимые свойства. Их названия говорят сами за себя */ void initGroups(){ // Здесь выставим все "возможности" } /* Это хороший пример создания в сцене источников света, которые можно рассматривать как вариант примитива и которыми можно аналогично управлять */ void createBaseLights(BranchGroup objRoot){ Color3f lColor1 = new Color3f(0.7f, 0.7f, 0.7f); Vector3f lDir1 = new Vector3f(-1.0f, -1.0f, -1.0f); Color3f alColor = new Color3f(0.2f, 0.2f, 0.2f); AmbientLight aLgt = new AmbientLight(alColor); aLgt.setInfluencingBounds(bounds); DirectionalLight lgt1 = new DirectionalLight(lColor1, lDir1); lgt1.setInfluencingBounds(bounds); objRoot.addChild(aLgt); objRoot.addChild(lgt1); } /* Этот метод мы используем для вставки примитивов, причем не только в сцену */ static public void insertBranch (BranchGroup bg){ shBranch.addChild(bg); } }
Рассмотрим класс Bar. Напомню, что все классы хранятся в одноименных файлах с расширением Java. Класс Bar имеет несколько кнопок и, что самое главное, раскрывающийся список JcomboBox. Выбрав какой-либо элемент этого списка, можно разместить объекты-примитивы в сцене.
package kofestudio; public class Bar extends JPanel { JButton openFile = new JButton("Open File"); JButton closeFile = new JButton("Close File"); JButton help = new JButton("Help"); JButton mod = new JButton("View Model"); JButton scen = new JButton("View Scena"); String[] data = {"Primitives", "Box", "ColorCube", "Cone", "Cylinder", "Sphere", "Text2D"}; JComboBox cbox = new JComboBox(data); JButton lm = new JButton("Load Model"); /* JFileChooser нужен для того, чтобы вставить в сцену объекты, созданные в других программах моделирования формата .obj и .lwo */ JFileChooser chooser = new JfileChooser ("Выберите файл модели"); Frame frame = new Frame(); /* А это пользовательский (определенный разработчиком) фильтр, позволяющий высвечивать в окне выбора файлов только каталоги и файлы с расширением .obj */ ExampleFileFilter objFilter; public Bar() { objFilter = new ExampleFileFilter("obj", "WaveFront Files"); chooser.addChoosableFileFilter(objFilter); chooser.setFileFilter(objFilter); this.setLayout(new GridLayout(40,1)); /* Раскрывающийся список был создан строкой выше. Здесь ему назначается обработчик события, реализующий примитив в сцене, вызвав метод вставки объекта в ее граф. При этом в качестве параметра передается объект пользовательского типа, чьему конструктору сообщается имя выбранного элемента */ cbox.addItemListener(new java.awt.event.ItemListener() { public void itemStateChanged (ItemEvent ie){ if(ie.getStateChange() == ItemEvent.SELECTED) SimpleCanvas3D.insertBranch (new Prim(SimpleCanvas3D. bcan, ie.getItem())); }}); lm.setActionCommand("Load_Model"); /* Здесь мы назначаем обработчик события кнопке Load Model, в котором вызывается окно выбора файла */ lm.addActionListener(new java.awt. event.ActionListener() { public void actionPerformed (ActionEvent e) { int retval = chooser.showDialog (frame, null); if(retval == JFileChooser .APPROVE_OPTION) { File theFile = chooser .getSelectedFile(); JOptionPane.showMessageDialog( frame, "You chose this file: " + chooser.getSelectedFile().getPath()); } } }); help.setToolTipText("Help"); help.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed (ActionEvent e) { }}); /* Эта кнопка вызовет окно Model Editor, где пока можно лишь посмотреть, какие имеются параметры для настройки поверхности модели */ mod.setActionCommand("Model_Editor"); mod.addActionListener(new java.awt. event.ActionListener() { public void actionPerformed (ActionEvent e) { ViewModel wm = new ViewModel(); wm.show(); } }); scen.setActionCommand("Scene_Editor"); scen.addActionListener(new java.awt. event.ActionListener() { public void actionPerformed (ActionEvent e) { } }); /* Вставим все элементы в несущую панель */ add(openFile); add(closeFile); add(mod, null); add(scen, null); add(cbox); add(lm); add(help); } }
Теперь возьмем один из наиболее простых классов проекта — ModeCanvas. В данном случае в его панели имеется одна маленькая кнопка. Если нажать на нее, то можно, манипулируя мышью, рассмотреть сцену со всех сторон. Повторно нажав эту кнопку, вы отмените трансформацию всей сцены и сможете воздействовать мышью только на вставленные им примитивы:
package kofestudio; public class ModeCanvas extends JPanel { /* Здесь будет применена кнопка типа JtoggleButton. При первом же нажатии она останется нажатой, а при повторном нажатии - возвратится в исходное состояние */ JToggleButton vs = new JToggleButton(); public ModeCanvas() { setLayout(new GridLayout(40,1)); vs.setPreferredSize(new Dimension(20,20)); vs.addActionListener(new java.awt. event.ActionListener() { public void actionPerformed (ActionEvent e) { JToggleButton t = (JToggleButton) e.getSource(); /* Обработчик в зависимости от состояния кнопки вызывает выше рассмотренные нами методы установки трансформации для сцены и ее запрещения */ if(t.isSelected())MainFrame.can. onTransformScene(); else MainFrame.can.offTransformScene(); } }); add(vs); } }
Последним пойдет класс, создающий примитив при определении одного из пунктов списка их выбора. Он расширяет класс BranchGroup, так как только объекты типа Group можно вставлять в сцену в процессе работы:
package kofestudio; public class Prim extends BranchGroup { /* Здесь должны быть описаны все примитивы и объект определения свойств материала */ Appearance app = new Appearance(); Box bx = new Box(.5f,.3f,.4f, app); Sphere sf = new Sphere(.5f, app); public Prim(Canvas3D bcan, Object name) { /* В данном конструкторе реализован метод, общий для всех примитивов, что позволяет значительно сократить количество исходного текста при описании повторяющихся операций. Создадим узел трансформации для каждого примитива. С помощью этого узла можно будет взаимодействовать с элементами сцены */ TransformGroup spinTg = new TransformGroup(); spinTg.setCapability(TransformGroup. ALLOW_TRANSFORM_WRITE); spinTg.setCapability(TransformGroup. ALLOW_TRANSFORM_READ); spinTg.setCapability(TransformGroup. ENABLE_PICK_REPORTING); spinTg.setCapability(TransformGroup. ALLOW_CHILDREN_WRITE); Bounds bounds = new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0); PickRotateBehavior rbeh; PickZoomBehavior zbeh; PickTranslateBehavior tbeh; TransformGroup objTrans = new TransformGroup(); /* Подготовим материал примитивов, он будет одинаков для всех */ Color3f objColor = new Color3f(1.0f, 0.7f, 0.8f); Color3f black = new Color3f(0.0f, 0.0f, 0.0f); app.setMaterial(new Material (objColor, black, objColor, black, 80.0f)); /* Будут выполняться выборочные операции для каждого примитива, а его имя - передаваться конструктору в переменной name */ if(name == "Box"){ bx.setCapability(TransformGroup. ALLOW_TRANSFORM_WRITE); bx.setCapability(TransformGroup. ALLOW_TRANSFORM_READ); spinTg.addChild(bx); } if(name == "ColorCube"); if(name == "Cone"); if(name == "Cylinder"); if(name == "Sphere"){ sf.setCapability(TransformGroup. ALLOW_TRANSFORM_ WRITE); sf.setCapability(TransformGroup. ALLOW_TRANSFORM_READ); spinTg.addChild(sf); } if(name == "Text2D"); /* К каждому узлу, к которому будет прикреплен примитив, присоединяются классы манипулирования ими. Они будут включаться тогда, когда, указав на примитив, вы начнете передвигать мышь с нажатой кнопкой */ rbeh = new PickRotateBehavior (this, bcan, bounds, PickTool.GEOMETRY); addChild(rbeh); zbeh = new PickZoomBehavior(this, bcan, bounds); addChild(zbeh); tbeh = new PickTranslateBehavior (this, bcan, bounds); addChild(tbeh); objTrans.addChild(spinTg); addChild(objTrans); } }
* * *
Здесь показано, как можно, имея небольшое количество исходного текста, создавать такие 3D-приложения, на реализацию которых другими средствами потребуется на порядок больше энергии и времени.
В статье на примере работающего приложения были представлены азы технологии Java 3D. Желающим подробнее узнать про все это имеет смысл посетить сайт www.javasoft.com, где можно найти достаточно много примеров разработки программ и документацию на английском языке. Описанную выше программу в виде jar-архива можно переписать с сайта www.javastudio.h1.ru и запустить в Windows, Linux и Sun OS командой: java -jar javastudio.jar
Конечно, для этого нужно сначала установить Java 3D RE (runtime environment) объемом около 2 Мбайт.
ОБ АВТОРЕ
Виталий Галактионов — независимый эксперт, e-mail: vit3d@mail.ru