/* * Copyright (C) 2014 James Lawrence. * * This file is part of GrimEdi. * * GrimEdi is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Created by JFormDesigner on Fri Mar 22 15:04:56 GMT 2013 */ package com.sqrt4.grimedi.ui.editor; import com.sqrt.liblab.LabCollection; import com.sqrt.liblab.codec.CodecMapper; import com.sqrt.liblab.codec.EntryCodec; import com.sqrt.liblab.entry.model.GrimModel; import com.sqrt.liblab.entry.model.ModelNode; import com.sqrt.liblab.entry.model.anim.Animation; import com.sqrt.liblab.entry.model.anim.AnimationNode; import com.sqrt.liblab.entry.model.anim.KeyFrame; import com.sqrt.liblab.io.DataSource; import com.sqrt.liblab.threed.Angle; import com.sqrt.liblab.threed.Vector3f; import com.sqrt4.grimedi.ui.MainWindow; import com.sqrt4.grimedi.ui.component.FrameCallback; import com.sqrt4.grimedi.ui.component.ModelRenderer; import com.sqrt4.grimedi.util.AnimatedGifCreator; import javax.imageio.ImageIO; import javax.imageio.stream.ImageOutputStream; import javax.media.opengl.GL; import javax.media.opengl.GL2; import javax.swing.*; import javax.swing.border.TitledBorder; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.nio.ByteBuffer; import java.util.LinkedList; import java.util.Queue; import java.util.Vector; import java.util.concurrent.atomic.AtomicBoolean; /** * @author James Lawrence */ public class AnimationEditor extends EditorPanel<Animation> { private LabCollection _container; private boolean _animating; private Runnable animator = new Runnable() { public void run() { long last = System.currentTimeMillis(); float frame = 0f; while(_animating) { int fps = (int) fpsSelect.getValue(); int frameTime = 1000/fps; long time = System.currentTimeMillis(); long delta = time - last; last = time; frame += ((float) delta / (float) frameTime); while(frame >= data.numFrames) frame -= data.numFrames; setFrame(frame); try { Thread.sleep(10); } catch(Exception e) { e.printStackTrace(); } } } }; private Thread animateThread; public AnimationEditor() { initComponents(); } ImageIcon icon = new ImageIcon(getClass().getResource("/run.png")); public ImageIcon getIcon() { return icon; } public void onNewData() { if (_container != data.container.container) { Vector<DataSource> allModels = new Vector<DataSource>(); allModels.addAll(data.container.container.findByType(GrimModel.class)); _container = data.container.container; modelSelector.setModel(new DefaultComboBoxModel(allModels)); modelSelectorItemStateChanged(null); } renderer.setModel(renderer.getModel()); // Forces the renderer to reset the view... stopAction.actionPerformed(null); frameSlider.setMaximum(data.numFrames - 1); frameSlider.setValue(0); } public void setFrame(float frame) { // Todo: move most of this into the model... if(frameSlider.getValue() != (int) frame) frameSlider.setValue((int) frame); // Reset model... renderer.getModel().reset(); if (data.nodes.isEmpty()) return; if (frame > data.numFrames) frame = data.numFrames; for (AnimationNode node : data.nodes) { if (node == null || node.entries.isEmpty()) continue; ModelNode mn = renderer.getModel().findNode(node.meshName); if (mn == null) return; boolean useDelta = (data.flags & 256) == 0; // Do a binary search for the nearest previous frame // Loop invariant: entries_[low].frame_ <= frame < entries_[high].frame_ int low = 0, high = node.entries.size(); while (high > low + 1) { int mid = (low + high) / 2; if (node.entries.get(mid).frame <= frame) low = mid; else high = mid; } KeyFrame last = node.entries.get(low); float dt = frame - last.frame; Vector3f pos = last.pos; Angle pitch = last.pitch; Angle yaw = last.yaw; Angle roll = last.roll; if (useDelta) { pos = pos.add(last.dpos.mult(dt)); pitch = pitch.add(last.dpitch.mult(dt)); yaw = yaw.add(last.dyaw.mult(dt)); roll = roll.add(last.droll.mult(dt)); } mn.animPos = pos.sub(mn.pos); mn.animPitch = pitch.sub(mn.pitch).normalize(-180); mn.animYaw = yaw.sub(mn.yaw).normalize(-180); mn.animRoll = roll.sub(mn.roll).normalize(-180); } renderer.refreshModelCache(); } private void changeFrame(ChangeEvent e) { setFrame(frameSlider.getValue()); } private void modelSelectorItemStateChanged(ItemEvent e) { DataSource selected = (DataSource) modelSelector.getSelectedItem(); try { selected.position(0); EntryCodec<GrimModel> codec = (EntryCodec<GrimModel>) CodecMapper.codecForProvider(selected); GrimModel model = codec.read(selected); if (model == renderer.getModel()) return; renderer.setModel(model); frameSlider.setValue(0); } catch (IOException e1) { e1.printStackTrace(); } } private void initComponents() { // JFormDesigner - Component initialization - DO NOT MODIFY //GEN-BEGIN:initComponents // Generated using JFormDesigner non-commercial license renderer = new ModelRenderer(); panel3 = new JPanel(); label1 = new JLabel(); modelSelector = new JComboBox(); panel4 = new JPanel(); label2 = new JLabel(); frameSlider = new JSlider(); label3 = new JLabel(); fpsSelect = new JSpinner(); panel1 = new JPanel(); playButton = new JButton(); button1 = new JButton(); button2 = new JButton(); playAction = new PlayAction(); stopAction = new StopAction(); exportGif = new ExportGIFAction(); //======== this ======== setLayout(new BorderLayout()); add(renderer, BorderLayout.CENTER); //======== panel3 ======== { panel3.setLayout(new GridBagLayout()); ((GridBagLayout)panel3.getLayout()).columnWidths = new int[] {0, 0, 0, 0}; ((GridBagLayout)panel3.getLayout()).rowHeights = new int[] {0, 0, 0, 0}; ((GridBagLayout)panel3.getLayout()).columnWeights = new double[] {0.0, 1.0, 1.0, 1.0E-4}; ((GridBagLayout)panel3.getLayout()).rowWeights = new double[] {0.0, 1.0, 0.0, 1.0E-4}; //---- label1 ---- label1.setText("Target model: "); label1.setHorizontalAlignment(SwingConstants.TRAILING); panel3.add(label1, new GridBagConstraints(1, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); //---- modelSelector ---- modelSelector.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { modelSelectorItemStateChanged(e); } }); panel3.add(modelSelector, new GridBagConstraints(2, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); //======== panel4 ======== { panel4.setBorder(new TitledBorder("Anim control")); panel4.setLayout(new GridBagLayout()); ((GridBagLayout)panel4.getLayout()).columnWidths = new int[] {0, 0, 0}; ((GridBagLayout)panel4.getLayout()).rowHeights = new int[] {0, 0, 0, 0}; ((GridBagLayout)panel4.getLayout()).columnWeights = new double[] {0.0, 1.0, 1.0E-4}; ((GridBagLayout)panel4.getLayout()).rowWeights = new double[] {1.0, 0.0, 0.0, 1.0E-4}; //---- label2 ---- label2.setText("Frame: "); label2.setHorizontalAlignment(SwingConstants.TRAILING); panel4.add(label2, new GridBagConstraints(0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); //---- frameSlider ---- frameSlider.setSnapToTicks(true); frameSlider.setMajorTickSpacing(1); frameSlider.setMaximum(50); frameSlider.setPaintTicks(true); frameSlider.setValue(0); frameSlider.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { changeFrame(e); } }); panel4.add(frameSlider, new GridBagConstraints(1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); //---- label3 ---- label3.setText("FPS: "); label3.setHorizontalAlignment(SwingConstants.TRAILING); panel4.add(label3, new GridBagConstraints(0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); //---- fpsSelect ---- fpsSelect.setModel(new SpinnerNumberModel(15, 1, null, 1)); panel4.add(fpsSelect, new GridBagConstraints(1, 1, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); //======== panel1 ======== { panel1.setLayout(new FlowLayout()); //---- playButton ---- playButton.setAction(playAction); panel1.add(playButton); //---- button1 ---- button1.setText("text"); button1.setAction(stopAction); button1.setEnabled(false); panel1.add(button1); //---- button2 ---- button2.setAction(exportGif); panel1.add(button2); } panel4.add(panel1, new GridBagConstraints(1, 2, 1, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); } panel3.add(panel4, new GridBagConstraints(1, 1, 2, 1, 0.0, 0.0, GridBagConstraints.CENTER, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0)); } add(panel3, BorderLayout.PAGE_END); // JFormDesigner - End of component initialization //GEN-END:initComponents } // JFormDesigner - Variables declaration - DO NOT MODIFY //GEN-BEGIN:variables // Generated using JFormDesigner non-commercial license private ModelRenderer renderer; private JPanel panel3; private JLabel label1; private JComboBox modelSelector; private JPanel panel4; private JLabel label2; private JSlider frameSlider; private JLabel label3; private JSpinner fpsSelect; private JPanel panel1; private JButton playButton; private JButton button1; private JButton button2; private PlayAction playAction; private StopAction stopAction; private ExportGIFAction exportGif; // JFormDesigner - End of variables declaration //GEN-END:variables private class PlayAction extends AbstractAction { private PlayAction() { // JFormDesigner - Action initialization - DO NOT MODIFY //GEN-BEGIN:initComponents // Generated using JFormDesigner non-commercial license putValue(NAME, "Play"); putValue(SHORT_DESCRIPTION, "play animation"); // JFormDesigner - End of action initialization //GEN-END:initComponents } public void actionPerformed(ActionEvent e) { _animating = true; animateThread = new Thread(animator); animateThread.setPriority(1); animateThread.setDaemon(true); animateThread.start(); stopAction.setEnabled(true); setEnabled(false); } } private class StopAction extends AbstractAction { private StopAction() { // JFormDesigner - Action initialization - DO NOT MODIFY //GEN-BEGIN:initComponents // Generated using JFormDesigner non-commercial license putValue(NAME, "Stop"); putValue(SHORT_DESCRIPTION, "stop animation"); // JFormDesigner - End of action initialization //GEN-END:initComponents } public void actionPerformed(ActionEvent e) { _animating = false; setEnabled(false); try { if(animateThread != null) animateThread.join(); animateThread = null; } catch (InterruptedException e1) { /**/ } playAction.setEnabled(true); } } private class ExportGIFAction extends AbstractAction { private ExportGIFAction() { // JFormDesigner - Action initialization - DO NOT MODIFY //GEN-BEGIN:initComponents // Generated using JFormDesigner non-commercial license putValue(NAME, "Export GIF"); putValue(SHORT_DESCRIPTION, "exports the current animation as a GIF (from the current viewpoint)"); // JFormDesigner - End of action initialization //GEN-END:initComponents } public void actionPerformed(ActionEvent e) { stopAction.actionPerformed(null); final AtomicBoolean cancel = new AtomicBoolean(false); window.runAsyncWithPopup("Rendering animation (frame 1/" + data.numFrames + ")", new Runnable() { public void run() { try { final Queue<BufferedImage> images = new LinkedList<>(); File temp = File.createTempFile("anim", ".gif"); ImageOutputStream ios = ImageIO.createImageOutputStream(temp); AnimatedGifCreator agc = new AnimatedGifCreator(ios, BufferedImage.TYPE_INT_RGB, 14.99992f, true, 0); frameSlider.setValue(0); FrameCallback screenshotCallback = new FrameCallback() { public void preDisplay(GL2 gl2) { } public void postDisplay(GL2 gl2) { int frame = frameSlider.getValue(); synchronized (images) { gl2.glReadBuffer(GL.GL_BACK); int w = renderer.getViewportWidth(), h = renderer.getViewportHeight(); ByteBuffer glBB = ByteBuffer.allocateDirect(4 * w * h); int[] buf = new int[w*h]; gl2.glReadPixels(0, 0, w, h, GL.GL_RGBA, GL.GL_BYTE, glBB); BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); // build RGB buffer int off = (h-1) * w; for(int y = 0; y < h; y++) { for(int x = 0; x < w; x++) { int r = 2 * glBB.get(); int g = 2 * glBB.get(); int b = 2 * glBB.get(); int a = glBB.get(); buf[off + x] = (r << 16) | (g << 8) | b; } off -= w; } bi.setRGB(0, 0, w, h, buf, 0, w); images.add(bi); images.notify(); } frameSlider.setValue(frameSlider.getValue() + 1); window.setBusyMessage("Rendering animation (frame " + frameSlider.getValue() + "/" + data.numFrames + ")"); if (frame >= data.numFrames - 1 || cancel.get()) renderer.setCallback(null); } }; renderer.setCallback(screenshotCallback); int processed = 0; while (processed < data.numFrames && !cancel.get()) { // Wait for notify... BufferedImage image = null; synchronized (images) { if (images.isEmpty()) { try { images.wait(10); } catch (InterruptedException ignore) { } } if (images.isEmpty()) continue; image = images.poll(); } if(renderer.getCallback() != screenshotCallback) window.setBusyMessage("Generating GIF (frame " + processed + " / " + data.numFrames + ")"); agc.addFrame(image); processed++; if(processed >= data.numFrames && !cancel.get()) { agc.finish(); ios.close(); if (Desktop.isDesktopSupported()) Desktop.getDesktop().open(temp); } } if(cancel.get()) { agc.finish(); ios.close(); } } catch (Exception e) { MainWindow.getInstance().handleException(e); } } }, true, new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { cancel.set(true); } }); } } }