diff --git a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java index 2c24ef8..ff984df 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java @@ -127,7 +127,6 @@ public final class ModelRender { FragColor = vec4(vDebugPos * 0.5 + 0.5, 0.0, 1.0); return; } - vec4 tex = texture(uTexture, vTexCoord); vec4 finalColor = tex * uColor; if (uBlendMode == 1) finalColor.rgb = tex.rgb + uColor.rgb; @@ -347,24 +346,27 @@ public final class ModelRender { } private static void renderMesh(Mesh2D mesh, Matrix3f modelMatrix) { - // 使用默认 shader(保证 shader 已被 use) + if (!mesh.isVisible()) return; + + // 使用默认 shader defaultProgram.use(); // 如果 mesh 已经被烘焙到世界坐标,则传 identity 矩阵给 shader(防止重复变换) Matrix3f matToUse = mesh.isBakedToWorld() ? new Matrix3f().identity() : modelMatrix; - // 确保 shader 中的矩阵 uniform 已更新(再次设置以防遗漏) - setUniformMatrix3(defaultProgram, "uModelMatrix", matToUse); - setUniformMatrix3(defaultProgram, "uModel", matToUse); - - // 调用 Mesh2D 的 draw 重载(传 program id 与实际矩阵) - try { - mesh.draw(defaultProgram.programId, matToUse); - } catch (AbstractMethodError | NoSuchMethodError e) { - // 回退:仍然兼容旧的无参 draw(在这种情况下 shader 的 uModelMatrix 已经被设置) - mesh.draw(); + // 设置纹理相关的uniform + if (mesh.getTexture() != null) { + mesh.getTexture().bind(0); // 绑定到纹理单元0 + setUniformIntInternal(defaultProgram, "uTexture", 0); + } else { + // 使用默认白色纹理 + GL11.glBindTexture(GL11.GL_TEXTURE_2D, defaultTextureId); + setUniformIntInternal(defaultProgram, "uTexture", 0); } + // 调用 Mesh2D 的 draw 方法,传入当前使用的着色器程序和变换矩阵 + mesh.draw(defaultProgram.programId, matToUse); + checkGLError("renderMesh"); } diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java index b3ba248..274a8ce 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/ModelPart.java @@ -30,6 +30,7 @@ public class ModelPart { private final Vector2f scale; private final Matrix3f localTransform; private final Matrix3f worldTransform; + private final Vector2f pivot = new Vector2f(0, 0); // ==================== 渲染属性 ==================== private boolean visible; @@ -42,6 +43,7 @@ public class ModelPart { // ==================== 状态标记 ==================== private boolean transformDirty; private boolean boundsDirty; + private boolean pivotInitialized; // ==================== 构造器 ==================== @@ -191,16 +193,22 @@ public class ModelPart { // 更新局部矩阵 private void updateLocalTransform() { - float cos = (float)Math.cos(rotation); - float sin = (float)Math.sin(rotation); + float cos = (float) Math.cos(rotation); + float sin = (float) Math.sin(rotation); - float m00 = cos * scale.x; - float m01 = -sin * scale.y; - float m10 = sin * scale.x; - float m11 = cos * scale.y; + float sx = scale.x; + float sy = scale.y; - float m02 = position.x; // 平移直接用 position - float m12 = position.y; + // 旋转 + 缩放矩阵 + float m00 = cos * sx; + float m01 = -sin * sy; + float m10 = sin * sx; + float m11 = cos * sy; + + // 平移部分考虑 pivot + // pivot 影响旋转中心,而 position 是最终放置位置 + float m02 = position.x - (m00 * pivot.x + m01 * pivot.y) + pivot.x; + float m12 = position.y - (m10 * pivot.x + m11 * pivot.y) + pivot.y; localTransform.set( m00, m01, m02, @@ -360,6 +368,44 @@ public class ModelPart { boundsDirty = true; } + /** + * 设置中心点 + */ + public void setPivot(float x, float y) { + if (!pivotInitialized) { + // 确保第一次设置 pivot 的时候,必须是 (0,0) 因为这个为非0,0时后面如果想要热变换就会出问题 + if (x != 0 || y != 0) { + System.out.println("The first time you set the pivot, it must be (0,0), which is automatically adjusted to (0,0)."); + x = 0; + y = 0; + } + pivotInitialized = true; + } + float dx = x - pivot.x; + float dy = y - pivot.y; + + pivot.set(x, y); + + for (Mesh2D mesh : meshes) { + for (int i = 0; i < mesh.getVertexCount(); i++) { + Vector2f v = mesh.getVertex(i); + v.sub(dx, dy); + mesh.setVertex(i, v.x, v.y); + } + } + + markTransformDirty(); + updateLocalTransform(); + recomputeWorldTransformRecursive(); + } + + /** + * 获取旋转中心 + */ + public Vector2f getPivot() { + return new Vector2f(pivot); + } + /** * 移除网格 */ diff --git a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java index d7039a2..f758b9a 100644 --- a/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java +++ b/src/main/java/com/chuangzhou/vivid2D/render/model/util/Mesh2D.java @@ -475,18 +475,33 @@ public class Mesh2D { // 绑定 VAO GL30.glBindVertexArray(vaoId); - // 将 modelMatrix 上传到 shader 的 uniform "uModel"(如果 shader 有此 uniform) - int loc = GL20.glGetUniformLocation(shaderProgram, "uModel"); + // 关键修改:使用传入的着色器程序 + GL20.glUseProgram(shaderProgram); + + // 将 modelMatrix 上传到 shader 的 uniform "uModelMatrix"(与ModelRender中的命名一致) + int loc = GL20.glGetUniformLocation(shaderProgram, "uModelMatrix"); + if (loc == -1) { + // 如果找不到 uModelMatrix,尝试 uModel + loc = GL20.glGetUniformLocation(shaderProgram, "uModel"); + } + if (loc != -1) { - // 用一个 FloatBuffer 传递 3x3 矩阵 java.nio.FloatBuffer fb = org.lwjgl.system.MemoryUtil.memAllocFloat(9); try { - modelMatrix.get(fb); // JOML 将矩阵写入 buffer(列主序,适合 OpenGL) + modelMatrix.get(fb); fb.flip(); GL20.glUniformMatrix3fv(loc, false, fb); + + // 调试信息 + //System.out.println("Mesh2D: 应用模型矩阵到着色器 - " + name); + //System.out.printf(" [%.2f, %.2f, %.2f]\n", modelMatrix.m00(), modelMatrix.m01(), modelMatrix.m02()); + //System.out.printf(" [%.2f, %.2f, %.2f]\n", modelMatrix.m10(), modelMatrix.m11(), modelMatrix.m12()); + //System.out.printf(" [%.2f, %.2f, %.2f]\n", modelMatrix.m20(), modelMatrix.m21(), modelMatrix.m22()); } finally { org.lwjgl.system.MemoryUtil.memFree(fb); } + } else { + System.err.println("警告: 着色器中未找到 uModelMatrix 或 uModel uniform"); } // 绘制 diff --git a/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest2.java b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest2.java new file mode 100644 index 0000000..c9fbf33 --- /dev/null +++ b/src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest2.java @@ -0,0 +1,230 @@ +package com.chuangzhou.vivid2D.test; + +import com.chuangzhou.vivid2D.render.ModelRender; +import com.chuangzhou.vivid2D.render.model.Model2D; +import com.chuangzhou.vivid2D.render.model.ModelPart; +import com.chuangzhou.vivid2D.render.model.util.Mesh2D; +import com.chuangzhou.vivid2D.render.model.util.Texture; +import org.joml.Matrix3f; +import org.joml.Vector2f; +import org.lwjgl.glfw.GLFW; +import org.lwjgl.glfw.GLFWErrorCallback; +import org.lwjgl.glfw.GLFWVidMode; +import org.lwjgl.opengl.GL; +import org.lwjgl.system.MemoryUtil; + +import java.nio.ByteBuffer; + +/** + * 用于测试中心点旋转 + * @author tzdwindows 7 + */ +public class ModelRenderTest2 { + + private static final int WINDOW_WIDTH = 800; + private static final int WINDOW_HEIGHT = 600; + private static final String WINDOW_TITLE = "Simple Pivot Test"; + + private long window; + private boolean running = true; + private Model2D testModel; + private float rotationAngle = 0f; + private int testCase = 0; + private Mesh2D squareMesh; + public static void main(String[] args) { + new ModelRenderTest2().run(); + } + + public void run() { + try { + init(); + loop(); + } catch (Throwable t) { + t.printStackTrace(); + } finally { + cleanup(); + } + } + + private void init() { + GLFWErrorCallback.createPrint(System.err).set(); + + if (!GLFW.glfwInit()) { + throw new IllegalStateException("Unable to initialize GLFW"); + } + + GLFW.glfwDefaultWindowHints(); + GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE); + GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3); + GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3); + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE); + GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GLFW.GLFW_TRUE); + + window = GLFW.glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT, WINDOW_TITLE, MemoryUtil.NULL, MemoryUtil.NULL); + if (window == MemoryUtil.NULL) throw new RuntimeException("Failed to create GLFW window"); + + GLFWVidMode vidMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor()); + GLFW.glfwSetWindowPos(window, + (vidMode.width() - WINDOW_WIDTH) / 2, + (vidMode.height() - WINDOW_HEIGHT) / 2); + + GLFW.glfwSetKeyCallback(window, (wnd, key, scancode, action, mods) -> { + if (key == GLFW.GLFW_KEY_ESCAPE && action == GLFW.GLFW_RELEASE) running = false; + if (key == GLFW.GLFW_KEY_SPACE && action == GLFW.GLFW_RELEASE) { + testCase = (testCase + 1) % 3; + updatePivotPoint(); + } + if (key == GLFW.GLFW_KEY_R && action == GLFW.GLFW_RELEASE) { + resetPosition(); + } + }); + + GLFW.glfwSetWindowSizeCallback(window, (wnd, w, h) -> ModelRender.setViewport(w, h)); + + GLFW.glfwMakeContextCurrent(window); + GLFW.glfwSwapInterval(1); + GLFW.glfwShowWindow(window); + + GL.createCapabilities(); + + createSimpleTestModel(); + ModelRender.initialize(); + + System.out.println("Simple pivot test initialized"); + System.out.println("Controls: ESC = exit | SPACE = change pivot | R = reset position"); + } + + /** + * Create a single square mesh with vertices centered at origin + */ + private void createSimpleTestModel() { + testModel = new Model2D("SimpleTest"); + + ModelPart square = testModel.createPart("square"); + square.setPosition(0, 0); // center of window + square.setPivot(0,0); + // Create 80x80 quad centered at origin + squareMesh = Mesh2D.createQuad("square_mesh", 80, 80); + // Shift vertices so center is at (0,0) + //for (int i = 0; i < squareMesh.getVertexCount(); i++) { + // Vector2f v = squareMesh.getVertex(i); + // v.sub(0, 0); + // squareMesh.setVertex(i, v.x, v.y); + //} + + squareMesh.setTexture(createDiagnosticTexture()); + square.addMesh(squareMesh); // do NOT bake to world coordinates + + System.out.println("Simple test model created with one part only"); + } + + /** + * Create a diagnostic texture to see pivot visually + */ + private Texture createDiagnosticTexture() { + int width = 80, height = 80; + ByteBuffer buf = MemoryUtil.memAlloc(width * height * 4); + + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + // center cross + if (x == width / 2 || y == height / 2) { + buf.put((byte) 255).put((byte) 255).put((byte) 255).put((byte) 255); + } else if (x < width / 2 && y < height / 2) { + buf.put((byte) 255).put((byte) 0).put((byte) 0).put((byte) 255); // top-left red + } else if (x >= width / 2 && y < height / 2) { + buf.put((byte) 0).put((byte) 255).put((byte) 0).put((byte) 255); // top-right green + } else if (x < width / 2 && y >= height / 2) { + buf.put((byte) 0).put((byte) 0).put((byte) 255).put((byte) 255); // bottom-left blue + } else { + buf.put((byte) 255).put((byte) 255).put((byte) 0).put((byte) 255); // bottom-right yellow + } + } + } + + buf.flip(); + Texture texture = new Texture("diagnostic", width, height, Texture.TextureFormat.RGBA, buf); + MemoryUtil.memFree(buf); + return texture; + } + + private void updatePivotPoint() { + ModelPart square = testModel.getPart("square"); + if (square != null) { + switch (testCase) { + case 0: + square.setPivot(0, 0); + System.out.println("Pivot: center (0,0) - should rotate around center"); + break; + case 1: + square.setPivot(-40, 40); // top-left corner + System.out.println("Pivot: top-left (-40,40) - should rotate around top-left"); + break; + case 2: + square.setPivot(40, -40); // bottom-right corner + System.out.println("Pivot: bottom-right (40,-40) - should rotate around bottom-right"); + break; + } + } + } + + + private void resetPosition() { + ModelPart square = testModel.getPart("square"); + if (square != null) { + square.setPosition(400, 300); + square.setRotation(0); + rotationAngle = 0; + System.out.println("Position reset"); + } + } + + private void loop() { + long last = System.nanoTime(); + double nsPerUpdate = 1_000_000_000.0 / 60.0; + double accumulator = 0.0; + + while (running && !GLFW.glfwWindowShouldClose(window)) { + long now = System.nanoTime(); + accumulator += (now - last) / nsPerUpdate; + last = now; + + while (accumulator >= 1.0) { + update(1.0f / 60.0f); + accumulator -= 1.0; + } + + render(); + + GLFW.glfwSwapBuffers(window); + GLFW.glfwPollEvents(); + } + } + + private void update(float dt) { + rotationAngle += dt * 1.5f; + + ModelPart square = testModel.getPart("square"); + if (square != null) { + square.setRotation(rotationAngle); + } + + testModel.update(dt); + } + + private void render() { + ModelRender.setClearColor(0.2f, 0.2f, 0.3f, 1.0f); + ModelRender.render(1.0f / 60.0f, testModel); + } + + private void cleanup() { + System.out.println("Cleaning up resources..."); + ModelRender.cleanup(); + Texture.cleanupAll(); + if (window != MemoryUtil.NULL) GLFW.glfwDestroyWindow(window); + GLFW.glfwTerminate(); + GLFW.glfwSetErrorCallback(null).free(); + System.out.println("Test finished"); + } +}