Backstory:
I'm trying to draw as many squares the the screen as possible using a single draw call. I'm using a custom glsl vertex shader that is specialized for 2D drawing, and that is supposed to be pulling position data for the vertices of the squares from a samplerBuffer. Since I don't need to worry about rotating or scaling the squares all I should need to do is load the position data into a buffer, bind a texture to that buffer, and then use the sampler to get each vertex's position in the shader. In order to get an index into the texture I store each elements index as the z-component of the vertices.
Everything seems to work really well for a thousand or so squares, but after that I start to get weird blinking. It sort of seems like it's not drawing all of the squares every draw step, or possibly not using all of the positions so that many of the squares are overlapping.
The weird thing is, that if I use drawElements instead of drawElementsMulti, the blinking goes away (but of course then all the squares are drawn as one single object, which I don't want)
One question I have is if my position data is limited to the max texture size, or the max texture buffer size. And if I am limited to the much smaller max texture size, how do I get around it? There's got to be a reason all of that texture buffer space is there, but I obviously don't get how to properly use it.
I'm also thinking maybe glMultiDrawElements is doing something I'm not accounting for with the sampler somehow. Idk, I'm really lost at this point, and yet..it works perfectly for smaller numbers of squares, so I must be doing something right.
[EDIT] Code had changed to reflect suggestions below (and for readability), but the problem persists.
Ok, so here's some code. First the vertex shader:
uniform mat3 projection;
attribute vec3 vertex;
uniform samplerBuffer positionSampler;
attribute vec4 vertex_color;
varying vec4 color;
float positionFetch(int index)
{
// I've tried texelFetch here as well, same effect
float value = texelFetchBuffer(positionSampler, index).r;
return value;
}
void main(void)
{
color = vec4(1, 1, 1, 1);
// use the z-component of the vertex to look up the position of this instance in the texture
vec3 real_position = vec3(vertex.x + positionFetch(int(vertex.z)*2), vertex.y + positionFetch(int(vertex.z)*2+1), 1);
gl_Position = vec4(projection * real_position, 1);
}
And now my GLRenderer, sorry there is so much code, I just really want to make sure there's enough info here to get an answer. This has really been driving me nuts, and examples for java seem to be hard to come by (maybe this code will help someone else on their quest):
public class GLRenderer extends GLCanvas implements GLEventListener, WindowListener
{
private static final long serialVersionUID = -8513201172428486833L;
private static final int bytesPerFloat = Float.SIZE / Byte.SIZE;
private static final int bytesPerShort = Short.SIZE / Byte.SIZE;
public float viewWidth, viewHeight;
public float screenWidth, screenHeight;
private FPSAnimator animator;
private boolean didInit = false;
JFrame the_frame;
SquareGeometry geometry;
// Thought power of 2 might be required, doesn't seem to make a difference
private static final int NUM_THINGS = 2*2*2*2*2*2*2*2*2*2*2*2*2*2;
float[] position = new float[NUM_THINGS*2];
// Shader attributes
private int shaderProgram, projectionAttribute, vertexAttribute, positionAttribute;
public static void main(String[] args)
{
new GLRenderer();
}
public GLRenderer()
{
// setup OpenGL Version 2
super(new GLCapabilities(GLProfile.get(GLProfile.GL2)));
addGLEventListener(this);
setSize(1800, 1000);
the_frame = new JFrame("Hello World");
the_frame.getContentPane().add(this);
the_frame.setSize(the_frame.getContentPane().getPreferredSize());
the_frame.setVisible(true);
the_frame.addWindowListener(this);
animator = new FPSAnimator(this, 60);
animator.start();
}
// Called by the drivers when the gl context is first made available
public void init(GLAutoDrawable d)
{
final GL2 gl = d.getGL().getGL2();
IntBuffer asd = IntBuffer.allocate(1);
gl.glGetIntegerv(GL2.GL_MAX_TEXTURE_BUFFER_SIZE, asd);
System.out.println(asd.get(0));
asd = IntBuffer.allocate(1);
gl.glGetIntegerv(GL2.GL_MAX_TEXTURE_SIZE, asd);
System.out.println(asd.get(0));
shaderProgram = ShaderLoader.compileProgram(gl, "default");
gl.glLinkProgram(shaderProgram);
_getShaderAttributes(gl);
gl.glUseProgram(shaderProgram);
_checkGLCapabilities(gl);
_initGLSettings(gl);
// Calculate batch of vertex data from dirt geometry
geometry = new SquareGeometry(.1f);
geometry.buildGeometry(viewWidth, viewHeight);
geometry.finalizeGeometry(NUM_THINGS);
geometry.vertexBufferID = _generateBufferID(gl);
_loadVertexBuffer(gl, geometry);
geometry.indexBufferID = _generateBufferID(gl);
_loadIndexBuffer(gl, geometry);
geometry.positionBufferID = _generateBufferID(gl);
// initialize buffer object
int size = NUM_THINGS * 2 * bytesPerFloat;
System.out.println(size);
IntBuffer bla = IntBuffer.allocate(1);
gl.glGenTextures(1, bla);
geometry.positionTextureID = bla.get(0);
gl.glUniform1i(positionAttribute, 0);
gl.glActiveTexture(GL2.GL_TEXTURE0);
gl.glBindTexture(GL2.GL_TEXTURE_BUFFER, geometry.positionTextureID);
gl.glBindBuffer(GL2.GL_TEXTURE_BUFFER, geometry.positionBufferID);
gl.glBufferData(GL2.GL_TEXTURE_BUFFER, size, null, GL2.GL_DYNAMIC_DRAW);
gl.glTexBuffer(GL2.GL_TEXTURE_BUFFER, GL2.GL_R32F, geometry.positionBufferID);
}
private void _initGLSettings(GL2 gl)
{
gl.glClearColor(0f, 0f, 0f, 1f);
}
private void _loadIndexBuffer(GL2 gl, SquareGeometry geometry)
{
gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, geometry.indexBufferID);
gl.glBufferData(GL2.GL_ELEMENT_ARRAY_BUFFER, bytesPerShort*NUM_THINGS*geometry.getNumPoints(), geometry.indexBuffer, GL2.GL_STATIC_DRAW);
}
private void _loadVertexBuffer(GL2 gl, SquareGeometry geometry)
{
int numBytes = geometry.getNumPoints() * 3 * bytesPerFloat * NUM_THINGS;
gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, geometry.vertexBufferID);
gl.glBufferData(GL2.GL_ARRAY_BUFFER, numBytes, geometry.vertexBuffer, GL2.GL_STATIC_DRAW);
gl.glEnableVertexAttribArray(vertexAttribute);
gl.glVertexAttribPointer(vertexAttribute, 3, GL2.GL_FLOAT, false, 0, 0);
}
private int _generateBufferID(GL2 gl)
{
IntBuffer bufferIDBuffer = IntBuffer.allocate(1);
gl.glGenBuffers(1, bufferIDBuffer);
return bufferIDBuffer.get(0);
}
private void _checkGLCapabilities(GL2 gl)
{
// TODO: Respond to this information in a meaningful way.
boolean VBOsupported = gl.isFunctionAvailable("glGenBuffersARB") && gl.isFunctionAvailable("glBindBufferARB")
&& gl.isFunctionAvailable("glBufferDataARB") && gl.isFunctionAvailable("glDeleteBuffersARB");
System.out.println("VBO Supported: " + VBOsupported);
}
private void _getShaderAttributes(GL2 gl)
{
vertexAttribute = gl.glGetAttribLocation(shaderProgram, "vertex");
projectionAttribute = gl.glGetUniformLocation(shaderProgram, "projection");
positionAttribute = gl.glGetUniformLocation(shaderProgram, "positionSampler");
}
// Called by me on the first resize call, useful for things that can't be initialized until the screen size is known
public void viewInit(GL2 gl)
{
for(int i = 0; i < NUM_THINGS; i++)
{
position[i*2] = (float) (Math.random()*viewWidth);
position[i*2+1] = (float) (Math.random()*viewHeight);
}
gl.glUniformMatrix3fv(projectionAttribute, 1, false, Matrix.projection3f, 0);
// Load position data into a texture buffer
gl.glBindBuffer(GL2.GL_TEXTURE_BUFFER, geometry.positionBufferID);
ByteBuffer textureBuffer = gl.glMapBuffer(GL2.GL_TEXTURE_BUFFER, GL2.GL_WRITE_ONLY);
FloatBuffer textureFloatBuffer = textureBuffer.order(ByteOrder.nativeOrder()).asFloatBuffer();
for(int i = 0; i < position.length; i++)
{
textureFloatBuffer.put(position[i]);
}
gl.glUnmapBuffer(GL2.GL_TEXTURE_BUFFER);
gl.glBindBuffer(GL2.GL_TEXTURE_BUFFER, 0);
}
public void display(GLAutoDrawable d)
{
if (!didInit || geometry.vertexBufferID == 0)
{
return;
}
//long startDrawTime = System.currentTimeMillis();
final GL2 gl = d.getGL().getGL2();
gl.glClear(GL2.GL_COLOR_BUFFER_BIT | GL2.GL_DEPTH_BUFFER_BIT);
// If we were drawing any other buffers here we'd need to set this every time
// but instead we just leave them bound after initialization, saves a little render time
// No combination of these seems to fix the problem
//gl.glBindBuffer(GL2.GL_ARRAY_BUFFER, geometry.vertexBufferID);
//gl.glVertexAttribPointer(vertexAttribute, 3, GL2.GL_FLOAT, false, 0, 0);
//gl.glBindBuffer(GL2.GL_ELEMENT_ARRAY_BUFFER, geometry.indexBufferID);
gl.glBindBuffer(GL2.GL_TEXTURE_BUFFER, geometry.positionBufferID);
//gl.glActiveTexture(GL2.GL_TEXTURE0);
//gl.glTexBuffer(GL2.GL_TEXTURE_BUFFER, GL2.GL_R32F, geometry.positionBufferID);
_render(gl, geometry);
// Also tried these
//gl.glFlush();
//gl.glFinish();
}
public void _render(GL2 gl, SquareGeometry geometry)
{
gl.glMultiDrawElements(geometry.drawMode, geometry.countBuffer, GL2.GL_UNSIGNED_SHORT, geometry.offsetBuffer, NUM_THINGS);
// This one works, but isn't what I want
//gl.glDrawElements(GL2.GL_LINE_LOOP, count, GL2.GL_UNSIGNED_SHORT, 0);
}
public void reshape(GLAutoDrawable d, int x, int y, int width, int height)
{
final GL2 gl = d.getGL().getGL2();
gl.glViewport(0, 0, width, height);
float ratio = (float) height / width;
screenWidth = width;
screenHeight = height;
viewWidth = 100;
viewHeight = viewWidth * ratio;
Matrix.ortho3f(0, viewWidth, 0, viewHeight);
if (!didInit)
{
viewInit(gl);
didInit = true;
}
else
{
// respond to view size changing
}
}
}
The final bit is the SquareGeometry class which holds all the bufferIDs and vertex data, but also is responsible for filling the vertex buffer correctly so that each vertex's z component can function as an index into the position texture:
public class SquareGeometry
{
public float[] vertices = null;
ShortBuffer indexBuffer;
IntBuffer countBuffer;
PointerBuffer offsetBuffer;
FloatBuffer vertexBuffer;
public int vertexBufferID = 0;
public int indexBufferID = 0;
public int positionBufferID = 0;
public int positionTextureID = 0;
public int drawMode;
protected float width = 0;
protected float height = 0;
public SquareGeometry(float size)
{
width = size;
height = size;
}
public void buildGeometry(float viewWidth, float viewHeight)
{
vertices = new float[4 * 2];
vertices[0] = -width/2;
vertices[1] = -height/2;
vertices[2] = -width/2;
vertices[3] = height/2;
vertices[4] = width/2;
vertices[5] = height/2;
vertices[6] = width/2;
vertices[7] = -height/2;
drawMode = GL2.GL_POLYGON;
}
public void finalizeGeometry(int numInstances)
{
if(vertices == null) return;
int num_vertices = this.getNumPoints();
int total_num_vertices = numInstances * num_vertices;
// initialize vertex Buffer (# of coordinate values * 4 bytes per float)
ByteBuffer vbb = ByteBuffer.allocateDirect(total_num_vertices * 3 * Float.SIZE);
vbb.order(ByteOrder.nativeOrder());
vertexBuffer = vbb.asFloatBuffer();
for(int i = 0; i < numInstances; i++)
{
for(int v = 0; v < num_vertices; v++)
{
int vertex_index = v * 2;
vertexBuffer.put(vertices[vertex_index]);
vertexBuffer.put(vertices[vertex_index+1]);
vertexBuffer.put(i);
}
}
vertexBuffer.rewind();
// Create the indices
vbb = ByteBuffer.allocateDirect(total_num_vertices * Short.SIZE);
vbb.order(ByteOrder.nativeOrder());
indexBuffer = vbb.asShortBuffer();
for(int i = 0; i < total_num_vertices; i++)
{
indexBuffer.put((short) (i));
}
indexBuffer.rewind();
// Create the counts
vbb = ByteBuffer.allocateDirect(numInstances * Integer.SIZE);
vbb.order(ByteOrder.nativeOrder());
countBuffer = vbb.asIntBuffer();
for(int i = 0; i < numInstances; i++)
{
countBuffer.put(num_vertices);
}
countBuffer.rewind();
// create the offsets
offsetBuffer = PointerBuffer.allocateDirect(numInstances);
for(int i = 0; i < numInstances; i++)
{
offsetBuffer.put(num_vertices*i*2);
}
offsetBuffer.rewind();
}
public int getNumPoints()
{
return vertices.length/2;
}
}
Ok first things first, you are not setting gl_Color in the shader maybe that can be the issue here and you only lucky with small numbers. It is a varying, but do you also have fragment shader that picks up the value?
At no point do you ensure that NUM_THINGS*2 < GL_MAX_TEXTURE_SIZE. I don't know how FloatBuffer.put reacts; being Java probably / hopefully an exception.
Also you bind the positionBufferID buffer, then unbind it but never rebind it.
You create positionTextureID but never put any data there. This also what you put into the sampler positionSampler and try to access.
Yea well lots of issues but my gut tells me the last one may be the real issue here.
Alright, I've got it solved, though I'm still really not clear on what the original problem was. I fixed it by simplifying the drawing to use drawArrays instead of drawElements or multiDrawElements. I'm really not sure why I thought I needed them, as I really don't in this case. I'm pretty sure I was messing up a few things with the indexes and offsets.
Furthermore, as far as the proper way to bind the texture buffer, neither the code I have above, nor example found at the link I posted in a comment are correct at all.
If anyone is interested in the correct way to use the texture buffer like this I just did a pretty extensive write-up on it here http://zebadiah.me/?p=44. Thanks all for the help.
Related
I am using a tutorial to understand how sprites work using a draw() method as well as using a gameloop. I adjusted the code as far as I understand it for my own project.
The question I have is how can I access a different row of my sprite sheet besides the second row. My sprite sheet has 9 columns and 20 rows.
public class Sprite implements Drawable {
private static final int BMP_COLUMNS = 9;
private static final int BMP_ROWS = 20;
private int x = 0;
private int y = 0;
private int xSpeed = 5;
private Bitmap bmp;
float fracsect = 30;
private GameContent gameContent;
private int currentFrame = 0;
private int width;
private int height;
public Sprite(GameContent gameContent, Bitmap bmp) {
this.gameContent = gameContent;
this.bmp = bmp;
this.width = bmp.getWidth() / BMP_COLUMNS;
this.height = bmp.getHeight() / BMP_ROWS;
}
#Override
public void update(float fracsec) {
if (x > gameContent.getGameWidth() - width - xSpeed) {
xSpeed = -5;
}
if (x + xSpeed < 0) {
xSpeed = 5;
}
x = x + xSpeed;
currentFrame = ++currentFrame % BMP_COLUMNS;
}
#Override
public void draw(Canvas canvas) {
update(fracsect);
int srcX = currentFrame * width;
int srcY = 1*height - 41;
Rect src = new Rect(srcX +20 , srcY,srcX + width,srcY + height-38); // Generates
Rect dst = new Rect(x,y,x+width -30, y+height-30); // Scales
canvas.drawBitmap(bmp, src, dst, null);
}
}
How do I gain access to the second row and how can I change it for instance to the third or 4th row ?
What I understand so far is that using a sprite as an object as a bitmap instead via imageview the code implementation of the code works differently. Is there any advice on how to access a different row for the sprite sheet ? I used the android documentation as well as the tutorial to understand this process as far as I can.
Here is the tutorial also:
http://www.edu4java.com/en/androidgame/androidgame4.html
From what I can see you are iterating through the first row, drawing each sprite in turn, perhaps to create an animation.
If you want to draw the animation for the second row, you can simply add 'height' to 'srcY'. It looks like you had this idea but you substracted instead of adding. Remember that Y-coordinates on computers go from top to bottom (i.e. (0,0) is top-left, (0, 10) is 10 pixels lower).
Likewise for the third and fourth rows, just add 'height' to 'srcY' again.
When I run the following code, my sketch draws as expected:
void draw() {
int[] nextColor = getNextColor();
stroke(nextColor[0], nextColor[1], nextColor[2]);
float[] nextPoint = getNextLocation();
point(nextPoint[0], nextPoint[1]);
}
However, if I add the fourth argument for the alpha transparency value to stroke(), nothing is drawn to the canvas at all:
float alpha = 0.8;
void draw() {
int[] nextColor = getNextColor();
stroke(nextColor[0], nextColor[1], nextColor[2], alpha);
float[] nextPoint = getNextLocation();
point(nextPoint[0], nextPoint[1]);
}
So far I have tried setting the alpha value to 1.0 directly in the argument (rather than using a variable) to be sure that I wasn't accidentally setting it to 0 somewhere. I have also double checked the documentation for stroke() and there is indeed an overridden version matching my arguments.
What am I doing incorrectly?
Here is all of my code in case there is something elsewhere that should be considered. Thank you.
import java.util.Random;
Random generator;
int meanX, stdevX, meanY, stdevY;
int meanR, meanG, meanB, stdevR, stdevG, stdevB;
float alpha = 0.8;
// returns two random numbers (for x, y, coordinates)
float[] getNextLocation() {
float[] retArr = new float[2];
retArr[0] = (float) (generator.nextGaussian() * stdevX + meanX);
retArr[1] = (float) (generator.nextGaussian() * stdevY + meanY);
return retArr;
}
int[] getNextColor() {
int[] retArr = new int[3];
retArr[0] = (int) (generator.nextGaussian() * stdevR + meanR);
retArr[1] = (int) (generator.nextGaussian() * stdevG + meanG);
retArr[2] = (int) (generator.nextGaussian() * stdevB + meanB);
return retArr;
}
void setup() {
background(255);
size(500, 500);
generator = new Random();
strokeWeight(10);
// play around with these
meanX = width/6;
stdevX = width/8;
meanY = height/2;
stdevY = height/30;
meanR = 224;
stdevR = 20;
meanG = 169;
stdevG = 60;
meanB = 20;
stdevB = 5;
}
void draw() {
int[] nextColor = getNextColor();
stroke(nextColor[0], nextColor[1], nextColor[2]);
float[] nextPoint = getNextLocation();
point(nextPoint[0], nextPoint[1]);
}
You are misunderstanding what the value of alpha is.
Alpha means transparency.
Alpha values range from 0 to 255, with 0 being completely transparent (i.e., 0% opaque) and 255 completely opaque (i.e., 100% opaque).
So in your case, when you set your alpha to 0.8, you don't see anything because it's very close to transparent. When you do not set the alpha, it is by default 100% opaque, so you see the drawing.
Take a look here if interested to know more.
I have an org.eclipse.swt.graphics.Image, loaded from a PNG, and want to scale it in high quality (antialiasing, interpolation). But I do not want to lose transparency and get just a white background. (I need this Image to put it on an org.eclipse.swt.widgets.Label .)
Does anybody know how to do that?
Thank you!
Based on Mark's answer I found a better solution without the "hacky bit": first copy the alphaData from the origin then use GC to scale the image.
public static Image scaleImage(final Device device, final Image orig, final int scaledWidth, final int scaledHeight) {
final Rectangle origBounds = orig.getBounds();
if (origBounds.width == scaledWidth && origBounds.height == scaledHeight) {
return orig;
}
final ImageData origData = orig.getImageData();
final ImageData destData = new ImageData(scaledWidth, scaledHeight, origData.depth, origData.palette);
if (origData.alphaData != null) {
destData.alphaData = new byte[destData.width * destData.height];
for (int destRow = 0; destRow < destData.height; destRow++) {
for (int destCol = 0; destCol < destData.width; destCol++) {
final int origRow = destRow * origData.height / destData.height;
final int origCol = destCol * origData.width / destData.width;
final int o = origRow * origData.width + origCol;
final int d = destRow * destData.width + destCol;
destData.alphaData[d] = origData.alphaData[o];
}
}
}
final Image dest = new Image(device, destData);
final GC gc = new GC(dest);
gc.setAntialias(SWT.ON);
gc.setInterpolation(SWT.HIGH);
gc.drawImage(orig, 0, 0, origBounds.width, origBounds.height, 0, 0, scaledWidth, scaledHeight);
gc.dispose();
return dest;
}
This way we don't have to make assumptions about the underlying ImageData.
Using a method described by Sean Bright here: https://stackoverflow.com/a/15685473/6245535, we can extract the alpha information from the image and use it to fill the ImageData.alphaData array which is responsible for the transparency:
public static Image resizeImage(Display display, Image image, int width, int height) {
Image scaled = new Image(display, width, height);
GC gc = new GC(scaled);
gc.setAntialias(SWT.ON);
gc.setInterpolation(SWT.HIGH);
gc.drawImage(image, 0, 0, image.getBounds().width, image.getBounds().height, 0, 0, width, height);
gc.dispose();
ImageData canvasData = scaled.getImageData();
canvasData.alphaData = new byte[width * height];
// This is the hacky bit that is making assumptions about
// the underlying ImageData. In my case it is 32 bit data
// so every 4th byte in the data array is the alpha for that
// pixel...
for (int idx = 0; idx < (width * height); idx++) {
int coord = (idx * 4) + 3;
canvasData.alphaData[idx] = canvasData.data[coord];
}
// Now that we've set the alphaData, we can create our
// final image
Image finalImage = new Image(display, canvasData);
scaled.dispose();
return finalImage;
}
Note that this method assumes that you are working with 32 bit depth of color; it won't work otherwise.
I am looking at this tutorial and looking at the code for some reason their arrays draw to the screen (I tested it) but my slightly edited code doesn't draw anything to the screen.
Here's the GitHub: https://github.com/BigBadE/GLHelp
Here's the relevant code:
This is called to draw everything:
private void drawTextureRegion(float x1, float y1, float x2, float y2, float s1, float t1, float s2, float t2, Color c) {
if (vertices.remaining() < 8*6) {
/* We need more space in the buffer, so flush it */
flush();
}
float r = c.getRed();
float g = c.getGreen();
float b = c.getBlue();
float a = c.getAlpha();
vertices.put(x1).put(y1).put(r).put(g).put(b).put(a).put(s1).put(t1);
vertices.put(x1).put(y2).put(r).put(g).put(b).put(a).put(s1).put(t2);
vertices.put(x2).put(y2).put(r).put(g).put(b).put(a).put(s2).put(t2);
vertices.put(x1).put(y1).put(r).put(g).put(b).put(a).put(s1).put(t1);
vertices.put(x2).put(y2).put(r).put(g).put(b).put(a).put(s2).put(t2);
vertices.put(x2).put(y1).put(r).put(g).put(b).put(a).put(s2).put(t1);
numVertices += 6;
flush();
}
Flush method:
private void flush() {
if (numVertices > 0) {
vertices.flip();
if (vao != null) {
vao.bind();
} else {
vbo.bind(GL_ARRAY_BUFFER);
specifyVertexAttributes();
}
program.use();
int uniTex = program.getUniformLocation("texImage");
program.setUniform(uniTex, 0);
/* Upload the new vertex data */
vbo.bind(GL_ARRAY_BUFFER);
vbo.uploadSubData(GL_ARRAY_BUFFER, 0, vertices);
/* Draw batch */
glDrawArrays(GL_TRIANGLES, 0, numVertices);
/* Clear vertex data for next batch */
vertices.clear();
numVertices = 0;
}
}
This is how I setup the VAO, VBO, and shaders:
private void setupShaders() {
if (!main.isLegacy()) {
/* Generate Vertex Array Object */
vao = new VertexArrayObject();
vao.bind();
} else {
vao = null;
}
/* Generate Vertex Buffer Object */
vbo = new VertexBufferObject();
vbo.bind(GL_ARRAY_BUFFER);
/* Create FloatBuffer */
vertices = MemoryUtil.memAllocFloat(4096);
/* Upload null data to allocate storage for the VBO */
long size = vertices.capacity() * Float.BYTES;
vbo.uploadData(GL_ARRAY_BUFFER, size, GL_DYNAMIC_DRAW);
/* Initialize variables */
numVertices = 0;
/* Load shaders */
Shader vertexShader, fragmentShader;
if (!main.isLegacy()) {
vertexShader = Shader.loadShader(GL_VERTEX_SHADER, "resources/shaders/vertex.txt");
fragmentShader = Shader.loadShader(GL_FRAGMENT_SHADER, "resources/shaders/fragment.txt");
} else {
vertexShader = Shader.loadShader(GL_VERTEX_SHADER, "resources/shaders/legacyVertex.txt");
fragmentShader = Shader.loadShader(GL_FRAGMENT_SHADER, "resources/shaders/legacyFragment.txt");
}
/* Create shader program */
program = new ShaderProgram();
program.attachShader(vertexShader);
program.attachShader(fragmentShader);
if (!main.isLegacy()) {
program.bindFragmentDataLocation(0, "fragColor");
}
program.link();
program.use();
/* Delete linked shaders */
vertexShader.delete();
fragmentShader.delete();
/* Get width and height of framebuffer */
long window = GLFW.glfwGetCurrentContext();
int width, height;
try (MemoryStack stack = MemoryStack.stackPush()) {
IntBuffer widthBuffer = stack.mallocInt(1);
IntBuffer heightBuffer = stack.mallocInt(1);
GLFW.glfwGetFramebufferSize(window, widthBuffer, heightBuffer);
width = widthBuffer.get();
height = heightBuffer.get();
}
/* Specify Vertex Pointers */
specifyVertexAttributes();
/* Set texture uniform */
int uniTex = program.getUniformLocation("texImage");
program.setUniform(uniTex, 0);
/* Set model matrix to identity matrix */
Matrix4f model = new Matrix4f();
int uniModel = program.getUniformLocation("model");
program.setUniform(uniModel, model);
/* Set view matrix to identity matrix */
Matrix4f view = new Matrix4f();
int uniView = program.getUniformLocation("view");
program.setUniform(uniView, view);
/* Set projection matrix to an orthographic projection */
Matrix4f projection = Matrix4f.orthographic(0f, width, 0f, height, -1f, 1f);
int uniProjection = program.getUniformLocation("projection");
program.setUniform(uniProjection, projection);
}
Also a few questions about OpenGL in general:
Should I bind textures every frame, or bind/unbind them whenever they are on/off screen?
Should I have one VAO for everything, or one VAO for the same textures?
Looking at the tutorial, my code is literally all the same, the only difference is that I store textures in an array and draw them every frame instead of having a draw method called every frame elsewhere. In going to inprove on this system and whatever but for now this is a good render engine for my project.
looking into it, textures are supposed to be bound every frame and VAIs are best with the same frames.
I am using code similar to Java - get pixel array from image to get low-level access to pixel data of a BMP image, along the lines of:
BufferedImage image = ImageIO.read(is);
DataBuffer buffer = image.getRaster().getDataBuffer();
byte[] rawPixels = ((DataBufferByte) buffer).getData();
The resulting array is laid bottom to top (ie. its first bytes are the beginning of the last image line), which makes sense considering that BMP files usually have the same layout.
I would like to hide this low-level detail from callers by flipping the lines in this situation. Is there a way I can query the pixels orientation/layout of the loaded BufferedImage?
I have checked the source code of the Java 7 BMPImageReader, and it does translate from bottom-up to top-down order while reading, as I expected it to do. The DataBuffers backing array will thus be in the normal top-down order. I cannot reproduce this behavior using Oracle Java 7 JRE on Windows.
The OP has verified that the problem was indeed in another part of the code, not posted as part of the question.
I think what is described just might be possible, using a special subclass of SampleModel that translates all incoming y-coordinates, but there's no standard method to query for orientation (all Rasters are assumed to be top-down).
Anyway, just for fun, I created some code, to test if it is at all possible. Below is a fully runnable example.
public class SampleModelOrientationTest {
public static void main(String[] args) {
BufferedImage image = new BufferedImage(16, 9, BufferedImage.TYPE_3BYTE_BGR);
WritableRaster raster = image.getRaster();
DataBuffer dataBuffer = raster.getDataBuffer();
SampleModel sampleModel = image.getSampleModel();
QueryingDataBuffer queryBuffer = new QueryingDataBuffer(dataBuffer, sampleModel.getWidth(), sampleModel.getNumDataElements());
sampleModel.getDataElements(0, 0, null, queryBuffer);
System.out.println(queryBuffer.getOrientation());
queryBuffer.resetOrientation();
SampleModel bottomUpSampleModel = new BottomUpSampleModel(sampleModel);
bottomUpSampleModel.getDataElements(0, 0, null, queryBuffer);
System.out.println(queryBuffer.getOrientation());
}
private static class QueryingDataBuffer extends DataBuffer {
enum Orientation {
Undefined,
TopDown,
BottomUp,
Unsupported
}
private final int width;
private final int numDataElements;
private Orientation orientation = Orientation.Undefined;
public QueryingDataBuffer(final DataBuffer dataBuffer, final int width, final int numDataElements) {
super(dataBuffer.getDataType(), dataBuffer.getSize());
this.width = width;
this.numDataElements = numDataElements;
}
#Override public int getElem(final int bank, final int i) {
if (bank == 0 && i < numDataElements && isOrientationUndefinedOrEqualTo(Orientation.TopDown)) {
orientation = Orientation.TopDown;
}
else if (bank == 0 && i >= (size - (width * numDataElements) - numDataElements) && isOrientationUndefinedOrEqualTo(Orientation.BottomUp)) {
orientation = Orientation.BottomUp;
}
else {
// TODO: Expand with more options as apropriate
orientation = Orientation.Unsupported;
}
return 0;
}
private boolean isOrientationUndefinedOrEqualTo(final Orientation orientation) {
return this.orientation == Orientation.Undefined || this.orientation == orientation;
}
#Override public void setElem(final int bank, final int i, final int val) {
}
public final void resetOrientation() {
orientation = Orientation.Undefined;
}
public final Orientation getOrientation() {
return orientation;
}
}
// TODO: This has to be generalized to be used for any BufferedImage type.
// I justy happen to know that 3BYTE_BGR uses PixelInterleavedSampleModel and has BGR order.
private static class BottomUpSampleModel extends PixelInterleavedSampleModel {
public BottomUpSampleModel(final SampleModel sampleModel) {
super(sampleModel.getDataType(), sampleModel.getWidth(), sampleModel.getHeight(),
sampleModel.getNumDataElements(), sampleModel.getNumDataElements() * sampleModel.getWidth(),
new int[] {2, 1, 0} // B, G, R
);
}
#Override public Object getDataElements(final int x, final int y, final Object obj, final DataBuffer data) {
return super.getDataElements(x, getHeight() - 1 - y, obj, data);
}
#Override public int getSample(final int x, final int y, final int b, final DataBuffer data) {
return super.getSample(x, getHeight() - 1 - y, b, data);
}
}
}