java - LWJGL OBJ File Loader not working correctly - java

I've been trying to load an obj. file into my LWJGL application. I have only the bare bones to get something to appear. My obj. is loading and displaying but its contorted/distorted. I've tried it several ways and still not luck. This is the image I get.
Resulting render of code
Main.java
package Model;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.util.glu.GLU.*;
import org.lwjgl.LWJGLException;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.util.vector.Vector3f;
public class Main {
private Model m;
private Vector3f location, rotation;
public static void main(String[] args) {
new Main().run();
}
private void run() {
try {
Display.setDisplayMode(new DisplayMode(1200, 800));
Display.setTitle("3D Game");
Display.create();
} catch (LWJGLException e) {
e.printStackTrace();
Display.destroy();
System.exit(1);
}
location = new Vector3f(0.0f, 0.0f, 0.0f);
rotation = new Vector3f(0.0f, 0.0f, 0.0f);
try {
m = Model.getModel("res/monkey.obj");
} catch (Exception e){
e.printStackTrace();
}
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(30f, (float) (1200/800), 0.3f, 100);
glMatrixMode(GL_MODELVIEW);
glEnable(GL_DEPTH_TEST);
while(!Display.isCloseRequested()){
input();
render();
Display.update();
Display.sync(60);
}
Display.destroy();
System.exit(0);
}
private void input() {
boolean up = Keyboard.isKeyDown(Keyboard.KEY_W);
boolean down = Keyboard.isKeyDown(Keyboard.KEY_S);
boolean left = Keyboard.isKeyDown(Keyboard.KEY_A);
boolean right = Keyboard.isKeyDown(Keyboard.KEY_D);
boolean flyUp = Keyboard.isKeyDown(Keyboard.KEY_E);
boolean flyDown = Keyboard.isKeyDown(Keyboard.KEY_Q);
boolean speedUp = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT);
boolean slowDown = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL);
float walkspeed = 0.15f;
float mx = Mouse.getDX();
float my = Mouse.getDY();
mx *= 0.15f;
my *= 0.15f;
if (Mouse.isButtonDown(0)){
rotation.y += mx;
if (rotation.y > 360){
rotation.y -= 360;
}
rotation.y += my;
if (rotation.x > 85){
rotation.x = 85;
}
if (rotation.x < -85){
rotation.x = -85;
}
}
if(speedUp && !slowDown){
walkspeed = 0.25f;
}
if(slowDown && !speedUp){
walkspeed = 0.15f;
}
if(up && !down){
float cz = (float) (walkspeed * 2 * Math.cos(Math.toRadians(rotation.y)));
float cx = (float) (walkspeed * Math.sin(Math.toRadians(rotation.y)));
location.z += cz;
location.x -= cx;
}
if(down && !up){
float cz = (float) (walkspeed * 2 * Math.cos(Math.toRadians(rotation.y)));
float cx = (float) (walkspeed * Math.sin(Math.toRadians(rotation.y)));
location.z -= cz;
location.x += cx;
}
if(right && !left){
float cz = (float) (walkspeed * 2 * Math.cos(Math.toRadians(rotation.y)) + 90);
float cx = (float) (walkspeed * Math.sin(Math.toRadians(rotation.y)));
location.z += cz;
location.x -= cx;
}
if(left && !right){
float cz = (float) (walkspeed * 2 * Math.cos(Math.toRadians(rotation.y)) + 90);
float cx = (float) (walkspeed * Math.sin(Math.toRadians(rotation.y)));
location.z -= cz;
location.x += cx;
}
if(flyUp && !flyDown){
location.y -= walkspeed;
}
if(flyDown && !flyUp){
location.y += walkspeed;
}
}
private void render() {
glPushMatrix();
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glRotatef(rotation.x, 1f, 0f, 0f);
glRotatef(rotation.y, 0f, 1f, 0f);
glRotatef(rotation.z, 0f, 0f, 1f);
glTranslatef(location.x, location.y, location.z);
m.render();
glPopMatrix();
}
}
Model.java
package Model;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.lwjgl.util.vector.Vector3f;
import static org.lwjgl.opengl.GL11.*;
public class Model {
public List<Vector3f> verts;
public List<Vector3f> norms;
public List<Face> faces;
public static Model getModel(String s) throws IOException{
return new Model(s);
}
private Model(String path) throws IOException{
verts = new ArrayList<Vector3f>();
norms = new ArrayList<Vector3f>();
faces = new ArrayList<Face>();
new ModelLoader(this, path);
}
public void render(){
// glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
glBegin(GL_TRIANGLES);
for (Face f : faces){
Vector3f v1 = verts.get((int) f.verts.x -1);
Vector3f n1 = verts.get((int) f.norms.x -1);
Vector3f v2 = verts.get((int) f.verts.y -1);
Vector3f n2 = verts.get((int) f.norms.y -1);
Vector3f v3 = verts.get((int) f.verts.z -1);
Vector3f n3 = verts.get((int) f.norms.z -1);
glNormal3f(n1.x, n1.y, n1.z);
glVertex3f(v1.x, v1.y, v1.z);
glNormal3f(n2.x, n2.y, n2.z);
glVertex3f(v2.x, v2.y, v2.z);
glNormal3f(n3.x, n3.y, n3.z);
glVertex3f(v2.x, v3.y, v3.z);
}
glEnd();
}
}
ModelLoader.java
package Model;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import org.lwjgl.util.vector.Vector3f;
public class ModelLoader {
public ModelLoader(Model m, String path) throws IOException {
BufferedReader read = new BufferedReader(new FileReader(new File(path)));
String line;
while((line = read.readLine()) != null) {
if (line.startsWith("v ")){
float x = Float.valueOf(line.split(" ")[1]);
float y = Float.valueOf(line.split(" ")[2]);
float z = Float.valueOf(line.split(" ")[3]);
Vector3f v = new Vector3f(x, y, z);
m.verts.add(v);
} else if (line.startsWith("vn ")) {
float x = Float.valueOf(line.split(" ")[1]);
float y = Float.valueOf(line.split(" ")[2]);
float z = Float.valueOf(line.split(" ")[3]);
Vector3f v = new Vector3f(x, y, z);
m.norms.add(v);
} else if (line.startsWith("f ")) {
float x1 = Float.valueOf(line.split(" ")[1].split("/")[0]);
float y1 = Float.valueOf(line.split(" ")[2].split("/")[0]);
float z1 = Float.valueOf(line.split(" ")[3].split("/")[0]);
float x2 = Float.valueOf(line.split(" ")[1].split("/")[2]);
float y2 = Float.valueOf(line.split(" ")[2].split("/")[2]);
float z2 = Float.valueOf(line.split(" ")[3].split("/")[2]);
Face f = new Face(new Vector3f(x2, y2, z2), new Vector3f(x1, y1, z1));
m.faces.add(f);
}
}
read.close();
}
}
Face.java
package Model;
import org.lwjgl.util.vector.Vector3f;
public class Face {
public Vector3f norms, verts;
public Face(Vector3f n, Vector3f v){
this.norms = n;
this.verts = v;
}
}

You are in fact very close. If you look at the image of the monkey head,from blender I believe, it has most of the triangles being drawn. Half of the triangles are rendered. This leads me to believe that there is a problem with the order that indices for some of the faces are made.
Secondly I have another idea. Since this has been exported from blender I would suggest making sure that you exported the monkey head with triangulation selected in the export options. This would ensure triangles to be created and not quads. Possibly you are extracting only the first three indices from the face line ("f ") and this would result in these triangles.
For the first possible fix I would recommend trying to disable face culling to see if it is fixed. If this does fix the model then the problem is withing the order of indices for faces.
Secondly I have just noticed that in your last glVertex3f of the render you have written v2 instead of v3 for the vertices x reference. This most likely is the problem.
Third if these do not work, then try exporting from blender again making sure to triangulate the model.

Related

reposition object in circle

As you can see on the image, I have a p1 and p2 objects with (x,y) coordinates which I know the values, and I know radius of all these circle objects.
However, I want to calculate new position x,y which would be p3 center point. Basically, as you can see it's p2 position + radius.
I am doing this for java game which is based on libgdx. I would appreciate any math or java language directions/examples.
See code comments for explanation.
import java.awt.*;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import javax.swing.*;
class CenteredCircle extends Ellipse2D.Double {
CenteredCircle(Point2D.Double p, double radius) {
super(p.x - radius, p.y - radius, 2 * radius, 2 * radius);
}
}
public class CircleDemo extends JFrame {
public CircleDemo() {
int width = 640; int height = 480;
setSize(new Dimension(width, height));
setLocationRelativeTo(null);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
JPanel p = new JPanel() {
#Override
public void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
// center p1
Point2D.Double p1 = new Point2D.Double(getSize().width/2, getSize().height/2);
double radius = 130.0;
// big circle
Shape circle2 = new CenteredCircle(p1, radius);
g2d.draw(circle2);
// 12 small circles
for (int angle = 0; angle < 360; angle += 30) {
// this is the magic part
// a polar co-ordinate has a length and an angle
// by changing the angle we rotate
// the transformed co-ordinate is the center of the small circle
Point2D.Double newCenter = polarToCartesian(radius, angle);
// draw line just for visualization
Line2D line = new Line2D.Double(p1.x, p1.y, p1.x + newCenter.x, p1.y+ newCenter.y);
g2d.draw(line);
// draw the small circle
Shape circle = new CenteredCircle(
new Point2D.Double(p1.x + newCenter.x, p1.y + newCenter.y),
radius/4);
g2d.draw(circle);
}
}
};
setTitle("Circle Demo");
getContentPane().add(p);
}
public static void main(String arg[]) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new CircleDemo();
}
});
}
static Point2D.Double polarToCartesian(double r, double theta) {
theta = (theta * Math.PI) / 180.0; // multiply first, then divide to keep error small
return new Point2D.Double(r * Math.cos(theta), r * Math.sin(theta));
}
// not needed, just for completeness
public static Point2D.Double cartesianToPolar(double x, double y) {
return new Point2D.Double(Math.sqrt(x * x + y * y), (Math.atan2(y, x) * 180) / Math.PI);
}
}
Now using libgdx for the graphics. Thus no need for polar co-ordinates, on the outside.
I am not doing frame rate relative animation. Therefore, this is no perfect match to your code.
Using the following calculation (if (theta >= 360) { theta = 0.0f; }) at the end of the render method will let the animation restart with its original value.
package org.demo;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.ScreenUtils;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
public class CircleDemo extends ApplicationAdapter {
ShapeRenderer shapeRenderer;
float theta = 0.0f;
#Override
public void create () {
shapeRenderer = new ShapeRenderer();
}
#Override
public void render () {
ScreenUtils.clear(0, 0.4f, 0.4f, 1);
Vector2 p1 = new Vector2( Gdx.graphics.getWidth() / 2.0f , Gdx.graphics.getHeight() / 2.0f);
Vector2 smallCircleCenter = new Vector2(150.0f, 0.0f);
smallCircleCenter.add(p1); // translate center by p1
shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
// static lines and circles
for (int angle = 0; angle < 360; angle += 30) {
Vector2 lineEnd = new Vector2(smallCircleCenter);
lineEnd.rotateAroundDeg(p1, angle);
shapeRenderer.line(p1, lineEnd);
shapeRenderer.circle(lineEnd.x, lineEnd.y, 20);
}
// animated line and circle in red
shapeRenderer.setColor(0.75f, 0, 0, 1);
Vector2 movingCircleCenter = new Vector2(smallCircleCenter);
movingCircleCenter.rotateAroundDeg(p1, theta);
shapeRenderer.line(p1, movingCircleCenter);
shapeRenderer.circle(movingCircleCenter.x, movingCircleCenter.y, 20);
shapeRenderer.setColor(1, 1, 1, 1);
shapeRenderer.end();
theta++;
// for the screenshot stop at 90 degrees
if (theta >= 90) {
theta = 90.0f;
}
}
#Override
public void dispose () {
shapeRenderer.dispose();
}
}
So I wrote a test in my project, based on your approach:
package com.bigbang.test.impl;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.bigbang.Game;
import com.bigbang.graphics.g2d.shapes.impl.Ellipse;
import com.bigbang.graphics.g2d.shapes.impl.Line;
import com.bigbang.graphics.gl.Color;
import com.bigbang.math.BBMath;
public class PolarToCartesianTest extends AbstractTest {
private Array<GraphicalObject> graphicalObjectArray;
private GraphicalObject dynamicGraphicalObject;
private float radius, smallCircleRadius;
private float centerX, centerY;
public PolarToCartesianTest(Game game) {
super(game);
}
#Override
public void create() {
radius = 200f;
centerX = game.getScreenController().getScreenWidth() / 2;
centerY = game.getScreenController().getScreenHeight() / 2;
smallCircleRadius = radius / 4;
graphicalObjectArray = new Array<>();
for (int angle = 0; angle < 360; angle += 30) {
GraphicalObject graphicalObject = new GraphicalObject();
graphicalObject.angle = angle;
graphicalObjectArray.add(graphicalObject);
}
dynamicGraphicalObject = new GraphicalObject();
game.getCameraController().getCamera().position.x = game.getScreenController().getScreenWidth() / 2;
game.getCameraController().getCamera().position.y = game.getScreenController().getScreenHeight() / 2;
}
#Override
public void update(float deltaTime) {
for (GraphicalObject graphicalObject : graphicalObjectArray) {
Vector2 polarToCartesianPosition = BBMath.polarToCartesian(radius, graphicalObject.angle);
graphicalObject.line.x1 = centerX + 0;
graphicalObject.line.y1 = centerY + 0;
graphicalObject.line.x2 = centerX + polarToCartesianPosition.x;
graphicalObject.line.y2 = centerY + polarToCartesianPosition.y;
graphicalObject.line.color = Color.WHITE_COLOR;
graphicalObject.ellipse.x = centerX + polarToCartesianPosition.x;
graphicalObject.ellipse.y = centerY + polarToCartesianPosition.y;
graphicalObject.ellipse.width = 2 * smallCircleRadius;
graphicalObject.ellipse.height = 2 * smallCircleRadius;
graphicalObject.ellipse.color = Color.WHITE_COLOR;
}
float shift = 0;
float theta = (shift * smallCircleRadius) * (centerY / centerX);
Vector2 pos = BBMath.polarToCartesian(radius, theta);
dynamicGraphicalObject.line.color = new Color(Color.RED);
dynamicGraphicalObject.line.x1 = centerX + 0;
dynamicGraphicalObject.line.y1 = centerY + 0;
dynamicGraphicalObject.line.x2 = centerX + pos.x;
dynamicGraphicalObject.line.y2 = centerY + pos.y;
dynamicGraphicalObject.ellipse.x = centerX + pos.x;
dynamicGraphicalObject.ellipse.y = centerY + pos.y;
dynamicGraphicalObject.ellipse.width = 2 * smallCircleRadius;
dynamicGraphicalObject.ellipse.height = 2 * smallCircleRadius;
dynamicGraphicalObject.ellipse.color = new Color(Color.RED);
}
#Override
public void draw() {
game.getShapeRenderer().begin(ShapeRenderer.ShapeType.Line);
for (GraphicalObject graphicalObject : graphicalObjectArray) {
graphicalObject.line.draw();
graphicalObject.ellipse.draw();
}
dynamicGraphicalObject.line.draw();
dynamicGraphicalObject.ellipse.draw();
game.getShapeRenderer().end();
}
class GraphicalObject {
Ellipse ellipse;
Line line;
float angle;
public GraphicalObject() {
this.ellipse = new Ellipse(game);
this.line = new Line(game);
}
}
}
Which is same math like in your example, with some modifications:
However, you can notice I have this dynamicGraphicalObject (red circle), which I want to shift position around circle by using theta value calculated as (shift * smallCircleRadius) * (centerY / centerX);. This works perfect for shift=0 value. It's properly positioned/overlapping white. But if I would change shift variable to 1, 2, 3, or 11, you can see that it's not precisely aligned with white circles. Is this floating point issue or am I missing something in calculation of theta ?
shift values used: 2,6 and 11 in order by images
--
SOLUTION:
float fixPrecision = 1.1f;
float theta = (shift * fixPrecision) + ((shift * smallCircleRadius) * (centerY / centerX));

Raycasting giving erratic values

Using LWJGL3 and JOML.
I am trying to work out how to get the point on the terrain using a raycast system. I use the point to set a characters position to see what point is being outputted.
I don't think it is my terrain causing the issue since moving the character (with the keyboard or by just adding a value each frame) and attaching it to the terrain works fine.
The issues I get:
Inverting the projection matrix causes the point to flicker between the correct position and some other point but I am not sure about the relationship with this other value.
Not inverting the projection matrix stops the flickering but now the point moves away from the mouse position exponentially.
Near the center of the screen the 2 positions will merge together.
If I print out the terrain point vector it comes out in scientific notation for some reason:
( 5.335E+1 3.849E-2 -9.564E+1)
( 8.804E+1 -6.256E-3 -2.815E+2)
( 5.335E+1 3.849E-2 -9.564E+1)
( 8.804E+1 -6.256E-3 -2.815E+2)
( 5.335E+1 3.849E-2 -9.564E+1)
If I print out each of the x, y and z values individually it is actually showing the correct position but is also flicking to another (the difference between which increases the further from the center of the screen that the mouse moves):
5.8912144, 0.016174316, -7.771721
6.1992702, 0.01574707, -11.79966
5.8912144, 0.016174316, -7.771721
6.1992702, 0.01574707, -11.79966
6.609352, 0.01815033, -8.793705
Raycasting class:
public class Raycast
{
private static final int RECURSION_COUNT = 200;
private static final float RAY_RANGE = 600;
private Input input;
private Vector3f currentRay = new Vector3f();
private Matrix4f projectionMatrix;
private Matrix4f viewMatrix;
private Camera camera;
private Terrain terrain;
private Vector3f currentTerrainPoint;
public Raycast(Camera camera, Matrix4f projectionMatrix, Terrain terrain, Input input) {
this.camera = camera;
this.projectionMatrix = projectionMatrix;
this.input = input;
this.viewMatrix = MathUtils.createViewMatrix(camera);
this.terrain = terrain;
}
public void update()
{
viewMatrix = MathUtils.createViewMatrix(camera);
currentRay = calculateRay();
if (intersectionInRange(0, RAY_RANGE, currentRay)) {
currentTerrainPoint = binarySearch(0, 0, RAY_RANGE, currentRay);
} else {
currentTerrainPoint = null;
}
}
private Vector3f calculateRay()
{
float mouseX = (float) input.getMouseDx();
float mouseY = (float) input.getMouseDy();
Vector2f deviceCoords = getNormalizedDeviceCoordinates(mouseX, mouseY);
//System.out.println(deviceCoords.x+", "+deviceCoords.y);
Vector4f clipCoords = new Vector4f(deviceCoords.x, deviceCoords.y, -1f, 1f);
Vector4f eyeCoords = toEyeCoords(clipCoords);
Vector3f worldRay = toWorldCoords(eyeCoords);
return worldRay;
}
private Vector3f toWorldCoords(Vector4f eyeCoords)
{
Matrix4f invertedView = viewMatrix.invert();
Vector4f rayWorld = invertedView.transform(eyeCoords);
Vector3f mouseRay = new Vector3f(rayWorld.x, rayWorld.y, rayWorld.z);
mouseRay.normalize();
return mouseRay;
}
private Vector4f toEyeCoords(Vector4f clipCoords)
{
Matrix4f invertedProjection = projectionMatrix.invert();
Vector4f eyeCoords = invertedProjection.transform(clipCoords);
return new Vector4f(eyeCoords.x, eyeCoords.y, -1f, 0f);
}
private Vector2f getNormalizedDeviceCoordinates(float mouseX, float mouseY)
{
float x = (2f * mouseX) / Constants.DISPLAY_WIDTH - 1f;
float y = (2f * mouseY) / Constants.DISPLAY_HEIGHT - 1f;
return new Vector2f(x, -y);
}
private Vector3f getPointOnRay(Vector3f ray, float distance) {
//Vector3f camPos = new Vector3f(camera.getPosX(), camera.getPosY(), camera.getPosZ());
Vector3f start = new Vector3f(camera.getPosX(), camera.getPosY(), camera.getPosZ());
Vector3f scaledRay = new Vector3f(ray.x * distance, ray.y * distance, ray.z * distance);
return start.add(scaledRay);
}
private Vector3f binarySearch(int count, float start, float finish, Vector3f ray) {
float half = start + ((finish - start) / 2f);
if (count >= RECURSION_COUNT) {
Vector3f endPoint = getPointOnRay(ray, half);
Terrain terrain = getTerrain(endPoint.x, endPoint.z);
if (terrain != null) {
return endPoint;
} else {
return null;
}
}
if (intersectionInRange(start, half, ray)) {
return binarySearch(count + 1, start, half, ray);
} else {
return binarySearch(count + 1, half, finish, ray);
}
}
private boolean intersectionInRange(float start, float finish, Vector3f ray) {
Vector3f startPoint = getPointOnRay(ray, start);
Vector3f endPoint = getPointOnRay(ray, finish);
if (!isUnderGround(startPoint) && isUnderGround(endPoint)) {
return true;
} else {
return false;
}
}
private boolean isUnderGround(Vector3f testPoint) {
Terrain terrain = getTerrain(testPoint.x, testPoint.z);
float height = 0;
if (terrain != null) {
height = terrain.getTerrainHeight(testPoint.x, testPoint.z);
}
if (testPoint.y < height) {
return true;
} else {
return false;
}
}
private Terrain getTerrain(float worldX, float worldZ) {
return terrain;
}
public Vector3f getCurrentTerrainPoint() {
return currentTerrainPoint;
}
public Vector3f getCurrentRay() {
return currentRay;
}
}
MathUtils class with view and projection matrices:
public class MathUtils {
public static float baryCentric(Vector3f p1, Vector3f p2, Vector3f p3, Vector2f pos) {
float det = (p2.z - p3.z) * (p1.x - p3.x) + (p3.x - p2.x) * (p1.z - p3.z);
float l1 = ((p2.z - p3.z) * (pos.x - p3.x) + (p3.x - p2.x) * (pos.y - p3.z)) / det;
float l2 = ((p3.z - p1.z) * (pos.x - p3.x) + (p1.x - p3.x) * (pos.y - p3.z)) / det;
float l3 = 1.0f - l1 - l2;
return l1 * p1.y + l2 * p2.y + l3 * p3.y;
}
public static Matrix4f createTransformationMatrix(Vector2f translation, Vector2f scale) {
Matrix4f matrix = new Matrix4f();
matrix.identity();
matrix.translate(translation.x,translation.y,0f);
matrix.scale(scale.x,scale.y,1f);
return matrix;
}
public static Matrix4f createTransformationMatrix(Vector3f translation, float rx, float ry, float rz, float scale) {
Matrix4f transformationMatrix = new Matrix4f();
transformationMatrix.identity();
transformationMatrix.translate(translation);
transformationMatrix.rotate((float) Math.toRadians(rx), 1,0,0);
transformationMatrix.rotate((float) Math.toRadians(ry), 0,1,0);
transformationMatrix.rotate((float) Math.toRadians(rz), 0,0,1);
transformationMatrix.scale(scale);
return transformationMatrix;
}
public static Matrix4f createViewMatrix(Camera camera) {
Matrix4f viewMatrix = new Matrix4f();
viewMatrix.identity();
viewMatrix = viewMatrix.rotate((float) Math.toRadians(camera.getPitch()), 1,0,0);//((float) Math.toRadians(camera.getPitch()), new Vector3f(1, 0, 0), viewMatrix);
viewMatrix = viewMatrix.rotate((float) Math.toRadians(camera.getYaw()),0, 1, 0);
Vector3f cameraPos = new Vector3f(camera.getPosX(), camera.getPosY(), camera.getPosZ());
Vector3f negativeCameraPos = new Vector3f(-cameraPos.x, -cameraPos.y, -cameraPos.z);
viewMatrix = viewMatrix.translate(negativeCameraPos);
return viewMatrix;
}
public static Matrix4f createProjectionMatrix() {
Matrix4f projectionMatrix = new Matrix4f();
float aspectRatio = (float) Constants.DISPLAY_WIDTH / (float) Constants.DISPLAY_HEIGHT;
float fov = Constants.FOV;
float near = Constants.NEAR_PLANE;
float far = Constants.FAR_PLANE;
projectionMatrix = projectionMatrix.perspective((float) java.lang.Math.toRadians(fov), aspectRatio, near, far);
return projectionMatrix;
}
A coordinate in view space is a Cartesian coordinates with 3 components x, y and z. The projection matrix transforms from view space to clip space. Clip space coordinates are Homogeneous coordinates with 4 components x, y, z and w.
Clip space coordinates can be transformed to normalized device coordinates by a Perspective divide.
This means the x, y and z component is divided by w component.
If you want to transform from normalized device space to view space, then you've to do the inverse operation. this means you've to transform by the inverse projection matrix and to divide the x, y and z component of the result by the w component of the result.
private Vector4f toEyeCoords(Vector4f ndcCoords)
{
Matrix4f invertedProjection = projectionMatrix.invert(new Matrix4f());
Vector4f eyeCoords = invertedProjection.transform(clipCoords);
return new Vector4f(eyeCoords.x/eyeCoords.w, eyeCoords.y/eyeCoords.w, eyeCoords.z/eyeCoords.w, 0.0f);
}

Strange PolyBatch Positioning in libGDX

Excuse the basic question, just getting into the guts of LibGDX
I'm creating a radial bar to show my countdown timer
I've found some code that does what I need it to, the problem is the radial sprite's positioning. I can't seem to get it to center in the Image object (Since it seems to be ignoring the Image's local coordinates and is defaulting to the stage's) so 0,0 places it close to the bottom left of my screen.
I've tried using a localtoStage and vice versa to calculate the correct positions, but that doesn't seem to give me the right values either.
Please advise
package com.goplayplay.klpoker.CSS.Classes;
import com.badlogic.gdx.graphics.g2d.*;
import com.badlogic.gdx.math.EarClippingTriangulator;
import com.badlogic.gdx.math.Intersector;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.utils.ShortArray;
public class ProgressCircle extends Image {
TextureRegion texture;
PolygonSpriteBatch polyBatch;
Vector2 center;
Vector2 centerTop;
Vector2 leftTop;
Vector2 leftBottom;
Vector2 rightBottom;
Vector2 rightTop;
Vector2 progressPoint;
float[] fv;
IntersectAt intersectAt;
public ProgressCircle(TextureRegion region, PolygonSpriteBatch polyBatch) {
super(region);
this.texture = region;
this.polyBatch = polyBatch;
center = new Vector2(this.getWidth() / 2, this.getHeight() / 2);
centerTop = new Vector2(this.getWidth() / 2, this.getHeight());
leftTop = new Vector2(0, this.getHeight());
leftBottom = new Vector2(0, 0);
rightBottom = new Vector2(this.getWidth(), 0);
rightTop = new Vector2(this.getWidth(), this.getHeight());
progressPoint = new Vector2(this.getWidth() / 2, this.getHeight() / 2);
setPercentage(0);
}
private Vector2 IntersectPoint(Vector2 line) {
Vector2 v = new Vector2();
boolean isIntersect;
//check top
isIntersect = Intersector.intersectSegments(leftTop, rightTop, center, line, v);
//check bottom
if (isIntersect) {
intersectAt = IntersectAt.TOP;
return v;
} else isIntersect = Intersector.intersectSegments(leftBottom, rightBottom, center, line, v);
//check left
if (isIntersect) {
intersectAt = IntersectAt.BOTTOM;
return v;
} else isIntersect = Intersector.intersectSegments(leftTop, leftBottom, center, line, v);
//check bottom
if (isIntersect) {
intersectAt = IntersectAt.LEFT;
return v;
} else isIntersect = Intersector.intersectSegments(rightTop, rightBottom, center, line, v);
if (isIntersect) {
intersectAt = IntersectAt.RIGHT;
return v;
} else {
intersectAt = IntersectAt.NONE;
return null;
}
}
public void setPercentage(float percent) {
//100 % = 360 degree
//==> percent % => (percent * 360 / 100) degree
float angle = convertToRadians(90); //percent = 0 => angle = -90
angle -= convertToRadians(percent * 360 / 100);
float len = this.getWidth() > this.getHeight() ? this.getWidth() : this.getHeight();
float dy = (float) (Math.sin(angle) * len);
float dx = (float) (Math.cos(angle) * len);
Vector2 line = new Vector2(center.x + dx, center.y + dy);
Vector2 v = IntersectPoint(line);
if (intersectAt == IntersectAt.TOP) {
if (v.x >= this.getWidth() / 2)
{
fv = new float[]{
center.x,
center.y,
centerTop.x,
centerTop.y,
leftTop.x,
leftTop.y,
leftBottom.x,
leftBottom.y,
rightBottom.x,
rightBottom.y,
rightTop.x,
rightTop.y,
v.x,
v.y
};
} else {
fv = new float[]{
center.x,
center.y,
centerTop.x,
centerTop.y,
v.x,
v.y
};
}
} else if (intersectAt == IntersectAt.BOTTOM) {
fv = new float[]{
center.x,
center.y,
centerTop.x,
centerTop.y,
leftTop.x,
leftTop.y,
leftBottom.x,
leftBottom.y,
v.x,
v.y
};
} else if (intersectAt == IntersectAt.LEFT) {
fv = new float[]{
center.x,
center.y,
centerTop.x,
centerTop.y,
leftTop.x,
leftTop.y,
v.x,
v.y
};
} else if (intersectAt == IntersectAt.RIGHT) {
fv = new float[]{
center.x,
center.y,
centerTop.x,
centerTop.y,
leftTop.x,
leftTop.y,
leftBottom.x,
leftBottom.y,
rightBottom.x,
rightBottom.y,
v.x,
v.y
};
} else // if (intersectAt == IntersectAt.NONE)
{
fv = null;
}
}
//
#Override
public void draw(Batch batch, float parentAlpha) {
// super.draw(batch, parentAlpha);
if (fv == null) return;
batch.end();
drawMe();
batch.begin();
}
public void drawMe() {
Vector2 acc = new Vector2();
acc.set(getWidth() / 2, getHeight() / 2);
localToStageCoordinates(acc);
EarClippingTriangulator e = new EarClippingTriangulator();
ShortArray sv = e.computeTriangles(fv);
PolygonRegion polyReg = new PolygonRegion(texture, fv, sv.toArray());
PolygonSprite poly = new PolygonSprite(polyReg);
poly.setOrigin(this.getOriginX(), this.getOriginY());
poly.setPosition(this.getX(), this.getY());
// poly.setPosition(acc.x, acc.y); //Attempting to calculate correct positioning - Doesnt work
poly.setRotation(this.getRotation());
poly.setColor(this.getColor());
polyBatch.begin();
poly.draw(polyBatch);
polyBatch.end();
}
float convertToDegrees(float angleInRadians) {
float angleInDegrees = angleInRadians * 57.2957795f;
return angleInDegrees;
}
//-----------------------------------------------------------------
float convertToRadians(float angleInDegrees) {
float angleInRadians = angleInDegrees * 0.0174532925f;
return angleInRadians;
}
public enum IntersectAt {
NONE, TOP, BOTTOM, LEFT, RIGHT
}
}
You forgot to set the camera's projection matrix on the polygon batch. You can get a copy of it from the Batch that's passed in:
public void draw(Batch batch, float parentAlpha) {
// super.draw(batch, parentAlpha);
if (fv == null) return;
batch.end();
drawMe(batch.getProjectionMatrix());
batch.begin();
}
public void drawMe(Matrix4 projection) {
polyBatch.setProjectionMatrix(projection);
//...
}
Or more simply, you can use a PolygonBatch as your Stage's batch, so you don't have to be swapping batches:
stage = new Stage(myViewport, new PolygonBatch());
//...
public void draw(Batch batch, float parentAlpha) {
// super.draw(batch, parentAlpha);
if (fv == null) return;
//don't need to call begin and end on the batch
drawMe((PolygonBatch)batch);
}
public void drawMe(PolygonBatch polyBatch) {
//...
//don't need to call begin or end on the batch
}
By the way, your drawMe method instantiates quite a few objects, some large. You should avoid this if you have more than a few actors that do this, or you'll get stutters from the GC. Try to instantiate objects only once in the constructor and reuse them.

OpenGL doesn't draw with my own matrix

I am trying to make a 2D game with opengl.I made the the class which draw meshes.And i made my own Matrix4f class.I have experience with 3D opengl.
When i pass my matrix4f to the shader it doesn't draw.But when i remove it from multiplying with the position the triangle appears on the screen.I tried my Matrix4f from my 3D Game Engine but it doesn't work as well.
Matrix4f class
package com.game.main.maths;
import java.nio.FloatBuffer;
import com.game.main.util.Util;
public class Matrix4f {
private float[][] m;
public Matrix4f() {
m = new float[4][4];
initIdentity();
}
public Matrix4f initIdentity() {
for (int x = 0; x < 4; x++)
for (int y = 0; y < 4; y++) {
if (x == y)
m[x][y] = 1.0f;
else
m[x][y] = 0;
}
return this;
}
public Matrix4f translate(float x, float y, float z) {
initIdentity();
m[0][3] = x;
m[1][3] = y;
m[2][3] = z;
return this;
}
public Matrix4f translate(Vector3f pos) {
initIdentity();
m[0][3] = pos.getX();
m[1][3] = pos.getY();
m[2][3] = pos.getZ();
return this;
}
public Matrix4f rotate(float angle) {
initIdentity();
float rad = (float) Math.toRadians(angle);
float cos = (float) Math.cos(rad);
float sin = (float) Math.sin(rad);
m[0][0] = cos;
m[1][0] = sin;
m[0][1] = -sin;
m[1][1] = cos;
return this;
}
public Matrix4f initOrthographic(float left, float right, float bottom,
float top, float near, float far) {
initIdentity();
m[0][0] = 2.0f / (right - left);
m[1][1] = 2.0f / (top - bottom);
m[2][2] = 2.0f / (near - far);
m[0][3] = (left + right) / (left - right);
m[1][3] = (bottom + top) / (bottom - top);
m[2][3] = (near + far) / (far - near);
return this;
}
public Matrix4f mul(Matrix4f matrix) {
Matrix4f result = new Matrix4f();
for (int y = 0; y < 4; y++) {
for (int x = 0; x < 4; x++) {
float sum = 0.0f;
for (int index = 0; index < 4; index++)
sum += m[index][y] * matrix.get(x, index);
result.set(x, y, sum);
}
}
return result;
}
public float[][] getM() {
return m;
}
public float get(int x, int y) {
return m[x][y];
}
public void setM(float[][] m) {
this.m = m;
}
public void set(int x, int y, float value) {
m[x][y] = value;
}
public FloatBuffer toFloatBuffer() {
return Util.createFlippedBufferBuffer(this);
}
}
Shader class function
public void setUniform(String uniformName, Matrix4f matrix){
int uniformLocation = getUniformLocation(uniformName);
glUniformMatrix4(uniformLocation, false, matrix.toFloatBuffer());
}
Util class
package com.game.main.util;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import com.game.main.graphics.Vertex;
import com.game.main.maths.Matrix4f;
public class Util {
public static FloatBuffer createFlippedBuffer(float[] data){
FloatBuffer buffer = ByteBuffer.allocateDirect(data.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
buffer.put(data).flip();
return buffer;
}
public static IntBuffer createFlippedBuffer(int[] data){
IntBuffer buffer = ByteBuffer.allocateDirect(data.length * 4).order(ByteOrder.nativeOrder()).asIntBuffer();
buffer.put(data).flip();
return buffer;
}
public static IntBuffer createIntBuffer(int size){
IntBuffer buffer = ByteBuffer.allocateDirect(size * 4).order(ByteOrder.nativeOrder()).asIntBuffer();
return buffer;
}
public static FloatBuffer createFloatBuffer(int size){
FloatBuffer buffer = ByteBuffer.allocateDirect(size * 4).order(ByteOrder.nativeOrder()).asFloatBuffer();
return buffer;
}
public static FloatBuffer createFlippedBufferBuffer(Matrix4f matrix){
FloatBuffer buffer = createFloatBuffer(4 * 4);
for(int x = 0; x < 4; x++)
for(int y = 0; y < 4; y++)
buffer.put(matrix.get(x, y));
buffer.flip();
return buffer;
}
public static FloatBuffer createFlippedBuffer(Vertex[] vertecies){
FloatBuffer buffer = createFloatBuffer(vertecies.length * Vertex.SIZE);
for(int i = 0; i < vertecies.length; i++){
buffer.put(vertecies[i].getPos().getX());
buffer.put(vertecies[i].getPos().getY());
buffer.put(vertecies[i].getPos().getZ());
buffer.put(vertecies[i].getTexCoord().getX());
buffer.put(vertecies[i].getTexCoord().getY());
}
buffer.flip();
return buffer;
}
}
Initializing the matrix uniform
Matrix4f matrix = new Matrix4f().initOrthographic(0, 800, 0, 600, -1.0f, 1.0f);
Shader.Object.setUniform("mat", matrix);
Vertex Shader
#version 330
layout(location = 0) in vec3 pos;
uniform mat4 mat;
void main(){
gl_Position = mat * vec4(pos, 1.0f);
}
Mesh class
package com.game.main.graphics;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import com.game.main.util.Util;
public class Mesh {
private int vbo, ibo, size;
public Mesh(Vertex[] vertecies, int[] indices){
vbo = glGenBuffers();
ibo = glGenBuffers();
size = indices.length;
createMesh(vertecies, indices);
}
private void createMesh(Vertex[] vertecies, int[] indices){
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, Util.createFlippedBuffer(vertecies), GL_STATIC_DRAW);
glEnableVertexAttribArray(0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(0, 3, GL_FLOAT, false, 5 * 4, 0);
glVertexAttribPointer(1, 2, GL_FLOAT, false, 5 * 4, 0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, Util.createFlippedBuffer(indices), GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
public void bind(){
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
}
public void unbind(){
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
}
public void drawWithBinding()
{
bind();
glDrawElements(GL_TRIANGLES, size, GL_UNSIGNED_INT, 0);
unbind();
}
public void draw(){
glDrawElements(GL_TRIANGLES, size, GL_UNSIGNED_INT, 0);
}
public void destroy(){
glDeleteBuffers(ibo);
glDeleteBuffers(vbo);
}
}
Vertex class
package com.game.main.graphics;
import com.game.main.maths.Vector2f;
import com.game.main.maths.Vector3f;
public class Vertex {
private Vector3f pos = new Vector3f();
private Vector2f texCoord = new Vector2f();
public static final int SIZE = 5;
public Vertex(Vector3f pos){
this(pos, new Vector2f());
}
public Vertex(Vector3f pos, Vector2f texCoord){
this.pos = pos;
this.texCoord = texCoord;
}
public Vector3f getPos() {
return pos;
}
public void setPos(Vector3f pos) {
this.pos = pos;
}
public Vector2f getTexCoord() {
return texCoord;
}
public void setTexCoord(Vector2f texCoord) {
this.texCoord = texCoord;
}
}
transpose code
public Matrix4f transpose() {
float m00 = get(0, 0);
float m01 = get(1, 0);
float m02 = get(2, 0);
float m03 = get(3, 0);
float m10 = get(0, 1);
float m11 = get(1, 1);
float m12 = get(2, 1);
float m13 = get(3, 1);
float m20 = get(0, 2);
float m21 = get(1, 2);
float m22 = get(2, 2);
float m23 = get(3, 2);
float m30 = get(0, 3);
float m31 = get(1, 3);
float m32 = get(2, 3);
float m33 = get(3, 3);
set(0, 0, m00);
set(0, 1, m01);
set(0, 2, m02);
set(0, 3, m03);
set(1, 0, m10);
set(1, 1, m11);
set(1, 2, m12);
set(1, 3, m13);
set(2, 0, m20);
set(2, 1, m21);
set(2, 2, m22);
set(2, 3, m23);
set(3, 0, m30);
set(3, 1, m31);
set(3, 2, m32);
set(3, 3, m33);
return this;
}
Game class
package com.game.main;
import static org.lwjgl.opengl.GL11.*;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import com.game.main.graphics.Mesh;
import com.game.main.graphics.Shader;
import com.game.main.graphics.Vertex;
import com.game.main.maths.Matrix4f;
import com.game.main.maths.Vector3f;
public class Game{
private boolean running = false;
private Mesh mesh;
public Game(int width, int height){
try {
Display.setDisplayMode(new DisplayMode(800, 600));
Display.setTitle("2D Game");
Display.create();
} catch (LWJGLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
Matrix4f matrix = new Matrix4f().initOrthographic(-1, 1, -1, 1, -1.0f, 1.0f);
Shader.Object.setUniform("mat", matrix);
Vertex[] vertecies = new Vertex[]{
new Vertex(new Vector3f(0.5f, 0.5f, 0)),
new Vertex(new Vector3f(0.5f, 0,0)),
new Vertex(new Vector3f(0, 0.5f,0)),
new Vertex(new Vector3f(0.5f, 0,0))
};
/*float[] vertecies = new float[]{
-10f, -10f * 9.0f / 16.0f, 1,
-10f, 10f * 9.0f / 16.0f,1,
0, 10f * 9.0f / 1.0f,1,
0, -10f * 9.0f / 6.0f,1
};*/
int[] ind = new int[]{
0, 1, 2,
2, 3, 0
};
mesh = new Mesh(vertecies, ind);
}
private void init(){
glEnable(GL_DEPTH_TEST);
}
public void start(){
if(running)
return;
running = true;
gameLoop();
}
public void stop(){
if(!running)
return;
running = false;
}
private void gameLoop(){
init();
long timer = System.currentTimeMillis();
long lastTime = System.nanoTime();
double delta = 0;
int frames = 0, updates = 0;
while(running){
long now = System.nanoTime();
delta += (now - lastTime) / (1000000000.0 / 60.0);
lastTime = now;
while(delta >= 1){
if(Display.isCloseRequested())
stop();
update();
updates++;
delta--;
}
render();
frames++;
if(System.currentTimeMillis() - timer >= 1000){
timer += 1000;
Display.setTitle("Frames: "+frames+" Updates: "+updates);
frames = updates = 0;
}
}
destroy();
}
private void update(){
}
private void render(){
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
Shader.Object.bind();
mesh.drawWithBinding();
Shader.Object.unbind();
Display.update();
}
private void destroy(){
mesh.destroy();
Display.destroy();
}
}
It seems the problem is in setting the matrix uniform. OpenGL is a state machine and doesn't remember uniform values. More importantly, glUniformMatrix4 must be called while the shader is in use.
Currently, the matrix is set only at initialization (in the Game constructor), when it needs to be in render().
Shader.Object.bind(); //glUseProgram(programHandle)
Shader.Object.setUniform(...) //glUniformMatrix4
mesh.drawWithBinding(); //glDrawElements
Shader.Object.unbind(); //glUseProgram(0), but not necessary unless you want fixed function rendering
With mat not being set it'd probably be a zero matrix that was stopping things from drawing.
In addition to the above, there are two other potential issues:
Take a look in the bottom left of your screen and see if that pixel is on. I suspect everything's working, just really small. If the following works:
gl_Position = vec4(pos, 1.0f);
which is essentially a multiply by the identity matrix and nearly ortho(-1,1,-1,1,-1,1) (except the ortho call gives mat[2][2]=-1), then I'm guessing your mesh is around the size of a unit cube.
Introducing mat = initOrthographic(0, 800, 0, 600, -1.0f, 1.0f) looks like it's set up for a scene where vertices are given in pixels. The origin will be bottom left and if the mesh fits in a unit cube will draw to at most one pixel.
See #Jerem's answer about row/column major matrices.
It looks like your matrix is row major. You need to transpose it before sending it to the shader.
Note that there is a boolean parameter called transpose in glUniformMatrix4f but you can't use it in opengl es - the doc says it has to be false - you have to transpose it yourself.

Java - Use the vertex buffer object (vbo) LWJGL

OK, I have the following code, i am trying to convert this code into the vertex buffer object render way. but it doesn't seem to work..
So here is the code:
package com.game.base;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.util.glu.GLU.gluPerspective;
import java.nio.FloatBuffer;
import java.util.Random;
import org.lwjgl.BufferUtils;
import org.lwjgl.input.Keyboard;
import com.game.display.graphics.Entity;
public class Game {
float speed = 0.0f;
float speedChange = 0.000001f;
int stars = 20000;
int starsDistance = 4000;
float starSize = 2.0f;
int vertices = 3;
int vertex_size = 3; // X, Y, Z,
int color_size = 3; // R, G, B,
Entity[] points;
Random random = new Random();
public Game() {
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective((float) 60, (float) (MainClass.WIDTH / MainClass.HEIGHT), 0.001f, 100);
glMatrixMode(GL_MODELVIEW);
glEnable(GL_DEPTH_TEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
points = new Entity[stars];
for (int i = 0; i < points.length; i++) {
points[i] = new Entity((random.nextFloat() - 0.5f) * 100f, (random.nextFloat() - 0.5f) * 100f, random.nextInt(starsDistance) - starsDistance);
}
}
public void render() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glTranslatef(0, 0, speed);
glPointSize(2.0f);
glBegin(GL_POINTS);
for (Entity p : points) {
glVertex3f(p.x, p.y, p.z);
}
glEnd();
}
public void update() {
}
public void dispose() {
}
public void input() {
if (Keyboard.isKeyDown(Keyboard.KEY_UP)) {
speed += speed < 0 ? speedChange * 5 : speedChange;
}
if (Keyboard.isKeyDown(Keyboard.KEY_DOWN)) {
speed -= speed > 0 ? speedChange * 5 : speedChange;
}
if (Keyboard.isKeyDown(Keyboard.KEY_SPACE)) {
speed = 0f;
}
if (Keyboard.isKeyDown(Keyboard.KEY_C)) {
speed = 0;
glLoadIdentity();
}
if (Keyboard.isKeyDown(Keyboard.KEY_ESCAPE)) {
MainClass.stop();
}
}
}
Any ideas how to convert this into VBO (Vertex Buffer Object) ?
Can someone convert this into VBO and show me how?
Thanks in Advance,
And I accept fast :)

Categories