initial engine commit
This commit is contained in:
13
core/build.gradle
Normal file
13
core/build.gradle
Normal file
@@ -0,0 +1,13 @@
|
||||
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
|
||||
eclipse.project.name = appName + '-core'
|
||||
|
||||
dependencies {
|
||||
api "com.badlogicgames.gdx-controllers:gdx-controllers-core:$gdxControllersVersion"
|
||||
api "com.badlogicgames.gdx:gdx-bullet:$gdxVersion"
|
||||
api "com.badlogicgames.gdx:gdx-freetype:$gdxVersion"
|
||||
api "com.badlogicgames.gdx:gdx:$gdxVersion"
|
||||
|
||||
if(enableGraalNative == 'true') {
|
||||
implementation "io.github.berstanio:gdx-svmhelper-annotations:$graalHelperVersion"
|
||||
}
|
||||
}
|
||||
200
core/src/main/java/wtf/beatrice/retrorender/GameScreen.java
Normal file
200
core/src/main/java/wtf/beatrice/retrorender/GameScreen.java
Normal file
@@ -0,0 +1,200 @@
|
||||
package wtf.beatrice.retrorender;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.Screen;
|
||||
import com.badlogic.gdx.graphics.GL20;
|
||||
import com.badlogic.gdx.graphics.PerspectiveCamera;
|
||||
import com.badlogic.gdx.graphics.Texture;
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||
import com.badlogic.gdx.graphics.g2d.TextureRegion;
|
||||
import com.badlogic.gdx.graphics.g3d.Environment;
|
||||
import com.badlogic.gdx.graphics.g3d.ModelBatch;
|
||||
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
|
||||
import com.badlogic.gdx.graphics.g3d.environment.DirectionalShadowLight;
|
||||
import com.badlogic.gdx.graphics.g3d.utils.DepthShaderProvider;
|
||||
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
|
||||
import wtf.beatrice.retrorender.engine.DebugHud;
|
||||
import wtf.beatrice.retrorender.engine.FpsCameraController;
|
||||
import wtf.beatrice.retrorender.engine.World3D;
|
||||
|
||||
public class GameScreen implements Screen {
|
||||
|
||||
private final Main game;
|
||||
|
||||
private PerspectiveCamera camera;
|
||||
private ModelBatch modelBatch;
|
||||
private Environment environment;
|
||||
|
||||
private FpsCameraController cameraController;
|
||||
private World3D world;
|
||||
private DebugHud hud;
|
||||
|
||||
// Shadow Mapping
|
||||
private ModelBatch shadowBatch;
|
||||
private DirectionalShadowLight shadowLight;
|
||||
|
||||
// Retro rendering
|
||||
private FrameBuffer frameBuffer;
|
||||
private TextureRegion frameRegion;
|
||||
private SpriteBatch screenBatch;
|
||||
|
||||
private static final int RETRO_WIDTH = 320;
|
||||
private static final int RETRO_HEIGHT = 180;
|
||||
|
||||
public GameScreen(Main game) {
|
||||
this.game = game;
|
||||
}
|
||||
|
||||
private void initCamera() {
|
||||
camera = new PerspectiveCamera(
|
||||
67f,
|
||||
Gdx.graphics.getWidth(),
|
||||
Gdx.graphics.getHeight()
|
||||
);
|
||||
|
||||
// near/far + initial direction handled in controller constructor
|
||||
cameraController = new FpsCameraController(camera);
|
||||
}
|
||||
|
||||
private void initEnvironment() {
|
||||
environment = new Environment();
|
||||
|
||||
// ambient color
|
||||
environment.set(
|
||||
new ColorAttribute(ColorAttribute.AmbientLight,
|
||||
0.4f, 0.4f, 0.5f, 1f)
|
||||
);
|
||||
|
||||
// shadow-casting directional light
|
||||
shadowLight = new DirectionalShadowLight(
|
||||
1024, 1024, // shadow map resolution
|
||||
60f, 60f, // viewport size (render distance)
|
||||
1f, 50f // near/far for the light camera
|
||||
);
|
||||
shadowLight.set(
|
||||
1.0f, 0.85f, 0.9f, // casting light color
|
||||
-0.7f, -1.0f, -0.3f // direction
|
||||
);
|
||||
|
||||
environment.add(shadowLight);
|
||||
environment.shadowMap = shadowLight;
|
||||
|
||||
shadowBatch = new ModelBatch(new DepthShaderProvider());
|
||||
}
|
||||
|
||||
private void initWorld() {
|
||||
world = new World3D();
|
||||
}
|
||||
|
||||
private void initHud() {
|
||||
hud = new DebugHud();
|
||||
}
|
||||
|
||||
// --- Screen methods ---
|
||||
private void initRetroBuffer() {
|
||||
// RGB + depth
|
||||
frameBuffer = new FrameBuffer(
|
||||
com.badlogic.gdx.graphics.Pixmap.Format.RGBA8888,
|
||||
RETRO_WIDTH,
|
||||
RETRO_HEIGHT,
|
||||
true
|
||||
);
|
||||
|
||||
Texture fbTex = frameBuffer.getColorBufferTexture();
|
||||
frameRegion = new TextureRegion(fbTex);
|
||||
fbTex.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest);
|
||||
// FBO textures are Y-flipped in libGDX, so flip once here
|
||||
frameRegion.flip(false, true);
|
||||
|
||||
screenBatch = new com.badlogic.gdx.graphics.g2d.SpriteBatch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void show() {
|
||||
modelBatch = new ModelBatch();
|
||||
initCamera();
|
||||
initEnvironment();
|
||||
initWorld();
|
||||
initHud();
|
||||
initRetroBuffer();
|
||||
|
||||
cameraController.onShow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(float delta) {
|
||||
|
||||
cameraController.update(delta, world);
|
||||
world.update(delta);
|
||||
|
||||
// --- shadow pass: render depth from light's point of view
|
||||
// point to center the shadow camera on;
|
||||
// can be adjusted this later (e.g. follow player).
|
||||
shadowLight.begin(new com.badlogic.gdx.math.Vector3(0f, 0f, 0f), camera.direction);
|
||||
|
||||
shadowBatch.begin(shadowLight.getCamera());
|
||||
world.render(shadowBatch, environment); // depth-only pass
|
||||
shadowBatch.end();
|
||||
|
||||
shadowLight.end();
|
||||
|
||||
// --- render scene into low res framebuffer
|
||||
frameBuffer.begin();
|
||||
Gdx.gl.glViewport(0, 0, RETRO_WIDTH, RETRO_HEIGHT);
|
||||
Gdx.gl.glClearColor(0.5f, 0.6f, 1.0f, 1f);
|
||||
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT | GL20.GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
camera.update();
|
||||
modelBatch.begin(camera);
|
||||
world.render(modelBatch, environment); // DefaultShader uses shadowLight + env.shadowMap
|
||||
modelBatch.end();
|
||||
|
||||
// --- HUD
|
||||
hud.render(RETRO_WIDTH, RETRO_HEIGHT, camera);
|
||||
|
||||
frameBuffer.end();
|
||||
|
||||
// -- scale framebuffer to screen
|
||||
int screenW = Gdx.graphics.getWidth();
|
||||
int screenH = Gdx.graphics.getHeight();
|
||||
int fbWidth = Gdx.graphics.getBackBufferWidth();
|
||||
int fbHeight = Gdx.graphics.getBackBufferHeight();
|
||||
|
||||
Gdx.gl.glViewport(0, 0, fbWidth, fbHeight);
|
||||
Gdx.gl.glClearColor(0f, 0f, 0f, 1f);
|
||||
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
|
||||
|
||||
screenBatch.begin();
|
||||
// draw retro texture stretched to window size
|
||||
screenBatch.draw(frameRegion, 0, 0, screenW, screenH);
|
||||
screenBatch.end();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resize(int width, int height) {
|
||||
camera.viewportWidth = width;
|
||||
camera.viewportHeight = height;
|
||||
camera.update();
|
||||
|
||||
cameraController.resize(width, height);
|
||||
}
|
||||
|
||||
@Override public void pause() { }
|
||||
@Override public void resume() { }
|
||||
|
||||
@Override
|
||||
public void hide() {
|
||||
cameraController.onHide();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
cameraController.dispose();
|
||||
world.dispose();
|
||||
modelBatch.dispose();
|
||||
hud.dispose();
|
||||
|
||||
if (shadowBatch != null) shadowBatch.dispose();
|
||||
if (shadowLight != null) shadowLight.dispose();
|
||||
}
|
||||
}
|
||||
24
core/src/main/java/wtf/beatrice/retrorender/Main.java
Normal file
24
core/src/main/java/wtf/beatrice/retrorender/Main.java
Normal file
@@ -0,0 +1,24 @@
|
||||
package wtf.beatrice.retrorender;
|
||||
|
||||
import com.badlogic.gdx.ApplicationAdapter;
|
||||
import com.badlogic.gdx.Game;
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.graphics.*;
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||
import com.badlogic.gdx.graphics.g3d.*;
|
||||
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
|
||||
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
|
||||
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
|
||||
import com.badlogic.gdx.math.Vector3;
|
||||
import com.badlogic.gdx.utils.ScreenUtils;
|
||||
|
||||
/** {@link com.badlogic.gdx.ApplicationListener} implementation shared by all platforms. */
|
||||
public class Main extends Game
|
||||
{
|
||||
|
||||
@Override
|
||||
public void create()
|
||||
{
|
||||
setScreen(new GameScreen(this));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package wtf.beatrice.retrorender.engine;
|
||||
|
||||
import com.badlogic.gdx.graphics.Color;
|
||||
import com.badlogic.gdx.graphics.PerspectiveCamera;
|
||||
import com.badlogic.gdx.graphics.Texture;
|
||||
import com.badlogic.gdx.graphics.g2d.BitmapFont;
|
||||
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.graphics.g2d.freetype.FreeTypeFontGenerator;
|
||||
|
||||
public class DebugHud {
|
||||
|
||||
private final SpriteBatch batch;
|
||||
private final BitmapFont font;
|
||||
|
||||
public DebugHud() {
|
||||
batch = new SpriteBatch();
|
||||
|
||||
FreeTypeFontGenerator generator =
|
||||
new FreeTypeFontGenerator(Gdx.files.internal("fonts/red-hat-mono.ttf"));
|
||||
FreeTypeFontGenerator.FreeTypeFontParameter param =
|
||||
new FreeTypeFontGenerator.FreeTypeFontParameter();
|
||||
|
||||
param.size = 10; // size in defined scaled framebuffer
|
||||
param.color = Color.WHITE;
|
||||
|
||||
// reduce aliasing and smoothing
|
||||
param.mono = true; // 1-bit glyphs, no grayscale AA
|
||||
param.hinting = FreeTypeFontGenerator.Hinting.None; // no font hinting
|
||||
param.borderWidth = 0.2f;
|
||||
param.borderColor = Color.WHITE;
|
||||
param.shadowOffsetX = 0;
|
||||
param.shadowOffsetY = 0;
|
||||
|
||||
// sample font texture as pixels
|
||||
param.minFilter = Texture.TextureFilter.Nearest;
|
||||
param.magFilter = Texture.TextureFilter.Nearest;
|
||||
|
||||
font = generator.generateFont(param);
|
||||
generator.dispose();
|
||||
}
|
||||
|
||||
// render HUD in virtual resolution
|
||||
public void render(int width, int height, PerspectiveCamera camera) {
|
||||
// set up orthographic projection matching target size
|
||||
batch.getProjectionMatrix().setToOrtho2D(0, 0, width, height);
|
||||
|
||||
batch.begin();
|
||||
|
||||
String fpsText = Gdx.graphics.getFramesPerSecond() + " FPS";
|
||||
font.draw(batch, fpsText, 5f, height - 5f);
|
||||
|
||||
if (camera != null) {
|
||||
float x = camera.position.x;
|
||||
float y = camera.position.y;
|
||||
float z = camera.position.z;
|
||||
|
||||
String posText = String.format("X: %.2f Y: %.2f Z: %.2f", x, y, z);
|
||||
font.draw(batch, posText, 5f, height - 15f);
|
||||
}
|
||||
|
||||
batch.end();
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
batch.dispose();
|
||||
font.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
package wtf.beatrice.retrorender.engine;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.Input;
|
||||
import com.badlogic.gdx.graphics.Cursor;
|
||||
import com.badlogic.gdx.graphics.Pixmap;
|
||||
import com.badlogic.gdx.graphics.PerspectiveCamera;
|
||||
import com.badlogic.gdx.math.Vector3;
|
||||
|
||||
public class FpsCameraController {
|
||||
|
||||
private final PerspectiveCamera camera;
|
||||
|
||||
// movement + look
|
||||
private float moveSpeed = 5f; // units / second
|
||||
private float mouseSensitivity = 0.15f;
|
||||
private float yaw = 0f;
|
||||
private float pitch = 0f;
|
||||
|
||||
private float eyeHeight = 1.6f;
|
||||
private float velocityY = 0f;
|
||||
private float gravity = -50f;
|
||||
private float jumpSpeed = 12f;
|
||||
private boolean grounded = false;
|
||||
|
||||
private final Vector3 tmpForward = new Vector3();
|
||||
private final Vector3 tmpRight = new Vector3();
|
||||
|
||||
// mouse capture / centering
|
||||
private boolean mouseCaptured = true;
|
||||
private int centerX;
|
||||
private int centerY;
|
||||
private int captureWarmupFrames = 0;
|
||||
private static final int WARMUP_FRAMES = 6; // frames to ignore after capture
|
||||
|
||||
private Cursor invisibleCursor;
|
||||
|
||||
public FpsCameraController(PerspectiveCamera camera) {
|
||||
this.camera = camera;
|
||||
|
||||
// default camera setup
|
||||
camera.position.set(0f, eyeHeight, 6f);
|
||||
camera.near = 0.1f;
|
||||
camera.far = 100f;
|
||||
|
||||
yaw = 180f;
|
||||
pitch = -15f;
|
||||
updateCameraDirection();
|
||||
|
||||
centerX = Gdx.graphics.getWidth() / 2;
|
||||
centerY = Gdx.graphics.getHeight() / 2;
|
||||
|
||||
grounded = true;
|
||||
}
|
||||
|
||||
/** Call from Screen.show() */
|
||||
public void onShow() {
|
||||
mouseCaptured = true;
|
||||
centerX = Gdx.graphics.getWidth() / 2;
|
||||
centerY = Gdx.graphics.getHeight() / 2;
|
||||
Gdx.input.setCursorPosition(centerX, centerY);
|
||||
|
||||
ensureInvisibleCursor();
|
||||
Gdx.graphics.setCursor(invisibleCursor);
|
||||
|
||||
captureWarmupFrames = WARMUP_FRAMES;
|
||||
}
|
||||
|
||||
/** Call from Screen.hide() */
|
||||
public void onHide() {
|
||||
mouseCaptured = false;
|
||||
Gdx.graphics.setSystemCursor(Cursor.SystemCursor.Arrow);
|
||||
}
|
||||
|
||||
/** Call from Screen.resize() */
|
||||
public void resize(int width, int height) {
|
||||
centerX = width / 2;
|
||||
centerY = height / 2;
|
||||
if (mouseCaptured) {
|
||||
Gdx.input.setCursorPosition(centerX, centerY);
|
||||
}
|
||||
// after resize, skip frames to avoid mouse skew
|
||||
captureWarmupFrames = WARMUP_FRAMES;
|
||||
}
|
||||
|
||||
/** Update each frame with delta time */
|
||||
public void update(float delta, World3D world) {
|
||||
// --- ESC: release mouse, stop movement / camera
|
||||
if (Gdx.input.isKeyJustPressed(Input.Keys.ESCAPE)) {
|
||||
mouseCaptured = false;
|
||||
Gdx.graphics.setSystemCursor(Cursor.SystemCursor.Arrow);
|
||||
}
|
||||
|
||||
// only capture inputs if focused/captured
|
||||
if (!mouseCaptured) {
|
||||
if (Gdx.input.justTouched()) { // if clicked
|
||||
mouseCaptured = true;
|
||||
centerX = Gdx.graphics.getWidth() / 2;
|
||||
centerY = Gdx.graphics.getHeight() / 2;
|
||||
Gdx.input.setCursorPosition(centerX, centerY);
|
||||
ensureInvisibleCursor();
|
||||
Gdx.graphics.setCursor(invisibleCursor);
|
||||
captureWarmupFrames = WARMUP_FRAMES;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// skip a few frames after capture to avoid mouse skew
|
||||
// teleport cursor to center
|
||||
if (captureWarmupFrames > 0) {
|
||||
captureWarmupFrames--;
|
||||
Gdx.input.setCursorPosition(centerX, centerY);
|
||||
return;
|
||||
}
|
||||
|
||||
// --- mouse look: keep cursor centred
|
||||
int mouseX = Gdx.input.getX();
|
||||
int mouseY = Gdx.input.getY();
|
||||
|
||||
int dx = mouseX - centerX;
|
||||
int dy = mouseY - centerY;
|
||||
|
||||
// recenter for next frame
|
||||
Gdx.input.setCursorPosition(centerX, centerY);
|
||||
|
||||
float deltaX = -dx * mouseSensitivity;
|
||||
float deltaY = -dy * mouseSensitivity;
|
||||
|
||||
yaw += deltaX;
|
||||
pitch += deltaY;
|
||||
|
||||
// clamp pitch to avoid flipping
|
||||
pitch = Math.max(-89f, Math.min(89f, pitch));
|
||||
|
||||
// keep yaw in [0, 360)
|
||||
if (yaw >= 360f) yaw -= 360f;
|
||||
if (yaw < 0f) yaw += 360f;
|
||||
|
||||
updateCameraDirection();
|
||||
|
||||
// --- keyboard movement
|
||||
// forward vector (flattened on XZ)
|
||||
tmpForward.set(camera.direction.x, 0f, camera.direction.z).nor();
|
||||
// right vector (perpendicular)
|
||||
tmpRight.set(tmpForward.z, 0f, -tmpForward.x).nor();
|
||||
|
||||
float moveAmount = moveSpeed * delta;
|
||||
float moveX = 0f;
|
||||
float moveZ = 0f;
|
||||
|
||||
if (Gdx.input.isKeyPressed(Input.Keys.W)) {
|
||||
moveX += tmpForward.x * moveAmount;
|
||||
moveZ += tmpForward.z * moveAmount;
|
||||
}
|
||||
if (Gdx.input.isKeyPressed(Input.Keys.S)) {
|
||||
moveX -= tmpForward.x * moveAmount;
|
||||
moveZ -= tmpForward.z * moveAmount;
|
||||
}
|
||||
if (Gdx.input.isKeyPressed(Input.Keys.A)) {
|
||||
moveX += tmpRight.x * moveAmount;
|
||||
moveZ += tmpRight.z * moveAmount;
|
||||
}
|
||||
if (Gdx.input.isKeyPressed(Input.Keys.D)) {
|
||||
moveX -= tmpRight.x * moveAmount;
|
||||
moveZ -= tmpRight.z * moveAmount;
|
||||
}
|
||||
|
||||
float playerRadius = 0.4f;
|
||||
// --- try to move in X/Z with collision
|
||||
float newX = camera.position.x + moveX;
|
||||
float newZ = camera.position.z + moveZ;
|
||||
float curY = camera.position.y;
|
||||
|
||||
if (!world.collidesAt(newX, curY, camera.position.z, playerRadius, eyeHeight)) {
|
||||
camera.position.x = newX;
|
||||
}
|
||||
|
||||
if (!world.collidesAt(camera.position.x, curY, newZ, playerRadius, eyeHeight)) {
|
||||
camera.position.z = newZ;
|
||||
}
|
||||
boolean jumpHeld = Gdx.input.isKeyPressed(Input.Keys.SPACE);
|
||||
|
||||
// apply gravity
|
||||
velocityY += gravity * delta;
|
||||
float proposedY = camera.position.y + velocityY * delta;
|
||||
|
||||
// feet position if we accept this Y
|
||||
float feetY = proposedY - eyeHeight;
|
||||
|
||||
// find ground at current X/Z
|
||||
float groundY = world.getGroundHeightAt(camera.position.x, camera.position.z, 0f);
|
||||
|
||||
if (feetY <= groundY) {
|
||||
// ground or other surface below feet
|
||||
grounded = true;
|
||||
velocityY = 0f;
|
||||
camera.position.y = groundY + eyeHeight;
|
||||
} else {
|
||||
grounded = false;
|
||||
camera.position.y = proposedY;
|
||||
}
|
||||
|
||||
// start jump again if holding down key
|
||||
if (grounded && jumpHeld) {
|
||||
velocityY = jumpSpeed;
|
||||
grounded = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateCameraDirection() {
|
||||
// build direction vector from yaw/pitch
|
||||
float yawRad = (float) Math.toRadians(yaw);
|
||||
float pitchRad = (float) Math.toRadians(pitch);
|
||||
|
||||
float x = (float) (Math.cos(pitchRad) * Math.sin(yawRad));
|
||||
float y = (float) Math.sin(pitchRad);
|
||||
float z = (float) (Math.cos(pitchRad) * Math.cos(yawRad));
|
||||
|
||||
camera.direction.set(x, y, z).nor();
|
||||
}
|
||||
|
||||
private void ensureInvisibleCursor() {
|
||||
if (invisibleCursor == null) {
|
||||
Pixmap pm = new Pixmap(16, 16, Pixmap.Format.RGBA8888);
|
||||
pm.setColor(0, 0, 0, 0);
|
||||
pm.fill();
|
||||
invisibleCursor = Gdx.graphics.newCursor(pm, 0, 0);
|
||||
pm.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (invisibleCursor != null) {
|
||||
invisibleCursor.dispose();
|
||||
invisibleCursor = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setMoveSpeed(float moveSpeed) { this.moveSpeed = moveSpeed; }
|
||||
public void setMouseSensitivity(float mouseSensitivity) { this.mouseSensitivity = mouseSensitivity; }
|
||||
}
|
||||
196
core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java
Normal file
196
core/src/main/java/wtf/beatrice/retrorender/engine/World3D.java
Normal file
@@ -0,0 +1,196 @@
|
||||
package wtf.beatrice.retrorender.engine;
|
||||
|
||||
import com.badlogic.gdx.Gdx;
|
||||
import com.badlogic.gdx.graphics.GL20;
|
||||
import com.badlogic.gdx.graphics.Texture;
|
||||
import com.badlogic.gdx.graphics.VertexAttributes;
|
||||
import com.badlogic.gdx.graphics.g3d.*;
|
||||
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
|
||||
import com.badlogic.gdx.graphics.g3d.attributes.TextureAttribute;
|
||||
import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder;
|
||||
import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
|
||||
import com.badlogic.gdx.math.Vector3;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class World3D {
|
||||
|
||||
// textures
|
||||
private final Texture groundTexture;
|
||||
|
||||
// models (base mesh)
|
||||
private final Model groundModel;
|
||||
private final Model cubeModel;
|
||||
|
||||
// instances
|
||||
private final ModelInstance groundInstance;
|
||||
private final List<ModelInstance> cubeInstances = new ArrayList<>();
|
||||
|
||||
public World3D() {
|
||||
ModelBuilder modelBuilder = new ModelBuilder();
|
||||
|
||||
// --- load floor texture
|
||||
groundTexture = new Texture(Gdx.files.internal("textures/paving.png"));
|
||||
groundTexture.setFilter(Texture.TextureFilter.Nearest, Texture.TextureFilter.Nearest);
|
||||
groundTexture.setWrap(Texture.TextureWrap.Repeat, Texture.TextureWrap.Repeat);
|
||||
|
||||
// --- create ground material
|
||||
Material groundMat = new Material(
|
||||
ColorAttribute.createDiffuse(1f, 1f, 1f, 1f),
|
||||
TextureAttribute.createDiffuse(groundTexture)
|
||||
);
|
||||
|
||||
// plane size (halfSize in X/Z)
|
||||
float halfSize = 20f;
|
||||
|
||||
// times the texture repeats across the whole plane
|
||||
float tileScale = 8f;
|
||||
|
||||
modelBuilder.begin();
|
||||
|
||||
MeshPartBuilder mpb = modelBuilder.part(
|
||||
"ground",
|
||||
GL20.GL_TRIANGLES,
|
||||
VertexAttributes.Usage.Position
|
||||
| VertexAttributes.Usage.Normal
|
||||
| VertexAttributes.Usage.TextureCoordinates, // 👈 important
|
||||
groundMat
|
||||
);
|
||||
|
||||
// set UV range so texture repeats tileScale times
|
||||
mpb.setUVRange(0f, 0f, tileScale, tileScale);
|
||||
|
||||
// build the quad
|
||||
mpb.rect(
|
||||
-halfSize, 0f, halfSize, // top-left
|
||||
halfSize, 0f, halfSize, // top-right
|
||||
halfSize, 0f, -halfSize, // bottom-right
|
||||
-halfSize, 0f, -halfSize, // bottom-left
|
||||
0f, 1f, 0f // normal up
|
||||
);
|
||||
|
||||
groundModel = modelBuilder.end();
|
||||
groundInstance = new ModelInstance(groundModel);
|
||||
|
||||
// --- create reusable cube model
|
||||
cubeModel = modelBuilder.createBox(
|
||||
2f, 2f, 2f,
|
||||
new Material(ColorAttribute.createDiffuse(1f, 1f, 1f, 1f)),
|
||||
VertexAttributes.Usage.Position | VertexAttributes.Usage.Normal
|
||||
);
|
||||
|
||||
// --- place random cubes in the world
|
||||
// center
|
||||
cubeInstances.add(new ModelInstance(cubeModel)); // at origin
|
||||
|
||||
// four around
|
||||
cubeInstances.add(new ModelInstance(cubeModel));
|
||||
cubeInstances.get(1).transform.setToTranslation(8f, 1f, 8f);
|
||||
|
||||
cubeInstances.add(new ModelInstance(cubeModel));
|
||||
cubeInstances.get(2).transform.setToTranslation(-8f, 1f, 8f);
|
||||
|
||||
cubeInstances.add(new ModelInstance(cubeModel));
|
||||
cubeInstances.get(3).transform.setToTranslation(8f, 1f, -8f);
|
||||
|
||||
cubeInstances.add(new ModelInstance(cubeModel));
|
||||
cubeInstances.get(4).transform.setToTranslation(-8f, 1f, -8f);
|
||||
|
||||
// one tall pillar in front of you
|
||||
ModelInstance pillar = new ModelInstance(cubeModel);
|
||||
pillar.transform
|
||||
.setToScaling(1f, 3f, 1f)
|
||||
.translate(0f, 3f, -10f);
|
||||
cubeInstances.add(pillar);
|
||||
}
|
||||
|
||||
public void update(float delta) {
|
||||
// rotate cubes around y axis
|
||||
for (ModelInstance instance : cubeInstances) {
|
||||
instance.transform.rotate(Vector3.Y, 20f * delta);
|
||||
}
|
||||
}
|
||||
|
||||
public void render(ModelBatch batch, Environment environment) {
|
||||
// draw ground
|
||||
batch.render(groundInstance, environment);
|
||||
// draw cubes
|
||||
for (ModelInstance instance : cubeInstances) {
|
||||
batch.render(instance, environment);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the ground height (y) at a given XZ position.
|
||||
* Starts from baseHeight (e.g. 0) and checks if you're standing on top of any cube.
|
||||
*/
|
||||
public float getGroundHeightAt(float x, float z, float baseHeight) {
|
||||
float maxY = baseHeight; // ground plane at y = 0
|
||||
|
||||
// a cube is 2 units tall, from y = 0 to y = 2
|
||||
float cubeHalfSize = 1f;
|
||||
float cubeTopY = 2f;
|
||||
|
||||
Vector3 tmp = new Vector3();
|
||||
for (ModelInstance cube : cubeInstances) {
|
||||
cube.transform.getTranslation(tmp);
|
||||
|
||||
float minX = tmp.x - cubeHalfSize;
|
||||
float maxX = tmp.x + cubeHalfSize;
|
||||
float minZ = tmp.z - cubeHalfSize;
|
||||
float maxZ = tmp.z + cubeHalfSize;
|
||||
|
||||
if (x >= minX && x <= maxX && z >= minZ && z <= maxZ) {
|
||||
if (cubeTopY > maxY) {
|
||||
maxY = cubeTopY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return maxY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple horizontal collision check between a player and cube sides.
|
||||
* Player is approximated as a capsule: radius in XZ, height = eyeHeight.
|
||||
*/
|
||||
public boolean collidesAt(float x, float y, float z, float radius, float eyeHeight) {
|
||||
float feetY = y - eyeHeight;
|
||||
float headY = y;
|
||||
|
||||
float cubeHalfSize = 1f;
|
||||
float cubeBottomY = 0f;
|
||||
float cubeTopY = 2f;
|
||||
|
||||
Vector3 tmp = new Vector3();
|
||||
for (ModelInstance cube : cubeInstances) {
|
||||
cube.transform.getTranslation(tmp);
|
||||
|
||||
float minX = tmp.x - cubeHalfSize;
|
||||
float maxX = tmp.x + cubeHalfSize;
|
||||
float minZ = tmp.z - cubeHalfSize;
|
||||
float maxZ = tmp.z + cubeHalfSize;
|
||||
|
||||
// no vertical overlap -> ignore
|
||||
if (headY <= cubeBottomY || feetY >= cubeTopY) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// circle (player) vs AABB (cube) in XZ
|
||||
boolean overlapX = (x + radius > minX) && (x - radius < maxX);
|
||||
boolean overlapZ = (z + radius > minZ) && (z - radius < maxZ);
|
||||
|
||||
if (overlapX && overlapZ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
groundModel.dispose();
|
||||
cubeModel.dispose();
|
||||
groundTexture.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user