This commit is contained in:
2026-04-27 19:21:50 -07:00
parent 98856b3f54
commit a3d5fa846b
16 changed files with 513 additions and 337 deletions

View File

@ -610,19 +610,30 @@ class LeenkxExporter:
return None return None
def write_bone_matrices(self, scene, action): def write_bone_matrices(self, scene, action):
# profile_time = time.time()
begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1]) begin_frame, end_frame = int(action.frame_range[0]), int(action.frame_range[1])
if len(self.bone_tracks) > 0: if len(self.bone_tracks) > 0:
for i in range(begin_frame, end_frame + 1): hidden_states = {}
scene.frame_set(i) try:
for track in self.bone_tracks: for obj in list(scene.collection.all_objects):
values, pose_bone = track[0], track[1] if obj is not None and obj.type != 'ARMATURE' and not obj.hide_viewport:
parent = pose_bone.parent hidden_states[obj] = False
if parent: obj.hide_viewport = True
values += LeenkxExporter.write_matrix((parent.matrix.inverted_safe() @ pose_bone.matrix))
else: for i in range(begin_frame, end_frame + 1):
values += LeenkxExporter.write_matrix(pose_bone.matrix) scene.frame_set(i)
# print('Bone matrices exported in ' + str(time.time() - profile_time)) for track in self.bone_tracks:
values, pose_bone = track[0], track[1]
parent = pose_bone.parent
if parent:
values += LeenkxExporter.write_matrix((parent.matrix.inverted_safe() @ pose_bone.matrix))
else:
values += LeenkxExporter.write_matrix(pose_bone.matrix)
finally:
for obj, was_hidden in hidden_states.items():
try:
obj.hide_viewport = was_hidden
except ReferenceError:
pass
@staticmethod @staticmethod
def has_baked_material(bobject, materials): def has_baked_material(bobject, materials):
@ -849,23 +860,36 @@ class LeenkxExporter:
out_object['tilesheet_ref'] = bobject.lnx_tilesheet out_object['tilesheet_ref'] = bobject.lnx_tilesheet
out_object['tilesheet_action_ref'] = bobject.lnx_tilesheet_action out_object['tilesheet_action_ref'] = bobject.lnx_tilesheet_action
if len(bobject.vertex_groups) > 0 and bobject.data is not None and len(bobject.data.vertices) > 0:
if len(bobject.vertex_groups) > 0:
out_object['vertex_groups'] = [] out_object['vertex_groups'] = []
_verts = bobject.data.vertices
_num_verts = len(_verts)
_all_co = np.empty(_num_verts * 3, dtype='<f8')
_verts.foreach_get('co', _all_co)
_all_str = _all_co.astype('U32').tolist()
_num_groups = len(bobject.vertex_groups)
_vg_idx = [[] for _ in range(_num_groups)]
for v in _verts:
vi = v.index
for g in v.groups:
gi = g.group
if 0 <= gi < _num_groups:
_vg_idx[gi].append(vi)
for group in bobject.vertex_groups: for group in bobject.vertex_groups:
verts = [] indices = _vg_idx[group.index]
for v in bobject.data.vertices: if len(indices) > 0:
for g in v.groups: _values = []
if g.group == group.index: for vi in indices:
verts.append(str(v.co.x)) base = vi * 3
verts.append(str(v.co.y)) _values.append(_all_str[base])
verts.append(str(v.co.z)) _values.append(_all_str[base + 1])
_values.append(_all_str[base + 2])
out_vertex_groups = { else:
_values = []
out_object['vertex_groups'].append({
'name': group.name, 'name': group.name,
'value': verts 'value': _values
} })
out_object['vertex_groups'].append(out_vertex_groups)
if len(bobject.lnx_camera_list) > 0: if len(bobject.lnx_camera_list) > 0:
out_camera_list = [] out_camera_list = []
@ -1114,13 +1138,27 @@ class LeenkxExporter:
_o.select_set(False) _o.select_set(False)
skelobj.select_set(True) skelobj.select_set(True)
bake_result = bpy.ops.nla.bake( _bake_hidden = {}
frame_start=int(action.frame_range[0]), try:
frame_end=int(action.frame_range[1]), for _obj in list(bpy.context.scene.collection.all_objects):
step=1, if _obj is not None and _obj.type != 'ARMATURE' and not _obj.hide_viewport:
only_selected=False, _bake_hidden[_obj] = False
visual_keying=True _obj.hide_viewport = True
)
bake_result = bpy.ops.nla.bake(
frame_start=int(action.frame_range[0]),
frame_end=int(action.frame_range[1]),
step=1,
only_selected=False,
visual_keying=True
)
finally:
for _obj, _was_hidden in _bake_hidden.items():
try:
_obj.hide_viewport = _was_hidden
except ReferenceError:
pass
action = skelobj.animation_data.action action = skelobj.animation_data.action
skelobj.select_set(False) skelobj.select_set(False)
@ -1225,44 +1263,67 @@ class LeenkxExporter:
else: else:
group_remap.append(-1) group_remap.append(-1)
bone_count_array = np.empty(len(export_mesh.loops), dtype='<i2') num_loops = len(export_mesh.loops)
bone_index_array = np.empty(len(export_mesh.loops) * 4, dtype='<i2') _loop_vidx = np.empty(num_loops, dtype='<i4')
bone_weight_array = np.empty(len(export_mesh.loops) * 4, dtype='<f4') export_mesh.loops.foreach_get('vertex_index', _loop_vidx)
num_verts = len(bobject.data.vertices)
verts = bobject.data.vertices
vert_bone_count = np.zeros(num_verts, dtype='<i2')
vert_bone_index = np.zeros((num_verts, 4), dtype='<i2')
vert_bone_weight = np.zeros((num_verts, 4), dtype='<f4')
_group_remap = group_remap
for vi in range(num_verts):
groups = verts[vi].groups
n = len(groups)
if n == 0:
continue
vertices = bobject.data.vertices
count = 0
for index, l in enumerate(export_mesh.loops):
bone_count = 0
total_weight = 0.0
bone_values = [] bone_values = []
for g in vertices[l.vertex_index].groups: total_weight = 0.0
bone_index = group_remap[g.group] for gi in range(n):
bone_weight = g.weight g = groups[gi]
if bone_index >= 0: #and bone_weight != 0.0: bone_index = _group_remap[g.group]
bone_values.append((bone_weight, bone_index)) if bone_index >= 0:
total_weight += bone_weight bone_values.append((g.weight, bone_index))
bone_count += 1 total_weight += g.weight
bone_count = len(bone_values)
if bone_count == 0:
continue
if bone_count > 4: if bone_count > 4:
bone_count = 4
bone_values.sort(reverse=True) bone_values.sort(reverse=True)
bone_values = bone_values[:4] bone_values = bone_values[:4]
bone_count = 4
bone_count_array[index] = bone_count total_weight = sum(bv[0] for bv in bone_values)
for bv in bone_values:
bone_weight_array[count] = bv[0]
bone_index_array[count] = bv[1]
count += 1
if total_weight not in (0.0, 1.0): if total_weight not in (0.0, 1.0):
normalizer = 1.0 / total_weight normalizer = 1.0 / total_weight
for i in range(bone_count): bone_values = [(bv[0] * normalizer, bv[1]) for bv in bone_values]
bone_weight_array[count - i - 1] *= normalizer
vert_bone_count[vi] = bone_count
for j in range(bone_count):
vert_bone_weight[vi, j] = bone_values[j][0]
vert_bone_index[vi, j] = bone_values[j][1]
bone_count_array = vert_bone_count[_loop_vidx]
loop_bone_weight = vert_bone_weight[_loop_vidx]
loop_bone_index = vert_bone_index[_loop_vidx]
loop_bone_count = bone_count_array
slot_idx = np.arange(4, dtype='<i2')
active_mask = slot_idx[np.newaxis, :] < loop_bone_count[:, np.newaxis]
bone_weight_array = loop_bone_weight[active_mask].astype('<f4')
bone_index_array = loop_bone_index[active_mask].astype('<i2')
bone_index_array = bone_index_array[:count]
bone_weight_array = bone_weight_array[:count]
bone_weight_array *= 32767 bone_weight_array *= 32767
bone_weight_array = np.array(bone_weight_array, dtype='<i2') bone_weight_array = bone_weight_array.astype('<i2')
oskin['bone_count_array'] = bone_count_array oskin['bone_count_array'] = bone_count_array
oskin['bone_index_array'] = bone_index_array oskin['bone_index_array'] = bone_index_array
@ -1291,6 +1352,8 @@ class LeenkxExporter:
shape_key_base = bobject.data.shape_keys.key_blocks[0] shape_key_base = bobject.data.shape_keys.key_blocks[0]
count = 0 count = 0
_base_co = None
_base_nor = None
# Loop through all shape keys # Loop through all shape keys
for shape_key in bobject.data.shape_keys.key_blocks[1:]: for shape_key in bobject.data.shape_keys.key_blocks[1:]:
@ -1299,7 +1362,13 @@ class LeenkxExporter:
# get vertex data from shape key # get vertex data from shape key
if shape_key.mute: if shape_key.mute:
continue continue
vert_data = self.get_vertex_data_from_shape_key(shape_key_base, shape_key) vert_data = self.get_vertex_data_from_shape_key(
shape_key_base, shape_key,
_base_co=_base_co, _base_nor=_base_nor)
# Cache base arrays after first call
if _base_co is None:
_base_co = vert_data['_base_co']
_base_nor = vert_data['_base_nor']
vert_pos.append(vert_data['pos']) vert_pos.append(vert_data['pos'])
vert_nor.append(vert_data['nor']) vert_nor.append(vert_data['nor'])
names.append(shape_key.name) names.append(shape_key.name)
@ -1342,37 +1411,39 @@ class LeenkxExporter:
morph_target['morph_target_ref'] = names morph_target['morph_target_ref'] = names
morph_target['morph_target_defaults'] = default_values morph_target['morph_target_defaults'] = default_values
morph_target['num_morph_targets'] = count morph_target['num_morph_targets'] = count
morph_target['morph_scale'] = max - min morph_target['morph_scale'] = float(max - min)
morph_target['morph_offset'] = min morph_target['morph_offset'] = float(min)
morph_target['morph_img_size'] = img_size morph_target['morph_img_size'] = int(img_size)
morph_target['morph_block_size'] = block_size morph_target['morph_block_size'] = int(block_size)
out_mesh['morph_target'] = morph_target out_mesh['morph_target'] = morph_target
return return
def get_vertex_data_from_shape_key(self, shape_key_base, shape_key_data): def get_vertex_data_from_shape_key(self, shape_key_base, shape_key_data, _base_co=None, _base_nor=None):
base_vert_pos = shape_key_base.data.values() num_verts = len(shape_key_data.data)
base_vert_nor = shape_key_base.normals_split_get()
vert_pos = shape_key_data.data.values()
vert_nor = shape_key_data.normals_split_get()
num_verts = len(vert_pos) sk_co = np.empty(num_verts * 3, dtype='<f4')
shape_key_data.data.foreach_get('co', sk_co)
sk_co = sk_co.reshape(num_verts, 3)
pos = [] if _base_co is None:
nor = [] _base_co = np.empty(num_verts * 3, dtype='<f4')
shape_key_base.data.foreach_get('co', _base_co)
_base_co = _base_co.reshape(num_verts, 3)
# Loop through all vertices pos_delta = sk_co - _base_co
for i in range(num_verts):
# Vertex position relative to base vertex
pos.append(list(vert_pos[i].co - base_vert_pos[i].co))
temp = []
for j in range(3):
# Vertex normal relative to base vertex
temp.append(vert_nor[j + i * 3] - base_vert_nor[j + i * 3])
nor.append(temp)
return {'pos': pos, 'nor': nor} sk_nor_raw = np.array(shape_key_data.normals_split_get(), dtype='<f4')
sk_nor = sk_nor_raw[:num_verts * 3].reshape(num_verts, 3)
if _base_nor is None:
base_nor_raw = np.array(shape_key_base.normals_split_get(), dtype='<f4')
_base_nor = base_nor_raw[:num_verts * 3].reshape(num_verts, 3)
nor_delta = sk_nor - _base_nor
return {'pos': pos_delta, 'nor': nor_delta, '_base_co': _base_co, '_base_nor': _base_nor}
def bake_to_image(self, pos_array, nor_array, pos_max, pos_min, extra_x, img_size, name, output_dir): def bake_to_image(self, pos_array, nor_array, pos_max, pos_min, extra_x, img_size, name, output_dir):
# Scale position data between [0, 1] to bake to image # Scale position data between [0, 1] to bake to image
@ -1387,21 +1458,19 @@ class LeenkxExporter:
def write_output_image(self, data, extra_x, img_size, name, output_dir): def write_output_image(self, data, extra_x, img_size, name, output_dir):
# Pad data with zeros to make up for required number of pixels of 2^n format # Pad data with zeros to make up for required number of pixels of 2^n format
data = np.pad(data, ((0, 0), (0, extra_x), (0, 0)), 'minimum') data = np.pad(data, ((0, 0), (0, int(extra_x)), (0, 0)), 'minimum')
pixel_list = []
for y in range(len(data)): total_pixels = data.shape[0] * data.shape[1]
for x in range(len(data[0])): rgba = np.ones((total_pixels, 4), dtype=np.float32)
# assign RGBA rgba[:, :3] = data.reshape(-1, 3)
pixel_list.append(data[y, x, 0])
pixel_list.append(data[y, x, 1])
pixel_list.append(data[y, x, 2])
pixel_list.append(1.0)
pixel_list = (pixel_list + [0] * (img_size * img_size * 4 - len(pixel_list))) img_total = img_size * img_size
if total_pixels < img_total:
padding = np.zeros((img_total - total_pixels, 4), dtype=np.float32)
rgba = np.concatenate((rgba, padding), axis=0)
image = bpy.data.images.new(name, width = img_size, height = img_size, is_data = True) image = bpy.data.images.new(name, width = img_size, height = img_size, is_data = True)
image.pixels = pixel_list image.pixels = rgba.ravel().tolist()
output_path = os.path.join(output_dir, name + ".png") output_path = os.path.join(output_dir, name + ".png")
image.save_render(output_path, scene= bpy.context.scene) image.save_render(output_path, scene= bpy.context.scene)
bpy.data.images.remove(image) bpy.data.images.remove(image)
@ -1579,30 +1648,28 @@ class LeenkxExporter:
# Scale for packed coords # Scale for packed coords
lay0 = uv_layers[t0map] lay0 = uv_layers[t0map]
maxdim_uvlayer = lay0 maxdim_uvlayer = lay0
for v in lay0.data: _uv0_raw = np.empty(len(lay0.data) * 2, dtype='<f4')
if abs(v.uv[0]) > maxdim: lay0.data.foreach_get('uv', _uv0_raw)
maxdim = abs(v.uv[0]) _uv0_absmax = float(np.abs(_uv0_raw).max()) if len(_uv0_raw) > 0 else 0.0
if abs(v.uv[1]) > maxdim: if _uv0_absmax > maxdim:
maxdim = abs(v.uv[1]) maxdim = _uv0_absmax
if has_tex1: if has_tex1:
lay1 = uv_layers[t1map] lay1 = uv_layers[t1map]
for v in lay1.data: _uv1_raw = np.empty(len(lay1.data) * 2, dtype='<f4')
if abs(v.uv[0]) > maxdim: lay1.data.foreach_get('uv', _uv1_raw)
maxdim = abs(v.uv[0]) _uv1_absmax = float(np.abs(_uv1_raw).max()) if len(_uv1_raw) > 0 else 0.0
maxdim_uvlayer = lay1 if _uv1_absmax > maxdim:
if abs(v.uv[1]) > maxdim: maxdim = _uv1_absmax
maxdim = abs(v.uv[1]) maxdim_uvlayer = lay1
maxdim_uvlayer = lay1
if has_morph_target: if has_morph_target:
morph_data = np.empty(num_verts * 2, dtype='<f4') morph_data = np.empty(num_verts * 2, dtype='<f4')
lay2 = uv_layers[morph_uv_index] lay2 = uv_layers[morph_uv_index]
for v in lay2.data: _uv2_raw = np.empty(len(lay2.data) * 2, dtype='<f4')
if abs(v.uv[0]) > maxdim: lay2.data.foreach_get('uv', _uv2_raw)
maxdim = abs(v.uv[0]) _uv2_absmax = float(np.abs(_uv2_raw).max()) if len(_uv2_raw) > 0 else 0.0
maxdim_uvlayer = lay2 if _uv2_absmax > maxdim:
if abs(v.uv[1]) > maxdim: maxdim = _uv2_absmax
maxdim = abs(v.uv[1]) maxdim_uvlayer = lay2
maxdim_uvlayer = lay2
if maxdim > 1: if maxdim > 1:
o['scale_tex'] = maxdim o['scale_tex'] = maxdim
invscale_tex = (1 / o['scale_tex']) * 32767 invscale_tex = (1 / o['scale_tex']) * 32767
@ -1636,102 +1703,87 @@ class LeenkxExporter:
invscale_pos = (1 / scale_pos) * 32767 invscale_pos = (1 / scale_pos) * 32767
verts = export_mesh.vertices verts = export_mesh.vertices
_all_co = np.empty(len(verts) * 3, dtype='<f4')
verts.foreach_get('co', _all_co)
_all_co = _all_co.reshape(-1, 3)
_loop_vidx = np.empty(num_verts, dtype='<i4')
loops.foreach_get('vertex_index', _loop_vidx)
_loop_co = _all_co[_loop_vidx]
_all_normals = np.empty(num_verts * 3, dtype='<f4')
loops.foreach_get('normal', _all_normals)
_all_normals = _all_normals.reshape(-1, 3)
pdata[0::4] = _loop_co[:, 0]
pdata[1::4] = _loop_co[:, 1]
pdata[2::4] = _loop_co[:, 2]
pdata[3::4] = _all_normals[:, 2] * scale_pos
ndata[0::2] = _all_normals[:, 0]
ndata[1::2] = _all_normals[:, 1]
if has_tex: if has_tex:
lay0 = export_mesh.uv_layers[t0map] lay0 = export_mesh.uv_layers[t0map]
lay0.data.foreach_get('uv', t0data)
t0data[1::2] = 1.0 - t0data[1::2]
if has_tex1: if has_tex1:
lay1 = export_mesh.uv_layers[t1map] lay1 = export_mesh.uv_layers[t1map]
lay1.data.foreach_get('uv', t1data)
t1data[1::2] = 1.0 - t1data[1::2]
if has_tang:
loops.foreach_get('tangent', tangdata)
if has_morph_target: if has_morph_target:
lay2 = export_mesh.uv_layers[morph_uv_index] lay2 = export_mesh.uv_layers[morph_uv_index]
lay2.data.foreach_get('uv', morph_data)
morph_data[1::2] = 1.0 - morph_data[1::2]
if has_col: if has_col:
vcol0 = self.get_nth_vertex_colors(export_mesh, 0).data vcol0 = self.get_nth_vertex_colors(export_mesh, 0).data
_col_raw = np.empty(len(vcol0) * 4, dtype='<f4')
loop: bpy.types.MeshLoop vcol0.foreach_get('color', _col_raw)
for i, loop in enumerate(loops): _col_raw = _col_raw.reshape(-1, 4)
v = verts[loop.vertex_index] cdata[0::3] = _col_raw[:, 0]
co = v.co cdata[1::3] = _col_raw[:, 1]
normal = loop.normal cdata[2::3] = _col_raw[:, 2]
tang = loop.tangent
i4 = i * 4
i2 = i * 2
pdata[i4 ] = co[0]
pdata[i4 + 1] = co[1]
pdata[i4 + 2] = co[2]
pdata[i4 + 3] = normal[2] * scale_pos # Cancel scale
ndata[i2 ] = normal[0]
ndata[i2 + 1] = normal[1]
if has_tex:
uv = lay0.data[loop.index].uv
t0data[i2 ] = uv[0]
t0data[i2 + 1] = 1.0 - uv[1] # Reverse Y
if has_tex1:
uv = lay1.data[loop.index].uv
t1data[i2 ] = uv[0]
t1data[i2 + 1] = 1.0 - uv[1]
if has_tang:
i3 = i * 3
tangdata[i3 ] = tang[0]
tangdata[i3 + 1] = tang[1]
tangdata[i3 + 2] = tang[2]
if has_morph_target:
uv = lay2.data[loop.index].uv
morph_data[i2 ] = uv[0]
morph_data[i2 + 1] = 1.0 - uv[1]
if has_col:
col = vcol0[loop.index].color
i3 = i * 3
cdata[i3 ] = col[0]
cdata[i3 + 1] = col[1]
cdata[i3 + 2] = col[2]
mats = export_mesh.materials mats = export_mesh.materials
poly_map = []
for i in range(max(len(mats), 1)):
poly_map.append([])
for poly in export_mesh.polygons:
poly_map[poly.material_index].append(poly)
o['index_arrays'] = [] o['index_arrays'] = []
# map polygon indices to triangle loops num_tris = len(export_mesh.loop_triangles)
tri_loops = {} if num_tris > 0:
for loop in export_mesh.loop_triangles: _tri_loops = np.empty(num_tris * 3, dtype='<i4')
if loop.polygon_index not in tri_loops: export_mesh.loop_triangles.foreach_get('loops', _tri_loops)
tri_loops[loop.polygon_index] = []
tri_loops[loop.polygon_index].append(loop)
for index, polys in enumerate(poly_map): _tri_polys = np.empty(num_tris, dtype='<i4')
tris = 0 export_mesh.loop_triangles.foreach_get('polygon_index', _tri_polys)
for poly in polys:
tris += poly.loop_total - 2
if tris == 0: # No face assigned
continue
prim = np.empty(tris * 3, dtype='<i4')
v_map = np.empty(tris * 3, dtype='<i4')
i = 0 num_polys = len(export_mesh.polygons)
for poly in polys: _poly_mat_idx = np.empty(num_polys, dtype='<i4')
for loop in tri_loops[poly.index]: export_mesh.polygons.foreach_get('material_index', _poly_mat_idx)
prim[i ] = loops[loop.loops[0]].index
prim[i + 1] = loops[loop.loops[1]].index
prim[i + 2] = loops[loop.loops[2]].index
i += 3
j = 0 _tri_mat_idx = _poly_mat_idx[_tri_polys]
for poly in polys:
for loop in tri_loops[poly.index]:
v_map[j ] = loops[loop.loops[0]].vertex_index
v_map[j + 1] = loops[loop.loops[1]].vertex_index
v_map[j + 2] = loops[loop.loops[2]].vertex_index
j += 3
ia = {'values': prim, 'material': 0, 'vertex_map': v_map} _tri_loops_2d = _tri_loops.reshape(-1, 3)
if len(mats) > 1:
for i in range(len(mats)): # Multi-mat mesh for mat_idx in range(max(len(mats), 1)):
if mats[i] == mats[index]: # Default material for empty slots mask = (_tri_mat_idx == mat_idx)
ia['material'] = i num_mat_tris = int(np.count_nonzero(mask))
break if num_mat_tris == 0:
o['index_arrays'].append(ia) continue
mat_tri_loops = _tri_loops_2d[mask].flatten().astype('<i4')
prim = mat_tri_loops.copy()
v_map = _loop_vidx[mat_tri_loops].astype('<i4')
ia = {'values': prim, 'material': 0, 'vertex_map': v_map}
if len(mats) > 1:
for i in range(len(mats)): # Multi-mat mesh
if mats[i] == mats[mat_idx]: # Default material for empty slots
ia['material'] = i
break
o['index_arrays'].append(ia)
# Pack # Pack
pdata *= invscale_pos pdata *= invscale_pos
@ -1821,7 +1873,6 @@ class LeenkxExporter:
def export_mesh(self, object_ref): def export_mesh(self, object_ref):
"""Exports a single mesh object.""" """Exports a single mesh object."""
# profile_time = time.time()
table = object_ref[1]["objectTable"] table = object_ref[1]["objectTable"]
bobject = table[0] bobject = table[0]
oid = lnx.utils.safestr(object_ref[1]["structName"]) oid = lnx.utils.safestr(object_ref[1]["structName"])
@ -1921,7 +1972,6 @@ class LeenkxExporter:
out_mesh['dynamic_usage'] = bobject.data.lnx_dynamic_usage out_mesh['dynamic_usage'] = bobject.data.lnx_dynamic_usage
self.write_mesh(bobject, fp, out_mesh) self.write_mesh(bobject, fp, out_mesh)
# print('Mesh exported in ' + str(time.time() - profile_time))
if hasattr(bobject, 'evaluated_get'): if hasattr(bobject, 'evaluated_get'):
bobject_eval.to_mesh_clear() bobject_eval.to_mesh_clear()
@ -2680,13 +2730,12 @@ class LeenkxExporter:
} }
# Set viewport camera projection # Set viewport camera projection
if is_viewport_camera: proj, is_persp = self.get_viewport_projection_matrix()
proj, is_persp = self.get_viewport_projection_matrix() if proj is not None:
if proj is not None: if is_persp:
if is_persp: self.extract_projection(out_camera, proj, with_planes=False)
self.extract_projection(out_camera, proj, with_planes=False) else:
else: self.extract_ortho(out_camera, proj)
self.extract_ortho(out_camera, proj)
self.output['camera_datas'].append(out_camera) self.output['camera_datas'].append(out_camera)
out_object = { out_object = {

View File

@ -43,6 +43,8 @@ _last_render_engine = None
def on_depsgraph_update_post(self): def on_depsgraph_update_post(self):
if state.proc_build is not None: if state.proc_build is not None:
return return
if state.is_exporting:
return
# Recache # Recache
depsgraph = bpy.context.evaluated_depsgraph_get() depsgraph = bpy.context.evaluated_depsgraph_get()

View File

@ -1,19 +1,75 @@
import atexit import atexit
import gzip
import http.server import http.server
import io
import os
import socketserver import socketserver
import subprocess import subprocess
haxe_server = None haxe_server = None
_GZIP_MIN_SIZE = 1400
_COMPRESSIBLE = {
'text/html', 'text/css', 'text/javascript', 'text/plain',
'application/javascript', 'application/json', 'application/xml',
'application/wasm', 'image/svg+xml',
}
def run_tcp(port: int, do_log: bool): def run_tcp(port: int, do_log: bool):
class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler): class HTTPRequestHandler(http.server.SimpleHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def log_message(self, format, *args): def log_message(self, format, *args):
if do_log: if do_log:
print(format % args) print(format % args)
def end_headers(self):
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Cache-Control', 'no-cache')
super().end_headers()
def do_GET(self):
ae = self.headers.get('Accept-Encoding', '')
if 'gzip' not in ae:
return super().do_GET()
fpath = self.translate_path(self.path)
if os.path.isdir(fpath):
return super().do_GET()
try:
ctype = self.guess_type(fpath)
if ctype.split(';')[0].strip() not in _COMPRESSIBLE:
return super().do_GET()
fsize = os.path.getsize(fpath)
if fsize < _GZIP_MIN_SIZE:
return super().do_GET()
with open(fpath, 'rb') as f:
raw = f.read()
buf = io.BytesIO()
with gzip.GzipFile(fileobj=buf, mode='wb', compresslevel=1) as gz:
gz.write(raw)
compressed = buf.getvalue()
self.send_response(200)
self.send_header('Content-Type', ctype)
self.send_header('Content-Length', str(len(compressed)))
self.send_header('Content-Encoding', 'gzip')
self.end_headers()
self.wfile.write(compressed)
except (FileNotFoundError, PermissionError):
self.send_error(404)
class ThreadedHTTPServer(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
try: try:
http_server = socketserver.TCPServer(("", port), HTTPRequestHandler) http_server = ThreadedHTTPServer(("", port), HTTPRequestHandler)
http_server.serve_forever() http_server.serve_forever()
except: except:
print("Server already running") print("Server already running")

View File

@ -5,7 +5,7 @@ from fractions import Fraction
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
if bpy.app.version < (4, 0, 0): if bpy.app.version < (4, 0, 0):
import bgl import bgl
def splitLogLuvAlphaAtlas(imageIn, outDir, quality): def splitLogLuvAlphaAtlas(imageIn, outDir, quality):
pass pass

View File

@ -2,8 +2,7 @@ import bpy, blf, os, gpu
from gpu_extras.batch import batch_for_shader from gpu_extras.batch import batch_for_shader
if bpy.app.version < (4, 0, 0): if bpy.app.version < (4, 0, 0):
import bgl import bgl
class ViewportDraw: class ViewportDraw:
def __init__(self, context, text): def __init__(self, context, text):

View File

@ -9,6 +9,7 @@ import shutil
import stat import stat
from string import Template from string import Template
import subprocess import subprocess
import tempfile
import threading import threading
import time import time
import traceback import traceback
@ -181,6 +182,14 @@ def clear_external_scenes():
appended_scenes = [] appended_scenes = []
def export_data(fp, sdk_path): def export_data(fp, sdk_path):
state.is_exporting = True
try:
export_data_impl(fp, sdk_path)
finally:
state.is_exporting = False
def export_data_impl(fp, sdk_path):
load_external_blends() load_external_blends()
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']
@ -316,7 +325,11 @@ def export_data(fp, sdk_path):
shaders_path = build_dir + '/compiled/Shaders' shaders_path = build_dir + '/compiled/Shaders'
if not os.path.exists(shaders_path): if not os.path.exists(shaders_path):
os.makedirs(shaders_path) os.makedirs(shaders_path)
write_data.write_compiledglsl(defs + cdefs, make_variants=has_config) inc_changed = write_data.write_compiledglsl(defs + cdefs, make_variants=has_config)
if inc_changed:
for g in glob.glob(shaders_path + '/*.glsl'):
os.utime(g, None)
# Write referenced shader passes # Write referenced shader passes
if not os.path.isfile(build_dir + '/compiled/Shaders/shader_datas.lnx') or state.last_world_defs != wrd.world_defs: if not os.path.isfile(build_dir + '/compiled/Shaders/shader_datas.lnx') or state.last_world_defs != wrd.world_defs:
@ -625,7 +638,7 @@ def _kill_all_viewport_processes():
for viewport_id in list(_viewport_processes.keys()): for viewport_id in list(_viewport_processes.keys()):
_kill_viewport_process(viewport_id) _kill_viewport_process(viewport_id)
if lnx.utils.get_os() == 'win': if lnx.utils.get_os() == 'win':
try: try:
result = subprocess.run( result = subprocess.run(
@ -643,20 +656,20 @@ def _kill_all_viewport_processes():
def run_viewport_runtime(viewport_id, width=1920, height=1080): def run_viewport_runtime(viewport_id, width=1920, height=1080):
"""Launch a viewport which gets its own Krom process with unique shared memory.""" """Launch a viewport which gets its own Krom process with unique shared memory."""
global _viewport_processes global _viewport_processes
if 'Lnx' not in bpy.data.worlds: if 'Lnx' not in bpy.data.worlds:
log.warn('No Lnx world found - cannot start viewport server') log.warn('No Lnx world found - cannot start viewport server')
return None, None return None, None
_kill_viewport_process(viewport_id) _kill_viewport_process(viewport_id)
shmem_name = _get_viewport_shmem_name(viewport_id) shmem_name = _get_viewport_shmem_name(viewport_id)
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']
krom_location, krom_path = lnx.utils.krom_paths() krom_location, krom_path = lnx.utils.krom_paths()
path = lnx.utils.get_fp_build() + '/debug/krom' path = lnx.utils.get_fp_build() + '/debug/krom'
path_resources = path + '-resources' path_resources = path + '-resources'
if not os.path.exists(path + '/krom.js'): if not os.path.exists(path + '/krom.js'):
log.warn(f'Krom build not found at {path}/krom.js - build project first') log.warn(f'Krom build not found at {path}/krom.js - build project first')
return None, None return None, None
@ -669,7 +682,7 @@ def run_viewport_runtime(viewport_id, width=1920, height=1080):
cmd.append(str(os.getpid())) cmd.append(str(os.getpid()))
if wrd.lnx_audio == 'Disabled': if wrd.lnx_audio == 'Disabled':
cmd.append('--nosound') cmd.append('--nosound')
cmd.append('--viewport-server') cmd.append('--viewport-server')
cmd.append('--shmem') cmd.append('--shmem')
cmd.append(shmem_name) cmd.append(shmem_name)
@ -703,13 +716,13 @@ def build_viewport(viewport_id, width=1920, height=1080):
if not wrd: if not wrd:
log.warn('No Lnx world found - cannot build for viewport') log.warn('No Lnx world found - cannot build for viewport')
return return
krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js' krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js'
if os.path.exists(krom_js_path) and not wrd.lnx_recompile: if os.path.exists(krom_js_path) and not wrd.lnx_recompile:
log.info(f'Using cached viewport build for {viewport_id}') log.info(f'Using cached viewport build for {viewport_id}')
run_viewport_runtime(viewport_id, width, height) run_viewport_runtime(viewport_id, width, height)
return return
pending_entry = (viewport_id, width, height) pending_entry = (viewport_id, width, height)
if pending_entry not in _viewport_pending_launches: if pending_entry not in _viewport_pending_launches:
_viewport_pending_launches.append(pending_entry) _viewport_pending_launches.append(pending_entry)
@ -719,7 +732,7 @@ def build_viewport(viewport_id, width=1920, height=1080):
# _viewport_proc_build checks viewport ,not state.proc_build (which is for play button) # _viewport_proc_build checks viewport ,not state.proc_build (which is for play button)
if _viewport_build_in_progress: if _viewport_build_in_progress:
if _viewport_proc_build is not None and _viewport_proc_build.poll() is None: if _viewport_proc_build is not None and _viewport_proc_build.poll() is None:
# log.info(f'Build in progress: {viewport_id} - launching when ready') log.info(f'Build in progress, viewport {viewport_id} will launch when ready')
return return
else: else:
log.info(f'Resetting stale viewport build state') log.info(f'Resetting stale viewport build state')
@ -738,20 +751,20 @@ def build_viewport(viewport_id, width=1920, height=1080):
state.is_play = False # NOT play mode state.is_play = False # NOT play mode
state.is_publish = False state.is_publish = False
state.is_export = False state.is_export = False
log.clear(clear_warnings=True, clear_errors=True) log.clear(clear_warnings=True, clear_errors=True)
sdk_path = lnx.utils.get_sdk_path() sdk_path = lnx.utils.get_sdk_path()
fp = lnx.utils.get_fp() fp = lnx.utils.get_fp()
os.chdir(fp) os.chdir(fp)
sources_path = 'Sources/' + lnx.utils.safestr(wrd.lnx_project_package) sources_path = 'Sources/' + lnx.utils.safestr(wrd.lnx_project_package)
if not os.path.exists(sources_path): if not os.path.exists(sources_path):
os.makedirs(sources_path) os.makedirs(sources_path)
log.info('Exporting scene data...') log.info('Exporting scene data...')
export_data(fp, sdk_path) export_data(fp, sdk_path)
log.info('Starting Krom compilation for viewport...') log.info('Starting Krom compilation for viewport...')
compile_viewport(assets_only=(not wrd.lnx_recompile)) compile_viewport(assets_only=(not wrd.lnx_recompile))
@ -811,7 +824,7 @@ def viewport_build_done():
if result == 0: if result == 0:
bpy.data.worlds['Lnx'].lnx_recompile = False bpy.data.worlds['Lnx'].lnx_recompile = False
if _viewport_pending_launches: if _viewport_pending_launches:
pending = _viewport_pending_launches.copy() pending = _viewport_pending_launches.copy()
_viewport_pending_launches.clear() _viewport_pending_launches.clear()
@ -829,18 +842,20 @@ def play_viewport(viewport_id, width=1920, height=1080):
global _viewport_build_in_progress, _viewport_pending_launches, _viewport_proc_build global _viewport_build_in_progress, _viewport_pending_launches, _viewport_proc_build
if not viewport_id: if not viewport_id:
log.error('No viewport_id: play_viewport requires an id')
return return
if 'Lnx' not in bpy.data.worlds: if 'Lnx' not in bpy.data.worlds:
log.error('No Lnx world found - cannot start viewport server')
return return
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']
krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js' krom_js_path = lnx.utils.get_fp_build() + '/debug/krom/krom.js'
if os.path.exists(krom_js_path) and not wrd.lnx_recompile: if os.path.exists(krom_js_path) and not wrd.lnx_recompile:
run_viewport_runtime(viewport_id, width, height) run_viewport_runtime(viewport_id, width, height)
return return
pending_entry = (viewport_id, width, height) pending_entry = (viewport_id, width, height)
if pending_entry not in _viewport_pending_launches: if pending_entry not in _viewport_pending_launches:
_viewport_pending_launches.append(pending_entry) _viewport_pending_launches.append(pending_entry)
@ -851,13 +866,13 @@ def play_viewport(viewport_id, width=1920, height=1080):
return return
else: else:
_viewport_build_in_progress = False # Reset stale state _viewport_build_in_progress = False # Reset stale state
_viewport_build_in_progress = True _viewport_build_in_progress = True
sdk_path = lnx.utils.get_sdk_path() sdk_path = lnx.utils.get_sdk_path()
fp = lnx.utils.get_fp() fp = lnx.utils.get_fp()
os.chdir(fp) os.chdir(fp)
sources_path = 'Sources/' + lnx.utils.safestr(wrd.lnx_project_package) sources_path = 'Sources/' + lnx.utils.safestr(wrd.lnx_project_package)
if not os.path.exists(sources_path): if not os.path.exists(sources_path):
os.makedirs(sources_path) os.makedirs(sources_path)
@ -867,7 +882,7 @@ def play_viewport(viewport_id, width=1920, height=1080):
state.is_publish = False state.is_publish = False
state.is_export = False state.is_export = False
export_data(fp, sdk_path) export_data(fp, sdk_path)
compile_viewport(assets_only=(not wrd.lnx_recompile)) compile_viewport(assets_only=(not wrd.lnx_recompile))
@ -982,16 +997,8 @@ def build_success():
if wrd.lnx_runtime == 'Browser': if wrd.lnx_runtime == 'Browser':
os.chdir(lnx.utils.get_fp()) os.chdir(lnx.utils.get_fp())
prefs = lnx.utils.get_lnx_preferences() prefs = lnx.utils.get_lnx_preferences()
host = 'localhost'
t = threading.Thread(name='localserver',
target=lnx.lib.server.run_tcp,
args=(prefs.html5_server_port,
prefs.html5_server_log),
daemon=True)
t.start()
build_dir = lnx.utils.build_dir() build_dir = lnx.utils.build_dir()
path = '{}/debug/html5/'.format(build_dir) path = '{}/debug/html5/'.format(build_dir)
url = 'http://{}:{}/{}'.format(host, prefs.html5_server_port, path)
browser = webbrowser.get() browser = webbrowser.get()
browsername = None browsername = None
if hasattr(browser, "name"): if hasattr(browser, "name"):
@ -1004,6 +1011,14 @@ def build_success():
if len(envcmd) == 0: if len(envcmd) == 0:
log.warn(f"Your {envvar} environment variable is set to an empty string") log.warn(f"Your {envvar} environment variable is set to an empty string")
else: else:
host = 'localhost'
t = threading.Thread(name='localserver',
target=lnx.lib.server.run_tcp,
args=(prefs.html5_server_port,
prefs.html5_server_log),
daemon=True)
t.start()
url = 'http://{}:{}/{}'.format(host, prefs.html5_server_port, path)
tplstr = Template(envcmd).safe_substitute({ tplstr = Template(envcmd).safe_substitute({
'host': host, 'host': host,
'port': prefs.html5_server_port, 'port': prefs.html5_server_port,
@ -1016,10 +1031,27 @@ def build_success():
}) })
cmd = re.split(' +', tplstr) cmd = re.split(' +', tplstr)
if len(cmd) == 0: if len(cmd) == 0:
# try file:// protocol with a Chromium-based browser
fast = browsername if browsername and any(b in browsername.lower() for b in ('chrome', 'chromium', 'edge', 'msedge')) else lnx.utils.find_browser()
if fast is not None:
file_url = 'file:///' + os.path.abspath(path + 'index.html').replace('\\', '/')
subprocess.Popen([fast, '--allow-file-access-from-files', '--no-first-run',
'--user-data-dir=' + os.path.join(tempfile.gettempdir(), 'leenkx_browser'),
file_url])
return
host = 'localhost'
t = threading.Thread(name='localserver',
target=lnx.lib.server.run_tcp,
args=(prefs.html5_server_port,
prefs.html5_server_log),
daemon=True)
t.start()
url = 'http://{}:{}/{}'.format(host, prefs.html5_server_port, path)
if browsername in (None, '', 'default'): if browsername in (None, '', 'default'):
webbrowser.open(url) webbrowser.open(url)
return return
cmd = [browsername, url] else:
cmd = [browsername, url]
elif wrd.lnx_runtime == 'Krom': elif wrd.lnx_runtime == 'Krom':
if wrd.lnx_live_patch: if wrd.lnx_live_patch:
live_patch.start() live_patch.start()

View File

@ -16,6 +16,7 @@ if not lnx.is_reload(__name__):
proc_publish_build = None proc_publish_build = None
mod_scripts = [] mod_scripts = []
is_export = False is_export = False
is_exporting = False
is_play = False is_play = False
is_publish = False is_publish = False
is_viewport = False is_viewport = False

View File

@ -1007,7 +1007,7 @@ def make_texture(
if max_size > 0 and image is not None: if max_size > 0 and image is not None:
original_filepath = filepath original_filepath = filepath
filepath = resize_texture_if_needed(image, filepath, max_size) filepath = resize_texture_if_needed(image, filepath, max_size)
if filepath != original_filepath: if filepath != original_filepath:
resized_filename = lnx.utils.extract_filename(filepath) resized_filename = lnx.utils.extract_filename(filepath)
tex['file'] = lnx.utils.safestr(resized_filename) tex['file'] = lnx.utils.safestr(resized_filename)

View File

@ -150,7 +150,7 @@ if bpy.app.version > (4, 1, 0):
if state.parse_surface: if state.parse_surface:
c.write_normal(node.inputs[5]) c.write_normal(node.inputs[5])
state.out_basecol = c.parse_vector_input(node.inputs[0]) state.out_basecol = c.parse_vector_input(node.inputs[0])
sss_input = node.inputs.get('Subsurface Weight') or node.inputs.get('Subsurface') sss_input = node.inputs.get('Subsurface Weight') or node.inputs.get('Subsurface')
if sss_input is not None: if sss_input is not None:
if sss_input.is_linked or sss_input.default_value > 0.0: if sss_input.is_linked or sss_input.default_value > 0.0:

View File

@ -310,7 +310,6 @@ def parse_tex_noise(node: bpy.types.ShaderNodeTexNoise, out_socket: bpy.types.No
if bpy.app.version < (5, 0, 0): if bpy.app.version < (5, 0, 0):
def parse_tex_pointdensity(node: bpy.types.ShaderNodeTexPointDensity, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]: def parse_tex_pointdensity(node: bpy.types.ShaderNodeTexPointDensity, out_socket: bpy.types.NodeSocket, state: ParserState) -> Union[floatstr, vec3str]:
# Pass through # Pass through
# Color # Color
if out_socket == node.outputs[0]: if out_socket == node.outputs[0]:
return c.to_vec3([0.0, 0.0, 0.0]) return c.to_vec3([0.0, 0.0, 0.0])

View File

@ -166,18 +166,14 @@ def material_needs_sss(material: Material) -> bool:
if sss_node is not None and sss_node.outputs[0].is_linked: if sss_node is not None and sss_node.outputs[0].is_linked:
return True return True
for sss_node in lnx.node_utils.iter_nodes_by_type(material.node_tree, 'BSDF_PRINCIPLED'):
if sss_node is not None and sss_node.outputs[0].is_linked and (sss_node.inputs[1].is_linked or sss_node.inputs[1].default_value != 0.0):
return True
for sss_node in mat_utils.iter_nodes_leenkxpbr(material.node_tree):
if sss_node is not None and sss_node.outputs[0].is_linked and (sss_node.inputs[8].is_linked or sss_node.inputs[8].default_value != 0.0):
return True
for principled_node in lnx.node_utils.iter_nodes_by_type(material.node_tree, 'BSDF_PRINCIPLED'): for principled_node in lnx.node_utils.iter_nodes_by_type(material.node_tree, 'BSDF_PRINCIPLED'):
if principled_node is not None and principled_node.outputs[0].is_linked: if principled_node is not None and principled_node.outputs[0].is_linked:
sss_input = principled_node.inputs.get('Subsurface Weight') or principled_node.inputs.get('Subsurface') sss_input = principled_node.inputs.get('Subsurface Weight') or principled_node.inputs.get('Subsurface')
if sss_input is not None and (sss_input.is_linked or sss_input.default_value > 0.0): if sss_input is not None and (sss_input.is_linked or sss_input.default_value > 0.0):
return True return True
for sss_node in mat_utils.iter_nodes_leenkxpbr(material.node_tree):
if sss_node is not None and sss_node.outputs[0].is_linked and (sss_node.inputs[8].is_linked or sss_node.inputs[8].default_value != 0.0):
return True
return False return False

View File

@ -94,15 +94,16 @@ def make(context_id, rpasses, shadowmap=False, shadowmap_transparent=False):
make_attrib.write_norpos(con_depth, vert) make_attrib.write_norpos(con_depth, vert)
frag.write_attrib('vec3 n = normalize(wnormal);') frag.write_attrib('vec3 n = normalize(wnormal);')
cycles.parse(mat_state.nodes, con_depth, vert, frag, geom, tesc, tese, basecol_only=True, parse_opacity=True) cycles.parse(mat_state.nodes, con_depth, vert, frag, geom, tesc, tese, basecol_only=True, parse_opacity=True)
elif parse_opacity: else:
frag.write('float opacity;') if parse_opacity:
frag.write('float ior;') frag.write('float opacity;')
frag.write('float ior;')
if(con_depth).is_elem('morph'): if(con_depth).is_elem('morph'):
make_morph_target.morph_pos(vert) make_morph_target.morph_pos(vert)
if con_depth.is_elem('bone'): if con_depth.is_elem('bone'):
make_skin.skin_pos(vert) make_skin.skin_pos(vert)
if (not is_disp and parse_custom_particle): if (not is_disp and parse_custom_particle):
cycles.parse(mat_state.nodes, con_depth, vert, frag, geom, tesc, tese, parse_surface=False, parse_opacity=parse_opacity) cycles.parse(mat_state.nodes, con_depth, vert, frag, geom, tesc, tese, parse_surface=False, parse_opacity=parse_opacity)

View File

@ -133,6 +133,7 @@ def build(material: Material, mat_users: Dict[Material, List[Object]], mat_lnxus
shader_data_path = lnx.utils.get_fp_build() + '/compiled/Shaders/' + shader_data_name + '.lnx' shader_data_path = lnx.utils.get_fp_build() + '/compiled/Shaders/' + shader_data_name + '.lnx'
assets.add_shader_data(shader_data_path) assets.add_shader_data(shader_data_path)
# Store SSS state in the return tuple so it's preserved per-material
needs_sss_result = mat_state.needs_sss needs_sss_result = mat_state.needs_sss
return rpasses, mat_state.data, shader_data_name, bind_constants, bind_textures, needs_sss_result return rpasses, mat_state.data, shader_data_name, bind_constants, bind_textures, needs_sss_result
@ -172,10 +173,9 @@ def write_shader(rel_path: str, shader: Shader, ext: str, rpass: str, matname: s
shader_path = lnx.utils.get_fp() + '/' + rel_path + '/' + shader_file shader_path = lnx.utils.get_fp() + '/' + rel_path + '/' + shader_file
assets.add_shader(shader_path) assets.add_shader(shader_path)
if not os.path.isfile(shader_path) or not keep_cache: if not os.path.isfile(shader_path) or not keep_cache:
with open(shader_path, 'w') as f: written = lnx.utils.write_if_changed(shader_path, shader.get())
f.write(shader.get())
if shader.noprocessing: if written and shader.noprocessing:
cwd = os.getcwd() cwd = os.getcwd()
os.chdir(lnx.utils.get_fp() + '/' + rel_path) os.chdir(lnx.utils.get_fp() + '/' + rel_path)
hlslbin_path = lnx.utils.get_sdk_path() + '/lib/leenkx_tools/hlslbin/hlslbin.exe' hlslbin_path = lnx.utils.get_sdk_path() + '/lib/leenkx_tools/hlslbin/hlslbin.exe'

View File

@ -154,14 +154,14 @@ def _deferred_set_rendered_mode():
_rendered_mode_pending = False _rendered_mode_pending = False
_rendered_mode_retries = 0 _rendered_mode_retries = 0
return None return None
space.shading.type = 'RENDERED' space.shading.type = 'RENDERED'
area.tag_redraw() area.tag_redraw()
_rendered_mode_pending = False _rendered_mode_pending = False
_rendered_mode_retries = 0 _rendered_mode_retries = 0
return None return None
_rendered_mode_retries += 1 _rendered_mode_retries += 1
if _rendered_mode_retries < 10: if _rendered_mode_retries < 10:
print(f"Viewport VIEW_3D not found, retrying... ({_rendered_mode_retries})") print(f"Viewport VIEW_3D not found, retrying... ({_rendered_mode_retries})")
@ -232,14 +232,14 @@ class KromViewportEngine(bpy.types.RenderEngine):
return return
current_mode = space.shading.type current_mode = space.shading.type
if self._last_shading_mode == 'SOLID' and current_mode == 'RENDERED': if self._last_shading_mode == 'SOLID' and current_mode == 'RENDERED':
self._initialized = False self._initialized = False
self._rendered_mode_set = False self._rendered_mode_set = False
self._krom_launch_attempted = False self._krom_launch_attempted = False
self._cleanup_shared_memory() self._cleanup_shared_memory()
self._pending_camera_sync = True self._pending_camera_sync = True
if self._last_shading_mode == 'RENDERED' and current_mode != 'RENDERED': if self._last_shading_mode == 'RENDERED' and current_mode != 'RENDERED':
self._rendered_mode_set = False self._rendered_mode_set = False
# keep krom running in solid mode for switching back ti rendered # keep krom running in solid mode for switching back ti rendered
@ -260,9 +260,9 @@ class KromViewportEngine(bpy.types.RenderEngine):
view_matrix = rv3d.view_matrix view_matrix = rv3d.view_matrix
proj_matrix = rv3d.window_matrix proj_matrix = rv3d.window_matrix
view_data = struct.pack('<16f', *[view_matrix[i][j] for j in range(4) for i in range(4)]) view_data = struct.pack('<16f', *[view_matrix[i][j] for j in range(4) for i in range(4)])
proj_data = struct.pack('<16f', *[proj_matrix[i][j] for j in range(4) for i in range(4)]) proj_data = struct.pack('<16f', *[proj_matrix[i][j] for j in range(4) for i in range(4)])
if sys.platform == 'win32': if sys.platform == 'win32':
@ -275,7 +275,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._shm.write(view_data) self._shm.write(view_data)
self._shm.seek(OFFSET_PROJ_MATRIX) self._shm.seek(OFFSET_PROJ_MATRIX)
self._shm.write(proj_data) self._shm.write(proj_data)
except Exception as e: except Exception as e:
print(f"Failed camera sync: {e}") print(f"Failed camera sync: {e}")
@ -295,11 +295,11 @@ class KromViewportEngine(bpy.types.RenderEngine):
if not self._viewport_id: if not self._viewport_id:
import time import time
self._viewport_id = hex(int(time.time() * 1000) % 0xFFFFFF)[-6:] self._viewport_id = hex(int(time.time() * 1000) % 0xFFFFFF)[-6:]
region = context.region region = context.region
width = region.width // 2 if region else 960 width = region.width // 2 if region else 960
height = region.height if region else 540 height = region.height if region else 540
viewport_id = self._viewport_id viewport_id = self._viewport_id
engine_id = self._engine_id engine_id = self._engine_id
shmem_name = _get_viewport_shmem_name(viewport_id) shmem_name = _get_viewport_shmem_name(viewport_id)
@ -331,7 +331,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._ensure_initialized() self._ensure_initialized()
if self._initialized: if self._initialized:
return True return True
if self._viewport_id is None and context is not None: if self._viewport_id is None and context is not None:
try: try:
space = context.space_data space = context.space_data
@ -339,11 +339,11 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._viewport_id = hex(space.as_pointer())[-6:] self._viewport_id = hex(space.as_pointer())[-6:]
except: except:
pass pass
if self._viewport_id is None: if self._viewport_id is None:
import time import time
self._viewport_id = hex(int(time.time() * 1000) % 0xFFFFFF)[-6:] self._viewport_id = hex(int(time.time() * 1000) % 0xFFFFFF)[-6:]
shmem_name = _get_viewport_shmem_name(self._viewport_id) shmem_name = _get_viewport_shmem_name(self._viewport_id)
self._shmem_name = shmem_name self._shmem_name = shmem_name
@ -356,7 +356,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
kernel32 = ctypes.windll.kernel32 kernel32 = ctypes.windll.kernel32
FILE_MAP_ALL_ACCESS = 0x000F001F FILE_MAP_ALL_ACCESS = 0x000F001F
kernel32.OpenFileMappingW.argtypes = [ctypes.wintypes.DWORD, ctypes.wintypes.BOOL, ctypes.wintypes.LPCWSTR] kernel32.OpenFileMappingW.argtypes = [ctypes.wintypes.DWORD, ctypes.wintypes.BOOL, ctypes.wintypes.LPCWSTR]
kernel32.OpenFileMappingW.restype = ctypes.wintypes.HANDLE kernel32.OpenFileMappingW.restype = ctypes.wintypes.HANDLE
@ -371,7 +371,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
kernel32.VirtualQuery.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t] kernel32.VirtualQuery.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
kernel32.VirtualQuery.restype = ctypes.c_size_t kernel32.VirtualQuery.restype = ctypes.c_size_t
handle = kernel32.OpenFileMappingW( handle = kernel32.OpenFileMappingW(
FILE_MAP_ALL_ACCESS, FILE_MAP_ALL_ACCESS,
False, False,
@ -395,7 +395,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
kernel32.CloseHandle(handle) kernel32.CloseHandle(handle)
print(f"Failed to map shared memory: {error}") print(f"Failed to map shared memory: {error}")
return False return False
self._shm_handle = handle self._shm_handle = handle
self._shm_ptr = ptr self._shm_ptr = ptr
@ -420,11 +420,10 @@ class KromViewportEngine(bpy.types.RenderEngine):
actual_size = mbi.RegionSize actual_size = mbi.RegionSize
self._shm_size = actual_size self._shm_size = actual_size
self._shm_buffer = (ctypes.c_ubyte * actual_size).from_address(ptr) self._shm_buffer = (ctypes.c_ubyte * actual_size).from_address(ptr)
self._initialized = True self._initialized = True
# print(f"Connected: {shmem_name}")
return True return True
except Exception as e: except Exception as e:
print(f"Failed to open shared memory: {e}") print(f"Failed to open shared memory: {e}")
@ -451,7 +450,6 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._shm_file = open(shm_path, 'r+b') self._shm_file = open(shm_path, 'r+b')
self._shm = mmap.mmap(self._shm_file.fileno(), max_size, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE) self._shm = mmap.mmap(self._shm_file.fileno(), max_size, mmap.MAP_SHARED, mmap.PROT_READ | mmap.PROT_WRITE)
self._initialized = True self._initialized = True
# print(f"Connected: {shm_path}")
return True return True
except Exception as e: except Exception as e:
print(f"Failed to open /dev/shm: {e}") print(f"Failed to open /dev/shm: {e}")
@ -548,7 +546,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
header = self._read_header() header = self._read_header()
if not header: if not header:
return None return None
if header['frame_id'] == self._last_frame_id: if header['frame_id'] == self._last_frame_id:
return None return None
@ -570,7 +568,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
pixel_buffer = (ctypes.c_ubyte * pixel_size)() pixel_buffer = (ctypes.c_ubyte * pixel_size)()
ctypes.memmove(pixel_buffer, self._shm_ptr + VIEWPORT_HEADER_SIZE, pixel_size) ctypes.memmove(pixel_buffer, self._shm_ptr + VIEWPORT_HEADER_SIZE, pixel_size)
pixels = bytes(pixel_buffer) pixels = bytes(pixel_buffer)
ready_bytes = struct.pack('<I', 0) ready_bytes = struct.pack('<I', 0)
ctypes.memmove(self._shm_ptr + OFFSET_READY_FLAG, ready_bytes, 4) ctypes.memmove(self._shm_ptr + OFFSET_READY_FLAG, ready_bytes, 4)
else: else:
@ -602,9 +600,9 @@ class KromViewportEngine(bpy.types.RenderEngine):
view_matrix = rv3d.view_matrix view_matrix = rv3d.view_matrix
proj_matrix = rv3d.window_matrix proj_matrix = rv3d.window_matrix
view_data = struct.pack('<16f', *[view_matrix[i][j] for j in range(4) for i in range(4)]) view_data = struct.pack('<16f', *[view_matrix[i][j] for j in range(4) for i in range(4)])
proj_data = struct.pack('<16f', *[proj_matrix[i][j] for j in range(4) for i in range(4)]) proj_data = struct.pack('<16f', *[proj_matrix[i][j] for j in range(4) for i in range(4)])
if sys.platform == 'win32': if sys.platform == 'win32':
@ -640,7 +638,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
dirty = struct.unpack('<I', dirty_data)[0] dirty = struct.unpack('<I', dirty_data)[0]
if dirty == 0: if dirty == 0:
return return
if sys.platform == 'win32': if sys.platform == 'win32':
pos_data = ctypes.string_at(self._shm_ptr + OFFSET_KROM_CAMERA_POS, 12) pos_data = ctypes.string_at(self._shm_ptr + OFFSET_KROM_CAMERA_POS, 12)
rot_data = ctypes.string_at(self._shm_ptr + OFFSET_KROM_CAMERA_ROT, 16) rot_data = ctypes.string_at(self._shm_ptr + OFFSET_KROM_CAMERA_ROT, 16)
@ -652,16 +650,16 @@ class KromViewportEngine(bpy.types.RenderEngine):
pos = struct.unpack('<3f', pos_data) pos = struct.unpack('<3f', pos_data)
rot = struct.unpack('<4f', rot_data) # quaternion (x, y, z, w) rot = struct.unpack('<4f', rot_data) # quaternion (x, y, z, w)
clear_data = struct.pack('<I', 0) clear_data = struct.pack('<I', 0)
if sys.platform == 'win32': if sys.platform == 'win32':
ctypes.memmove(self._shm_ptr + OFFSET_KROM_CAMERA_DIRTY, clear_data, 4) ctypes.memmove(self._shm_ptr + OFFSET_KROM_CAMERA_DIRTY, clear_data, 4)
else: else:
self._shm.seek(OFFSET_KROM_CAMERA_DIRTY) self._shm.seek(OFFSET_KROM_CAMERA_DIRTY)
self._shm.write(clear_data) self._shm.write(clear_data)
from mathutils import Quaternion, Matrix, Vector from mathutils import Quaternion, Matrix, Vector
rv3d = context.space_data.region_3d if context.space_data else None rv3d = context.space_data.region_3d if context.space_data else None
if not rv3d: if not rv3d:
return return
@ -670,12 +668,12 @@ class KromViewportEngine(bpy.types.RenderEngine):
quat = Quaternion((rot[3], rot[0], rot[1], rot[2])) quat = Quaternion((rot[3], rot[0], rot[1], rot[2]))
forward = quat @ Vector((0, 0, -1)) forward = quat @ Vector((0, 0, -1))
camera_pos = Vector(pos) camera_pos = Vector(pos)
view_dist = rv3d.view_distance if rv3d.view_distance > 0 else 10.0 view_dist = rv3d.view_distance if rv3d.view_distance > 0 else 10.0
target = camera_pos + forward * view_dist target = camera_pos + forward * view_dist
rv3d.view_location = target rv3d.view_location = target
rv3d.view_rotation = quat rv3d.view_rotation = quat
@ -687,7 +685,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._ensure_initialized() self._ensure_initialized()
if not self._initialized: if not self._initialized:
return return
if hasattr(self, '_last_requested_width') and hasattr(self, '_last_requested_height'): if hasattr(self, '_last_requested_width') and hasattr(self, '_last_requested_height'):
if self._last_requested_width == width and self._last_requested_height == height: if self._last_requested_width == width and self._last_requested_height == height:
return return
@ -764,23 +762,23 @@ class KromViewportEngine(bpy.types.RenderEngine):
# calculate event offset in ring buffer # calculate event offset in ring buffer
event_offset = OFFSET_INPUT_EVENTS + (write_idx % MAX_INPUT_EVENTS) * INPUT_EVENT_SIZE event_offset = OFFSET_INPUT_EVENTS + (write_idx % MAX_INPUT_EVENTS) * INPUT_EVENT_SIZE
event_data = struct.pack('<BBhhhI', event_type, button, x, y, delta, modifiers) event_data = struct.pack('<BBhhhI', event_type, button, x, y, delta, modifiers)
ctypes.memmove(self._shm_ptr + event_offset, event_data, len(event_data)) ctypes.memmove(self._shm_ptr + event_offset, event_data, len(event_data))
new_write_idx = struct.pack('<I', (write_idx + 1) % MAX_INPUT_EVENTS) new_write_idx = struct.pack('<I', (write_idx + 1) % MAX_INPUT_EVENTS)
ctypes.memmove(self._shm_ptr + OFFSET_INPUT_WRITE_IDX, new_write_idx, 4) ctypes.memmove(self._shm_ptr + OFFSET_INPUT_WRITE_IDX, new_write_idx, 4)
return True return True
else: else:
self._shm.seek(OFFSET_INPUT_WRITE_IDX) self._shm.seek(OFFSET_INPUT_WRITE_IDX)
write_idx = struct.unpack('<I', self._shm.read(4))[0] write_idx = struct.unpack('<I', self._shm.read(4))[0]
event_offset = OFFSET_INPUT_EVENTS + (write_idx % MAX_INPUT_EVENTS) * INPUT_EVENT_SIZE event_offset = OFFSET_INPUT_EVENTS + (write_idx % MAX_INPUT_EVENTS) * INPUT_EVENT_SIZE
self._shm.seek(event_offset) self._shm.seek(event_offset)
event_data = struct.pack('<BBhhhI', event_type, button, x, y, delta, modifiers) event_data = struct.pack('<BBhhhI', event_type, button, x, y, delta, modifiers)
self._shm.write(event_data) self._shm.write(event_data)
self._shm.seek(OFFSET_INPUT_WRITE_IDX) self._shm.seek(OFFSET_INPUT_WRITE_IDX)
self._shm.write(struct.pack('<I', (write_idx + 1) % MAX_INPUT_EVENTS)) self._shm.write(struct.pack('<I', (write_idx + 1) % MAX_INPUT_EVENTS))
return True return True
@ -799,7 +797,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
self._ensure_initialized() self._ensure_initialized()
try: try:
num_pixels = width * height * 4 num_pixels = width * height * 4
if len(pixels) < num_pixels: if len(pixels) < num_pixels:
return return
@ -823,7 +821,7 @@ class KromViewportEngine(bpy.types.RenderEngine):
else: else:
float_pixels.append(_srgb_to_linear_lut_list[p]) float_pixels.append(_srgb_to_linear_lut_list[p])
buffer = gpu.types.Buffer('FLOAT', len(float_pixels), float_pixels) buffer = gpu.types.Buffer('FLOAT', len(float_pixels), float_pixels)
self._texture = gpu.types.GPUTexture((width, height), format='RGBA32F', data=buffer) self._texture = gpu.types.GPUTexture((width, height), format='RGBA32F', data=buffer)
self._tex_width = width self._tex_width = width
self._tex_height = height self._tex_height = height
@ -856,11 +854,11 @@ class KromViewportEngine(bpy.types.RenderEngine):
was_initialized = self._initialized was_initialized = self._initialized
if not self._init_shared_memory(context): if not self._init_shared_memory(context):
if not self._krom_launch_attempted: if not self._krom_launch_attempted:
# print(f"Shared memory not found, launching Krom for viewport {self._viewport_id}") print(f"Shared memory not found, launching Krom for viewport {self._viewport_id}")
self._launch_krom_process(context) self._launch_krom_process(context)
self._draw_placeholder(context) self._draw_placeholder(context)
return return
if not was_initialized and self._initialized: if not was_initialized and self._initialized:
region = context.region region = context.region
if region and region.width > 0 and region.height > 0: if region and region.width > 0 and region.height > 0:
@ -879,13 +877,13 @@ class KromViewportEngine(bpy.types.RenderEngine):
# TODO: input forwarding when viewport is active # TODO: input forwarding when viewport is active
global _active_krom_engine, _active_krom_engines, _active_viewport_id, _input_operator_running global _active_krom_engine, _active_krom_engines, _active_viewport_id, _input_operator_running
_active_krom_engine = self # legacy single-engine reference _active_krom_engine = self # legacy single-engine reference
if self._viewport_id: if self._viewport_id:
_active_krom_engines[self._viewport_id] = self _active_krom_engines[self._viewport_id] = self
_active_viewport_id = self._viewport_id _active_viewport_id = self._viewport_id
self._set_input_enabled(True) self._set_input_enabled(True)
if not _input_operator_running: if not _input_operator_running:
bpy.app.timers.register(_start_input_capture, first_interval=0.1) bpy.app.timers.register(_start_input_capture, first_interval=0.1)
else: else:
@ -895,13 +893,13 @@ class KromViewportEngine(bpy.types.RenderEngine):
if self._input_check_counter > 60: if self._input_check_counter > 60:
self._input_check_counter = 0 self._input_check_counter = 0
bpy.app.timers.register(_start_input_capture, first_interval=0.5) bpy.app.timers.register(_start_input_capture, first_interval=0.5)
region = context.region region = context.region
if region and region.width > 0 and region.height > 0: if region and region.width > 0 and region.height > 0:
self._request_resize(region.width, region.height) self._request_resize(region.width, region.height)
self._read_krom_camera(context) self._read_krom_camera(context)
pixels = self._read_framebuffer() pixels = self._read_framebuffer()
if pixels: if pixels:
@ -910,16 +908,16 @@ class KromViewportEngine(bpy.types.RenderEngine):
if not self._texture: if not self._texture:
self._draw_placeholder(context) self._draw_placeholder(context)
return return
self._create_shader() self._create_shader()
if HAS_GPU_STATE: if HAS_GPU_STATE:
gpu.state.blend_set('NONE') gpu.state.blend_set('NONE')
else: else:
bgl.glDisable(bgl.GL_BLEND) bgl.glDisable(bgl.GL_BLEND)
region = context.region region = context.region
vertices = ( vertices = (
(0, 0), (0, 0),
(region.width, 0), (region.width, 0),
@ -1000,13 +998,13 @@ class KROM_OT_viewport_input(bpy.types.Operator):
pass pass
_input_operator_running = False _input_operator_running = False
return {'CANCELLED'} return {'CANCELLED'}
if event.type in {'SCREEN_SET', 'SCREEN_CHANGED'}: if event.type in {'SCREEN_SET', 'SCREEN_CHANGED'}:
return {'PASS_THROUGH'} return {'PASS_THROUGH'}
if not context.space_data or not context.area: if not context.space_data or not context.area:
return {'PASS_THROUGH'} return {'PASS_THROUGH'}
if context.space_data.type == 'VIEW_3D': if context.space_data.type == 'VIEW_3D':
if context.engine != 'KROM_VIEWPORT': if context.engine != 'KROM_VIEWPORT':
try: try:
@ -1016,18 +1014,18 @@ class KROM_OT_viewport_input(bpy.types.Operator):
pass pass
_input_operator_running = False _input_operator_running = False
return {'CANCELLED'} return {'CANCELLED'}
current_engine = self._get_engine_for_area(context, event) current_engine = self._get_engine_for_area(context, event)
if current_engine: if current_engine:
self._engine = current_engine self._engine = current_engine
try: try:
if not self._engine or not self._engine._initialized: if not self._engine or not self._engine._initialized:
return {'PASS_THROUGH'} return {'PASS_THROUGH'}
except ReferenceError: except ReferenceError:
_input_operator_running = False _input_operator_running = False
return {'CANCELLED'} return {'CANCELLED'}
region = context.region region = context.region
if not region: if not region:
return {'PASS_THROUGH'} return {'PASS_THROUGH'}
@ -1041,7 +1039,7 @@ class KROM_OT_viewport_input(bpy.types.Operator):
if not mouse_in_region: if not mouse_in_region:
return {'PASS_THROUGH'} return {'PASS_THROUGH'}
area = context.area area = context.area
if area: if area:
mouse_x_abs = event.mouse_x mouse_x_abs = event.mouse_x
@ -1059,14 +1057,14 @@ class KROM_OT_viewport_input(bpy.types.Operator):
mouse_x = event.mouse_region_x mouse_x = event.mouse_region_x
# flip Y for Krom # flip Y for Krom
mouse_y = region.height - event.mouse_region_y mouse_y = region.height - event.mouse_region_y
try: try:
if event.type == 'MOUSEMOVE': if event.type == 'MOUSEMOVE':
self._engine._send_input_event(INPUT_EVENT_MOUSE_MOVE, x=mouse_x, y=mouse_y) self._engine._send_input_event(INPUT_EVENT_MOUSE_MOVE, x=mouse_x, y=mouse_y)
self._last_mouse_x = mouse_x self._last_mouse_x = mouse_x
self._last_mouse_y = mouse_y self._last_mouse_y = mouse_y
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
if event.type == 'LEFTMOUSE': if event.type == 'LEFTMOUSE':
if event.value == 'PRESS': if event.value == 'PRESS':
self._engine._send_input_event(INPUT_EVENT_MOUSE_DOWN, button=MOUSE_BUTTON_LEFT, x=mouse_x, y=mouse_y) self._engine._send_input_event(INPUT_EVENT_MOUSE_DOWN, button=MOUSE_BUTTON_LEFT, x=mouse_x, y=mouse_y)
@ -1087,14 +1085,14 @@ class KROM_OT_viewport_input(bpy.types.Operator):
elif event.value == 'RELEASE': elif event.value == 'RELEASE':
self._engine._send_input_event(INPUT_EVENT_MOUSE_UP, button=MOUSE_BUTTON_MIDDLE, x=mouse_x, y=mouse_y) self._engine._send_input_event(INPUT_EVENT_MOUSE_UP, button=MOUSE_BUTTON_MIDDLE, x=mouse_x, y=mouse_y)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
if event.type == 'WHEELUPMOUSE': if event.type == 'WHEELUPMOUSE':
self._engine._send_input_event(INPUT_EVENT_MOUSE_WHEEL, delta=1) self._engine._send_input_event(INPUT_EVENT_MOUSE_WHEEL, delta=1)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
if event.type == 'WHEELDOWNMOUSE': if event.type == 'WHEELDOWNMOUSE':
self._engine._send_input_event(INPUT_EVENT_MOUSE_WHEEL, delta=-1) self._engine._send_input_event(INPUT_EVENT_MOUSE_WHEEL, delta=-1)
return {'RUNNING_MODAL'} return {'RUNNING_MODAL'}
if event.value in {'PRESS', 'RELEASE'}: if event.value in {'PRESS', 'RELEASE'}:
key_code = self._blender_to_krom_key(event.type) key_code = self._blender_to_krom_key(event.type)
if key_code is not None: if key_code is not None:
@ -1106,7 +1104,7 @@ class KROM_OT_viewport_input(bpy.types.Operator):
except ReferenceError: except ReferenceError:
_input_operator_running = False _input_operator_running = False
return {'CANCELLED'} return {'CANCELLED'}
return {'PASS_THROUGH'} return {'PASS_THROUGH'}
def _blender_to_krom_key(self, blender_key): def _blender_to_krom_key(self, blender_key):
@ -1136,15 +1134,15 @@ class KROM_OT_viewport_input(bpy.types.Operator):
if event: if event:
mouse_x = event.mouse_x mouse_x = event.mouse_x
mouse_y = event.mouse_y mouse_y = event.mouse_y
for window in bpy.context.window_manager.windows: for window in bpy.context.window_manager.windows:
for area in window.screen.areas: for area in window.screen.areas:
if area.type != 'VIEW_3D': if area.type != 'VIEW_3D':
continue continue
if (area.x <= mouse_x < area.x + area.width and if (area.x <= mouse_x < area.x + area.width and
area.y <= mouse_y < area.y + area.height): area.y <= mouse_y < area.y + area.height):
for space in area.spaces: for space in area.spaces:
if space.type == 'VIEW_3D': if space.type == 'VIEW_3D':
try: try:
@ -1154,7 +1152,7 @@ class KROM_OT_viewport_input(bpy.types.Operator):
except: except:
pass pass
break break
if context.space_data: if context.space_data:
try: try:
viewport_id = hex(context.space_data.as_pointer())[-6:] viewport_id = hex(context.space_data.as_pointer())[-6:]
@ -1162,10 +1160,10 @@ class KROM_OT_viewport_input(bpy.types.Operator):
return _active_krom_engines[viewport_id] return _active_krom_engines[viewport_id]
except: except:
pass pass
if _active_viewport_id and _active_viewport_id in _active_krom_engines: if _active_viewport_id and _active_viewport_id in _active_krom_engines:
return _active_krom_engines[_active_viewport_id] return _active_krom_engines[_active_viewport_id]
return _active_krom_engine return _active_krom_engine
def invoke(self, context, event): def invoke(self, context, event):
@ -1209,7 +1207,7 @@ def get_panels():
'VIEWLAYER_PT_filter', 'VIEWLAYER_PT_filter',
'VIEWLAYER_PT_layer_passes', 'VIEWLAYER_PT_layer_passes',
} }
compatible_engines = {'BLENDER_RENDER', 'BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'} compatible_engines = {'BLENDER_RENDER', 'BLENDER_EEVEE', 'BLENDER_EEVEE_NEXT'}
panels = [] panels = []
@ -1256,7 +1254,7 @@ class KROM_OT_stop_viewport(bpy.types.Operator):
def execute(self, context): def execute(self, context):
import lnx.make as make import lnx.make as make
viewport_id = self.viewport_id viewport_id = self.viewport_id
if not viewport_id and context.space_data: if not viewport_id and context.space_data:
viewport_id = hex(context.space_data.as_pointer())[-6:] viewport_id = hex(context.space_data.as_pointer())[-6:]
@ -1279,12 +1277,12 @@ def draw_viewport_stop_button(self, context):
space = context.space_data space = context.space_data
if not space or not hasattr(space, 'shading'): if not space or not hasattr(space, 'shading'):
return return
if space.shading.type != 'RENDERED': if space.shading.type != 'RENDERED':
return return
viewport_id = hex(space.as_pointer())[-6:] viewport_id = hex(space.as_pointer())[-6:]
global _active_krom_engines global _active_krom_engines
if viewport_id not in _active_krom_engines: if viewport_id not in _active_krom_engines:
layout = self.layout layout = self.layout
@ -1301,12 +1299,12 @@ def register():
bpy.utils.register_class(KromViewportEngine) bpy.utils.register_class(KromViewportEngine)
bpy.utils.register_class(KROM_OT_viewport_input) bpy.utils.register_class(KROM_OT_viewport_input)
bpy.utils.register_class(KROM_OT_stop_viewport) bpy.utils.register_class(KROM_OT_stop_viewport)
bpy.types.VIEW3D_HT_header.append(draw_viewport_stop_button) bpy.types.VIEW3D_HT_header.append(draw_viewport_stop_button)
for panel in get_panels(): for panel in get_panels():
panel.COMPAT_ENGINES.add('KROM_VIEWPORT') panel.COMPAT_ENGINES.add('KROM_VIEWPORT')
if _on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post: if _on_depsgraph_update not in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.append(_on_depsgraph_update) bpy.app.handlers.depsgraph_update_post.append(_on_depsgraph_update)
atexit.register(_cleanup_krom_on_exit) atexit.register(_cleanup_krom_on_exit)
@ -1319,21 +1317,21 @@ def unregister():
_active_krom_engines.clear() _active_krom_engines.clear()
_active_viewport_id = None _active_viewport_id = None
_input_operator_running = False _input_operator_running = False
if _on_depsgraph_update in bpy.app.handlers.depsgraph_update_post: if _on_depsgraph_update in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(_on_depsgraph_update) bpy.app.handlers.depsgraph_update_post.remove(_on_depsgraph_update)
_cleanup_krom_on_exit() _cleanup_krom_on_exit()
try: try:
atexit.unregister(_cleanup_krom_on_exit) atexit.unregister(_cleanup_krom_on_exit)
except: except:
pass pass
for panel in get_panels(): for panel in get_panels():
if 'KROM_VIEWPORT' in panel.COMPAT_ENGINES: if 'KROM_VIEWPORT' in panel.COMPAT_ENGINES:
panel.COMPAT_ENGINES.remove('KROM_VIEWPORT') panel.COMPAT_ENGINES.remove('KROM_VIEWPORT')
bpy.types.VIEW3D_HT_header.remove(draw_viewport_stop_button) bpy.types.VIEW3D_HT_header.remove(draw_viewport_stop_button)
bpy.utils.unregister_class(KROM_OT_stop_viewport) bpy.utils.unregister_class(KROM_OT_stop_viewport)

View File

@ -52,6 +52,46 @@ class WorkingDir:
def __exit__(self, exc_type, exc_val, exc_tb): def __exit__(self, exc_type, exc_val, exc_tb):
os.chdir(self.prev_cwd) os.chdir(self.prev_cwd)
def write_if_changed(filepath, content):
"""Write content to filepath only if it differs from existing file preserving mtime when unchanged"""
if os.path.isfile(filepath):
with open(filepath, 'r') as f:
if f.read() == content:
return False
with open(filepath, 'w') as f:
f.write(content)
return True
def find_browser():
"""Find a browser that supports file:// with --allow-file-access-from-files - Chrome/Chromium/Edge"""
osn = get_os()
if osn == 'win':
candidates = []
for p in (os.environ.get('PROGRAMFILES', ''), os.environ.get('PROGRAMFILES(X86)', ''), os.environ.get('LOCALAPPDATA', '')):
if p:
candidates.append(os.path.join(p, 'Google', 'Chrome', 'Application', 'chrome.exe'))
candidates.append(os.path.join(p, 'Google', 'Chrome', 'chrome.exe'))
candidates.append(os.path.join(p, 'Google', 'Chromium', 'chrome.exe'))
candidates.append(os.path.join(p, 'Chromium', 'Application', 'chrome.exe'))
for p in (os.environ.get('PROGRAMFILES(X86)', ''), os.environ.get('PROGRAMFILES', '')):
if p:
candidates.append(os.path.join(p, 'Microsoft', 'Edge', 'Application', 'msedge.exe'))
for c in candidates:
if os.path.isfile(c):
return c
elif osn == 'mac':
for app in ('/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
'/Applications/Chromium.app/Contents/MacOS/Chromium',
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'):
if os.path.isfile(app):
return app
else:
for name in ('google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'microsoft-edge'):
found = shutil.which(name)
if found:
return found
return None
def write_lnx(filepath, output): def write_lnx(filepath, output):
if filepath.endswith('.lz4'): if filepath.endswith('.lz4'):
with open(filepath, 'wb') as f: with open(filepath, 'wb') as f:
@ -279,7 +319,7 @@ def get_khamake_threads() -> int:
addon_prefs = get_lnx_preferences() addon_prefs = get_lnx_preferences()
if hasattr(addon_prefs, 'khamake_threads_use_auto') and addon_prefs.khamake_threads_use_auto: if hasattr(addon_prefs, 'khamake_threads_use_auto') and addon_prefs.khamake_threads_use_auto:
return -1 return -1
return 1 if not hasattr(addon_prefs, 'khamake_threads') else addon_prefs.khamake_threads return -1 if not hasattr(addon_prefs, 'khamake_threads') else addon_prefs.khamake_threads
def get_compilation_server(): def get_compilation_server():
addon_prefs = get_lnx_preferences() addon_prefs = get_lnx_preferences()

View File

@ -1,4 +1,5 @@
import glob import glob
import io
import json import json
import os import os
import shutil import shutil
@ -623,7 +624,8 @@ def write_compiledglsl(defs, make_variants):
rpdat = lnx.utils.get_rp() rpdat = lnx.utils.get_rp()
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']
shadowmap_size = lnx.utils.get_cascade_size(rpdat) if rpdat.rp_shadows else 0 shadowmap_size = lnx.utils.get_cascade_size(rpdat) if rpdat.rp_shadows else 0
with open(lnx.utils.build_dir() + '/compiled/Shaders/compiled.inc', 'w') as f: inc_path = lnx.utils.build_dir() + '/compiled/Shaders/compiled.inc'
with io.StringIO() as f:
f.write( f.write(
"""#ifndef _COMPILED_GLSL_ """#ifndef _COMPILED_GLSL_
#define _COMPILED_GLSL_ #define _COMPILED_GLSL_
@ -883,6 +885,7 @@ const float clusterNear = 3.0;
f.write("""#endif // _COMPILED_GLSL_ f.write("""#endif // _COMPILED_GLSL_
""") """)
return lnx.utils.write_if_changed(inc_path, f.getvalue())
def write_traithx(class_path): def write_traithx(class_path):
wrd = bpy.data.worlds['Lnx'] wrd = bpy.data.worlds['Lnx']