001/*
002 *   ImageCaptureCanvas3D -- A Canvas3D that allows you to capture the contents to an image.
003 *   
004 *   Copyright (C) 2009-2023, by Joseph A. Huwaldt.
005 *   All rights reserved.
006 *   
007 *   This library is free software; you can redistribute it and/or
008 *   modify it under the terms of the GNU Lesser General Public
009 *   License as published by the Free Software Foundation; either
010 *   version 2.1 of the License, or (at your option) any later version.
011 *   
012 *   This library is distributed in the hope that it will be useful,
013 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
014 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015 *   Lesser General Public License for more details.
016 *
017 *   You should have received a copy of the GNU Lesser General Public License
018 *   along with this program; if not, write to the Free Software
019 *   Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
020 *   Or visit:  http://www.gnu.org/licenses/lgpl.html
021 *
022 *   This is based on ImageCaptureCanvas3D from the org.j3d package which included
023 *   the following note:
024 *
025 *                        J3D.org Copyright (c) 2000
026 *                               Java Source
027 *
028 * This source is licensed under the GNU LGPL v2.1
029 */
030package jahuwaldt.j3d;
031
032import jahuwaldt.j3d.image.CapturedImageObserver;
033import java.awt.GraphicsConfiguration;
034import java.awt.image.BufferedImage;
035import java.util.ArrayList;
036import static java.util.Objects.requireNonNull;
037import java.util.logging.Level;
038import java.util.logging.Logger;
039import org.jogamp.java3d.*;
040import org.jogamp.vecmath.Point3f;
041
042/**
043 * A version of the standard Java3D Canvas3D class that allows you to capture the contents
044 * and write out the image information.
045 *
046 * <p>
047 * The canvas uses a callback mechanism to capture an image and notify the observer of the
048 * image data.</p>
049 * <p>
050 * The original code for this was written by Peter Z. Kunszt of John Hopkins University
051 * and posted to the Java 3D interest list. This version has been modified to make it more
052 * reusable and flexible. The image can be used to pass to a printer or written to a file
053 * for example.</p>
054 * <p>
055 * When the observer is notified, this class does not provide any separation of the
056 * notification from the rendering thread. A call to the observer will prevent the
057 * renderer from starting the next frame. If you are intending to save a lot of images,
058 * you should implement some form of buffering system to take the conversion process into
059 * a separate thread otherwise on-screen performance will be <I>severely</I> impacted.</p>
060 *
061 * <p> Modified by: Joseph A.Huwaldt </p>
062 *
063 * @author Justin Couch
064 * @version June 4, 2023
065 */
066public class ImageCaptureCanvas3D extends Canvas3D {
067
068    private static final long serialVersionUID = 1L;
069
070    /**
071     * Flag to indicate that the canvas should notify observers of an image. Faster than
072     * making a call to observers.size() each frame.
073     */
074    private boolean captureImage = false;
075
076    /**
077     * The list of registered observers
078     */
079    private final transient ArrayList<CapturedImageObserver> observers = new ArrayList();
080
081    /**
082     * Create a new canvas given the graphics configuration that runs as an on screen
083     * canvas.
084     *
085     * @param gc The graphics configuration for this canvas. May not be null.
086     */
087    public ImageCaptureCanvas3D(GraphicsConfiguration gc) {
088        super(requireNonNull(gc, "gc == null"));
089    }
090
091    /**
092     * Create a new canvas that allows capture and may operate either on screen or
093     * off-screen.
094     *
095     * @param gc        The graphics configuration to use for the canvas. May not be null.
096     * @param offScreen True if this is to operate in an offscreen mode
097     */
098    public ImageCaptureCanvas3D(GraphicsConfiguration gc, boolean offScreen) {
099        super(requireNonNull(gc, "gc == null"), offScreen);
100    }
101
102    /**
103     * Process code after we have swapped the image to the foreground. Overrides the
104     * standard implementation to fetch the image to call to the observers if needed.
105     */
106    @Override
107    public void postSwap() {
108        super.postSwap();
109
110        if (!captureImage)
111            return;
112
113        //  Do any of the observers want an image?
114        synchronized (observers) {
115            boolean captureNextFrame = false;
116            int size = observers.size();
117            int i = 0;
118            while (!captureNextFrame && i < size) {
119                captureNextFrame = observers.get(i++).captureNextFrame();
120            }
121            if (!captureNextFrame)
122                return;
123
124            //  Get the image from the frame buffer.
125            BufferedImage img = getCurrentImage();
126
127            notifyObservers(img);
128        }
129    }
130
131    /**
132     * Method that extracts an image from the current frame buffer of this canvas. If this
133     * method is called anywhere but from "postSwap", it could have incomplete information
134     * in it.
135     *
136     * @return The image from the current frame buffer.
137     */
138    protected BufferedImage getCurrentImage() {
139        GraphicsContext3D ctx = getGraphicsContext3D();
140        int width = this.getWidth();
141        int height = this.getHeight();
142
143        BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
144
145        ImageComponent2D comp = new ImageComponent2D(ImageComponent.FORMAT_RGB, img, false, false);
146
147        Raster ras = new Raster(new Point3f(-1.0f, -1.0f, -1.0f),
148                Raster.RASTER_COLOR,
149                0,
150                0,
151                width,
152                height,
153                comp,
154                null);
155
156        ctx.readRaster(ras);
157
158        return ras.getImage().getImage();
159    }
160
161    /**
162     * Add an observer to this canvas to listen for images. Each instance can only be
163     * registered once.
164     *
165     * @param obs The observer to be registered.
166     */
167    public void addCaptureObserver(CapturedImageObserver obs) {
168        if (obs != null) {
169            synchronized (observers) {
170                if (!observers.contains(obs)) {
171                    observers.add(obs);
172                    captureImage = true;
173                }
174            }
175        }
176    }
177
178    /**
179     * Remove a registered observer from this canvas. If the reference is null or cannot
180     * be found registered here it will silently ignore the request.
181     *
182     * @param obs The observer to be removed.
183     */
184    public void removeCaptureObserver(CapturedImageObserver obs) {
185        if (obs != null) {
186            synchronized (observers) {
187                observers.remove(obs);
188
189                if (observers.isEmpty())
190                    captureImage = false;
191            }
192        }
193    }
194
195    /**
196     * Notify all the observers that we have an image to send them.
197     *
198     * @param img The image to be sent to the observers.
199     */
200    private void notifyObservers(BufferedImage img) {
201        int num_obs = observers.size();
202        for (int i = 0; i < num_obs; i++) {
203            CapturedImageObserver obs = observers.get(i);
204            if (obs.captureNextFrame()) {
205                try {
206
207                    obs.canvasImageCaptured(img);
208
209                } catch (Exception ex) {
210                    Logger.getLogger(ImageCaptureCanvas3D.class.getName()).log(Level.SEVERE,
211                            "Error sending image to observer", ex);
212                }
213            }
214        }
215    }
216}