/* * Copyright 2016 MovingBlocks * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.terasology.rendering.opengl; import org.lwjgl.BufferUtils; import org.lwjgl.opengl.ARBHalfFloatPixel; import org.lwjgl.opengl.ARBTextureFloat; import org.lwjgl.opengl.EXTPackedDepthStencil; import org.lwjgl.opengl.GL11; import org.lwjgl.opengl.GL12; import org.lwjgl.opengl.GL14; import org.lwjgl.opengl.GL20; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.nio.ByteBuffer; import java.nio.IntBuffer; import static org.lwjgl.opengl.EXTFramebufferObject.*; import static org.lwjgl.opengl.GL11.glGenTextures; import org.terasology.assets.ResourceUrn; /** * FBO - Frame Buffer Object * * FBOs wrap OpenGL's FrameBuffer functionality for the needs of the rendering portion of the engine. * * In OpenGL a FrameBuffer is an entity that can have a number of attachments, i.e. textures storing per-pixel color data. * By binding FrameBuffers and their attachments, shaders can read from or write to them. For example the final image * presented on screen is a composite of a number of visual layers stored in the attachments of different FrameBuffers. * Shaders read from these attachments, process the per-pixel data and eventually produce the image seen on screen. * * This class simplifies the creation of FrameBuffers with specific attachments (see the create() method), the binding * and unbinding of both the FrameBuffer as a whole or its attachments, and the FrameBuffer's proper disposal. */ public final class FBO { private static final boolean DEFAULT_COLOR_MASK = true; private static final boolean DEFAULT_NORMAL_MASK = true; private static final boolean DEFAULT_LIGHT_BUFFER_MASK = true; private static final Logger logger = LoggerFactory.getLogger(FBO.class); // TODO: make accessors for these public int fboId; public int colorBufferTextureId; public int depthStencilTextureId; public int depthStencilRboId; public int normalsBufferTextureId; public int lightBufferTextureId; private final Dimensions dimensions; private boolean writeToColorBuffer; private boolean writeToNormalsBuffer; private boolean writeToLightBuffer; private Status status; public enum Type { DEFAULT, // 32 bit color buffer HDR, // 64 bit color buffer NO_COLOR // no color buffer } public enum Status { COMPLETE, // usable FBO INCOMPLETE, // creation failed the OpenGL completeness check DISPOSED, // no longer known to the GPU - can occur at creation time. See getStatus(). UNEXPECTED // creation failed in an unexpected way } // private constructor: the only way to generate an instance of this class // should be through the static create() method. private FBO(int width, int height) { dimensions = new Dimensions(width, height); writeToColorBuffer = DEFAULT_COLOR_MASK; writeToNormalsBuffer = DEFAULT_NORMAL_MASK; writeToLightBuffer = DEFAULT_LIGHT_BUFFER_MASK; } /** * Binds the FrameBuffer tracked by this FBO. The result of subsequent OpenGL draw calls will be stored * in the FrameBuffer's attachments until a different FrameBuffer is bound. */ public void bind() { // Originally the code contained a check to prevent the currently bound FrameBuffer from being re-bound. // By my understanding current OpenGL implementations are smart enough to prevent it on their own. If // necessary, it'd be easy to add a class variable tracking the currently bound FrameBuffer and the // associated checks. glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboId); } /** * Once an FBO is bound, opengl commands will act on it, i.e. by drawing on it. * Meanwhile shaders might output not just colors but additional per-pixel data. This method establishes on which * of an FBOs attachments, subsequent opengl commands and shaders will draw on. * * @param renderToColorBuffer If True the color buffer is set as drawable. If false subsequent commands and shaders won't be able to draw on it. * @param renderToNormalsBuffer If True the normal buffer is set as drawable. If false subsequent commands and shaders won't be able to draw on it. * @param renderToLightBuffer If True the light buffer is set as drawable. If false subsequent commands and shaders won't be able to draw on it. */ public void setRenderBufferMask(boolean renderToColorBuffer, boolean renderToNormalsBuffer, boolean renderToLightBuffer) { if (this.writeToColorBuffer == renderToColorBuffer && this.writeToNormalsBuffer == renderToNormalsBuffer && this.writeToLightBuffer == renderToLightBuffer) { return; } this.writeToColorBuffer = renderToColorBuffer; this.writeToNormalsBuffer = renderToNormalsBuffer; this.writeToLightBuffer = renderToLightBuffer; int attachmentId = 0; IntBuffer bufferIds = BufferUtils.createIntBuffer(3); // TODO: change GL_COLOR_ATTACHMENT0_EXT + attachmentId into something like COLOR_BUFFER_ATTACHMENT, // TODO: in turn set within the class or method if (colorBufferTextureId != 0) { if (this.writeToColorBuffer) { bufferIds.put(GL_COLOR_ATTACHMENT0_EXT + attachmentId); } attachmentId++; } if (normalsBufferTextureId != 0) { if (this.writeToNormalsBuffer) { bufferIds.put(GL_COLOR_ATTACHMENT0_EXT + attachmentId); } attachmentId++; } if (lightBufferTextureId != 0 && this.writeToLightBuffer) { // compacted if block because Jenkins was complaining about it. bufferIds.put(GL_COLOR_ATTACHMENT0_EXT + attachmentId); } bufferIds.flip(); GL20.glDrawBuffers(bufferIds); } /** * "Unbinding" a FrameBuffer can be more easily thought as binding the application's display, * i.e. the whole screen or an individual window. The result of subsequent OpenGL draw calls will * therefore be sent to the display until a different FrameBuffer is bound. */ public void unbind() { glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); } /** * Binds the color attachment to the currently active texture unit. * Once a texture is bound it can be sampled by shaders. */ public void bindTexture() { GL11.glBindTexture(GL11.GL_TEXTURE_2D, colorBufferTextureId); } /** * Binds the depth attachment to the currently active texture unit. * Once a texture is bound it can be sampled by shaders. */ public void bindDepthTexture() { GL11.glBindTexture(GL11.GL_TEXTURE_2D, depthStencilTextureId); } /** * Binds the normals attachment to the currently active texture unit. * Once a texture is bound it can be sampled by shaders. */ public void bindNormalsTexture() { GL11.glBindTexture(GL11.GL_TEXTURE_2D, normalsBufferTextureId); } /** * Binds the light buffer attachment to the currently active texture unit. * Once a texture is bound it can be sampled by shaders. */ public void bindLightBufferTexture() { GL11.glBindTexture(GL11.GL_TEXTURE_2D, lightBufferTextureId); } /** * Unbinds the texture attached to the currently active texture unit. * Quirk: this also works if the texture to be unbound is -not- an attachment * of the calling instance's FrameBuffer. */ public static void unbindTexture() { GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0); } /** * Attaches the calling instance's depth attachments to the target FBO. * Notice that the depth attachments remain attached to the calling instance too. * * @param target The FBO to attach the depth attachments to. */ public void attachDepthBufferTo(FBO target) { target.bind(); glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, depthStencilRboId); glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL11.GL_TEXTURE_2D, depthStencilTextureId, 0); target.unbind(); } /** * Properly disposes of the underlying FrameBuffer and its attachments, * effectively freeing memory on the graphic adapter. */ public void dispose() { glDeleteFramebuffersEXT(fboId); glDeleteRenderbuffersEXT(depthStencilRboId); GL11.glDeleteTextures(normalsBufferTextureId); GL11.glDeleteTextures(depthStencilTextureId); GL11.glDeleteTextures(colorBufferTextureId); status = Status.DISPOSED; } /** * @return Returns the (int) width of the FrameBuffer, in pixels. */ public int width() { return this.dimensions.width; } /** * @return Returns the (int) height of the FrameBuffer, in pixels. */ public int height() { return this.dimensions.height; } /** * @return Returns the width and height of the FrameBuffer, as a Dimensions object. */ public Dimensions dimensions() { return dimensions; } /** * Retrieves the status of the FBO. * * A usable FBO is one with a COMPLETE status. * * If the status is INCOMPLETE something went wrong during the allocation process on the GPU. Causes * can range from mismatched dimensions to missing attachments, among others. The precise error code * can be obtained browsing the log. Using an FrameBuffer that is not COMPLETE is an error and at this * stage it is probably unrecoverable. No exceptions are thrown however and it is up to the calling code * to decide how to react to an it. * * An FBO will have a DISPOSED status if the dispose() method has been called on it, which means the * underlying FrameBuffer is no longer available to the GPU. The FBO is also automatically * disposed if it is of Type.NO_COLOR and the internal call to glCheckFramebufferStatusEXT() * returns GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT. This occurs on some graphic cards and the * resulting FBO should not be used. * * An UNEXPECTED status cover all other (unknown) cases and the resulting FBO is probably as dysfunctional * as an INCOMPLETE or a DISPOSED one. * * @return Status.COMPLETE, Status.INCOMPLETE, Status.DISPOSED or Status.UNEXPECTED */ public Status getStatus() { return status; } private void setStatus(Status newStatus) { this.status = newStatus; } /** * Creates an FBO, allocating the underlying FrameBuffer and the desired attachments on the GPU. * * Check FBO create(String title, Dimensions dimensions, Type type ...) for more. * @param config A FBOConfig object that stores information used for creating FBO. * @return The resuting FBO object wrapping a FrameBuffer and its attachments. Use getStatus() before use to verify completeness. */ public static FBO create(FBOConfig config) { return FBO.create(config.getName(), config.getDimensions(), config.getType(), config.hasDepthBuffer(), config.hasNormalBuffer(), config.hasLightBuffer(), config.hasStencilBuffer()); } /** * Creates an FBO, allocating the underlying FrameBuffer and the desired attachments on the GPU. * * Also checks the resulting FBO for completeness and logs errors and their error codes as necessary. * Callers must check the returned FBO's status (see getStatus()). Only FBO with a Status.COMPLETE should be used. * * In what follows, the GL constants between parenthesis represent the (internal format, data type, filtering type) of a buffer. * * An FBO of Type.DEFAULT will have a 32 bit color buffer attached to it. (GL_RGBA, GL11.GL_UNSIGNED_BYTE, GL_LINEAR) * An FBO of Type.HDR will have a 64 bit color buffer attached to it. (GL_RGBA, GL_HALF_FLOAT_ARB, GL_LINEAR) * An FBO of Type.NO_COLOR will have -no- color buffer attached to it. * * If the creation process is successful (Status.COMPLETE) GPU memory has been allocated for the FrameBuffer and * its attachments. However, the content of the attachments is undefined. * * @param urn An identification string. It is currently used only to log creation errors and is not stored in the FBO. * @param dimensions A Dimensions object wrapping width and height of the FBO. * @param type Can be Type.DEFAULT, Type.HDR or Type.NO_COLOR * @param useDepthBuffer If true the FBO will have a 24 bit depth buffer attached to it. (GL_DEPTH_COMPONENT24, GL_UNSIGNED_INT, GL_NEAREST) * @param useNormalBuffer If true the FBO will have a 32 bit normals buffer attached to it. (GL_RGBA, GL_UNSIGNED_BYTE, GL_LINEAR) * @param useLightBuffer If true the FBO will have 32/64 bit light buffer attached to it, depending if Type is DEFAULT/HDR. * (GL_RGBA/GL_RGBA16F_ARB, GL_UNSIGNED_BYTE/GL_HALF_FLOAT_ARB, GL_LINEAR) * @param useStencilBuffer If true the depth buffer will also have an 8 bit Stencil buffer associated with it. * (GL_DEPTH24_STENCIL8_EXT, GL_UNSIGNED_INT_24_8_EXT, GL_NEAREST) * @return The resuting FBO object wrapping a FrameBuffer and its attachments. Use getStatus() before use to verify completeness. */ public static FBO create(ResourceUrn urn, Dimensions dimensions, Type type, boolean useDepthBuffer, boolean useNormalBuffer, boolean useLightBuffer, boolean useStencilBuffer) { FBO fbo = new FBO(dimensions.width, dimensions.height); // Create the FBO on the GPU fbo.fboId = glGenFramebuffersEXT(); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fbo.fboId); if (type != Type.NO_COLOR) { createColorBuffer(fbo, dimensions, type); } if (useNormalBuffer) { createNormalsBuffer(fbo, dimensions); } if (useLightBuffer) { createLightBuffer(fbo, dimensions, type); } if (useDepthBuffer) { createDepthBuffer(fbo, dimensions, useStencilBuffer); } GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0); IntBuffer bufferIds = BufferUtils.createIntBuffer(3); if (type != Type.NO_COLOR) { bufferIds.put(GL_COLOR_ATTACHMENT0_EXT); } if (useNormalBuffer) { bufferIds.put(GL_COLOR_ATTACHMENT1_EXT); } if (useLightBuffer) { bufferIds.put(GL_COLOR_ATTACHMENT2_EXT); } bufferIds.flip(); if (bufferIds.limit() == 0) { GL11.glReadBuffer(GL11.GL_NONE); GL20.glDrawBuffers(GL11.GL_NONE); } else { GL20.glDrawBuffers(bufferIds); } verifyCompleteness(urn, type, fbo); glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0); return fbo; } private static void verifyCompleteness(ResourceUrn urn, Type type, FBO fbo) { int checkFB = glCheckFramebufferStatusEXT(GL_FRAMEBUFFER_EXT); switch (checkFB) { case GL_FRAMEBUFFER_COMPLETE_EXT: fbo.setStatus(Status.COMPLETE); break; case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT: logger.error("FrameBuffer: " + urn + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT_EXT exception"); fbo.setStatus(Status.INCOMPLETE); break; case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT: logger.error("FrameBuffer: " + urn + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT_EXT exception"); fbo.setStatus(Status.INCOMPLETE); break; case GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT: logger.error("FrameBuffer: " + urn + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_DIMENSIONS_EXT exception"); fbo.setStatus(Status.INCOMPLETE); break; case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT: logger.error("FrameBuffer: " + urn + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER_EXT exception"); fbo.setStatus(Status.INCOMPLETE); break; case GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT: logger.error("FrameBuffer: " + urn + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_FORMATS_EXT exception"); fbo.setStatus(Status.INCOMPLETE); break; case GL_FRAMEBUFFER_UNSUPPORTED_EXT: logger.error("FrameBuffer: " + urn + ", has caused a GL_FRAMEBUFFER_UNSUPPORTED_EXT exception"); fbo.setStatus(Status.INCOMPLETE); break; case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT: logger.error("FrameBuffer: " + urn + ", has caused a GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT exception"); /* * On some graphics cards, FBO.Type.NO_COLOR can cause a GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER_EXT. * Code using NO_COLOR FBOs should check for this and -not use- the FBO if its status is DISPOSED */ if (type == Type.NO_COLOR) { logger.error("FrameBuffer: " + urn + ", ...but the FBO.Type was NO_COLOR, ignoring this error and continuing without this FBO."); fbo.dispose(); } else { fbo.setStatus(Status.INCOMPLETE); } break; default: logger.error("FBO '" + urn + "' generated an unexpected reply from glCheckFramebufferStatusEXT: " + checkFB); fbo.setStatus(Status.UNEXPECTED); break; } } /** * Returns the content of the color buffer from GPU memory as a ByteBuffer. * @return a ByteBuffer or null */ public ByteBuffer getColorBufferRawData() { ByteBuffer buffer = BufferUtils.createByteBuffer(this.width() * this.height() * 4); this.bindTexture(); GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer); FBO.unbindTexture(); return buffer; } private static void createColorBuffer(FBO fbo, Dimensions dimensions, Type type) { fbo.colorBufferTextureId = glGenTextures(); GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo.colorBufferTextureId); setTextureParameters(GL11.GL_LINEAR); if (type == Type.HDR) { allocateTexture(dimensions, GL11.GL_RGBA, GL11.GL_RGBA, ARBHalfFloatPixel.GL_HALF_FLOAT_ARB); } else { allocateTexture(dimensions, GL11.GL_RGBA, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE); } glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL11.GL_TEXTURE_2D, fbo.colorBufferTextureId, 0); } private static void createNormalsBuffer(FBO fbo, Dimensions dimensions) { fbo.normalsBufferTextureId = glGenTextures(); GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo.normalsBufferTextureId); setTextureParameters(GL11.GL_LINEAR); allocateTexture(dimensions, GL11.GL_RGBA, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE); glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT1_EXT, GL11.GL_TEXTURE_2D, fbo.normalsBufferTextureId, 0); } private static void createLightBuffer(FBO fbo, Dimensions dimensions, Type type) { fbo.lightBufferTextureId = glGenTextures(); GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo.lightBufferTextureId); setTextureParameters(GL11.GL_LINEAR); if (type == Type.HDR) { allocateTexture(dimensions, ARBTextureFloat.GL_RGBA16F_ARB, GL11.GL_RGBA, ARBHalfFloatPixel.GL_HALF_FLOAT_ARB); } else { allocateTexture(dimensions, GL11.GL_RGBA, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE); } glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT2_EXT, GL11.GL_TEXTURE_2D, fbo.lightBufferTextureId, 0); } private static void createDepthBuffer(FBO fbo, Dimensions dimensions, boolean useStencilBuffer) { fbo.depthStencilTextureId = glGenTextures(); GL11.glBindTexture(GL11.GL_TEXTURE_2D, fbo.depthStencilTextureId); setTextureParameters(GL11.GL_NEAREST); if (!useStencilBuffer) { allocateTexture(dimensions, GL14.GL_DEPTH_COMPONENT24, GL11.GL_DEPTH_COMPONENT, GL11.GL_UNSIGNED_INT); } else { allocateTexture(dimensions, EXTPackedDepthStencil.GL_DEPTH24_STENCIL8_EXT, EXTPackedDepthStencil.GL_DEPTH_STENCIL_EXT, EXTPackedDepthStencil.GL_UNSIGNED_INT_24_8_EXT); } fbo.depthStencilRboId = glGenRenderbuffersEXT(); glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, fbo.depthStencilRboId); if (!useStencilBuffer) { glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, GL14.GL_DEPTH_COMPONENT24, fbo.dimensions.width, fbo.dimensions.height); } else { glRenderbufferStorageEXT(GL_RENDERBUFFER_EXT, EXTPackedDepthStencil.GL_DEPTH24_STENCIL8_EXT, fbo.dimensions.width, fbo.dimensions.height); } glBindRenderbufferEXT(GL_RENDERBUFFER_EXT, 0); glFramebufferRenderbufferEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, fbo.depthStencilRboId); glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL11.GL_TEXTURE_2D, fbo.depthStencilTextureId, 0); if (useStencilBuffer) { glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_STENCIL_ATTACHMENT_EXT, GL11.GL_TEXTURE_2D, fbo.depthStencilTextureId, 0); } } private static void setTextureParameters(float filterType) { GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, filterType); GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, filterType); GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL12.GL_CLAMP_TO_EDGE); GL11.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL12.GL_CLAMP_TO_EDGE); } private static void allocateTexture(Dimensions dimensions, int internalFormat, int dataFormat, int dataType) { GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, internalFormat, dimensions.width, dimensions.height, 0, dataFormat, dataType, (ByteBuffer) null); } /** * Support class wrapping width and height of FBOs. Also provides some ad-hoc methods to make code more readable. */ public static class Dimensions { private int width; private int height; /** * Standard Constructor - returns a Dimensions object. * * @param width An integer, representing the width of the FBO in pixels. * @param height An integer, representing the height of the FBO in pixels. */ public Dimensions(int width, int height) { this.width = width; this.height = height; } /** * Copy constructor: construct a Dimensions instance with the dimensions of another. * * @param dimensions a Dimensions instance */ public Dimensions(Dimensions dimensions) { this(dimensions.width(), dimensions.height()); } /** * Returns a new Dimensions object whose width and height have been divided by the divisor. * I.e. new Dimensions(20,10).dividedBy(2) returns a Dimensions(10,5) object. * @param divisor An integer. * @return a new Dimensions object. */ public Dimensions dividedBy(int divisor) { return new Dimensions(width / divisor, height / divisor); } public Dimensions multiplyBy(float multiplier) { int w = (int) (width * multiplier); int h = (int) (height * multiplier); return new Dimensions(w, h); } /** * Multiplies (in place) both width and height of this Dimensions object by multiplier. * @param multiplier A float representing a multiplication factor. */ public void multiplySelfBy(float multiplier) { width *= multiplier; height *= multiplier; } /** * Returns true if the other instance of this class is null or has different width/height. * Similar to the more standard equals(), doesn't bother with checking if -other- is an instance * of Dimensions. It also makes for more readable code, i.e.: * * newDimensions.areDifferentFrom(oldDimensions) * * @param other A Dimensions object * @return True if the two objects are different as defined above. */ public boolean areDifferentFrom(Dimensions other) { return other == null || this.width != other.width || this.height != other.height; } /** * Identical in behaviour to areDifferentFrom(Dimensions other), * in some situation can be more semantically appropriate, i.e.: * * newResolution.isDifferentFrom(oldResolution); * * @param other A Dimensions object. * @return True if the two objects are different as defined in the javadoc for areDifferentFrom(other). */ public boolean isDifferentFrom(Dimensions other) { return areDifferentFrom(other); } /** * Returns the width. * @return An integer representing the width stored in the Dimensions instance. */ public int width() { return this.width; } /** * Returns the height. * @return An integer representing the height stored in the Dimensions instance. */ public int height() { return this.height; } } }