Object Occlusion Culling

This commit is contained in:
2026-10-15 02:41:03 -07:00
parent 3273054766
commit 612a3d0a09
33 changed files with 3629 additions and 84 deletions

View File

@ -1133,6 +1133,11 @@ class LeenkxExporter:
}
out_object['properties'].append(out_property)
if bobject.lnx_is_occluder:
if 'properties' not in out_object:
out_object['properties'] = []
out_object['properties'].append({'name': 'occluder', 'value': True})
# Export the object reference and material references
objref = bobject.data
@ -2125,7 +2130,8 @@ class LeenkxExporter:
fp = self.get_meshes_file_path('mesh_' + oid, compressed=LeenkxExporter.compress_enabled)
assets.add(fp)
# No export necessary
if bobject.data.lnx_cached and os.path.exists(fp) or fp in self.build_cache.exported_mesh_files:
fp_json = fp.split('.lnx')[0] + '.json'
if bobject.data.lnx_cached and (os.path.exists(fp) or os.path.exists(fp_json)) or fp in self.build_cache.exported_mesh_files:
return
# Mesh users have different modifier stack
@ -2202,6 +2208,22 @@ class LeenkxExporter:
if armature:
self.export_skin(bobject, armature, export_mesh, out_mesh)
rpdat = lnx.utils.get_rp()
is_occluder = bobject.lnx_is_occluder
if not is_occluder:
for prop in bobject.lnx_propertylist:
if prop.name_prop == 'occluder' and getattr(prop, prop.type_prop + '_prop') == True:
is_occluder = True
break
use_global = False
if not is_occluder and rpdat is not None and rpdat.lnx_occlusion_all_objects:
is_occluder = rpdat.lnx_occlusion_min_size <= 0.0 or \
max(bobject.dimensions) >= rpdat.lnx_occlusion_min_size
use_global = is_occluder
if is_occluder and rpdat is not None and rpdat.lnx_occlusion_culling:
self.export_occluder_proxy(export_mesh, out_mesh, bobject, use_global)
# Restore the morph state after mesh export
if shape_keys:
bobject.active_shape_key_index = active_shape_key_index
@ -2311,6 +2333,139 @@ class LeenkxExporter:
def export_occluder_proxy(self, export_mesh: bpy.types.Mesh, out_mesh, bobject, use_global=False):
rpdat = lnx.utils.get_rp()
mode = bobject.lnx_occluder_mode
shrink = bobject.lnx_occluder_shrink
if use_global or mode == 'default':
mode = rpdat.lnx_occlusion_proxy_mode
if use_global or shrink < 0.0:
shrink = rpdat.lnx_occlusion_proxy_shrink
if mode == 'box':
proxy_positions, proxy_indices = self._box_proxy(export_mesh)
self._apply_inward_offset(proxy_positions, shrink)
elif mode == 'convex':
proxy_positions, proxy_indices = self._convex_proxy(export_mesh)
self._apply_inward_offset(proxy_positions, shrink)
else:
proxy_positions, proxy_indices = self._apply_decimate_to_mesh(
export_mesh, rpdat, shrink)
if len(proxy_indices) == 0:
return
out_mesh['occluder_proxy'] = {
'positions': proxy_positions,
'indices': proxy_indices
}
def _box_proxy(self, export_mesh: bpy.types.Mesh):
"""12-triangle axis-aligned bounding box proxy in local space."""
if len(export_mesh.vertices) == 0:
return [], []
xs = [v.co.x for v in export_mesh.vertices]
ys = [v.co.y for v in export_mesh.vertices]
zs = [v.co.z for v in export_mesh.vertices]
lo = (min(xs), min(ys), min(zs))
hi = (max(xs), max(ys), max(zs))
proxy_positions = [
lo[0], lo[1], lo[2], hi[0], lo[1], lo[2],
hi[0], hi[1], lo[2], lo[0], hi[1], lo[2],
lo[0], lo[1], hi[2], hi[0], lo[1], hi[2],
hi[0], hi[1], hi[2], lo[0], hi[1], hi[2]]
proxy_indices = [
0, 2, 1, 0, 3, 2,
4, 5, 6, 4, 6, 7,
0, 1, 5, 0, 5, 4,
2, 3, 7, 2, 7, 6,
1, 2, 6, 1, 6, 5,
0, 4, 7, 0, 7, 3]
return proxy_positions, proxy_indices
def _convex_proxy(self, export_mesh: bpy.types.Mesh):
proxy_positions = []
proxy_indices = []
bm = bmesh.new()
try:
bm.from_mesh(export_mesh)
bmesh.ops.convex_hull(bm, input=bm.verts)
remap = {}
for f in bm.faces:
fv = f.verts
for i in range(1, len(fv) - 1):
for v in (fv[0], fv[i], fv[i + 1]):
idx = remap.get(v.index)
if idx is None:
idx = len(remap)
remap[v.index] = idx
co = v.co
proxy_positions.append(co.x)
proxy_positions.append(co.y)
proxy_positions.append(co.z)
proxy_indices.append(idx)
finally:
bm.free()
return proxy_positions, proxy_indices
def _apply_inward_offset(self, positions, shrink_pct):
n = len(positions) // 3
if n == 0 or shrink_pct <= 0.0:
return
factor = 1.0 - shrink_pct * 0.01
cx = sum(positions[0::3]) / n
cy = sum(positions[1::3]) / n
cz = sum(positions[2::3]) / n
for i in range(n):
positions[i * 3] = cx + (positions[i * 3] - cx) * factor
positions[i * 3 + 1] = cy + (positions[i * 3 + 1] - cy) * factor
positions[i * 3 + 2] = cz + (positions[i * 3 + 2] - cz) * factor
def _apply_decimate_to_mesh(self, export_mesh: bpy.types.Mesh, rpdat, shrink):
temp_mesh = export_mesh.copy()
temp_obj = bpy.data.objects.new('lnx_decimate_temp', temp_mesh)
try:
self.scene.collection.objects.link(temp_obj)
mod = temp_obj.modifiers.new(name='lnx_decimate', type='DECIMATE')
mod.decimate_type = rpdat.lnx_occlusion_decimate_mode
if rpdat.lnx_occlusion_decimate_mode == 'COLLAPSE':
mod.ratio = rpdat.lnx_occlusion_decimate_ratio
elif rpdat.lnx_occlusion_decimate_mode == 'DISSOLVE':
mod.angle_limit = math.radians(
rpdat.lnx_occlusion_decimate_ratio * 180)
elif rpdat.lnx_occlusion_decimate_mode == 'UNSUBDIV':
mod.iterations = max(1, int(rpdat.lnx_occlusion_decimate_ratio * 10))
self.depsgraph.update()
eval_obj = temp_obj.evaluated_get(self.depsgraph)
eval_mesh = eval_obj.to_mesh(
preserve_all_data_layers=False, depsgraph=self.depsgraph)
try:
eval_mesh.calc_loop_triangles()
proxy_positions = []
for v in eval_mesh.vertices:
co = v.co
proxy_positions.append(co.x)
proxy_positions.append(co.y)
proxy_positions.append(co.z)
proxy_indices = []
for tri in eval_mesh.loop_triangles:
proxy_indices.append(tri.vertices[0])
proxy_indices.append(tri.vertices[1])
proxy_indices.append(tri.vertices[2])
self._apply_inward_offset(proxy_positions, shrink)
finally:
eval_obj.to_mesh_clear()
finally:
bpy.data.objects.remove(temp_obj, do_unlink=True)
bpy.data.meshes.remove(temp_mesh, do_unlink=True)
return proxy_positions, proxy_indices
def export_light(self, object_ref):
"""Exports a single light object."""
rpdat = lnx.utils.get_rp()
@ -2496,23 +2651,57 @@ class LeenkxExporter:
b = proj[1][1]
c = proj[2][2]
d = proj[2][3]
k = (c - 1.0) / (c + 1.0)
o['fov'] = 2.0 * math.atan(1.0 / b)
if abs(a) > 1e-6:
o['aspect'] = b / a
if with_planes:
o['near_plane'] = (d * (1.0 - k)) / (2.0 * k)
o['far_plane'] = k * o['near_plane']
denom = c + 1.0
k = (c - 1.0) / denom if abs(denom) >= 1e-6 else 0.0
if abs(k) < 1e-6:
o['near_plane'] = 0.1
o['far_plane'] = 1000.0
else:
near = (d * (1.0 - k)) / (2.0 * k)
far = k * near
o['near_plane'] = max(0.001, min(near, 100000.0))
o['far_plane'] = max(0.001, min(far, 100000.0))
@staticmethod
def extract_ortho(o, proj):
a = proj[0][0]
b = proj[1][1]
# left, right, bottom, top
o['ortho'] = [-(1 + proj[3][0]) / proj[0][0], \
(1 - proj[3][0]) / proj[0][0], \
-(1 + proj[3][1]) / proj[1][1], \
(1 - proj[3][1]) / proj[1][1]]
o['ortho'] = [-(1 + proj[3][0]) / a, \
(1 - proj[3][0]) / a, \
-(1 + proj[3][1]) / b, \
(1 - proj[3][1]) / b]
o['near_plane'] = (1 + proj[3][2]) / proj[2][2]
o['far_plane'] = -(1 - proj[3][2]) / proj[2][2]
o['near_plane'] *= 2
o['far_plane'] *= 2
if abs(a) > 1e-6:
o['aspect'] = b / a
@staticmethod
def compute_effective_par(rpdat, render):
if rpdat is None:
return render.pixel_aspect_x, render.pixel_aspect_y
mode = rpdat.lnx_aspect_mode
if mode == 'blender':
return render.pixel_aspect_x, render.pixel_aspect_y
elif mode == 'square':
return 1.0, 1.0
elif mode == 'ntsc_43':
return 0.889, 1.0
elif mode == 'pal_43':
return 1.067, 1.0
elif mode == 'ntsc_169':
return 1.212, 1.0
elif mode == 'pal_169':
return 1.459, 1.0
elif mode == 'custom':
return rpdat.lnx_aspect_custom, 1.0
return render.pixel_aspect_x, render.pixel_aspect_y
def export_camera(self, objectRef):
o = {}
@ -2521,12 +2710,16 @@ class LeenkxExporter:
camera = objectRef[1]["objectTable"][0]
render = self.scene.render
rpdat = lnx.utils.get_rp()
par_x, par_y = self.compute_effective_par(rpdat, render)
proj = camera.calc_matrix_camera(
self.depsgraph,
x=render.resolution_x,
y=render.resolution_y,
scale_x=render.pixel_aspect_x,
scale_y=render.pixel_aspect_y)
scale_x=par_x,
scale_y=par_y)
if objref.type == 'PERSP':
self.extract_projection(o, proj)
else:
@ -2594,7 +2787,7 @@ class LeenkxExporter:
self.output['material_datas'].append(o)
bpy.data.materials.remove(mat)
rpdat = lnx.utils.get_rp()
if not rpdat.lnx_culling:
if not rpdat.lnx_backface_culling:
o['override_context'] = {}
o['override_context']['cull_mode'] = 'none'
@ -2673,7 +2866,7 @@ class LeenkxExporter:
o['skip_context'] = material.lnx_skip_context
rpdat = lnx.utils.get_rp()
if material.lnx_two_sided or not rpdat.lnx_culling:
if material.lnx_two_sided or not rpdat.lnx_backface_culling:
o['override_context'] = {}
o['override_context']['cull_mode'] = 'none'
elif material.lnx_cull_mode != 'clockwise':
@ -3190,6 +3383,12 @@ class LeenkxExporter:
'clear_color': self.get_camera_clear_color()
}
render = self.scene.render
rpdat = lnx.utils.get_rp()
par_x, par_y = self.compute_effective_par(rpdat, render)
res_aspect = render.resolution_x / render.resolution_y if render.resolution_y > 0 else 1.0
effective_aspect = res_aspect * par_x / par_y
# Set viewport camera projection
proj, is_persp = self.get_viewport_projection_matrix()
if proj is not None:
@ -3197,6 +3396,9 @@ class LeenkxExporter:
self.extract_projection(out_camera, proj, with_planes=False)
else:
self.extract_ortho(out_camera, proj)
out_camera['aspect'] = effective_aspect
self.output['camera_datas'].append(out_camera)
out_object = {