Merge pull request 'main' (#117) from Onek8/LNXSDK:main into main

Reviewed-on: LeenkxTeam/LNXSDK#117
This commit is contained in:
2026-05-16 22:14:33 +00:00
15 changed files with 222 additions and 167 deletions

4
.gitignore vendored
View File

@ -2,4 +2,6 @@ __pycache__/
*.pyc *.pyc
*.DS_Store *.DS_Store
**/workspace.xml **/workspace.xml
**/vcs.xml **/vcs.xml
**/stderr.txt
**/kinc.dmp

Binary file not shown.

View File

@ -148,10 +148,9 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
vec3(1.0, 0.0, t.y), vec3(1.0, 0.0, t.y),
vec3(0.0, t.z, 0.0), vec3(0.0, t.z, 0.0),
vec3(t.w, 0.0, t.x)); vec3(t.w, 0.0, t.x));
const float PI = 3.1415926535; float ltcspec = ltcEvaluate(n, v, dotNV, p, invM, lightArea0, lightArea1, lightArea2, lightArea3);
float ltcspec = ltcEvaluate(n, v, dotNV, p, invM, lightArea0, lightArea1, lightArea2, lightArea3) / PI;
ltcspec *= textureLod(sltcMag, tuv, 0.0).a; ltcspec *= textureLod(sltcMag, tuv, 0.0).a;
float ltcdiff = ltcEvaluate(n, v, dotNV, p, mat3(1.0), lightArea0, lightArea1, lightArea2, lightArea3) / PI; float ltcdiff = ltcEvaluate(n, v, dotNV, p, mat3(1.0), lightArea0, lightArea1, lightArea2, lightArea3);
vec3 direct = albedo * ltcdiff + ltcspec * spec * 0.05; vec3 direct = albedo * ltcdiff + ltcspec * spec * 0.05;
#else #else
vec3 direct = lambertDiffuseBRDF(albedo, dotNL) + vec3 direct = lambertDiffuseBRDF(albedo, dotNL) +
@ -244,7 +243,7 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
#ifdef _ShadowMap #ifdef _ShadowMap
if (receiveShadow) { if (receiveShadow) {
#ifdef _SinglePoint #ifdef _SinglePoint
vec4 lPos = LWVPSpotArray[0] * vec4(p + n * bias * 2, 1.0); vec4 lPos = LWVPSpotArray[0] * vec4(p + n * bias * 10, 1.0);
direct *= shadowTest(shadowMapSpot[0], direct *= shadowTest(shadowMapSpot[0],
#ifdef _ShadowMapTransparent #ifdef _ShadowMapTransparent
shadowMapSpotTransparent[0], shadowMapSpotTransparent[0],
@ -256,7 +255,7 @@ vec3 sampleLight(const vec3 p, const vec3 n, const vec3 v, const float dotNV, co
); );
#endif #endif
#ifdef _Clusters #ifdef _Clusters
vec4 lPos = LWVPSpotArray[index] * vec4(p + n * bias * 2, 1.0); vec4 lPos = LWVPSpotArray[index] * vec4(p + n * bias * 10, 1.0);
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas
direct *= shadowTest( direct *= shadowTest(
#ifdef _ShadowMapTransparent #ifdef _ShadowMapTransparent
@ -436,10 +435,9 @@ vec3 sampleLightVoxels(const vec3 p, const vec3 n, const vec3 v, const float dot
vec3(1.0, 0.0, t.y), vec3(1.0, 0.0, t.y),
vec3(0.0, t.z, 0.0), vec3(0.0, t.z, 0.0),
vec3(t.w, 0.0, t.x)); vec3(t.w, 0.0, t.x));
const float PI = 3.1415926535; float ltcspec = ltcEvaluate(n, v, dotNV, p, invM, lightArea0, lightArea1, lightArea2, lightArea3);
float ltcspec = ltcEvaluate(n, v, dotNV, p, invM, lightArea0, lightArea1, lightArea2, lightArea3) / PI;
ltcspec *= textureLod(sltcMag, tuv, 0.0).a; ltcspec *= textureLod(sltcMag, tuv, 0.0).a;
float ltcdiff = ltcEvaluate(n, v, dotNV, p, mat3(1.0), lightArea0, lightArea1, lightArea2, lightArea3) / PI; float ltcdiff = ltcEvaluate(n, v, dotNV, p, mat3(1.0), lightArea0, lightArea1, lightArea2, lightArea3);
vec3 direct = albedo * ltcdiff + ltcspec * spec * 0.05; vec3 direct = albedo * ltcdiff + ltcspec * spec * 0.05;
#else #else
vec3 direct = lambertDiffuseBRDF(albedo, dotNL) + vec3 direct = lambertDiffuseBRDF(albedo, dotNL) +
@ -454,7 +452,7 @@ vec3 sampleLightVoxels(const vec3 p, const vec3 n, const vec3 v, const float dot
#ifdef _ShadowMap #ifdef _ShadowMap
if (receiveShadow) { if (receiveShadow) {
#ifdef _SinglePoint #ifdef _SinglePoint
vec4 lPos = LWVPSpot[0] * vec4(p + n * bias * 2, 1.0); vec4 lPos = LWVPSpot[0] * vec4(p + n * bias * 10, 1.0);
direct *= shadowTest(shadowMapSpot[0], direct *= shadowTest(shadowMapSpot[0],
#ifdef _ShadowMapTransparent #ifdef _ShadowMapTransparent
shadowMapSpotTransparent[0], shadowMapSpotTransparent[0],
@ -466,7 +464,7 @@ vec3 sampleLightVoxels(const vec3 p, const vec3 n, const vec3 v, const float dot
); );
#endif #endif
#ifdef _Clusters #ifdef _Clusters
vec4 lPos = LWVPSpot[index] * vec4(p + n * bias * 2, 1.0); vec4 lPos = LWVPSpot[index] * vec4(p + n * bias * 10, 1.0);
if (index == 0) direct *= shadowTest(shadowMapSpot[0], if (index == 0) direct *= shadowTest(shadowMapSpot[0],
#ifdef _ShadowMapTransparent #ifdef _ShadowMapTransparent
shadowMapSpotTransparent[0], shadowMapSpotTransparent[0],
@ -516,7 +514,7 @@ vec3 sampleLightVoxels(const vec3 p, const vec3 n, const vec3 v, const float dot
#ifdef _ShadowMap #ifdef _ShadowMap
if (receiveShadow) { if (receiveShadow) {
#ifdef _SinglePoint #ifdef _SinglePoint
vec4 lPos = LWVPSpotArray[0] * vec4(p + n * bias * 2, 1.0); vec4 lPos = LWVPSpotArray[0] * vec4(p + n * bias * 10, 1.0);
direct *= shadowTest(shadowMapSpot[0], direct *= shadowTest(shadowMapSpot[0],
#ifdef _ShadowMapTransparent #ifdef _ShadowMapTransparent
shadowMapSpotTransparent[0], shadowMapSpotTransparent[0],
@ -528,7 +526,7 @@ vec3 sampleLightVoxels(const vec3 p, const vec3 n, const vec3 v, const float dot
); );
#endif #endif
#ifdef _Clusters #ifdef _Clusters
vec4 lPos = LWVPSpotArray[index] * vec4(p + n * bias * 2, 1.0); vec4 lPos = LWVPSpotArray[index] * vec4(p + n * bias * 10, 1.0);
#ifdef _ShadowMapAtlas #ifdef _ShadowMapAtlas
direct *= shadowTest( direct *= shadowTest(
#ifdef _ShadowMapTransparent #ifdef _ShadowMapTransparent

View File

@ -97,12 +97,8 @@ class LightObject extends Object {
this.shadowMapScale = 1.0; this.shadowMapScale = 1.0;
#end #end
} }
else if (type == "point" || type == "area") { else //if (type == "point" || type == "area" || type == "spot") {
P = Mat4.persp(fov, 1, data.raw.near_plane, data.raw.far_plane); P = Mat4.persp(fov, 1, data.raw.near_plane, data.raw.far_plane);
}
else if (type == "spot") {
P = Mat4.persp(fov, 1, data.raw.near_plane, data.raw.far_plane);
}
Scene.active.lights.push(this); Scene.active.lights.push(this);
} }

View File

@ -19,12 +19,11 @@ class MeshObject extends Object {
#if lnx_particles #if lnx_particles
public var particleSystems: Array<ParticleSystem> = null; // Particle owner public var particleSystems: Array<ParticleSystem> = null; // Particle owner
public var render_emitter = true; public var render_emitter = true;
#end
#if lnx_gpu_particles #if lnx_gpu_particles
public var particleChildren: Array<MeshObject> = null;
public var particleOwner: MeshObject = null; // Particle object public var particleOwner: MeshObject = null; // Particle object
public var particleChildren: Array<MeshObject> = null;
public var particleIndex = -1; public var particleIndex = -1;
#end #end #end
public var cameraDistance: Float; public var cameraDistance: Float;
public var cameraList: Array<String> = null; public var cameraList: Array<String> = null;
public var screenSize = 0.0; public var screenSize = 0.0;

View File

@ -10,6 +10,8 @@ class Inc {
static var path: RenderPath; static var path: RenderPath;
public static var superSample = 1.0; public static var superSample = 1.0;
static var pointIndex = 0;
static var spotIndex = 0;
static var lastFrame = -1; static var lastFrame = -1;
#if lnx_shadowmap_atlas #if lnx_shadowmap_atlas
@ -164,7 +166,7 @@ class Inc {
continue; continue;
} }
for(k in 0...6) { for(k in 0...6) {
LightObject.pointLightsData[j ] = light.tileOffsetX[k]; // posx LightObject.pointLightsData[j ] = light.tileOffsetX[k]; // posx
LightObject.pointLightsData[j + 1] = light.tileOffsetY[k]; // posy LightObject.pointLightsData[j + 1] = light.tileOffsetY[k]; // posy
LightObject.pointLightsData[j + 2] = light.tileScale[k]; // tile scale factor relative to atlas LightObject.pointLightsData[j + 2] = light.tileScale[k]; // tile scale factor relative to atlas
LightObject.pointLightsData[j + 3] = 0; // padding LightObject.pointLightsData[j + 3] = 0; // padding
@ -177,39 +179,12 @@ class Inc {
} }
public static function bindShadowMapAtlas() { public static function bindShadowMapAtlas() {
var hasAtlas = false;
for (atlas in ShadowMapAtlas.shadowMapAtlases) { for (atlas in ShadowMapAtlas.shadowMapAtlases) {
path.bindTarget(atlas.target, atlas.target); path.bindTarget(atlas.target, atlas.target);
hasAtlas = true;
} }
if (!hasAtlas) {
#if lnx_shadowmap_atlas_single_map
path.bindTarget("empty_shadowmap", "shadowMapAtlas");
#else
path.bindTarget("empty_shadowmap", "shadowMapAtlasSun");
path.bindTarget("empty_shadowmap", "shadowMapAtlasPoint");
path.bindTarget("empty_shadowmap", "shadowMapAtlasSpot");
#end
}
#if rp_shadowmap_transparent #if rp_shadowmap_transparent
var hasAtlasT = false;
for (atlas in ShadowMapAtlas.shadowMapAtlasesTransparent) { for (atlas in ShadowMapAtlas.shadowMapAtlasesTransparent) {
path.bindTarget(atlas.target, atlas.target); path.bindTarget(atlas.target, atlas.target);
hasAtlasT = true;
}
if (!hasAtlasT) {
#if lnx_shadowmap_atlas_single_map
path.bindTarget("empty_shadowmap_transparent", "shadowMapAtlasTransparent");
#else
path.bindTarget("empty_shadowmap_transparent", "shadowMapAtlasSunTransparent");
path.bindTarget("empty_shadowmap_transparent", "shadowMapAtlasPointTransparent");
path.bindTarget("empty_shadowmap_transparent", "shadowMapAtlasSpotTransparent");
#end
} }
#end #end
} }
@ -271,11 +246,6 @@ class Inc {
} }
#end #end
} }
// update point light data before rendering
updatePointLightAtlasData(false);
#if rp_shadowmap_transparent
updatePointLightAtlasData(true);
#end
for (atlas in ShadowMapAtlas.shadowMapAtlases) { for (atlas in ShadowMapAtlas.shadowMapAtlases) {
tilesToRemove.resize(0); tilesToRemove.resize(0);
@ -352,6 +322,24 @@ class Inc {
} }
path.endStream(); path.endStream();
path.currentG = null; path.currentG = null;
updatePointLightAtlasData(false);
#if lnx_shadowmap_atlas_lod
for (tile in tilesToChangeSize) {
tilesToRemove.push(tile);
var newTile = ShadowMapTile.assignTiles(tile.light, atlas, tile);
if (newTile != null)
atlas.activeTiles.push(newTile);
}
updatePointLightAtlasData(false);
#end
for (tile in tilesToRemove) {
atlas.activeTiles.remove(tile);
tile.freeTile();
}
} }
#if rp_shadowmap_transparent #if rp_shadowmap_transparent
@ -432,6 +420,8 @@ class Inc {
path.currentG = null; path.currentG = null;
updatePointLightAtlasData(true);
#if lnx_shadowmap_atlas_lod #if lnx_shadowmap_atlas_lod
for (tile in tilesToChangeSize) { for (tile in tilesToChangeSize) {
tilesToRemove.push(tile); tilesToRemove.push(tile);
@ -461,39 +451,33 @@ class Inc {
path.bindTarget(n, n); path.bindTarget(n, n);
break; break;
} }
var lightIndex = 0; for (i in 0...pointIndex) {
for (l in iron.Scene.active.lights) { var n = "shadowMapPoint[" + i + "]";
if (iron.object.LightObject.discardLightCulled(l)) continue; path.bindTarget(n, n);
var n = "shadowMapPointTransparent[" + i + "]";
if (l.data.raw.type == "point") { path.bindTarget(n, n);
var n = "shadowMapPoint[" + lightIndex + "]"; }
path.bindTarget(n, n); for (i in 0...spotIndex) {
var n = "shadowMapPointTransparent[" + lightIndex + "]"; var n = "shadowMapSpot[" + i + "]";
path.bindTarget(n, n); path.bindTarget(n, n);
} var n = "shadowMapSpotTransparent[" + i + "]";
else if (l.data.raw.type == "spot" || l.data.raw.type == "area") { path.bindTarget(n, n);
var n = "shadowMapSpot[" + lightIndex + "]";
path.bindTarget(n, n);
var n = "shadowMapSpotTransparent[" + lightIndex + "]";
path.bindTarget(n, n);
}
lightIndex++;
} }
} }
static function shadowMapName(light: LightObject, index: Int, transparent: Bool): String { static function shadowMapName(light: LightObject, transparent: Bool): String {
switch (light.data.raw.type) { switch (light.data.raw.type) {
case "sun": case "sun":
return transparent ? "shadowMapTransparent" : "shadowMap"; return transparent ? "shadowMapTransparent" : "shadowMap";
case "point": case "point":
return transparent ? "shadowMapPointTransparent[" + index + "]" : "shadowMapPoint[" + index + "]"; return transparent ? "shadowMapPointTransparent[" + pointIndex + "]" : "shadowMapPoint[" + pointIndex + "]";
default: default:
return transparent ? "shadowMapSpotTransparent[" + index + "]" : "shadowMapSpot[" + index + "]"; return transparent ? "shadowMapSpotTransparent[" + spotIndex + "]" : "shadowMapSpot[" + spotIndex + "]";
} }
} }
static function getShadowMap(l: iron.object.LightObject, index: Int, transparent: Bool): String { static function getShadowMap(l: iron.object.LightObject, transparent: Bool): String {
var target = shadowMapName(l, index, transparent); var target = shadowMapName(l, transparent);
var rt = path.renderTargets.get(target); var rt = path.renderTargets.get(target);
// Create shadowmap on the fly // Create shadowmap on the fly
if (rt == null) { if (rt == null) {
@ -536,12 +520,13 @@ class Inc {
lastFrame = RenderPath.active.frame; lastFrame = RenderPath.active.frame;
#end #end
var lightIndex = 0; pointIndex = 0;
spotIndex = 0;
for (l in iron.Scene.active.lights) { for (l in iron.Scene.active.lights) {
if (!l.visible) continue; if (!l.visible) continue;
path.light = l; path.light = l;
var shadowmap = Inc.getShadowMap(l, lightIndex, false); var shadowmap = Inc.getShadowMap(l, false);
var faces = l.data.raw.shadowmap_cube ? 6 : 1; var faces = l.data.raw.shadowmap_cube ? 6 : 1;
for (i in 0...faces) { for (i in 0...faces) {
if (faces > 1) path.currentFace = i; if (faces > 1) path.currentFace = i;
@ -553,18 +538,18 @@ class Inc {
} }
path.currentFace = -1; path.currentFace = -1;
if (!iron.object.LightObject.discardLightCulled(l)) { if (l.data.raw.type == "point") pointIndex++;
lightIndex++; else if (l.data.raw.type == "spot" || l.data.raw.type == "area") spotIndex++;
}
} }
#if rp_shadowmap_transparent #if rp_shadowmap_transparent
lightIndex = 0; pointIndex = 0;
spotIndex = 0;
for (l in iron.Scene.active.lights) { for (l in iron.Scene.active.lights) {
if (!l.visible) continue; if (!l.visible) continue;
path.light = l; path.light = l;
var shadowmap_transparent = Inc.getShadowMap(l, lightIndex, true); var shadowmap_transparent = Inc.getShadowMap(l, true);
var faces = l.data.raw.shadowmap_cube ? 6 : 1; var faces = l.data.raw.shadowmap_cube ? 6 : 1;
for (i in 0...faces) { for (i in 0...faces) {
if (faces > 1) path.currentFace = i; if (faces > 1) path.currentFace = i;
@ -576,9 +561,8 @@ class Inc {
} }
path.currentFace = -1; path.currentFace = -1;
if (!iron.object.LightObject.discardLightCulled(l)) { if (l.data.raw.type == "point") pointIndex++;
lightIndex++; else if (l.data.raw.type == "spot" || l.data.raw.type == "area") spotIndex++;
}
} }
#end #end
#end // rp_shadowmap #end // rp_shadowmap

View File

@ -1104,7 +1104,6 @@ class RenderPathDeferred {
} }
#end #end
path.setTarget(target); path.setTarget(target);
path.clearTarget(0x00000000);
path.bindTarget("tex", "tex"); path.bindTarget("tex", "tex");
#if rp_compositordepth #if rp_compositordepth

View File

@ -754,8 +754,7 @@ class RenderPathForward {
} }
#end #end
path.setTarget(target); path.setTarget(target);
path.clearTarget(0x00000000);
#if rp_compositordepth #if rp_compositordepth
{ {
path.bindTarget("_main", "gbufferD"); path.bindTarget("_main", "gbufferD");

View File

@ -460,6 +460,9 @@ class RigidBody extends iron.Trait {
transform.loc.set(tx, ty, tz, 1.0); transform.loc.set(tx, ty, tz, 1.0);
transform.rot.set(tRot.x(), tRot.y(), tRot.z(), tRot.w()); transform.rot.set(tRot.x(), tRot.y(), tRot.z(), tRot.w());
} else {
transform.loc.set(currentPos.x(), currentPos.y(), currentPos.z(), 1.0);
transform.rot.set(currentRot.x(), currentRot.y(), currentRot.z(), currentRot.w());
} }
if (object.parent != null) { if (object.parent != null) {

View File

@ -495,8 +495,11 @@ class RigidBody extends Trait {
} }
var active = physics.bodyInterface.IsActive(bodyId); var active = physics.bodyInterface.IsActive(bodyId);
if (!active) if (!active) {
// Activate body if sleeping
physics.bodyInterface.ActivateBody(bodyId);
return; return;
}
// Read position and rotation from Jolt into cached state // Read position and rotation from Jolt into cached state
var p = physics.bodyInterface.GetPosition(bodyId); var p = physics.bodyInterface.GetPosition(bodyId);

View File

@ -111,6 +111,15 @@ FCURVE_TARGET_NAMES = {
current_output = None current_output = None
class BuildExportCache:
"""Shared cache across all scene exports in a single build.
Created once in make.py, passed to each LeenkxExporter instance."""
def __init__(self):
self.exported_mesh_files: set = set()
self.exported_action_files: set = set()
self.processed_mesh_names: set = set()
class LeenkxExporter: class LeenkxExporter:
"""Export to Leenkx format. """Export to Leenkx format.
@ -131,9 +140,11 @@ class LeenkxExporter:
# Class names of referenced traits # Class names of referenced traits
import_traits: List[str] = [] import_traits: List[str] = []
def __init__(self, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None): def __init__(self, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None, build_cache=None):
global current_output global current_output
self.build_cache = build_cache or BuildExportCache()
self.filepath = filepath self.filepath = filepath
self.scene = context.scene if scene is None else scene self.scene = context.scene if scene is None else scene
self.depsgraph = context.evaluated_depsgraph_get() if depsgraph is None else depsgraph self.depsgraph = context.evaluated_depsgraph_get() if depsgraph is None else depsgraph
@ -185,12 +196,12 @@ class LeenkxExporter:
LeenkxExporter.preprocess() LeenkxExporter.preprocess()
@classmethod @classmethod
def export_scene(cls, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None) -> None: def export_scene(cls, context: bpy.types.Context, filepath: str, scene: bpy.types.Scene = None, depsgraph: bpy.types.Depsgraph = None, build_cache=None) -> None:
"""Exports the given scene to the given file path. This is the """Exports the given scene to the given file path. This is the
function that is called in make.py and the entry point of the function that is called in make.py and the entry point of the
exporter.""" exporter."""
with lnx.profiler.Profile('profile_exporter.prof', lnx.utils.get_pref_or_default('profile_exporter', False)): with lnx.profiler.Profile('profile_exporter.prof', lnx.utils.get_pref_or_default('profile_exporter', False)):
cls(context, filepath, scene, depsgraph).execute() cls(context, filepath, scene, depsgraph, build_cache).execute()
@classmethod @classmethod
def preprocess(cls): def preprocess(cls):
@ -473,7 +484,6 @@ class LeenkxExporter:
if btype is not NodeType.MESH and LeenkxExporter.option_mesh_only: if btype is not NodeType.MESH and LeenkxExporter.option_mesh_only:
return return
is_local_to_linked_scene = bobject.name in self.scene.objects and bobject.name not in self.scene.collection.children and self.scene.library
if bobject.type == 'CAMERA' and bobject.library: if bobject.type == 'CAMERA' and bobject.library:
struct_name = bobject.name + '_' + (os.path.basename(self.scene.library.filepath) if self.scene.library else self.scene.name) struct_name = bobject.name + '_' + (os.path.basename(self.scene.library.filepath) if self.scene.library else self.scene.name)
else: else:
@ -1149,8 +1159,9 @@ class LeenkxExporter:
self.export_particle_system_ref(bobject.particle_systems[i], out_object) self.export_particle_system_ref(bobject.particle_systems[i], out_object)
aabb = bobject.data.lnx_aabb aabb = bobject.data.lnx_aabb
if aabb[0] == 0 and aabb[1] == 0 and aabb[2] == 0: if oid not in self.build_cache.processed_mesh_names or (aabb[0] == 0 and aabb[1] == 0 and aabb[2] == 0):
self.calc_aabb(bobject) self.calc_aabb(bobject)
self.build_cache.processed_mesh_names.add(oid)
out_object['dimensions'] = [aabb[0], aabb[1], aabb[2]] out_object['dimensions'] = [aabb[0], aabb[1], aabb[2]]
# shapeKeys = LeenkxExporter.get_shape_keys(objref) # shapeKeys = LeenkxExporter.get_shape_keys(objref)
@ -1295,7 +1306,7 @@ class LeenkxExporter:
skelobj.animation_data.action = action skelobj.animation_data.action = action
fp = self.get_meshes_file_path('action_' + armatureid + '_' + aname, compressed=LeenkxExporter.compress_enabled) fp = self.get_meshes_file_path('action_' + armatureid + '_' + aname, compressed=LeenkxExporter.compress_enabled)
assets.add(fp) assets.add(fp)
if not bdata.lnx_cached or not os.path.exists(fp): if (not bdata.lnx_cached or not os.path.exists(fp)) and fp not in self.build_cache.exported_action_files:
# Store action to use it after autobake was handled # Store action to use it after autobake was handled
original_action = action original_action = action
@ -1357,6 +1368,7 @@ class LeenkxExporter:
# Save action separately # Save action separately
action_obj = {'name': aname, 'objects': bones} action_obj = {'name': aname, 'objects': bones}
lnx.utils.write_lnx(fp, action_obj) lnx.utils.write_lnx(fp, action_obj)
self.build_cache.exported_action_files.add(fp)
# Use relative bone constraints # Use relative bone constraints
out_object['relative_bone_constraints'] = bdata.lnx_relative_bone_constraints out_object['relative_bone_constraints'] = bdata.lnx_relative_bone_constraints
@ -1696,6 +1708,7 @@ class LeenkxExporter:
mesh_obj = {'mesh_datas': [out_mesh]} mesh_obj = {'mesh_datas': [out_mesh]}
lnx.utils.write_lnx(fp, mesh_obj) lnx.utils.write_lnx(fp, mesh_obj)
bobject.data.lnx_cached = True bobject.data.lnx_cached = True
self.build_cache.exported_mesh_files.add(fp)
@staticmethod @staticmethod
def calc_aabb(bobject): def calc_aabb(bobject):
@ -2057,7 +2070,7 @@ class LeenkxExporter:
fp = self.get_meshes_file_path('mesh_' + oid, compressed=LeenkxExporter.compress_enabled) fp = self.get_meshes_file_path('mesh_' + oid, compressed=LeenkxExporter.compress_enabled)
assets.add(fp) assets.add(fp)
# No export necessary # No export necessary
if bobject.data.lnx_cached and os.path.exists(fp): if bobject.data.lnx_cached and os.path.exists(fp) or fp in self.build_cache.exported_mesh_files:
return return
# Mesh users have different modifier stack # Mesh users have different modifier stack
@ -2227,6 +2240,7 @@ class LeenkxExporter:
inner_angle = math.atan(math.tan(half_angle) * (1.0 - blend)) inner_angle = math.atan(math.tan(half_angle) * (1.0 - blend))
out_light['spot_size'] = outer_cos out_light['spot_size'] = outer_cos
out_light['spot_blend'] = max(0.0001, math.cos(inner_angle) - outer_cos) out_light['spot_blend'] = max(0.0001, math.cos(inner_angle) - outer_cos)
out_light['fov'] = light_ref.spot_size
if light_ref.shadow_soft_size > 0.0: if light_ref.shadow_soft_size > 0.0:
out_light['light_size'] = light_ref.shadow_soft_size * 10 out_light['light_size'] = light_ref.shadow_soft_size * 10
elif objtype == 'AREA': elif objtype == 'AREA':
@ -2282,7 +2296,6 @@ class LeenkxExporter:
# outside the collection, then instantiate the full object # outside the collection, then instantiate the full object
# child tree if the collection gets spawned as a whole # child tree if the collection gets spawned as a whole
if bobject.parent is None or bobject.parent.name not in collection.objects: if bobject.parent is None or bobject.parent.name not in collection.objects:
is_local_to_linked_scene = bobject.name in self.scene.objects and bobject.name not in self.scene.collection.children and self.scene.library
if bobject.type == 'CAMERA': if bobject.type == 'CAMERA':
asset_name = bobject.name + '_' + (os.path.basename(self.scene.library.filepath) if self.scene.library else self.scene.name) asset_name = bobject.name + '_' + (os.path.basename(self.scene.library.filepath) if self.scene.library else self.scene.name)
else: else:
@ -2925,7 +2938,7 @@ class LeenkxExporter:
if collection.name.startswith(('RigidBodyWorld', 'Trait|')): if collection.name.startswith(('RigidBodyWorld', 'Trait|')):
continue continue
if self.scene.user_of_id(collection) or collection.library and not self.scene.library or collection in self.referenced_collections: if self.scene.user_of_id(collection) or collection in self.referenced_collections:
if collection not in self.inlined_collections: if collection not in self.inlined_collections:
self.export_collection(collection) self.export_collection(collection)

View File

@ -19,6 +19,7 @@ import webbrowser
import bpy import bpy
from lnx import assets from lnx import assets
from lnx.exporter import BuildExportCache
from lnx.exporter import LeenkxExporter from lnx.exporter import LeenkxExporter
import lnx.lib.make_datas import lnx.lib.make_datas
import lnx.lib.server import lnx.lib.server
@ -265,19 +266,21 @@ def export_data_impl(fp, sdk_path):
continue continue
for o in scene.collection.all_objects: for o in scene.collection.all_objects:
if o.type in ('MESH', 'EMPTY'): if o.type in ('MESH', 'EMPTY'):
if o.name not in export_coll_names: if o.name not in export_coll_names or o.library:
export_coll.objects.link(o) export_coll.objects.link(o)
export_coll_names.add(o.name) export_coll_names.add(o.name)
depsgraph = bpy.context.evaluated_depsgraph_get() depsgraph = bpy.context.evaluated_depsgraph_get()
bpy.data.collections.remove(export_coll) # Destroy the "zoo" collection bpy.data.collections.remove(export_coll) # Destroy the "zoo" collection
build_cache = BuildExportCache()
for scene in bpy.data.scenes: for scene in bpy.data.scenes:
if scene.lnx_export: if scene.lnx_export:
# Reset shader comparison arrays to prevent cross-scene shader merging # Reset shader comparison arrays to prevent cross-scene shader merging
assets.reset_shader_cons() assets.reset_shader_cons()
ext = '.lz4' if LeenkxExporter.compress_enabled else '.lnx' ext = '.lz4' if LeenkxExporter.compress_enabled else '.lnx'
asset_path = build_dir + '/compiled/Assets/' + lnx.utils.safestr(scene.name + "_" + os.path.basename(scene.library.filepath).replace(".blend", "") if scene.library else scene.name) + ext asset_path = build_dir + '/compiled/Assets/' + lnx.utils.safestr(scene.name + "_" + os.path.basename(scene.library.filepath).replace(".blend", "") if scene.library else scene.name) + ext
LeenkxExporter.export_scene(bpy.context, asset_path, scene=scene, depsgraph=depsgraph) LeenkxExporter.export_scene(bpy.context, asset_path, scene=scene, depsgraph=depsgraph, build_cache=build_cache)
if LeenkxExporter.export_physics: if LeenkxExporter.export_physics:
physics_found = True physics_found = True
if LeenkxExporter.export_navigation: if LeenkxExporter.export_navigation:

View File

@ -8,6 +8,7 @@ import lnx.assets as assets
import lnx.log as log import lnx.log as log
import lnx.make_state as state import lnx.make_state as state
import lnx.utils import lnx.utils
from lnx.props_renderpath import auto_atlas_size
if lnx.is_reload(__name__): if lnx.is_reload(__name__):
lnx.api = lnx.reload_module(lnx.api) lnx.api = lnx.reload_module(lnx.api)
@ -68,12 +69,32 @@ def add_world_defs():
if rpdat.rp_shadowmap_atlas_single_map: if rpdat.rp_shadowmap_atlas_single_map:
assets.add_khafile_def('lnx_shadowmap_atlas_single_map') assets.add_khafile_def('lnx_shadowmap_atlas_single_map')
wrd.world_defs += '_SingleAtlas' wrd.world_defs += '_SingleAtlas'
assets.add_khafile_def('rp_shadowmap_atlas_max_size_point={0}'.format(int(rpdat.rp_shadowmap_atlas_max_size_point)))
assets.add_khafile_def('rp_shadowmap_atlas_max_size_spot={0}'.format(int(rpdat.rp_shadowmap_atlas_max_size_spot)))
assets.add_khafile_def('rp_shadowmap_atlas_max_size_sun={0}'.format(int(rpdat.rp_shadowmap_atlas_max_size_sun)))
assets.add_khafile_def('rp_shadowmap_atlas_max_size={0}'.format(int(rpdat.rp_shadowmap_atlas_max_size)))
assets.add_khafile_def('rp_max_lights_cluster={0}'.format(int(rpdat.rp_max_lights_cluster))) if rpdat.rp_shadowmap_atlas_auto:
max_lights = int(rpdat.rp_max_lights)
cube_size = int(rpdat.rp_shadowmap_cube)
cascade_size = int(rpdat.rp_shadowmap_cascade)
cascades = int(rpdat.rp_shadowmap_cascades)
auto_point = auto_atlas_size(max_lights, cube_size, 6)
auto_spot = auto_atlas_size(max_lights, cascade_size, 1)
auto_sun = auto_atlas_size(max_lights, cascade_size, cascades)
auto_max = max(auto_point, auto_spot, auto_sun)
assets.add_khafile_def('rp_shadowmap_atlas_max_size_point={0}'.format(auto_point))
assets.add_khafile_def('rp_shadowmap_atlas_max_size_spot={0}'.format(auto_spot))
assets.add_khafile_def('rp_shadowmap_atlas_max_size_sun={0}'.format(auto_sun))
assets.add_khafile_def('rp_shadowmap_atlas_max_size={0}'.format(auto_max))
else:
assets.add_khafile_def('rp_shadowmap_atlas_max_size_point={0}'.format(int(rpdat.rp_shadowmap_atlas_max_size_point)))
assets.add_khafile_def('rp_shadowmap_atlas_max_size_spot={0}'.format(int(rpdat.rp_shadowmap_atlas_max_size_spot)))
assets.add_khafile_def('rp_shadowmap_atlas_max_size_sun={0}'.format(int(rpdat.rp_shadowmap_atlas_max_size_sun)))
assets.add_khafile_def('rp_shadowmap_atlas_max_size={0}'.format(int(rpdat.rp_shadowmap_atlas_max_size)))
if rpdat.rp_shadowmap_atlas_auto:
assets.add_khafile_def('rp_max_lights_cluster={0}'.format(int(rpdat.rp_max_lights)))
else:
assets.add_khafile_def('rp_max_lights_cluster={0}'.format(int(rpdat.rp_max_lights_cluster)))
assets.add_khafile_def('rp_max_lights={0}'.format(int(rpdat.rp_max_lights))) assets.add_khafile_def('rp_max_lights={0}'.format(int(rpdat.rp_max_lights)))
if rpdat.rp_shadowmap_atlas_lod: if rpdat.rp_shadowmap_atlas_lod:
assets.add_khafile_def('lnx_shadowmap_atlas_lod') assets.add_khafile_def('lnx_shadowmap_atlas_lod')

View File

@ -1,5 +1,6 @@
from typing import Optional from typing import Optional
import math
import bpy import bpy
from bpy.props import * from bpy.props import *
@ -44,6 +45,18 @@ def update_point_atlas_size_options(scene: bpy.types.Scene, context: bpy.types.C
return atlas_sizes_from_min(int(rpdat.rp_shadowmap_cube) * 2) return atlas_sizes_from_min(int(rpdat.rp_shadowmap_cube) * 2)
def auto_atlas_size(max_lights: int, tile_size: int, tiles_per_light: int) -> int:
"""Automatically calculate the minimum atlas texture size needed to fit max_lights of a given type."""
tiles_needed = max_lights * tiles_per_light
grid_size = math.ceil(math.sqrt(tiles_needed))
needed_pixels = grid_size * tile_size
for size_entry in atlas_sizes:
size = int(size_entry[0])
if size >= needed_pixels:
return size
return int(atlas_sizes[-1][0])
def update_preset(self, context): def update_preset(self, context):
rpdat = self.lnx_rplist[-1] rpdat = self.lnx_rplist[-1]
@ -332,6 +345,7 @@ class LnxRPListItem(bpy.types.PropertyGroup):
('64', '64', '64'),], ('64', '64', '64'),],
name="Max Lights Shadows", description="Max number of rendered shadow maps that can be visible in the screen. Always equal or lower than Max Lights", default='16') name="Max Lights Shadows", description="Max number of rendered shadow maps that can be visible in the screen. Always equal or lower than Max Lights", default='16')
rp_shadowmap_atlas: BoolProperty(name="Shadow Map Atlasing", description="Group shadow maps of lights of the same type in the same texture", default=False, update=update_renderpath) rp_shadowmap_atlas: BoolProperty(name="Shadow Map Atlasing", description="Group shadow maps of lights of the same type in the same texture", default=False, update=update_renderpath)
rp_shadowmap_atlas_auto: BoolProperty(name="Automatic Atlasing", description="Automatically compute atlas sizes based on max lights and shadow map sizes", default=True, update=update_renderpath)
rp_shadowmap_atlas_single_map: BoolProperty(name="Shadow Map Atlas single map", description="Use a single texture for all different light types.", default=False, update=update_renderpath) rp_shadowmap_atlas_single_map: BoolProperty(name="Shadow Map Atlas single map", description="Use a single texture for all different light types.", default=False, update=update_renderpath)
rp_shadowmap_atlas_lod: BoolProperty(name="Shadow Map Atlas LOD (Experimental)", description="When enabled, the size of the shadow map will be determined on runtime based on the distance of the light to the camera", default=False, update=update_renderpath) rp_shadowmap_atlas_lod: BoolProperty(name="Shadow Map Atlas LOD (Experimental)", description="When enabled, the size of the shadow map will be determined on runtime based on the distance of the light to the camera", default=False, update=update_renderpath)
rp_shadowmap_transparent: BoolProperty(name="Transparency", description="Enable shadows for transparent objects", default=False, update=update_renderpath) rp_shadowmap_transparent: BoolProperty(name="Transparency", description="Enable shadows for transparent objects", default=False, update=update_renderpath)

View File

@ -1745,92 +1745,113 @@ class LNX_PT_RenderPathShadowsPanel(bpy.types.Panel):
layout.prop(rpdat, 'rp_shadowmap_atlas') layout.prop(rpdat, 'rp_shadowmap_atlas')
colatlas = layout.column() colatlas = layout.column()
colatlas.enabled = rpdat.rp_shadowmap_atlas colatlas.enabled = rpdat.rp_shadowmap_atlas
colatlas.prop(rpdat, 'rp_shadowmap_atlas_auto')
colatlas.prop(rpdat, 'rp_max_lights') colatlas.prop(rpdat, 'rp_max_lights')
colatlas.prop(rpdat, 'rp_max_lights_cluster')
if not rpdat.rp_shadowmap_atlas_auto:
colatlas.prop(rpdat, 'rp_max_lights_cluster')
if rpdat.rp_shadowmap_atlas_auto:
# Automatic mode: compute sizes from max lights
max_lights = int(rpdat.rp_max_lights)
cube_size = int(rpdat.rp_shadowmap_cube)
cascade_size = int(rpdat.rp_shadowmap_cascade)
cascades = int(rpdat.rp_shadowmap_cascades)
auto_point = lnx.props_renderpath.auto_atlas_size(max_lights, cube_size, 6)
auto_spot = lnx.props_renderpath.auto_atlas_size(max_lights, cascade_size, 1)
auto_sun = lnx.props_renderpath.auto_atlas_size(max_lights, cascade_size, cascades)
if auto_point > 2048 or auto_spot > 2048 or auto_sun > 2048:
size_warning = True
colatlas.prop(rpdat, 'rp_shadowmap_atlas_lod') colatlas.prop(rpdat, 'rp_shadowmap_atlas_lod')
colatlas_lod = colatlas.column() if rpdat.rp_shadowmap_atlas_lod:
colatlas_lod.enabled = rpdat.rp_shadowmap_atlas_lod colatlas_lod = colatlas.column()
colatlas_lod.prop(rpdat, 'rp_shadowmap_atlas_lod_subdivisions') colatlas_lod.prop(rpdat, 'rp_shadowmap_atlas_lod_subdivisions')
colatlas_lod_info = colatlas_lod.row()
colatlas_lod_info.alignment = 'RIGHT'
subdivs_list = self.compute_subdivs(int(rpdat.rp_shadowmap_cascade), int(rpdat.rp_shadowmap_atlas_lod_subdivisions))
subdiv_text = "Subdivisions for spot lights: " + ', '.join(map(str, subdivs_list))
colatlas_lod_info.label(text=subdiv_text, icon="IMAGE_ZDEPTH")
if not rpdat.rp_shadowmap_atlas_single_map:
colatlas_lod_info = colatlas_lod.row() colatlas_lod_info = colatlas_lod.row()
colatlas_lod_info.alignment = 'RIGHT' colatlas_lod_info.alignment = 'RIGHT'
subdivs_list = self.compute_subdivs(int(rpdat.rp_shadowmap_cube), int(rpdat.rp_shadowmap_atlas_lod_subdivisions)) subdivs_list = self.compute_subdivs(int(rpdat.rp_shadowmap_cascade), int(rpdat.rp_shadowmap_atlas_lod_subdivisions))
subdiv_text = "Subdivisions for point lights: " + ', '.join(map(str, subdivs_list)) subdiv_text = "Subdivisions for spot lights: " + ', '.join(map(str, subdivs_list))
colatlas_lod_info.label(text=subdiv_text, icon="IMAGE_ZDEPTH") colatlas_lod_info.label(text=subdiv_text, icon="IMAGE_ZDEPTH")
if not rpdat.rp_shadowmap_atlas_single_map:
colatlas_lod_info = colatlas_lod.row()
colatlas_lod_info.alignment = 'RIGHT'
subdivs_list = self.compute_subdivs(int(rpdat.rp_shadowmap_cube), int(rpdat.rp_shadowmap_atlas_lod_subdivisions))
subdiv_text = "Subdivisions for point lights: " + ', '.join(map(str, subdivs_list))
colatlas_lod_info.label(text=subdiv_text, icon="IMAGE_ZDEPTH")
size_warning = int(rpdat.rp_shadowmap_cascade) > 2048 or int(rpdat.rp_shadowmap_cube) > 2048 size_warning = int(rpdat.rp_shadowmap_cascade) > 2048 or int(rpdat.rp_shadowmap_cube) > 2048
colatlas.prop(rpdat, 'rp_shadowmap_atlas_single_map') colatlas.prop(rpdat, 'rp_shadowmap_atlas_single_map')
# show size for single texture if not rpdat.rp_shadowmap_atlas_auto:
if rpdat.rp_shadowmap_atlas_single_map: # show size for single texture
colatlas_single = colatlas.column() if rpdat.rp_shadowmap_atlas_single_map:
colatlas_single.prop(rpdat, 'rp_shadowmap_atlas_max_size') colatlas_single = colatlas.column()
if rpdat.rp_shadowmap_atlas_max_size != '': colatlas_single.prop(rpdat, 'rp_shadowmap_atlas_max_size')
atlas_size = int(rpdat.rp_shadowmap_atlas_max_size) if rpdat.rp_shadowmap_atlas_max_size != '':
shadowmap_size = int(rpdat.rp_shadowmap_cascade) atlas_size = int(rpdat.rp_shadowmap_atlas_max_size)
shadowmap_size = int(rpdat.rp_shadowmap_cascade)
if shadowmap_size > 2048: if shadowmap_size > 2048:
size_warning = True size_warning = True
point_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'point') point_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'point')
spot_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'spot') spot_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'spot')
dir_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'sun') dir_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'sun')
col = colatlas_single.row() col = colatlas_single.row()
col.alignment = 'RIGHT' col.alignment = 'RIGHT'
col.label(text=f'Enough space for { point_lights } point lights or { spot_lights } spot lights or { dir_lights } directional lights.') col.label(text=f'Enough space for { point_lights } point lights or { spot_lights } spot lights or { dir_lights } directional lights.')
else: else:
# show size for all types # show size for all types
colatlas_mixed = colatlas.column() colatlas_mixed = colatlas.column()
colatlas_mixed.prop(rpdat, 'rp_shadowmap_atlas_max_size_spot') colatlas_mixed.prop(rpdat, 'rp_shadowmap_atlas_max_size_spot')
if rpdat.rp_shadowmap_atlas_max_size_spot != '': if rpdat.rp_shadowmap_atlas_max_size_spot != '':
atlas_size = int(rpdat.rp_shadowmap_atlas_max_size_spot) atlas_size = int(rpdat.rp_shadowmap_atlas_max_size_spot)
shadowmap_size = int(rpdat.rp_shadowmap_cascade) shadowmap_size = int(rpdat.rp_shadowmap_cascade)
spot_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'spot') spot_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'spot')
if shadowmap_size > 2048: if shadowmap_size > 2048:
size_warning = True size_warning = True
col = colatlas_mixed.row() col = colatlas_mixed.row()
col.alignment = 'RIGHT' col.alignment = 'RIGHT'
col.label(text=f'Enough space for {spot_lights} spot lights.') col.label(text=f'Enough space for {spot_lights} spot lights.')
colatlas_mixed.prop(rpdat, 'rp_shadowmap_atlas_max_size_point') colatlas_mixed.prop(rpdat, 'rp_shadowmap_atlas_max_size_point')
if rpdat.rp_shadowmap_atlas_max_size_point != '': if rpdat.rp_shadowmap_atlas_max_size_point != '':
atlas_size = int(rpdat.rp_shadowmap_atlas_max_size_point) atlas_size = int(rpdat.rp_shadowmap_atlas_max_size_point)
shadowmap_size = int(rpdat.rp_shadowmap_cube) shadowmap_size = int(rpdat.rp_shadowmap_cube)
point_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'point') point_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'point')
if shadowmap_size > 2048: if shadowmap_size > 2048:
size_warning = True size_warning = True
col = colatlas_mixed.row() col = colatlas_mixed.row()
col.alignment = 'RIGHT' col.alignment = 'RIGHT'
col.label(text=f'Enough space for {point_lights} point lights.') col.label(text=f'Enough space for {point_lights} point lights.')
colatlas_mixed.prop(rpdat, 'rp_shadowmap_atlas_max_size_sun') colatlas_mixed.prop(rpdat, 'rp_shadowmap_atlas_max_size_sun')
if rpdat.rp_shadowmap_atlas_max_size_sun != '': if rpdat.rp_shadowmap_atlas_max_size_sun != '':
atlas_size = int(rpdat.rp_shadowmap_atlas_max_size_sun) atlas_size = int(rpdat.rp_shadowmap_atlas_max_size_sun)
shadowmap_size = int(rpdat.rp_shadowmap_cascade) shadowmap_size = int(rpdat.rp_shadowmap_cascade)
dir_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'sun') dir_lights = self.lights_number_atlas(rpdat, atlas_size, shadowmap_size, 'sun')
if shadowmap_size > 2048: if shadowmap_size > 2048:
size_warning = True size_warning = True
col = colatlas_mixed.row() col = colatlas_mixed.row()
col.alignment = 'RIGHT' col.alignment = 'RIGHT'
col.label(text=f'Enough space for {dir_lights} directional lights.') col.label(text=f'Enough space for {dir_lights} directional lights.')
# show warning when user picks a size higher than 2048 (arbitrary number). # show warning when user picks a size higher than 2048 (arbitrary number).
if size_warning: if size_warning: