所需依赖:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>plyviewer-lwjgl</artifactId>
<version>1.0</version>
<properties>
<lwjgl.version>3.3.1</lwjgl.version>
<java.version>8</java.version>
</properties>
<dependencies>
<!-- LWJGL Core -->
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl</artifactId>
<version>${lwjgl.version}</version>
</dependency>
<dependency>
<groupId>org.joml</groupId>
<artifactId>joml</artifactId>
<version>1.10.5</version>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-glfw</artifactId>
<version>${lwjgl.version}</version>
</dependency>
<dependency>
<groupId>org.lwjgl</groupId>
<artifactId>lwjgl-opengl</artifactId>
<version>${lwjgl.version}</version>
</dependency>
<!-- Native Bindings -->
<dependency><groupId>org.lwjgl</groupId><artifactId>lwjgl</artifactId><classifier>natives-windows</classifier><version>${lwjgl.version}</version></dependency>
<dependency><groupId>org.lwjgl</groupId><artifactId>lwjgl-glfw</artifactId><classifier>natives-windows</classifier><version>${lwjgl.version}</version></dependency>
<dependency><groupId>org.lwjgl</groupId><artifactId>lwjgl-opengl</artifactId><classifier>natives-windows</classifier><version>${lwjgl.version}</version></dependency>
</dependencies>
</project>
BinaryPLYViewer:
package com.example.java3d;
import org.joml.Matrix4f;
import org.lwjgl.*;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.*;
import org.lwjgl.system.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.util.*;
import static org.lwjgl.glfw.Callbacks.*;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL33.*;
import static org.lwjgl.system.MemoryStack.stackPush;
import static org.lwjgl.system.MemoryUtil.*;
/**
* Binary or ASCii PLY 文件查看器
*/
public class BinaryPLYViewer {
private long window;
private int width = 800;
private int height = 600;
private int vao;
private int vbo;
private int vertexCount;
private int shaderProgram;
// 视角控制变量
private float yaw = 0.0f;
private float pitch = 0.0f;
private float zoom = 1.0f;
private double lastX = -1;
private double lastY = -1;
private boolean rotating = false;
public void run() {
init();
loop();
// 释放资源
glDeleteVertexArrays(vao);
glDeleteBuffers(vbo);
glDeleteProgram(shaderProgram);
glfwFreeCallbacks(window);
glfwDestroyWindow(window);
glfwTerminate();
}
// 初始化
private void init() {
GLFWErrorCallback.createPrint(System.err).set();
if (!glfwInit())
throw new IllegalStateException("Unable to initialize GLFW");
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
window = glfwCreateWindow(width, height, "PLY Viewer", NULL, NULL);
if (window == NULL)
throw new RuntimeException("Failed to create GLFW window");
glfwMakeContextCurrent(window);
glfwSwapInterval(1); // VSync
glfwSetFramebufferSizeCallback(window, (w, newW, newH) -> {
width = newW;
height = newH;
glViewport(0, 0, width, height);
});
// 鼠标事件回调
glfwSetCursorPosCallback(window, (win, xpos, ypos) -> {
if (rotating) {
if (lastX >= 0 && lastY >= 0) {
double dx = xpos - lastX;
double dy = ypos - lastY;
yaw += dx * 0.3f;
pitch += dy * 0.3f;
// pitch = Math.max(-89.0f, Math.min(89.0f, pitch)); // 限制pitch范围
}
lastX = xpos;
lastY = ypos;
}
});
glfwSetMouseButtonCallback(window, (win, button, action, mods) -> {
if (button == GLFW_MOUSE_BUTTON_LEFT) {
if (action == GLFW_PRESS) {
rotating = true;
try (MemoryStack stack = stackPush()) {
DoubleBuffer px = stack.mallocDouble(1);
DoubleBuffer py = stack.mallocDouble(1);
glfwGetCursorPos(window, px, py);
lastX = px.get(0);
lastY = py.get(0);
}
} else if (action == GLFW_RELEASE) {
rotating = false;
lastX = lastY = -1;
}
}
});
glfwSetScrollCallback(window, (win, xoffset, yoffset) -> {
zoom *= 1.0f - yoffset * 0.1f;
zoom = Math.max(0.1f, Math.min(10.0f, zoom));
});
GL.createCapabilities();
glEnable(GL_PROGRAM_POINT_SIZE);
shaderProgram = createShaderProgram();
loadPLY("src/main/resources/c.ply");
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glEnable(GL_DEPTH_TEST);
}
// 创建着色器程序
private int createShaderProgram() {
String vertexShaderSrc = ""
+ "#version 330 core\n"
+ "layout(location = 0) in vec3 aPos;\n"
+ "layout(location = 1) in vec3 aColor;\n"
+ "uniform mat4 uMVP;\n"
+ "out vec3 vColor;\n"
+ "void main() {\n"
+ " gl_Position = uMVP * vec4(aPos, 1.0);\n"
+ " vColor = aColor;\n"
+ " gl_PointSize = 2.0;\n"
+ "}\n";
String fragmentShaderSrc = ""
+ "#version 330 core\n"
+ "in vec3 vColor;\n"
+ "out vec4 FragColor;\n"
+ "void main() {\n"
+ " FragColor = vec4(vColor, 1.0);\n"
+ "}\n";
int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, vertexShaderSrc);
glCompileShader(vertexShader);
checkCompileErrors(vertexShader, "VERTEX");
int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, fragmentShaderSrc);
glCompileShader(fragmentShader);
checkCompileErrors(fragmentShader, "FRAGMENT");
int program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
glLinkProgram(program);
checkCompileErrors(program, "PROGRAM");
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
return program;
}
// 检查编译错误
private void checkCompileErrors(int shader, String type) {
int success;
if (type.equals("PROGRAM")) {
success = glGetProgrami(shader, GL_LINK_STATUS);
if (success == GL_FALSE) {
String infoLog = glGetProgramInfoLog(shader);
System.err.println("ERROR::PROGRAM_LINKING_ERROR\n" + infoLog);
}
} else {
success = glGetShaderi(shader, GL_COMPILE_STATUS);
if (success == GL_FALSE) {
String infoLog = glGetShaderInfoLog(shader);
System.err.println("ERROR::SHADER_COMPILATION_ERROR of type: " + type + "\n" + infoLog);
}
}
}
// 加载 PLY 文件
private void loadPLY(String path) {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path))) {
DataInputStream dis = new DataInputStream(bis);
List<String> headerLines = new ArrayList<>();
String format = "";
int vertexCount = 0;
// --- 读取 header ---
while (true) {
String line = readLine(dis);
if (line == null) break;
headerLines.add(line);
if (line.startsWith("format")) {
format = line.split(" ")[1].trim();
} else if (line.startsWith("element vertex")) {
vertexCount = Integer.parseInt(line.split(" ")[2]);
} else if (line.trim().equals("end_header")) {
break;
}
}
this.vertexCount = vertexCount;
FloatBuffer buffer = BufferUtils.createFloatBuffer(vertexCount * 6); // xyz + rgb
// --- 判断格式并读取数据 ---
if (format.equals("ascii")) {
BufferedReader reader = new BufferedReader(new InputStreamReader(bis, StandardCharsets.UTF_8));
for (int i = 0; i < vertexCount; i++) {
String[] parts = reader.readLine().trim().split("\\s+");
float x = Float.parseFloat(parts[0]);
float y = Float.parseFloat(parts[1]);
float z = Float.parseFloat(parts[2]);
float r = Integer.parseInt(parts[3]) / 255.0f;
float g = Integer.parseInt(parts[4]) / 255.0f;
float b = Integer.parseInt(parts[5]) / 255.0f;
buffer.put(x).put(y).put(z).put(r).put(g).put(b);
}
} else if (format.equals("binary_little_endian")) {
for (int i = 0; i < vertexCount; i++) {
float x = Float.intBitsToFloat(Integer.reverseBytes(dis.readInt()));
float y = Float.intBitsToFloat(Integer.reverseBytes(dis.readInt()));
float z = Float.intBitsToFloat(Integer.reverseBytes(dis.readInt()));
float r = dis.readUnsignedByte() / 255.0f;
float g = dis.readUnsignedByte() / 255.0f;
float b = dis.readUnsignedByte() / 255.0f;
buffer.put(x).put(y).put(z).put(r).put(g).put(b);
}
} else {
throw new IOException("Unsupported PLY format: " + format);
}
buffer.flip();
// --- OpenGL VAO/VBO 初始化 ---
vao = glGenVertexArrays();
glBindVertexArray(vao);
vbo = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, buffer, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, false, 6 * Float.BYTES, 0);
glEnableVertexAttribArray(0);
glVertexAttribPointer(1, 3, GL_FLOAT, false, 6 * Float.BYTES, 3 * Float.BYTES);
glEnableVertexAttribArray(1);
glBindVertexArray(0);
System.out.println("Loaded PLY with " + vertexCount + " vertices, format: " + format);
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
}
// 读取一行
private String readLine(DataInputStream dis) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int c;
while ((c = dis.read()) != -1) {
if (c == '\n') break;
baos.write(c);
}
if (baos.size() == 0 && c == -1) return null;
return baos.toString("UTF-8").trim();
}
// 循环
private void loop() {
while (!glfwWindowShouldClose(window)) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glUseProgram(shaderProgram);
// 计算 MVP 矩阵
Matrix4f projection = new Matrix4f()
.perspective((float) Math.toRadians(45.0f), (float) width / height, 0.1f, 100.0f);
Matrix4f view = new Matrix4f()
.translate(0, 0, -3.0f * zoom)
.rotate((float) Math.toRadians(pitch), 1, 0, 0)
.rotate((float) Math.toRadians(yaw), 0, 1, 0);
Matrix4f model = new Matrix4f();
Matrix4f mvp = new Matrix4f();
projection.mul(view, mvp);
mvp.mul(model);
int mvpLoc = glGetUniformLocation(shaderProgram, "uMVP");
FloatBuffer fb = BufferUtils.createFloatBuffer(16);
mvp.get(fb);
glUniformMatrix4fv(mvpLoc, false, fb);
glBindVertexArray(vao);
glDrawArrays(GL_POINTS, 0, vertexCount);
glBindVertexArray(0);
glfwSwapBuffers(window);
glfwPollEvents();
}
}
// public static void main(String[] args) {
// new PlyViewer().run();
// }
public static void main(String[] args) {
new BinaryPLYViewer().run();
}
}
注意事项:
需要根据具体的ply文件内容修改loadPLY()方法 (解析文件属性字段)
例如我的ply文件
ply
format ascii 1.0
comment Created by Open3D
obj_info Generated by CloudCompare!
element vertex 1638113
property float x
property float y
property float z
property uchar red
property uchar green
property uchar blue
end_header