feat(render): 实现模型旋转中心点支持- 为 ModelPart 添加 pivot 属性,支持设置旋转中心点
- 更新局部变换矩阵计算,考虑 pivot 对旋转和平移的影响 - 在 Mesh2D 中增强着色器 uniform 设置,兼容 uModelMatrix 和 uModel- 添加 setPivot 和 getPivot 方法,支持动态调整旋转中心- 创建测试用例 ModelRenderTest2,验证不同 pivot 点的旋转效果 -修复纹理绑定逻辑,确保渲染时正确应用纹理 - 添加调试纹理生成功能,便于视觉验证 pivot 效果
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除网格
|
||||
*/
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
// 绘制
|
||||
|
||||
230
src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest2.java
Normal file
230
src/main/java/com/chuangzhou/vivid2D/test/ModelRenderTest2.java
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user