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);
|
FragColor = vec4(vDebugPos * 0.5 + 0.5, 0.0, 1.0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
vec4 tex = texture(uTexture, vTexCoord);
|
vec4 tex = texture(uTexture, vTexCoord);
|
||||||
vec4 finalColor = tex * uColor;
|
vec4 finalColor = tex * uColor;
|
||||||
if (uBlendMode == 1) finalColor.rgb = tex.rgb + uColor.rgb;
|
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) {
|
private static void renderMesh(Mesh2D mesh, Matrix3f modelMatrix) {
|
||||||
// 使用默认 shader(保证 shader 已被 use)
|
if (!mesh.isVisible()) return;
|
||||||
|
|
||||||
|
// 使用默认 shader
|
||||||
defaultProgram.use();
|
defaultProgram.use();
|
||||||
|
|
||||||
// 如果 mesh 已经被烘焙到世界坐标,则传 identity 矩阵给 shader(防止重复变换)
|
// 如果 mesh 已经被烘焙到世界坐标,则传 identity 矩阵给 shader(防止重复变换)
|
||||||
Matrix3f matToUse = mesh.isBakedToWorld() ? new Matrix3f().identity() : modelMatrix;
|
Matrix3f matToUse = mesh.isBakedToWorld() ? new Matrix3f().identity() : modelMatrix;
|
||||||
|
|
||||||
// 确保 shader 中的矩阵 uniform 已更新(再次设置以防遗漏)
|
// 设置纹理相关的uniform
|
||||||
setUniformMatrix3(defaultProgram, "uModelMatrix", matToUse);
|
if (mesh.getTexture() != null) {
|
||||||
setUniformMatrix3(defaultProgram, "uModel", matToUse);
|
mesh.getTexture().bind(0); // 绑定到纹理单元0
|
||||||
|
setUniformIntInternal(defaultProgram, "uTexture", 0);
|
||||||
// 调用 Mesh2D 的 draw 重载(传 program id 与实际矩阵)
|
} else {
|
||||||
try {
|
// 使用默认白色纹理
|
||||||
mesh.draw(defaultProgram.programId, matToUse);
|
GL11.glBindTexture(GL11.GL_TEXTURE_2D, defaultTextureId);
|
||||||
} catch (AbstractMethodError | NoSuchMethodError e) {
|
setUniformIntInternal(defaultProgram, "uTexture", 0);
|
||||||
// 回退:仍然兼容旧的无参 draw(在这种情况下 shader 的 uModelMatrix 已经被设置)
|
|
||||||
mesh.draw();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 调用 Mesh2D 的 draw 方法,传入当前使用的着色器程序和变换矩阵
|
||||||
|
mesh.draw(defaultProgram.programId, matToUse);
|
||||||
|
|
||||||
checkGLError("renderMesh");
|
checkGLError("renderMesh");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ public class ModelPart {
|
|||||||
private final Vector2f scale;
|
private final Vector2f scale;
|
||||||
private final Matrix3f localTransform;
|
private final Matrix3f localTransform;
|
||||||
private final Matrix3f worldTransform;
|
private final Matrix3f worldTransform;
|
||||||
|
private final Vector2f pivot = new Vector2f(0, 0);
|
||||||
|
|
||||||
// ==================== 渲染属性 ====================
|
// ==================== 渲染属性 ====================
|
||||||
private boolean visible;
|
private boolean visible;
|
||||||
@@ -42,6 +43,7 @@ public class ModelPart {
|
|||||||
// ==================== 状态标记 ====================
|
// ==================== 状态标记 ====================
|
||||||
private boolean transformDirty;
|
private boolean transformDirty;
|
||||||
private boolean boundsDirty;
|
private boolean boundsDirty;
|
||||||
|
private boolean pivotInitialized;
|
||||||
|
|
||||||
// ==================== 构造器 ====================
|
// ==================== 构造器 ====================
|
||||||
|
|
||||||
@@ -194,13 +196,19 @@ public class ModelPart {
|
|||||||
float cos = (float) Math.cos(rotation);
|
float cos = (float) Math.cos(rotation);
|
||||||
float sin = (float) Math.sin(rotation);
|
float sin = (float) Math.sin(rotation);
|
||||||
|
|
||||||
float m00 = cos * scale.x;
|
float sx = scale.x;
|
||||||
float m01 = -sin * scale.y;
|
float sy = scale.y;
|
||||||
float m10 = sin * scale.x;
|
|
||||||
float m11 = cos * 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(
|
localTransform.set(
|
||||||
m00, m01, m02,
|
m00, m01, m02,
|
||||||
@@ -360,6 +368,44 @@ public class ModelPart {
|
|||||||
boundsDirty = true;
|
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
|
// 绑定 VAO
|
||||||
GL30.glBindVertexArray(vaoId);
|
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) {
|
if (loc != -1) {
|
||||||
// 用一个 FloatBuffer 传递 3x3 矩阵
|
|
||||||
java.nio.FloatBuffer fb = org.lwjgl.system.MemoryUtil.memAllocFloat(9);
|
java.nio.FloatBuffer fb = org.lwjgl.system.MemoryUtil.memAllocFloat(9);
|
||||||
try {
|
try {
|
||||||
modelMatrix.get(fb); // JOML 将矩阵写入 buffer(列主序,适合 OpenGL)
|
modelMatrix.get(fb);
|
||||||
fb.flip();
|
fb.flip();
|
||||||
GL20.glUniformMatrix3fv(loc, false, fb);
|
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 {
|
} finally {
|
||||||
org.lwjgl.system.MemoryUtil.memFree(fb);
|
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