An attempt at Vulkan
jMonkeyEngine Hub
March 26, 2026
package com.jme3.lwjgl.test;
import com.jme3.app.LegacyApplication;
import com.jme3.asset.TextureKey;
import com.jme3.asset.plugins.ClasspathLocator;
import com.jme3.export.binary.BinaryImporter;
import com.jme3.input.FlyByCamera;
import com.jme3.material.Material;
import com.jme3.material.plugins.J3MLoader;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.Camera;
import com.jme3.renderer.ViewPort;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import com.jme3.scene.shape.Box;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeSystem;
import com.jme3.system.VulkanSystemDelegate;
import com.jme3.texture.Image;
import com.jme3.texture.Texture;
import com.jme3.texture.Texture2D;
import com.jme3.texture.image.ColorSpace;
import com.jme3.util.BufferUtils;
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.ByteBuffer;
import javax.imageio.ImageIO;
public class VulkanMultiSetTextureTest extends LegacyApplication {
//"Common/MatDefs/Aurora/Aurora.j3md"
//"Common/MatDefs/Misc/VKUnshaded.j3md"
private static final String MAT_DEF = "Common/MatDefs/Misc/VKUnshaded.j3md";
private final Node rootNode = new Node("Root");
private ViewPort viewPort;
private Camera cam;
private FlyByCamera flyCam;
private float fpsTime = 0f;
private int fpsFrames = 0;
// 新增:保存测试几何体,update 时可拿到 material
private Geometry testGeom;
@Override
public void initialize() {
super.initialize();
assetManager.registerLocator("/", ClasspathLocator.class);
assetManager.registerLoader(J3MLoader.class, "j3md", "j3m");
assetManager.registerLoader(BinaryImporter.class, "j3o");
cam = getCamera();
viewPort = renderManager.createMainView("Main", cam);
viewPort.setBackgroundColor(ColorRGBA.DarkGray);
viewPort.attachScene(rootNode);
cam.setLocation(new Vector3f(0f, 0f, 20f));
cam.setFrustumFar(1000f);
flyCam = new FlyByCamera(cam);
flyCam.setMoveSpeed(30f);
flyCam.setDragToRotate(true);
flyCam.registerWithInput(inputManager);
Box box = new Box(2f, 2f, 2f);
testGeom = new Geometry("MultiSetBox", box);
Material mat = new Material(assetManager, MAT_DEF);
Texture2D extra = loadPngFromFileNoJmeLoader("F:/JME/jmonkeyengine/jme3-core/src/main/resources/Common/Textures/MissingMaterial.png", true);
if (extra != null) {
mat.setTexture("ColorMap", extra);
//mat.setTexture("ExtraTex", extra);
}
// assetManager.registerLocator("F:/AYA2022年10月16日/Jme", com.jme3.asset.plugins.FileLocator.class);
// Spatial model = assetManager.loadModel("kasolia.j3o");
// model.setMaterial(mat);
// rootNode.attachChild(model);
testGeom.setMaterial(mat);
rootNode.attachChild(testGeom);
System.out.println("[App] mat.ExtraTex=" + mat.getParam("ExtraTex"));
System.out.println("[App] init done. children=" + rootNode.getQuantity()
+ ", ExtraTex=" + (extra != null));
}
private Texture loadTexture(String path) {
try {
TextureKey key = new TextureKey(path, false);
Texture tex = assetManager.loadTexture(key);
tex.setAnisotropicFilter(4);
return tex;
} catch (Exception e) {
System.err.println("[Warn] texture load failed: " + path + " -> " + e.getMessage());
return null;
}
}
@Override
public void update() {
super.update();
float tpf = timer.getTimePerFrame();
fpsTime += tpf;
fpsFrames++;
if (fpsTime >= 1.0f) {
int fps = Math.round(fpsFrames / fpsTime);
fpsTime = 0f;
fpsFrames = 0;
if (context != null) {
context.setTitle("Vulkan MultiSet Texture Test | FPS: " + fps);
}
}
rootNode.updateLogicalState(tpf);
rootNode.updateGeometricState();
if (context != null && context.isRenderable()) {
renderManager.render(tpf, true);
}
}
private static Texture2D loadPngFromFileNoJmeLoader(String absPath, boolean flipY) {
try {
BufferedImage bi = ImageIO.read(new File(absPath));
if (bi == null) {
return null;
}
int w = bi.getWidth();
int h = bi.getHeight();
ByteBuffer buf = BufferUtils.createByteBuffer(w * h * 4);
for (int row = 0; row < h; row++) {
int y = flipY ? (h - 1 - row) : row; // 关键:按需翻转Y
for (int x = 0; x < w; x++) {
int argb = bi.getRGB(x, y);
byte a = (byte) ((argb >> 24) & 0xFF);
byte r = (byte) ((argb >> 16) & 0xFF);
byte g = (byte) ((argb >> 8) & 0xFF);
byte b = (byte) (argb & 0xFF);
buf.put(r).put(g).put(b).put(a);
}
}
buf.flip();
Image img = new Image(Image.Format.RGBA8, w, h, buf, null, ColorSpace.Linear);
Texture2D tex = new Texture2D(img);
tex.setMinFilter(com.jme3.texture.Texture.MinFilter.BilinearNoMipMaps);
tex.setMagFilter(com.jme3.texture.Texture.MagFilter.Bilinear);
tex.setWrap(com.jme3.texture.Texture.WrapMode.Repeat);
return tex;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
JmeSystem.setSystemDelegate(new VulkanSystemDelegate());
AppSettings s = new AppSettings(true);
s.setCustomRenderer(com.jme3.renderer.vulkan.context.LwjglVulkanContext.class);
s.setWidth(1920);
s.setHeight(1080);
s.setTitle("Vulkan MultiSet Texture Test");
VulkanMultiSetTextureTest app = new VulkanMultiSetTextureTest();
app.setSettings(s);
app.start();
}
}
Made significant progress I attempted to modify a file named VKUnshaded.j3md for testing purposes.
#version 450
#define VULKAN_NATIVE 1
#import "Common/ShaderLib/VKGLSLCompat.glsllib"
#import "Common/ShaderLib/VKSkinning.glsllib"
// #import "Common/ShaderLib/VKInstancing.glsllib" // 先禁用,避免矩阵UBO冲突
#import "Common/ShaderLib/VKMorphAnim.glsllib"
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec2 inTexCoord;
layout(location = 2) in vec2 inTexCoord2;
layout(location = 3) in vec4 inColor;
layout(location = 0) out vec2 texCoord1;
layout(location = 1) out vec2 texCoord2;
layout(location = 2) out vec4 vertColor;
// 与 FS 对齐的主 UBO
layout(set = 0, binding = 0, std140) uniform JmeUniforms {
mat4 g_WorldViewProjectionMatrix;
vec4 m_Color;
vec4 g_Resolution;
vec4 g_Mouse;
vec4 g_Time;
};
#ifdef HAS_POINTSIZE
layout(set = 0, binding = 3) uniform PointSizeUbo {
float m_PointSize;
};
#endif
#if defined(HAS_COLORMAP) || (defined(HAS_LIGHTMAP) && !defined(SEPARATE_TEXCOORD))
#define NEED_TEXCOORD1
#endif
void main() {
#ifdef NEED_TEXCOORD1
texCoord1 = inTexCoord;
#endif
#ifdef SEPARATE_TEXCOORD
texCoord2 = inTexCoord2;
#endif
#ifdef HAS_VERTEXCOLOR
vertColor = inColor;
#else
vertColor = vec4(1.0);
#endif
#ifdef HAS_POINTSIZE
gl_PointSize = m_PointSize;
#endif
vec4 modelSpacePos = vec4(inPosition, 1.0);
// Phase-1:先注释,后续分批接回
// #ifdef NUM_MORPH_TARGETS
// Morph_Compute(modelSpacePos);
// #endif
//
// #ifdef NUM_BONES
// Skinning_Compute(modelSpacePos);
// #endif
gl_Position = g_WorldViewProjectionMatrix * modelSpacePos;
}
#version 450
#define VULKAN_NATIVE 1
#import "Common/ShaderLib/VKGLSLCompat.glsllib"
#if defined(HAS_GLOWMAP) || defined(HAS_COLORMAP) || (defined(HAS_LIGHTMAP) && !defined(SEPARATE_TEXCOORD))
#define NEED_TEXCOORD1
#endif
// 恢复基础材质/全局参数块(与 VS 统一 set/binding)
layout(set = 0, binding = 0, std140) uniform JmeUniforms {
mat4 g_WorldViewProjectionMatrix; // FS 不用,但保持跨阶段布局一致
vec4 m_Color; // HAS_COLOR 时使用
vec4 g_Resolution;
vec4 g_Mouse;
vec4 g_Time;
};
layout(set = 0, binding = 1) uniform sampler2D m_ColorMap;
layout(set = 0, binding = 2) uniform sampler2D m_LightMap;
#if defined(DISCARD_ALPHA)
layout(set = 0, binding = 3, std140) uniform AlphaParams {
float m_AlphaDiscardThreshold;
};
#endif
#ifdef DESATURATION
layout(set = 0, binding = 4, std140) uniform DesaturationParams {
float m_DesaturationValue;
};
#endif
layout(location = 0) in vec2 texCoord1;
layout(location = 1) in vec2 texCoord2;
layout(location = 2) in vec4 vertColor;
layout(location = 0) out vec4 fragColor;
void main() {
vec4 color = vec4(1.0);
#ifdef HAS_COLORMAP
color *= texture(m_ColorMap, texCoord1);
#endif
#ifdef HAS_VERTEXCOLOR
color *= vertColor;
#endif
#ifdef HAS_COLOR
color *= m_Color;
#endif
#ifdef HAS_LIGHTMAP
#ifdef SEPARATE_TEXCOORD
color.rgb *= texture(m_LightMap, texCoord2).rgb;
#else
color.rgb *= texture(m_LightMap, texCoord1).rgb;
#endif
#endif
#if defined(DISCARD_ALPHA)
if (color.a < m_AlphaDiscardThreshold) {
discard;
}
#endif
#ifdef DESATURATION
vec3 gray = vec3(dot(vec3(0.2126, 0.7152, 0.0722), color.rgb));
color.rgb = mix(color.rgb, gray, m_DesaturationValue);
#endif
fragColor = color;
}
@Override
public void updateBufferData(VertexBuffer vb) {
if (vb == null) {
return;
}
try {
runtime.invalidateMeshGpuByVertexBuffer(vb);
} catch (Throwable t) {
// 兜底全量失效
LOGGER.log(Level.WARNING, "[VKRenderer] precise invalidate failed, fallback to all, type="
+ vb.getBufferType(), t);
runtime.invalidateAllMeshGpu();
}
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("[VKRenderer] updateBufferData(VertexBuffer) -> precise invalidate, type="
+ vb.getBufferType());
}
}
Today, the function “updateBufferData” was also completed.
I am very satisfied with the current progress. I believe it won’t be long before the first version is released and then we can recruit volunteers to test it.
Discussion in the ATmosphere