feat(render): 实现独立的 OpenGL 上下文管理器

- 将 GL 上下文管理从 ModelRenderPanel 抽离到独立的 GLContextManager 类- 实现离屏渲染上下文的创建、初始化和资源管理
- 支持动态调整渲染缓冲区大小和缩放功能
- 提供线程安全的任务队列机制用于在 GL 线程执行操作
- 实现像素数据读取和转换为 BufferedImage 的完整流程- 添加摄像机拖拽状态和缩放控制的支持
-重构 ModelRenderPanel以使用新的 GLContextManager- 更新所有 GL 相关操作的调用方式指向新的上下文管理器
- 修改 dispose 流程以正确释放所有 OpenGL 资源
- 优化渲染循环和平滑缩放逻辑实现
This commit is contained in:
tzdwindows 7
2025-10-26 10:57:54 +08:00
parent 43aab9f0fd
commit 71aa2b8699
6 changed files with 679 additions and 647 deletions

View File

@@ -127,7 +127,7 @@ public class ModelLayerPanel extends JPanel {
// 使用更可靠的方式在GL上下文中创建纹理
try {
// 在GL上下文中同步执行所有图层的创建
renderPanel.executeInGLContext(() -> {
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
List<ModelPart> createdParts = new ArrayList<>();
@@ -665,7 +665,7 @@ public class ModelLayerPanel extends JPanel {
if (renderPanel != null) {
final String texName = name + "_tex";
final String filePath = f.getAbsolutePath();
renderPanel.executeInGLContext(() -> {
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
Texture texture = Texture.createFromFile(texName, filePath);
if (texture != null) {
@@ -730,7 +730,7 @@ public class ModelLayerPanel extends JPanel {
part.addMesh(mesh);
if (renderPanel != null) {
renderPanel.executeInGLContext(() -> {
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
Texture tex = createTextureFromBufferedImageInGL(img, name + "_tex");
if (tex != null) {
@@ -799,7 +799,7 @@ public class ModelLayerPanel extends JPanel {
final String texName = sel.getName() + "_tex";
if (renderPanel != null) {
renderPanel.executeInGLContext(() -> {
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
Texture texture = Texture.createFromFile(texName, filePath);
if (texture != null) {
@@ -1087,7 +1087,7 @@ public class ModelLayerPanel extends JPanel {
if (renderPanel == null) throw new IllegalStateException("需要 renderPanel 才能在 GL 上下文创建纹理");
try {
return renderPanel.executeInGLContext(() -> {
return renderPanel.getGlContextManager().executeInGLContext(() -> {
// 静态工厂尝试
try {
Method factory = findStaticMethod(Texture.class, "createFromBufferedImage", BufferedImage.class);

View File

@@ -236,7 +236,7 @@ public class TransformPanel extends JPanel implements ModelEvent {
// 旋转按钮监听器修改(支持多选)
rotate90CWButton.addActionListener(e -> {
if (!selectedParts.isEmpty()) {
renderPanel.executeInGLContext(() -> {
renderPanel.getGlContextManager().executeInGLContext(() -> {
Map<ModelPart, Float> oldRotations = new HashMap<>();
Map<ModelPart, Float> newRotations = new HashMap<>();
@@ -263,7 +263,7 @@ public class TransformPanel extends JPanel implements ModelEvent {
rotate90CCWButton.addActionListener(e -> {
if (!selectedParts.isEmpty()) {
renderPanel.executeInGLContext(() -> {
renderPanel.getGlContextManager().executeInGLContext(() -> {
Map<ModelPart, Float> oldRotations = new HashMap<>();
Map<ModelPart, Float> newRotations = new HashMap<>();
@@ -291,7 +291,7 @@ public class TransformPanel extends JPanel implements ModelEvent {
// 翻转按钮监听器修改(支持多选)
flipXButton.addActionListener(e -> {
if (!selectedParts.isEmpty()) {
renderPanel.executeInGLContext(() -> {
renderPanel.getGlContextManager().executeInGLContext(() -> {
Map<ModelPart, Vector2f> oldScales = new HashMap<>();
Map<ModelPart, Vector2f> newScales = new HashMap<>();
@@ -318,7 +318,7 @@ public class TransformPanel extends JPanel implements ModelEvent {
flipYButton.addActionListener(e -> {
if (!selectedParts.isEmpty()) {
renderPanel.executeInGLContext(() -> {
renderPanel.getGlContextManager().executeInGLContext(() -> {
Map<ModelPart, Vector2f> oldScales = new HashMap<>();
Map<ModelPart, Vector2f> newScales = new HashMap<>();
@@ -346,7 +346,7 @@ public class TransformPanel extends JPanel implements ModelEvent {
// 重置缩放按钮监听器修改(支持多选)
resetScaleButton.addActionListener(e -> {
if (!selectedParts.isEmpty()) {
renderPanel.executeInGLContext(() -> {
renderPanel.getGlContextManager().executeInGLContext(() -> {
Map<ModelPart, Vector2f> oldScales = new HashMap<>();
Map<ModelPart, Vector2f> newScales = new HashMap<>();
@@ -495,7 +495,7 @@ public class TransformPanel extends JPanel implements ModelEvent {
private void applyTransformChanges() {
if (updatingUI || selectedParts.isEmpty()) return;
renderPanel.executeInGLContext(() -> {
renderPanel.getGlContextManager().executeInGLContext(() -> {
try {
float posX = Float.parseFloat(positionXField.getText());
float posY = Float.parseFloat(positionYField.getText());

View File

@@ -0,0 +1,586 @@
package com.chuangzhou.vivid2D.render.awt.manager;
import com.chuangzhou.vivid2D.render.ModelRender;
import com.chuangzhou.vivid2D.render.model.Model2D;
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11;
import org.lwjgl.system.MemoryUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.IntBuffer;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
public class GLContextManager {
private static final Logger logger = LoggerFactory.getLogger(GLContextManager.class);
private long windowId;
private volatile boolean running = true;
private Thread renderThread;
// 改为可变的宽高以支持动态重建离屏上下文缓冲
private volatile int width;
private volatile int height;
private BufferedImage currentFrame;
private volatile boolean contextInitialized = false;
private final CompletableFuture<Void> contextReady = new CompletableFuture<>();
private final String modelPath;
private final AtomicReference<Model2D> modelRef = new AtomicReference<>();
private BufferedImage lastFrame = null;
private ByteBuffer pixelBuffer = null;
private int[] pixelInts = null;
private int[] argbInts = null;
public volatile float displayScale = 1.0f; // 当前可视缩放(用于检测阈值/角点等)
public volatile float targetScale = 1.0f; // 目标缩放(鼠标滚轮/程序改变时设置)
// 任务队列,用于在 GL 上下文线程执行代码
private final BlockingQueue<Runnable> glTaskQueue = new LinkedBlockingQueue<>();
private final ExecutorService taskExecutor = Executors.newSingleThreadExecutor();
private volatile boolean cameraDragging = false;
private static final float ZOOM_SMOOTHING = 0.18f; // 0..1, 越大收敛越快(建议 0.12-0.25
private RepaintCallback repaintCallback;
public GLContextManager(String modelPath, int width, int height) {
this.modelPath = modelPath;
this.width = width;
this.height = height;
}
public GLContextManager(Model2D model, int width, int height) {
this.modelPath = null;
this.width = width;
this.height = height;
this.modelRef.set(model);
}
public int getHeight() {
return height;
}
public int getWidth() {
return width;
}
/**
* 创建离屏 OpenGL 上下文
*/
private void createOffscreenContext() throws Exception {
// 设置窗口提示
GLFW.glfwDefaultWindowHints();
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);
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, GL11.GL_TRUE);
GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, 4);
// 创建离屏窗口(像素尺寸以当前 width/height 为准)
windowId = GLFW.glfwCreateWindow(width, height, "Offscreen Render", MemoryUtil.NULL, MemoryUtil.NULL);
if (windowId == MemoryUtil.NULL) {
throw new Exception("无法创建离屏 OpenGL 上下文");
}
// 设置为当前上下文并初始化
GLFW.glfwMakeContextCurrent(windowId);
GL.createCapabilities();
logger.info("OpenGL context created successfully");
// 然后初始化 RenderSystem
RenderSystem.beginInitialization();
RenderSystem.initRenderThread();
// 使用 RenderSystem 设置视口
RenderSystem.viewport(0, 0, width, height);
// 分配像素读取缓冲
int pixelCount = Math.max(1, width * height);
pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4);
pixelBuffer.order(ByteOrder.nativeOrder());
pixelInts = new int[pixelCount];
argbInts = new int[pixelCount];
// 初始化 ModelRender
ModelRender.initialize();
RenderSystem.finishInitialization();
// 在正确的上下文中加载模型(可能会耗时)
loadModelInContext();
// 标记上下文已初始化并完成通知(只 complete 一次)
contextInitialized = true;
contextReady.complete(null);
logger.info("Offscreen context initialization completed");
}
public void setRepaintCallback(RepaintCallback callback) {
this.repaintCallback = callback;
}
/**
* 在 OpenGL 上下文中加载模型
*/
private void loadModelInContext() {
try {
if (modelPath != null) {
Model2D model = Model2D.loadFromFile(modelPath);
modelRef.set(model);
logger.info("模型加载成功: {}", modelPath);
}
} catch (Exception e) {
logger.error("模型加载失败: {}", e.getMessage(), e);
e.printStackTrace();
}
}
/**
* 启动渲染线程
*/
public void startRendering() {
// 初始化 GLFW
if (!GLFW.glfwInit()) {
throw new RuntimeException("无法初始化 GLFW");
}
renderThread = new Thread(() -> {
try {
createOffscreenContext();
// 等待上下文就绪后再开始渲染循环contextReady 由 createOffscreenContext 完成)
contextReady.get();
// 确保当前线程一直持有该 GL 上下文(避免在每个任务/帧中重复 makeCurrent
GLFW.glfwMakeContextCurrent(windowId);
final long targetNs = 1_000_000_000L / 60L; // 60 FPS
while (running && !GLFW.glfwWindowShouldClose(windowId)) {
long start = System.nanoTime();
processGLTasks();
displayScale += (targetScale - displayScale) * ZOOM_SMOOTHING;
renderFrame();
long elapsed = System.nanoTime() - start;
long sleepNs = targetNs - elapsed;
if (sleepNs > 0) {
LockSupport.parkNanos(sleepNs);
}
}
} catch (Exception e) {
logger.error("渲染线程异常", e);
} finally {
cleanup();
}
});
renderThread.setDaemon(true);
renderThread.setName("GL-Render-Thread");
renderThread.start();
}
/**
* 渲染单帧并读取到 BufferedImage
*/
private void renderFrame() {
if (!contextInitialized || windowId == 0) return;
// 确保在当前上下文中
GLFW.glfwMakeContextCurrent(windowId);
Model2D currentModel = modelRef.get();
if (currentModel != null) {
try {
// 使用 RenderSystem 清除缓冲区
RenderSystem.setClearColor(0.18f, 0.18f, 0.25f, 1f);
RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT);
// 渲染模型
ModelRender.render(1.0f / 60f, currentModel);
// 读取像素数据到 BufferedImage
readPixelsToImage();
} catch (Exception e) {
System.err.println("渲染错误: " + e.getMessage());
renderErrorFrame(e.getMessage());
}
} else {
// 没有模型时显示默认背景
renderDefaultBackground();
}
// 在 Swing EDT 中更新显示
if (repaintCallback != null) {
repaintCallback.repaint();
}
}
/**
* 渲染默认背景
*/
private void renderDefaultBackground() {
RenderSystem.setClearColor(0.1f, 0.1f, 0.15f, 1f);
RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT);
readPixelsToImage();
}
/**
* 渲染错误帧
*/
private void renderErrorFrame(String errorMessage) {
GL11.glClearColor(0.3f, 0.1f, 0.1f, 1f);
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
readPixelsToImage();
BufferedImage errorImage = new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = errorImage.createGraphics();
g2d.setColor(Color.DARK_GRAY);
g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight());
g2d.setColor(Color.RED);
g2d.drawString("渲染错误: " + errorMessage, 10, 20);
g2d.dispose();
currentFrame = errorImage;
}
/**
* 读取 OpenGL 像素数据到 BufferedImage
*/
private void readPixelsToImage() {
try {
final int w = Math.max(1, this.width);
final int h = Math.max(1, this.height);
final int pixelCount = w * h;
// 确保缓冲区大小匹配(可能在 resize 后需要重建)
if (pixelBuffer == null || pixelInts == null || pixelInts.length != pixelCount) {
if (pixelBuffer != null) {
try {
MemoryUtil.memFree(pixelBuffer);
} catch (Throwable ignored) {
}
}
pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4);
pixelBuffer.order(ByteOrder.nativeOrder());
pixelInts = new int[pixelCount];
argbInts = new int[pixelCount];
}
pixelBuffer.clear();
// 从 GPU 读取 RGBA 字节到本地缓冲 - 使用 RenderSystem
RenderSystem.readPixels(0, 0, w, h, RenderSystem.GL_RGBA, RenderSystem.GL_UNSIGNED_BYTE, pixelBuffer);
// 以 int 批量读取(依赖本机字节序),然后转换为带 alpha 的 ARGB 并垂直翻转
IntBuffer ib = pixelBuffer.asIntBuffer();
ib.get(pixelInts, 0, pixelCount);
// 转换并翻转RGBA -> ARGB
for (int y = 0; y < h; y++) {
int srcRow = (h - y - 1) * w;
int dstRow = y * w;
for (int x = 0; x < w; x++) {
int rgba = pixelInts[srcRow + x];
// 提取字节(考虑 native order按 RGBA 存放)
int r = (rgba >> 0) & 0xFF;
int g = (rgba >> 8) & 0xFF;
int b = (rgba >> 16) & 0xFF;
int a = (rgba >> 24) & 0xFF;
// 组合为 ARGB (BufferedImage 使用 ARGB)
argbInts[dstRow + x] = (a << 24) | (r << 16) | (g << 8) | b;
}
}
// 使用一次 setRGB 写入 BufferedImage比逐像素 setRGB 快)
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
image.setRGB(0, 0, w, h, argbInts, 0, w);
currentFrame = image;
lastFrame = image;
} catch (Exception e) {
logger.error("读取像素数据错误", e);
// 创建错误图像(保持原逻辑)
BufferedImage errorImage = new BufferedImage(Math.max(1, this.width), Math.max(1, this.height), BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = errorImage.createGraphics();
g2d.setColor(Color.BLACK);
g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight());
g2d.setColor(Color.RED);
g2d.drawString("像素读取失败", 10, 20);
g2d.dispose();
currentFrame = errorImage;
}
}
/**
* 处理 GL 上下文任务队列
*/
private void processGLTasks() {
Runnable task;
while ((task = glTaskQueue.poll()) != null) {
try {
// 在渲染线程中执行,渲染线程已将上下文设为 current
task.run();
} catch (Exception e) {
logger.error("执行 GL 任务时出错", e);
}
}
}
/**
* 重新设置面板大小
* <p>
* 说明:当 Swing 面板被放大时,需要同时调整离屏 GLFW 窗口像素大小、GL 视口以及重分配像素读取缓冲,
* 否则将把较小分辨率的图像拉伸到更大面板上导致模糊。
*/
public void resize(int newWidth, int newHeight) {
executeInGLContext(() -> {
if (contextInitialized && windowId != 0) {
this.width = Math.max(1, newWidth);
this.height = Math.max(1, newHeight);
GLFW.glfwMakeContextCurrent(windowId);
GLFW.glfwSetWindowSize(windowId, this.width, this.height);
RenderSystem.viewport(0, 0, this.width, this.height);
ModelRender.setViewport(this.width, this.height);
try {
if (pixelBuffer != null) {
MemoryUtil.memFree(pixelBuffer);
pixelBuffer = null;
}
} catch (Throwable ignored) {}
int pixelCount = Math.max(1, this.width * this.height);
pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4);
pixelBuffer.order(ByteOrder.nativeOrder());
pixelInts = new int[pixelCount];
argbInts = new int[pixelCount];
currentFrame = null;
} else {
this.width = Math.max(1, newWidth);
this.height = Math.max(1, newHeight);
}
});
}
/**
* 等待渲染上下文准备就绪
*/
public CompletableFuture<Void> waitForContext() {
return contextReady;
}
/**
* 检查渲染上下文是否已初始化
* @return true 表示已初始化false 表示未初始化
*/
public boolean isContextInitialized() {
return contextInitialized;
}
/**
* 检查是否正在运行
*/
public boolean isRunning() {
return running && contextInitialized;
}
/**
* 清理资源
*/
public void dispose() {
running = false;
cameraDragging = false;
// 停止任务执行器
taskExecutor.shutdown();
if (renderThread != null) {
try {
renderThread.join(2000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
cleanup();
}
private void cleanup() {
// 清理 ModelRender
try {
if (ModelRender.isInitialized()) {
ModelRender.cleanup();
logger.info("ModelRender 已清理");
}
} catch (Exception e) {
logger.error("清理 ModelRender 时出错: {}", e.getMessage());
}
if (windowId != 0) {
try {
GLFW.glfwDestroyWindow(windowId);
} catch (Throwable ignored) {
}
windowId = 0;
}
// 释放像素缓冲
try {
if (pixelBuffer != null) {
MemoryUtil.memFree(pixelBuffer);
pixelBuffer = null;
}
} catch (Throwable t) {
logger.warn("释放 pixelBuffer 时出错: {}", t.getMessage());
}
// 终止 GLFW注意如果应用中还有其他 GLFW 窗口,这里会影响它们)
try {
GLFW.glfwTerminate();
} catch (Throwable ignored) {
}
logger.info("OpenGL 资源已清理");
}
/**
* 在 GL 上下文线程上异步执行任务
*
* @param task 要在 GL 上下文线程中执行的任务
* @return CompletableFuture 用于获取任务执行结果
*/
public CompletableFuture<Void> executeInGLContext(Runnable task) {
CompletableFuture<Void> future = new CompletableFuture<>();
if (!running) {
future.completeExceptionally(new IllegalStateException("渲染线程已停止"));
return future;
}
// 等待上下文就绪后再提交任务
contextReady.thenRun(() -> {
try {
// 使用 put 保证任务不会被丢弃,如果队列已满会阻塞调用者直到可入队
glTaskQueue.put(() -> {
try {
task.run();
future.complete(null);
} catch (Exception e) {
future.completeExceptionally(e);
}
});
} catch (Exception e) {
future.completeExceptionally(e);
}
});
return future;
}
/**
* 在 GL 上下文线程上异步执行任务并返回结果
*
* @param task 要在 GL 上下文线程中执行的有返回值的任务
* @return CompletableFuture 用于获取任务执行结果
*/
public <T> CompletableFuture<T> executeInGLContext(Callable<T> task) {
CompletableFuture<T> future = new CompletableFuture<>();
if (!running) {
future.completeExceptionally(new IllegalStateException("渲染线程已停止"));
return future;
}
contextReady.thenRun(() -> {
try {
glTaskQueue.put(() -> {
try {
T result = task.call();
future.complete(result);
} catch (Exception e) {
future.completeExceptionally(e);
}
});
} catch (Exception e) {
future.completeExceptionally(e);
}
});
return future;
}
/**
* 同步在 GL 上下文线程上执行任务(会阻塞当前线程直到任务完成)
*
* @param task 要在 GL 上下文线程中执行的任务
* @throws Exception 如果任务执行出错
*/
public void executeInGLContextSync(Runnable task) throws Exception {
if (!running) {
throw new IllegalStateException("渲染线程已停止");
}
CompletableFuture<Void> future = executeInGLContext(task);
future.get(10, TimeUnit.SECONDS); // 设置超时时间
}
/**
* 同步在 GL 上下文线程上执行任务并返回结果(会阻塞当前线程直到任务完成)
*
* @param task 要在 GL 上下文线程中执行的有返回值的任务
* @return 任务执行结果
* @throws Exception 如果任务执行出错或超时
*/
public <T> T executeInGLContextSync(Callable<T> task) throws Exception {
if (!running) {
throw new IllegalStateException("渲染线程已停止");
}
CompletableFuture<T> future = executeInGLContext(task);
return future.get(10, TimeUnit.SECONDS); // 设置超时时间
}
public void setDisplayScale(float scale) {
this.displayScale = scale;
}
public void setTargetScale(float scale) {
this.targetScale = scale;
}
public float getDisplayScale() {
return displayScale;
}
public float getTargetScale() {
return targetScale;
}
public interface RepaintCallback {
void repaint();
}
/**
* 获取当前帧
* @return 当前帧
*/
public BufferedImage getCurrentFrame() {
return currentFrame;
}
/**
* 获取上一帧
* @return 上一帧
*/
public BufferedImage getLastFrame() {
return lastFrame;
}
public boolean isCameraDragging() {
return cameraDragging;
}
public void setCameraDragging(boolean cameraDragging) {
this.cameraDragging = cameraDragging;
}
}

View File

@@ -96,7 +96,7 @@ public class ModelLayerPanelTest {
// 添加选中部件更新按钮
JButton updateSelectionBtn = new JButton("更新选中部件");
updateSelectionBtn.addActionListener(e -> {
renderPanel.executeInGLContext(() -> {
renderPanel.getGlContextManager().executeInGLContext(() -> {
List<ModelPart> selectedPart = renderPanel.getSelectedParts();
transformPanel.setSelectedParts(selectedPart);
});
@@ -134,7 +134,7 @@ public class ModelLayerPanelTest {
public void windowClosed(java.awt.event.WindowEvent e) {
// 进程退出(确保彻底关闭)
try {
renderPanel.dispose();
renderPanel.getGlContextManager().dispose();
} catch (Throwable ignored) {
}
model.saveToFile("C:\\Users\\Administrator\\Desktop\\testing.model");

View File

@@ -41,7 +41,7 @@ public class TestModelGLPanel {
// 在 GL 上下文中创建 mesh / part / physics 等资源
ModelRenderPanel finalGlPanel = glPanel;
glPanel.executeInGLContext(() -> {
glPanel.getGlContextManager().executeInGLContext(() -> {
setupModelInGL(testModel);
return null;
});
@@ -53,7 +53,7 @@ public class TestModelGLPanel {
if (!animate) return;
float dt = 1.0f / fps;
// 在 GL 上下文中更新模型状态(旋转、参数、物理更新等)
finalGlPanel.executeInGLContext(() -> {
finalGlPanel.getGlContextManager().executeInGLContext(() -> {
updateAnimation(testModel, dt);
return null;
});