From ae1fca5ecb90473551287f0f9358a440c70c3c16 Mon Sep 17 00:00:00 2001 From: monkeyman192 Date: Mon, 9 Jun 2025 00:15:40 +1000 Subject: [PATCH 01/11] Fixes for importing Beacons (5.7X) version models and fixed descriptors --- ModelExporter/export.py | 55 +++++++--- ModelImporter/import_scene.py | 117 +++++++++++++-------- ModelImporter/readers.py | 13 ++- NMS/LOOKUPS.py | 56 +++++----- NMS/material_node.py | 4 +- __init__.py | 2 +- serialization/NMS_Structures/NMS_types.py | 11 ++ serialization/NMS_Structures/Structures.py | 98 ++++++++++++++++- serialization/cereal_bin/structdata.py | 33 ++++-- serialization/serializers.py | 3 +- 10 files changed, 286 insertions(+), 106 deletions(-) diff --git a/ModelExporter/export.py b/ModelExporter/export.py index a330522..98b11c5 100644 --- a/ModelExporter/export.py +++ b/ModelExporter/export.py @@ -22,7 +22,7 @@ import struct from itertools import accumulate # Internal imports -from NMS.classes import TkAttachmentData, TkGeometryData +from NMS.classes import TkAttachmentData from NMS.LOOKUPS import SEMANTICS, REV_SEMANTICS, STRIDES from NMS.classes.Object import Model from serialization.NMS_Structures import MBINHeader @@ -32,10 +32,8 @@ from serialization.NMS_Structures.Structures import ( TkGeometryData as TkGeometryData_new, ) -from serialization.mbincompiler import mbinCompiler from serialization.StreamCompiler import StreamData -from serialization.serializers import (serialize_index_stream, - serialize_vertex_stream) +from serialization.serializers import serialize_vertex_stream from ModelExporter.utils import nmsHash, traverse @@ -98,7 +96,7 @@ def __init__(self, export_directory, scene_directory, scene_name, model: Model, # unique TkMaterialData struct in the set self.materials = set() self.hashes = odict() - self.mesh_names = list() + self.mesh_names: list[str] = list() self.np_index_data = np.array([], dtype=np.uint32) @@ -245,7 +243,7 @@ def preprocess_streams(self): 'provided for {} Object'.format(mesh.Name)) self.stream_list = list( - SEMANTICS[x] for x in streams.difference({'Indexes'})) + SEMANTICS[x] for x in streams.difference({'Indexes', 'Vertices'})) self.stream_list.sort() self.element_count = len(self.stream_list) @@ -277,22 +275,29 @@ def serialize_data(self): convert all the provided vertex and index data to bytes to be passed directly to the gstream and geometry file constructors """ - vertex_data = [] vertex_sizes = [] + vertex_pos_sizes = [] index_sizes = [] mesh_datas: list[TkMeshData] = [] for i, name in enumerate(self.mesh_names): + count = len(self.vertex_stream[name]) v_data = serialize_vertex_stream( requires=self.stream_list, - Vertices=self.vertex_stream[name], + count=count, UVs=self.uv_stream[name], Normals=self.n_stream[name], Tangents=self.t_stream[name], Colours=self.c_stream[name] ) + v_pos_data = serialize_vertex_stream( + requires={"Vertices"}, + count=count, + Vertices=self.vertex_stream[name], + ) v_len = len(v_data) - vertex_data.append(v_data) vertex_sizes.append(v_len) + v_pos_len = len(v_pos_data) + vertex_pos_sizes.append(v_pos_len) # new_indexes = self.index_stream[name] # # TODO: serialize the same way as they are in the actual data. # # This will also fail I think if there are indexes > 0xFFFF since it will serialize some as H and @@ -311,9 +316,11 @@ def serialize_data(self): md = TkMeshData( name.upper(), v_data + i_data, + v_pos_data, self.mesh_metadata[name]["hash"], i_len, - v_len + v_len, + v_pos_len, ) mesh_datas.append(md) gstream_data = TkGeometryStreamData(mesh_datas) @@ -325,23 +332,37 @@ def serialize_data(self): hdr.write(f) gstream_data.write(f) + # This is a list of 3-tuples with the structure (vert_offset, index_offset_vert_pos_offset) offsets = [] # A bit of a hack, but we need the offsets of the index and vert data. We'll use this code to get it # since it works. with open(self.gstream_fpath, "rb") as f: + # Read the number of TkMeshData's serialized. f.seek(0x28, 0) entries = struct.unpack(" Export/Import", "description": "Create NMS scene structures and export to NMS File format", diff --git a/serialization/NMS_Structures/NMS_types.py b/serialization/NMS_Structures/NMS_types.py index a62f96a..7a61660 100644 --- a/serialization/NMS_Structures/NMS_types.py +++ b/serialization/NMS_Structures/NMS_types.py @@ -13,6 +13,7 @@ class VariableSizeString(datatype): + _size = 0x10 _alignment = 8 @classmethod @@ -41,6 +42,7 @@ def serialize(cls, buf: BufferedWriter, value: str): class NMS_list(datatype): + _size = 0x10 _alignment = 8 _list_type: datatype _end_padding: int = 0xAAAAAA01 @@ -86,11 +88,20 @@ def serialize(cls, buf: BufferedWriter, value): class Vector4f(datatype): + _size = 0x10 _alignment = 0x10 _format = "ffff" +class Vector4i(datatype): + _size = 0x10 + _alignment = 0x10 + _format = "IIII" + + class Quaternion_list(datatype): + _size = 0x10 + @classmethod def deserialize(cls, buf: BufferedReader) -> list[int]: start = buf.tell() diff --git a/serialization/NMS_Structures/Structures.py b/serialization/NMS_Structures/Structures.py index 09474ac..24e0cbd 100644 --- a/serialization/NMS_Structures/Structures.py +++ b/serialization/NMS_Structures/Structures.py @@ -1,10 +1,14 @@ from typing import Annotated +from io import BufferedWriter, BufferedReader from dataclasses import dataclass +import struct from serialization.cereal_bin.structdata import datatype, Field import serialization.cereal_bin.basic_types as bt -from serialization.NMS_Structures.NMS_types import Vector4f, NMS_list, astring, VariableSizeString, Quaternion_list +from serialization.NMS_Structures.NMS_types import ( + Vector4f, NMS_list, astring, VariableSizeString, Quaternion_list, Vector4i +) # Materials structures @@ -12,16 +16,28 @@ @dataclass class TkMaterialFlags(datatype): - MaterialFlag: Annotated[int, Field(bt.uint32)] + MaterialFlagEnum: Annotated[int, Field(bt.uint32)] @dataclass -class TkMaterialUniform(datatype): +class TkMaterialFxFlags(datatype): + MaterialFxFlagEnum: Annotated[int, Field(bt.uint32)] + + +@dataclass +class TkMaterialUniform_Float(datatype): Values: Annotated[tuple[float, float, float, float], Field(Vector4f)] ExtendedValues: Annotated[list[tuple[float, float, float, float]], Field(NMS_list[Vector4f])] Name: Annotated[str, Field(VariableSizeString)] +@dataclass +class TkMaterialUniform_UInt(datatype): + Values: Annotated[tuple[int, int, int, int], Field(Vector4i)] + ExtendedValues: Annotated[list[tuple[int, int, int, int]], Field(NMS_list[Vector4i])] + Name: Annotated[str, Field(VariableSizeString)] + + @dataclass class TkMaterialSampler(datatype): MaterialAlternativeId: Annotated[str, Field(astring, 0x20)] @@ -39,18 +55,21 @@ class TkMaterialSampler(datatype): @dataclass class TkMaterialData(datatype): Flags: Annotated[list[TkMaterialFlags], Field(datatype=NMS_list[TkMaterialFlags])] + FxFlags: Annotated[list[TkMaterialFxFlags], Field(datatype=NMS_list[TkMaterialFxFlags])] Link: Annotated[str, Field(VariableSizeString)] Metamaterial: Annotated[str, Field(VariableSizeString)] Name: Annotated[str, Field(VariableSizeString)] Samplers: Annotated[list[TkMaterialSampler], Field(datatype=NMS_list[TkMaterialSampler])] Shader: Annotated[str, Field(VariableSizeString)] - Uniforms: Annotated[list[TkMaterialUniform], Field(datatype=NMS_list[TkMaterialUniform])] + Uniforms_Float: Annotated[list[TkMaterialUniform_Float], Field(datatype=NMS_list[TkMaterialUniform_Float])] + Uniforms_UInt: Annotated[list[TkMaterialUniform_UInt], Field(datatype=NMS_list[TkMaterialUniform_UInt])] ShaderMillDataHash: Annotated[int, Field(bt.int64)] TransparencyLayerID: Annotated[int, Field(bt.int32)] Class: Annotated[str, Field(bt.string, 0x20)] CastShadow: Annotated[bool, Field(bt.boolean)] CreateFur: Annotated[bool, Field(bt.boolean)] DisableZTest: Annotated[bool, Field(bt.boolean)] + EnableLodFade: Annotated[bool, Field(bt.boolean)] # Geometry structures @@ -113,6 +132,8 @@ class TkMeshMetaData(datatype): IndexDataSize: Annotated[int, Field(bt.int32)] VertexDataOffset: Annotated[int, Field(bt.int32)] VertexDataSize: Annotated[int, Field(bt.int32)] + VertexPositionDataOffset: Annotated[int, Field(bt.int32)] + VertexPositionDataSize: Annotated[int, Field(bt.int32)] DoubleBufferGeometry: Annotated[bool, Field(bt.boolean)] @@ -121,7 +142,7 @@ class TkMeshMetaData(datatype): @dataclass class TkGeometryData(datatype): - SmallVertexLayout: Annotated[TkVertexLayout, Field(TkVertexLayout)] + PositionVertexLayout: Annotated[TkVertexLayout, Field(TkVertexLayout)] VertexLayout: Annotated[TkVertexLayout, Field(TkVertexLayout)] BoundHullVertEd: Annotated[list[int], Field(NMS_list[bt.int32, 1])] BoundHullVerts: Annotated[list[tuple[float, float, float, float]], Field(NMS_list[Vector4f, 1])] @@ -151,9 +172,11 @@ class TkGeometryData(datatype): class TkMeshData(datatype): IdString: Annotated[str, Field(VariableSizeString)] MeshDataStream: Annotated[bytearray, Field(NMS_list[bt.uint8])] + MeshPositionDataStream: Annotated[bytearray, Field(NMS_list[bt.uint8])] Hash: Annotated[int, Field(bt.uint64)] IndexDataSize: Annotated[int, Field(bt.int32)] VertexDataSize: Annotated[int, Field(bt.int32)] + VertexPositionDataSize: Annotated[int, Field(bt.int32)] @dataclass @@ -224,10 +247,75 @@ class TkAnimMetadata(datatype): Has30HzFrames: Annotated[bool, Field(bt.boolean)] +class NMSTemplate(datatype): + _size = 0x10 + _alignment = 8 + _real_type: datatype + _end_padding: int = 0xEEEEEE01 + + @classmethod + def deserialize(cls, buf: BufferedReader) -> datatype: + start = buf.tell() + offset, namehash, _ = struct.unpack(" T: cls_ = cls.__new__(cls) for name, pytype in cls_.__annotations__.items(): if name.startswith("_"): @@ -186,7 +211,3 @@ class Field: length: Optional[int] = None encoding: Optional[str] = None deferred_loading: bool = False - - -T = TypeVar("T", bound=datatype) -N = TypeVar("N", bound=int) diff --git a/serialization/serializers.py b/serialization/serializers.py index c5e34e6..2efab97 100644 --- a/serialization/serializers.py +++ b/serialization/serializers.py @@ -9,7 +9,7 @@ def serialize_geometry_stream(data): pass -def serialize_vertex_stream(requires: List[str], **kwargs): +def serialize_vertex_stream(requires: List[str], count: int, **kwargs): """ Return a serialized version of the vertex data @@ -21,7 +21,6 @@ def serialize_vertex_stream(requires: List[str], **kwargs): include something. """ data = bytearray() - count = len(kwargs.get('Vertices', list())) if count != 0: for i in range(count): for stream_type in requires: From 179ee5c7236855779202ef8e52b3c345065e058a Mon Sep 17 00:00:00 2001 From: monkeyman192 Date: Wed, 11 Jun 2025 15:05:10 +1000 Subject: [PATCH 02/11] Get exporting working --- ModelExporter/export.py | 141 ++++++++------------- ModelImporter/readers.py | 2 +- __init__.py | 2 +- serialization/NMS_Structures/NMS_types.py | 22 +++- serialization/NMS_Structures/Structures.py | 8 +- serialization/cereal_bin/structdata.py | 2 +- 6 files changed, 82 insertions(+), 95 deletions(-) diff --git a/ModelExporter/export.py b/ModelExporter/export.py index 98b11c5..96f3b8c 100644 --- a/ModelExporter/export.py +++ b/ModelExporter/export.py @@ -23,7 +23,7 @@ from itertools import accumulate # Internal imports from NMS.classes import TkAttachmentData -from NMS.LOOKUPS import SEMANTICS, REV_SEMANTICS, STRIDES +from NMS.LOOKUPS import SEMANTICS, REV_SEMANTICS, STRIDES, VERTS from NMS.classes.Object import Model from serialization.NMS_Structures import MBINHeader from serialization.NMS_Structures.Structures import ( @@ -185,7 +185,7 @@ def __init__(self, export_directory, scene_directory, scene_name, model: Model, self.get_bounds() - # this creates the VertexLayout and SmallVertexLayout properties + # this creates the VertexLayout and PositionVertexLayout properties self.create_vertex_layouts() self.process_nodes() @@ -193,13 +193,6 @@ def __init__(self, export_directory, scene_directory, scene_name, model: Model, # other data. self.mix_streams() - # Assign each of the class objects that contain all of the data their - # data - # if (not self.preserve_node_info - # or (self.preserve_node_info - # and self.export_original_geom_data)): - # self.TkGeometryData = TkGeometryData(**self.GeometryData) - # self.TkGeometryData.make_elements(main=True) self.Model.construct_data() self.TkSceneNodeData = self.Model.get_data() for material in self.materials: @@ -250,7 +243,8 @@ def preprocess_streams(self): # Create a list to store the offset sizes for each data type offsets = list() for sid in self.stream_list: - offsets.append(STRIDES[sid]) + if sid != VERTS: + offsets.append(STRIDES[sid]) # Now create an ordered dictionary. Each kvp is the sid and the actual # offset as calculated by the sum of all the entries before it. self.offsets = odict() @@ -290,7 +284,7 @@ def serialize_data(self): Colours=self.c_stream[name] ) v_pos_data = serialize_vertex_stream( - requires={"Vertices"}, + requires={SEMANTICS["Vertices"]}, count=count, Vertices=self.vertex_stream[name], ) @@ -328,7 +322,7 @@ def serialize_data(self): with open(self.gstream_fpath, "wb") as f: hdr = MBINHeader() hdr.header_namehash = 0x40025754 - hdr.header_guid = 0x1D6CC846AC06B54C + hdr.header_guid = 0xCCB46895A8B36313 hdr.write(f) gstream_data.write(f) @@ -370,15 +364,15 @@ def serialize_data(self): StreamMetaDataArray = [] for i, md in enumerate(mesh_datas): StreamMetaDataArray.append(TkMeshMetaData( - md.IdString.upper(), - md.Hash, - offsets[i][1], - index_sizes[i], - offsets[i][2], - vertex_pos_sizes[i], - offsets[i][0], - vertex_sizes[i], - False, + IdString=md.IdString.upper(), + Hash=md.Hash, + IndexDataOffset=offsets[i][1], + IndexDataSize=index_sizes[i], + VertexDataOffset=offsets[i][0], + VertexDataSize=vertex_sizes[i], + VertexPositionDataOffset=offsets[i][2], + VertexPositionDataSize=vertex_pos_sizes[i], + DoubleBufferGeometry=False, )) # metadata = { # 'ID': m.ID, 'hash': m.hash, 'vert_size': m.vertex_size, @@ -581,11 +575,15 @@ def process_nodes(self): ent_path))) else: data['ATTACHMENT'] = obj.EntityPath + # TODO: Do we even need to add this mesh metadata? # enerate the mesh metadata for the geometry file: self.mesh_metadata[name]['Hash'] = data['HASH'] - self.mesh_metadata[name]['VertexDataSize'] = ( - self.stride * ( - data['VERTREND'] - data['VERTRSTART'] + 1)) + self.mesh_metadata[name]['VertexDataSize'] = self.stride * ( + data['VERTREND'] - data['VERTRSTART'] + 1 + ) + self.mesh_metadata[name]['VertexPositionDataSize'] = STRIDES[VERTS] * ( + data['VERTREND'] - data['VERTRSTART'] + 1 + ) if self.GeometryData['Indices16Bit'] == 0: m = 4 else: @@ -640,10 +638,19 @@ def process_nodes(self): def create_vertex_layouts(self): # sort out what streams are given and create appropriate vertex layouts VertexElements = [] - SmallVertexElements = [] + PositionVertexElements = [ + TkVertexElement( + SemanticID=VERTS, + Size=4, + Type=5131, + Offset=0, + Normalise=0, + Instancing=0 + ) + ] for sID in self.stream_list: # sID is the SemanticID - if sID in [0, 1]: + if sID == 1: Offset = self.offsets[sID] VertexElements.append(TkVertexElement(SemanticID=sID, Size=4, @@ -651,15 +658,7 @@ def create_vertex_layouts(self): Offset=Offset, Normalise=0, Instancing=0)) - # Also write the small vertex data - Offset = 8 * sID - SmallVertexElements.append( - TkVertexElement(SemanticID=sID, - Size=4, - Type=5131, - Offset=Offset, - Normalise=0, - Instancing=0)) + # for the INT_2_10_10_10_REV stuff elif sID in [2, 3]: Offset = self.offsets[sID] @@ -684,46 +683,14 @@ def create_vertex_layouts(self): PlatformData=0, VertexElements=VertexElements, ) - # TODO: do more generically - self.GeometryData['SmallVertexLayout'] = TkVertexLayout( - ElementCount=len(SmallVertexElements), - Stride=0x8 * len(SmallVertexElements), + self.GeometryData['PositionVertexLayout'] = TkVertexLayout( + ElementCount=len(PositionVertexElements), + Stride=0x8 * len(PositionVertexElements), PlatformData=0, - VertexElements=SmallVertexElements, + VertexElements=PositionVertexElements, ) def mix_streams(self): - # this combines all the input streams into one single stream with the - # correct offset etc as specified by the VertexLayout - # This also flattens each stream so it needs to be called pretty much - # last - - VertexStream = array('f') - SmallVertexStream = array('f') - for name, mesh_obj in self.Model.Meshes.items(): - for j in range(self.v_stream_lens[name]): - for sID in self.stream_list: - # get the j^th 4Vector element of i^th object of the - # corresponding stream as specified by the stream list. - # As self.stream_list is ordered this will be mixed in the - # correct way wrt. the VertexLayouts - try: - VertexStream.extend( - mesh_obj.__dict__[REV_SEMANTICS[sID]][j]) - if sID in [0, 1]: - SmallVertexStream.extend( - mesh_obj.__dict__[REV_SEMANTICS[sID]][j]) - except IndexError: - # in the case this fails there is an index error caused - # by collisions. In this case just add a default value - VertexStream.extend((0, 0, 0, 1)) - except TypeError: - print(f'{name} mesh has an error!') - raise - - self.GeometryData['VertexStream'] = VertexStream - self.GeometryData['SmallVertexStream'] = SmallVertexStream - # Handle the index streams. # First, we create a list which contains the cumulative count, and then we add this value to each # array. @@ -791,35 +758,35 @@ def write(self): hdr = MBINHeader( header_magic = 0xDDDDDDDDDDDDDDDD, header_namehash = 0x819C3220, - header_guid = 0x32F6AE7B03222A1F, + header_guid = 0xDA1F6CA99ADEF6A6, header_timestamp = 0xFFFFFFFFFFFFFFFF, ) hdr.write(f) gd = self.GeometryData thing = TkGeometryData_new( - SmallVertexLayout=gd["SmallVertexLayout"], # good - VertexLayout=gd["VertexLayout"], # good - BoundHullVertEd=gd["BoundHullVertEd"], # good - BoundHullVerts=gd["BoundHullVerts"], # good - BoundHullVertSt=gd["BoundHullVertSt"], # good + PositionVertexLayout=gd["PositionVertexLayout"], + VertexLayout=gd["VertexLayout"], + BoundHullVertEd=gd["BoundHullVertEd"], + BoundHullVerts=gd["BoundHullVerts"], + BoundHullVertSt=gd["BoundHullVertSt"], IndexBuffer=gd["IndexBuffer"], JointBindings=[], JointExtents=[], JointMirrorAxes=[], JointMirrorPairs=[], - MeshAABBMax=gd["MeshAABBMax"], # good - MeshAABBMin=gd["MeshAABBMin"], # good + MeshAABBMax=gd["MeshAABBMax"], + MeshAABBMin=gd["MeshAABBMin"], MeshBaseSkinMat=[], - MeshVertREnd=gd["MeshVertREnd"], # good - MeshVertRStart=gd["MeshVertRStart"], # good + MeshVertREnd=gd["MeshVertREnd"], + MeshVertRStart=gd["MeshVertRStart"], ProcGenNodeNames=[], ProcGenParentId=[], SkinMatrixLayout=[], - StreamMetaDataArray=gd["StreamMetaDataArray"], # good - CollisionIndexCount=gd["CollisionIndexCount"], # good - IndexCount=gd["IndexCount"], # good - Indices16Bit=self.Indices16Bit, # good - VertexCount=gd["VertexCount"], # good + StreamMetaDataArray=gd["StreamMetaDataArray"], + CollisionIndexCount=gd["CollisionIndexCount"], + IndexCount=gd["IndexCount"], + Indices16Bit=self.Indices16Bit, + VertexCount=gd["VertexCount"], ) thing.write(f) @@ -827,7 +794,7 @@ def write(self): with open(scene_path, "wb") as f: hdr = MBINHeader() hdr.header_namehash = 0x3DB87E47 - hdr.header_guid = 0x6A9DE02E8902AAC3 + hdr.header_guid = 0x42A57794F683F216 hdr.write(f) self.TkSceneNodeData.write(f) print(f'Scene written to {scene_path}') diff --git a/ModelImporter/readers.py b/ModelImporter/readers.py index 14e56c6..6626e01 100644 --- a/ModelImporter/readers.py +++ b/ModelImporter/readers.py @@ -155,7 +155,7 @@ def read_material(fname): if not op.exists(fname): return None with open(fname, "rb") as f: - header = MBINHeader.read(f) + MBINHeader.read(f) return TkMaterialData.read(f) diff --git a/__init__.py b/__init__.py index 433d602..37fb2e1 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "No Man's Sky Development Kit", "author": "gregkwaste, monkeyman192", - "version": (0, 9, "28-pre3"), + "version": (0, 9, "28-pre4"), "blender": (4, 2, 0), "location": "File > Export/Import", "description": "Create NMS scene structures and export to NMS File format", diff --git a/serialization/NMS_Structures/NMS_types.py b/serialization/NMS_Structures/NMS_types.py index 7a61660..9e5670e 100644 --- a/serialization/NMS_Structures/NMS_types.py +++ b/serialization/NMS_Structures/NMS_types.py @@ -15,6 +15,21 @@ class VariableSizeString(datatype): _size = 0x10 _alignment = 8 + _end_padding: int = 0xAAAAAA01 + _pad_with: bytes = b"" + + def __class_getitem__(cls: Type["VariableSizeString"], extra_data: Optional[tuple[int, bytes]]): + if extra_data is not None: + _end_padding, _pad_with = extra_data + else: + _end_padding = 0xAAAAAA01 + _pad_with = b"" + _cls: Type[NMS_list[T]] = types.new_class( + f"VariableSizeString[{extra_data}]", (cls,) + ) + _cls._end_padding = _end_padding + _cls._pad_with = _pad_with + return _cls @classmethod def deserialize(cls, buf: BufferedReader) -> str: @@ -30,9 +45,14 @@ def deserialize(cls, buf: BufferedReader) -> str: @classmethod def serialize(cls, buf: BufferedWriter, value: str): ptr = buf.tell() - buf.write(struct.pack(" T: try: setattr(cls_, name, type_._read(buf, meta)) except: - print(f"Error reading {name} ({pytype}) at offset 0x{buf.tell():X}") + print(f"Error reading {cls.__name__}.{name} ({pytype}) at offset 0x{buf.tell():X}") raise return cls_ From 15d2297f5486a93d624f3f474dd5b23a16e8e5b4 Mon Sep 17 00:00:00 2001 From: monkeyman192 Date: Thu, 12 Jun 2025 10:25:45 +1000 Subject: [PATCH 03/11] Fixes for collision scales and exporting LOD's --- BlenderExtensions/ContextMenu.py | 4 ++-- ModelImporter/import_scene.py | 2 +- NMS/classes/Object.py | 6 ++---- __init__.py | 2 +- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/BlenderExtensions/ContextMenu.py b/BlenderExtensions/ContextMenu.py index de9b337..cea9809 100644 --- a/BlenderExtensions/ContextMenu.py +++ b/BlenderExtensions/ContextMenu.py @@ -128,7 +128,7 @@ def execute(self, context): # Create a new sphere for collisions mesh = bpy.data.meshes.new('sphere') bm = bmesh.new() - bmesh.ops.create_icosphere(bm, subdivisions=4, radius=0.5) + bmesh.ops.create_icosphere(bm, subdivisions=4, radius=1) bm.to_mesh(mesh) bm.free() sphere = bpy.data.objects.new('sphere', mesh) @@ -163,7 +163,7 @@ def execute(self, context): mesh = bpy.data.meshes.new('cylinder') bm = bmesh.new() bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, - radius1=0.5, radius2=0.5, depth=1.0, + radius1=1, radius2=1, depth=1.0, segments=20, matrix=CONE_ROTATION_MAT) bm.to_mesh(mesh) bm.free() diff --git a/ModelImporter/import_scene.py b/ModelImporter/import_scene.py index c86f5b4..1e443b6 100644 --- a/ModelImporter/import_scene.py +++ b/ModelImporter/import_scene.py @@ -815,7 +815,7 @@ def _add_primitive_collision_to_scene(self, scene_node: SceneNodeData): float(scene_node.Attribute('RADIUS'))] elif coll_type == 'Cylinder': bmesh.ops.create_cone(bm, cap_ends=True, cap_tris=True, - radius1=0.5, radius2=0.5, depth=1.0, + radius1=1, radius2=1, depth=1.0, segments=20, matrix=ROT_MATRIX) scale_mult = [float(scene_node.Attribute('RADIUS')), float(scene_node.Attribute('HEIGHT')), diff --git a/NMS/classes/Object.py b/NMS/classes/Object.py index 3da93fb..9df5fd9 100644 --- a/NMS/classes/Object.py +++ b/NMS/classes/Object.py @@ -577,10 +577,8 @@ def create_attributes(self, data: dict, ignore_original: bool = False): # Add the LOD info for i, dist in enumerate(self.lod_distances): self.Attributes.append( - TkSceneNodeAttributeData( - Name=f'LODDIST{i + 1}', - Value=dist, - fmt='{0:.6f}')) + TkSceneNodeAttributeData(Name=f'LODDIST{i + 1}', Value=dist) + ) self.Attributes.append( TkSceneNodeAttributeData(Name='NUMLODS', Value=len(self.lod_distances) + 1)) diff --git a/__init__.py b/__init__.py index 37fb2e1..5a5f5a2 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "No Man's Sky Development Kit", "author": "gregkwaste, monkeyman192", - "version": (0, 9, "28-pre4"), + "version": (0, 9, "28-pre5"), "blender": (4, 2, 0), "location": "File > Export/Import", "description": "Create NMS scene structures and export to NMS File format", From aaa1e7eaede258fa5735405008c2b4712c645c53 Mon Sep 17 00:00:00 2001 From: monkeyman192 Date: Thu, 7 Aug 2025 13:12:09 +1000 Subject: [PATCH 04/11] Fix issue with importing some proc-gen scenes --- ModelImporter/import_scene.py | 2 +- __init__.py | 2 +- serialization/NMS_Structures/Structures.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ModelImporter/import_scene.py b/ModelImporter/import_scene.py index 1e443b6..eebc76d 100644 --- a/ModelImporter/import_scene.py +++ b/ModelImporter/import_scene.py @@ -188,7 +188,7 @@ def __init__(self, fpath, parent_obj=None, ref_scenes=dict(), self.PCBANKS_dir, self.scene_node_data.Attribute('GEOMETRY') + '.PC') - self.descriptor_data = dict() + self.descriptor_data = TkModelDescriptorList([]) self.geometry_stream_file = self.geometry_file.replace('GEOMETRY', 'GEOMETRY.DATA') diff --git a/__init__.py b/__init__.py index 5a5f5a2..de67431 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "No Man's Sky Development Kit", "author": "gregkwaste, monkeyman192", - "version": (0, 9, "28-pre5"), + "version": (0, 9, "28-pre6"), "blender": (4, 2, 0), "location": "File > Export/Import", "description": "Create NMS scene structures and export to NMS File format", diff --git a/serialization/NMS_Structures/Structures.py b/serialization/NMS_Structures/Structures.py index ddd0ff8..4fd494a 100644 --- a/serialization/NMS_Structures/Structures.py +++ b/serialization/NMS_Structures/Structures.py @@ -1,4 +1,4 @@ -from typing import Annotated +from typing import Annotated, Type from io import BufferedWriter, BufferedReader from dataclasses import dataclass import struct @@ -311,7 +311,7 @@ class TkModelDescriptorList(datatype): "TkModelDescriptorList": 0x4026294F, } -STRUCT_MAPPING: dict[int, datatype] = { +STRUCT_MAPPING: dict[int, Type[datatype]] = { 0x8E7F1986: TkAnimMetadata, 0x3DB87E47: TkSceneNodeData, 0x819C3220: TkGeometryData, From a3f0ded6667c171225a19533552c6df43d55b84f Mon Sep 17 00:00:00 2001 From: monkeyman192 Date: Thu, 21 Aug 2025 11:57:10 +1000 Subject: [PATCH 05/11] Fix issue writing complex models to geometry stream data --- ModelExporter/addon_script.py | 2 +- ModelExporter/export.py | 9 ++++++--- __init__.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ModelExporter/addon_script.py b/ModelExporter/addon_script.py index 65c8aab..a8f84bf 100644 --- a/ModelExporter/addon_script.py +++ b/ModelExporter/addon_script.py @@ -686,7 +686,7 @@ def mesh_parser(self, ob, is_coll_mesh: bool = False): uv = uv_layer_data[li].uv uvs[vi] = (uv[0], 1 - uv[1], 0, 1) else: - # Calculate the ev value to write then compare it to what + # Calculate the uv value to write then compare it to what # we have already to see if we need to split the vert. uv = uv_layer_data[li].uv uv = (uv[0], 1 - uv[1], 0, 1) diff --git a/ModelExporter/export.py b/ModelExporter/export.py index 96f3b8c..d796347 100644 --- a/ModelExporter/export.py +++ b/ModelExporter/export.py @@ -302,9 +302,12 @@ def serialize_data(self): # indexes = array('H', new_indexes) # i_data = serialize_index_stream(indexes) i_data = self.np_indexes[i] - if self.Indices16Bit: - i_data = i_data.astype(np.uint16) - i_data = i_data.tobytes() + if (max_idx := max(i_data)) > 0xFFFF: + raise ValueError( + f"The mesh {name} has too many vertexes (max index found = {max_idx}). " + "Please simplify the model to export." + ) + i_data = i_data.astype(np.uint16).tobytes() i_len = len(i_data) index_sizes.append(i_len) md = TkMeshData( diff --git a/__init__.py b/__init__.py index de67431..cc39d90 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "No Man's Sky Development Kit", "author": "gregkwaste, monkeyman192", - "version": (0, 9, "28-pre6"), + "version": (0, 9, "28-pre7"), "blender": (4, 2, 0), "location": "File > Export/Import", "description": "Create NMS scene structures and export to NMS File format", From c71c422ccdf08ffe6287a2a2bb6d6345deccff2c Mon Sep 17 00:00:00 2001 From: Gumsk Date: Fri, 22 Aug 2025 10:52:21 +0900 Subject: [PATCH 06/11] Gumsk 5.75 Updates Updated mask channels Updated xyzt to XYZW Updated BC settings Updated material nodes Added needed types for new materials --- BlenderExtensions/EntityPanels.py | 35 ++++++---- BlenderExtensions/NMSShaderNode.py | 26 ++++---- ModelExporter/addon_script.py | 66 +++++++++++-------- ModelExporter/export.py | 16 ++--- ModelImporter/animation_handler.py | 6 +- NMS/LOOKUPS.py | 40 +++++------ NMS/classes/Quaternion.py | 16 ++--- NMS/classes/TkAnimationData.py | 6 +- NMS/classes/TkMaterialData.py | 41 ++++++++---- NMS/classes/TkMaterialSampler.py | 2 +- ...lUniform.py => TkMaterialUniform_Float.py} | 6 +- NMS/classes/TkMaterialUniform_UInt.py | 17 +++++ NMS/classes/Vector4f.py | 18 ++--- NMS/classes/Vector4i.py | 28 ++++++++ NMS/classes/__init__.py | 4 +- NMS/material_node.py | 60 ++++------------- 16 files changed, 212 insertions(+), 175 deletions(-) rename NMS/classes/{TkMaterialUniform.py => TkMaterialUniform_Float.py} (77%) create mode 100644 NMS/classes/TkMaterialUniform_UInt.py create mode 100644 NMS/classes/Vector4i.py diff --git a/BlenderExtensions/EntityPanels.py b/BlenderExtensions/EntityPanels.py index 891dadb..bfe1d08 100644 --- a/BlenderExtensions/EntityPanels.py +++ b/BlenderExtensions/EntityPanels.py @@ -567,12 +567,18 @@ class NMS_GcInteractionType_Properties(bpy.types.PropertyGroup): class NMS_Vector4f_Properties(bpy.types.PropertyGroup): """ Properties for Vector4f """ - x: FloatProperty(name="x") - y: FloatProperty(name="y") - z: FloatProperty(name="z") - t: FloatProperty(name="t") - - + x: FloatProperty(name="X") + y: FloatProperty(name="Y") + z: FloatProperty(name="Z") + t: FloatProperty(name="W") + +class NMS_Vector4i_Properties(bpy.types.PropertyGroup): + """ Properties for Vector4i """ + x: IntProperty(name="X") + y: IntProperty(name="Y") + z: IntProperty(name="Z") + t: IntProperty(name="W") + class NMS_TkCameraWanderData_Properties(bpy.types.PropertyGroup): """ Properties for TkCameraWanderData """ CamWander: BoolProperty(name="CamWander") @@ -1048,10 +1054,10 @@ def GcInteractionComponentData(self, layout, obj): b2 = b1.box("Camera") b2.row("Distance") b3 = b2.box("Offset") - b3.row("x") - b3.row("y") - b3.row("z") - b3.row("t") + b3.row("X") + b3.row("Y") + b3.row("Z") + b3.row("W") b2.row("Pitch") b2.row("Rotate") b2.row("LightPitch") @@ -1120,10 +1126,10 @@ def TkModelRendererData(self, layout, obj, index=0): b1 = r.box("Camera") b1.row("Distance") b2 = b1.box("Offset") - b2.row("x") - b2.row("y") - b2.row("z") - b2.row("t") + b2.row("X") + b2.row("Y") + b2.row("Z") + b2.row("W") b1.row("Pitch") b1.row("Rotate") b1.row("LightPitch") @@ -1366,6 +1372,7 @@ def get_index(self, obj): NMS_GcSpaceshipComponentData_Properties, NMS_TkCameraWanderData_Properties, NMS_Vector4f_Properties, + NMS_Vector4i_Properties, NMS_TkModelRendererCameraData_Properties, NMS_GcAlienPuzzleMissionOverride_Properties, NMS_TkModelRendererData_Properties, diff --git a/BlenderExtensions/NMSShaderNode.py b/BlenderExtensions/NMSShaderNode.py index d0276c4..f6b59a7 100644 --- a/BlenderExtensions/NMSShaderNode.py +++ b/BlenderExtensions/NMSShaderNode.py @@ -12,8 +12,8 @@ FLAGS = [('_F01_DIFFUSEMAP', 'Diffuse Map', 'Diffuse Map'), ('_F03_NORMALMAP', 'Normal Map', 'Normal Map'), - ('_F21_VERTEXCOLOUR', 'Vertex Colour', 'Vertex Colour'), - ('_F25_ROUGHNESS_MASK', 'Roughness Mask', 'Roughness Mask')] + ('_F21_VERTEXCUSTOM', 'Vertex Custom', 'Vertex Custom'), + ('_F25_MASKS_MAP', 'Masks Map', 'Masks Map')] class NMSShader(bpy.types.NodeCustomGroup): @@ -27,8 +27,8 @@ def operators(self, context): context.space_data.edit_tree list = [('_F01_DIFFUSEMAP', 'Diffuse Map', 'Diffuse Map'), ('_F03_NORMALMAP', 'Normal Map', 'Normal Map'), - ('_F21_VERTEXCOLOUR', 'Vertex Colour', 'Vertex Colour'), - ('_F25_ROUGHNESS_MASK', 'Roughness Mask', 'Roughness Mask')] + ('_F21_VERTEXCUSTOM', 'Vertex Custom', 'Vertex Custom'), + ('_F25_MASKS_MAP', 'Masks Map', 'Masks Map')] return list # Manage the internal nodes to perform the chained operation - clear all @@ -38,7 +38,7 @@ def __nodetree_setup__(self): if self.F01_DIFFUSEMAP_choice: diffuse_texture = self._add_diffuse_texture_choice() - if self.F21_VERTEXCOLOUR_choice: + if self.F21_VERTEXCUSTOM_choice: self._add_vertex_colour_nodes() else: self._remove_vertex_colour_nodes() @@ -111,14 +111,14 @@ def update_nodes(self, context): description='Whether material has a normal map.', default=False, update=update_nodes) - F21_VERTEXCOLOUR_choice: BoolProperty( - name='Has vertex colour data', - description='Whether the material has vertex colour data.', + F21_VERTEXCUSTOM_choice: BoolProperty( + name='Has vertex custom data', + description='Whether the material has vertex custom data.', default=False, update=update_nodes) - F25_ROUGHNESS_MASK_choice: BoolProperty( - name='Has roughness mask', - description='Whether material has a roughness mask.', + F25_MASKS_MAP_choice: BoolProperty( + name='Has masks map', + description='Whether material has a masks map.', default=False, update=update_nodes) @@ -150,9 +150,9 @@ def draw_buttons(self, context, layout): row = layout.row() row.prop(self, 'F03_NORMALMAP_choice', text='Normal Map') row = layout.row() - row.prop(self, 'F21_VERTEXCOLOUR_choice', text='Vertex Colour') + row.prop(self, 'F21_VERTEXCUSTOM_choice', text='Vertex Custom') row = layout.row() - row.prop(self, 'F25_ROUGHNESS_MASK_choice', text='Roughness Mask') + row.prop(self, 'F25_MASKS_MAP_choice', text='Masks Map') # Copy def copy(self, node): diff --git a/ModelExporter/addon_script.py b/ModelExporter/addon_script.py index a8f84bf..1d36bf4 100644 --- a/ModelExporter/addon_script.py +++ b/ModelExporter/addon_script.py @@ -17,11 +17,11 @@ from ModelExporter.animations import process_anims from ModelExporter.export import Export from ModelExporter.Descriptor import Descriptor -from NMS.classes import (TkMaterialData, TkMaterialFlags, - TkVolumeTriggerType, TkMaterialSampler, - TkMaterialUniform, TkRotationComponentData, TkPhysicsComponentData) +from NMS.classes import (TkMaterialData, TkMaterialFlags, TkVolumeTriggerType, + TkMaterialSampler, TkMaterialUniform_Float, TkMaterialUniform_UInt, + TkRotationComponentData, TkPhysicsComponentData) from NMS.classes import TkAnimationComponentData, TkAnimationData -from NMS.classes import List, Vector4f +from NMS.classes import List, Vector4f, Vector4i from NMS.classes import TkAttachmentData from NMS.classes.Object import Object, Model, Mesh, Locator, Reference, Collision, Light, Joint from NMS.LOOKUPS import MATERIALFLAGS @@ -165,7 +165,7 @@ def create_sampler(image, sampler_name: str, texture_dir: str, if op.exists(out_tex_path) and not force_overwrite: print(f'Found existing texture at {out_tex_path}. Using this.') - return TkMaterialSampler(Name=sampler_name, Map=relpath, IsSRGB=True) + return TkMaterialSampler(Name=sampler_name, Map=relpath, IsSRGB=False) # If the textures are packed into the blend file, unpack them. if len(image.packed_files) > 0: @@ -181,7 +181,7 @@ def create_sampler(image, sampler_name: str, texture_dir: str, tex_path = image.filepath_from_user() shutil.copy(tex_path, out_tex_path) if op.exists(out_tex_path): - return TkMaterialSampler(Name=sampler_name, Map=relpath, IsSRGB=True) + return TkMaterialSampler(Name=sampler_name, Map=relpath, IsSRGB=False) else: raise FileNotFoundError(f'Texture not written to {out_tex_path}') @@ -404,26 +404,36 @@ def parse_material(self, ob): "label for the diffuse texture, etc.") # Fetch Uniforms - matuniforms.append(TkMaterialUniform(Name="gMaterialColourVec4", - Values=Vector4f(x=1.0, - y=1.0, - z=1.0, - t=1.0))) - matuniforms.append(TkMaterialUniform(Name="gMaterialParamsVec4", - Values=Vector4f(x=1.0, - y=0.5, - z=1.0, - t=0.0))) - matuniforms.append(TkMaterialUniform(Name="gMaterialSFXVec4", - Values=Vector4f(x=0.0, - y=0.0, - z=0.0, - t=0.0))) - matuniforms.append(TkMaterialUniform(Name="gMaterialSFXColVec4", - Values=Vector4f(x=0.0, - y=0.0, - z=0.0, - t=0.0))) + matuniforms.append(TkMaterialUniform_Float(Name="gMaterialColourVec4", + Values=Vector4f(X=1.000000, + Y=1.000000, + Z=1.000000, + W=1.000000))) + matuniforms.append(TkMaterialUniform_Float(Name="gMaterialParamsVec4", + Values=Vector4f(X=1.000000, + Y=0.500000, + Z=1.000000, + W=0.000000))) + matuniforms.append(TkMaterialUniform_Float(Name="gMaterialParams2Vec4", + Values=Vector4f(X=1.000000, + Y=0.500000, + Z=1.000000, + W=0.000000))) + matuniforms.append(TkMaterialUniform_Float(Name="gMaterialSFXVec4", + Values=Vector4f(X=0.000000, + Y=0.000000, + Z=0.000000, + W=0.000000))) + matuniforms.append(TkMaterialUniform_Float(Name="gMaterialSFXColVec4", + Values=Vector4f(X=0.000000, + Y=0.000000, + Z=0.000000, + W=0.000000))) + matuniforms.append(TkMaterialUniform_UInt(Name="gDynamicFlags", + Values=Vector4i(X=3, + Y=0, + Z=0, + W=0))) if self.settings.get('use_shared_textures'): texture_dir = self.settings.get('shared_texture_folder') @@ -451,10 +461,8 @@ def parse_material(self, ob): # Sort out Mask if mask_image: - # Set _F25_ROUGHNESS_MASK + # Set _F25_MASKS_MAP add_matflags.add(24) - # Set _F39_METALLIC_MASK - add_matflags.add(38) # Add the sampler to the list matsamplers.append(create_sampler( mask_image, "gMasksMap", texture_dir, diff --git a/ModelExporter/export.py b/ModelExporter/export.py index d796347..5458b07 100644 --- a/ModelExporter/export.py +++ b/ModelExporter/export.py @@ -535,12 +535,12 @@ def process_nodes(self): data['BOUNDHULLED'] = self.hull_bounds[name][1] if mesh_obj._Type == 'MESH': # add the AABBMIN/MAX(XYZ) values: - data['AABBMINX'] = self.mesh_bounds[name]['x'][0] - data['AABBMINY'] = self.mesh_bounds[name]['y'][0] - data['AABBMINZ'] = self.mesh_bounds[name]['z'][0] - data['AABBMAXX'] = self.mesh_bounds[name]['x'][1] - data['AABBMAXY'] = self.mesh_bounds[name]['y'][1] - data['AABBMAXZ'] = self.mesh_bounds[name]['z'][1] + data['AABBMINX'] = self.mesh_bounds[name]['X'][0] + data['AABBMINY'] = self.mesh_bounds[name]['Y'][0] + data['AABBMINZ'] = self.mesh_bounds[name]['Z'][0] + data['AABBMAXX'] = self.mesh_bounds[name]['X'][1] + data['AABBMAXY'] = self.mesh_bounds[name]['Y'][1] + data['AABBMAXZ'] = self.mesh_bounds[name]['Z'][1] data['HASH'] = self.hashes.get(name, 0) # we only care about entity and material data for Mesh # Objects @@ -741,8 +741,8 @@ def get_bounds(self): self.GeometryData['MeshAABBMax'].append((x_bounds[1], y_bounds[1], z_bounds[1], 1)) if obj._Type == "MESH": # only add the meshes to the self.mesh_bounds dict: - self.mesh_bounds[obj.Name] = {'x': x_bounds, 'y': y_bounds, - 'z': z_bounds} + self.mesh_bounds[obj.Name] = {'X': x_bounds, 'Y': y_bounds, + 'Z': z_bounds} # TODO: Change this here too... def write(self): diff --git a/ModelImporter/animation_handler.py b/ModelImporter/animation_handler.py index da719a4..42e21d0 100644 --- a/ModelImporter/animation_handler.py +++ b/ModelImporter/animation_handler.py @@ -285,9 +285,9 @@ def _create_anim_channels(self, obj, anim_name: str): Tuple of collections.namedtuple's: (location, rotation, scale) """ - location = namedtuple('location', ['x', 'y', 'z']) - rotation = namedtuple('rotation', ['x', 'y', 'z', 'w']) - scale = namedtuple('scale', ['x', 'y', 'z']) + location = namedtuple('location', ['X', 'Y', 'Z']) + rotation = namedtuple('rotation', ['X', 'Y', 'Z', 'W']) + scale = namedtuple('scale', ['X', 'Y', 'Z']) loc_x = obj.animation_data.action.fcurves.new(data_path='location', index=0, action_group=anim_name) diff --git a/NMS/LOOKUPS.py b/NMS/LOOKUPS.py index e7b4940..eb6e402 100644 --- a/NMS/LOOKUPS.py +++ b/NMS/LOOKUPS.py @@ -3,31 +3,21 @@ import numpy as np -MATERIALFLAGS = ['_F01_DIFFUSEMAP', '_F02_SKINNED', '_F03_NORMALMAP', '_F04_', - '_F05_INVERT_ALPHA', '_F06_BRIGHT_EDGE', '_F07_UNLIT', - '_F08_REFLECTIVE', '_F09_TRANSPARENT', '_F10_NORECEIVESHADOW', - '_F11_ALPHACUTOUT', '_F12_BATCHED_BILLBOARD', - '_F13_UVANIMATION', '_F14_UVSCROLL', '_F15_WIND', - '_F16_DIFFUSE2MAP', '_F17_MULTIPLYDIFFUSE2MAP', - '_F18_UVTILES', '_F19_BILLBOARD', '_F20_PARALLAXMAP', - '_F21_VERTEXCOLOUR', '_F22_TRANSPARENT_SCALAR', - '_F23_TRANSLUCENT', '_F24_AOMAP', '_F25_ROUGHNESS_MASK', - '_F26_STRETCHY_PARTICLE', '_F27_VBTANGENT', '_F28_VBSKINNED', - '_F29_VBCOLOUR', '_F30_REFRACTION', '_F31_DISPLACEMENT', - '_F32_REFRACTION_MASK', '_F33_SHELLS', '_F34_GLOW', - '_F35_GLOW_MASK', '_F36_DOUBLESIDED', '_F37_', - '_F38_NO_DEFORM', '_F39_METALLIC_MASK', - '_F40_SUBSURFACE_MASK', '_F41_DETAIL_DIFFUSE', - '_F42_DETAIL_NORMAL', '_F43_NORMAL_TILING', '_F44_IMPOSTER', - '_F45_VERTEX_BLEND', '_F46_BILLBOARD_AT', - '_F47_REFLECTION_PROBE', '_F48_WARPED_DIFFUSE_LIGHTING', - '_F49_DISABLE_AMBIENT', '_F50_DISABLE_POSTPROCESS', - '_F51_DECAL_DIFFUSE', '_F52_DECAL_NORMAL', - '_F53_COLOURISABLE', '_F54_COLOURMASK', '_F55_MULTITEXTURE', - '_F56_MATCH_GROUND', '_F57_DETAIL_OVERLAY', - '_F58_USE_CENTRAL_NORMAL', '_F59_SCREENSPACE_FADE', - '_F60_ACUTE_ANGLE_FADE', '_F61_CLAMP_AMBIENT', - '_F62_DETAIL_ALPHACUTOUT', '_F63_DISSOLVE', '_F64_'] +MATERIALFLAGS = ['_F01_DIFFUSEMAP', '_F02_SKINNED', '_F03_NORMALMAP', '_F04_FEATURESMAP', + '_F05_DEPTH_EFFECT', '_F06_', '_F07_UNLIT', '_F08_', '_F09_REFLECTIVE', + '_F10_', '_F11_ALPHACUTOUT', '_F12_BATCHED_BILLBOARD', '_F13_UV_EFFECT', + '_F14_', '_F15_WIND', '_F16_DIFFUSE2MAP', '_F17_', '_F18_', + '_F19_BILLBOARD', '_F20_PARALLAX', '_F21_VERTEXCUSTOM', + '_F22_OCCLUSION_MAP', '_F23_', '_F24_', '_F25_MASKS_MAP', '_F26_', '_F27_', + '_F28_', '_F29_', '_F30_REFRACTION', '_F31_DISPLACEMENT', + '_F32_REFRACTION_MASK', '_F33_SHELLS', '_F34_', '_F35_', '_F36_DOUBLESIDED', + '_F37_EXPLICIT_MOTION_VECTORS', '_F38_', '_F39_', '_F40_', '_F41_', + '_F42_DETAIL_NORMAL', '_F43_', '_F44_IMPOSTER', '_F45_', '_F46_', + '_F47_REFLECTION_PROBE', '_F48_', '_F49_', '_F50_DISABLE_POSTPROCESS', + '_F51_', '_F52_', '_F53_COLOURISABLE', '_F54_', '_F55_MULTITEXTURE', + '_F56_MATCH_GROUND', '_F57_', '_F58_USE_CENTRAL_NORMAL', + '_F59_BIASED_REACTIVITY', '_F60_', '_F61_', '_F62_', '_F63_DISSOLVE', + '_F64_RESERVED_FLAG_FOR_EARLY_Z_PATCHING_DO_NOT_USE'] # Mesh vertex types VERTS = 0 diff --git a/NMS/classes/Quaternion.py b/NMS/classes/Quaternion.py index ba0f30f..3d50212 100644 --- a/NMS/classes/Quaternion.py +++ b/NMS/classes/Quaternion.py @@ -8,17 +8,17 @@ def __init__(self, **kwargs): super(Quaternion, self).__init__() """ Contents of the struct """ - self.data['x'] = kwargs.get('x', 0.0) - self.data['y'] = kwargs.get('y', 0.0) - self.data['z'] = kwargs.get('z', 0.0) - self.data['w'] = kwargs.get('w', 0.0) + self.data['X'] = kwargs.get('X', 0.0) + self.data['Y'] = kwargs.get('Y', 0.0) + self.data['Z'] = kwargs.get('Z', 0.0) + self.data['W'] = kwargs.get('W', 0.0) """ End of the struct contents""" def __str__(self): - return 'Quaternion({0}, {1}, {2}, {3})'.format(self.data['x'], - self.data['y'], - self.data['z'], - self.data['w']) + return 'Quaternion({0}, {1}, {2}, {3})'.format(self.data['X'], + self.data['Y'], + self.data['Z'], + self.data['W']) def __repr__(self): return str(self) diff --git a/NMS/classes/TkAnimationData.py b/NMS/classes/TkAnimationData.py index 311046c..abf39df 100644 --- a/NMS/classes/TkAnimationData.py +++ b/NMS/classes/TkAnimationData.py @@ -26,9 +26,9 @@ def __init__(self, **kwargs): self.data['ActionFrame'] = kwargs.get('ActionFrame', -1) self.data['ControlCreatureSize'] = kwargs.get('ControlCreatureSize', 'AllSizes') - self.data['Additive'] = kwargs.get('Additive', 'False') - self.data['Mirrored'] = kwargs.get('Mirrored', 'False') - self.data['Active'] = kwargs.get('Active', 'True') + self.data['Additive'] = kwargs.get('Additive', 'false') + self.data['Mirrored'] = kwargs.get('Mirrored', 'false') + self.data['Active'] = kwargs.get('Active', 'true') self.data['AdditiveBaseAnim'] = kwargs.get('AdditiveBaseAnim', '') self.data['AdditiveBaseFrame'] = kwargs.get('AdditiveBaseFrame', 0) self.data['GameData'] = kwargs.get('GameData', TkAnimationGameData()) diff --git a/NMS/classes/TkMaterialData.py b/NMS/classes/TkMaterialData.py index a00caf6..5b2ec91 100644 --- a/NMS/classes/TkMaterialData.py +++ b/NMS/classes/TkMaterialData.py @@ -3,8 +3,11 @@ from .Struct import Struct from .List import List from .TkMaterialFlags import TkMaterialFlags -from .TkMaterialUniform import TkMaterialUniform +from .TkMaterialUniform_Float import TkMaterialUniform_Float +from .TkMaterialUniform_UInt import TkMaterialUniform_UInt +from .TkMaterialSampler import TkMaterialSampler from .Vector4f import Vector4f +from .Vector4i import Vector4i from .String import String @@ -14,28 +17,42 @@ def __init__(self, **kwargs): """ Contents of the struct """ self.data['Name'] = String(kwargs.get('Name', ""), 0x80) + self.data['Metamaterial'] = String(kwargs.get('Metamaterial', ""), 0x80) self.data['Class'] = String(kwargs.get('Class', "Opaque"), 0x20) self.data['TransparencyLayerID'] = kwargs.get('TransparencyLayerID', 0) - self.data['CastShadow'] = kwargs.get('CastShadow', "False") - self.data['DisableZTest'] = kwargs.get('DisableZTest', "False") + self.data['CastShadow'] = kwargs.get('CastShadow', "false") + self.data['DisableZTest'] = kwargs.get('DisableZTest', "false") + self.data['CreateFur'] = kwargs.get('CreateFur', "false") + self.data['EnableLodFade'] = kwargs.get('EnableLodFade', "false") self.data['Link'] = String(kwargs.get('Link', ""), 0x80) self.data['Shader'] = String( kwargs.get('Shader', "SHADERS/UBERSHADER.SHADER.BIN"), 0x80) self.data['Flags'] = kwargs.get('Flags', List(TkMaterialFlags())) - self.data['Uniforms'] = kwargs.get( - 'Uniforms', + self.data['FxFlags'] = kwargs.get('FxFlags', None) + self.data['Uniforms_Float'] = kwargs.get( + 'Uniforms_Float', List( - TkMaterialUniform( + TkMaterialUniform_Float( Name="gMaterialColourVec4", - Values=Vector4f(x=1.0, y=1.0, z=1.0, t=1.0)), - TkMaterialUniform( + Values=Vector4f(X=1.000000, Y=1.000000, Z=1.000000, W=1.000000)), + TkMaterialUniform_Float( Name="gMaterialParamsVec4", - Values=Vector4f(x=0.9, y=0.5, z=0.0, t=0.0)), - TkMaterialUniform( + Values=Vector4f(X=0.900000, Y=0.500000, Z=0.000000, W=0.000000)), + TkMaterialUniform_Float( + Name="gMaterialParams2Vec4", + Values=Vector4f(X=0.900000, Y=0.500000, Z=0.000000, W=0.000000)), + TkMaterialUniform_Float( Name="gMaterialSFXVec4", Values=Vector4f()), - TkMaterialUniform( + TkMaterialUniform_Float( Name="gMaterialSFXColVec4", Values=Vector4f()))) - self.data['Samplers'] = kwargs.get('Samplers', None) + self.data['Uniforms_UInt'] = kwargs.get( + 'Uniforms_UInt', + List( + TkMaterialUniform_UInt( + Name="gDynamicFlags", + Values=Vector4i(X=3, Y=0, Z=0, W=0)))) + self.data['Samplers'] = kwargs.get('Samplers', TkMaterialSampler()) + self.data['ShaderMillDataHash'] = kwargs.get('Metamaterial', 0) """ End of the struct contents""" diff --git a/NMS/classes/TkMaterialSampler.py b/NMS/classes/TkMaterialSampler.py index 33c68bf..f6efda8 100644 --- a/NMS/classes/TkMaterialSampler.py +++ b/NMS/classes/TkMaterialSampler.py @@ -16,7 +16,7 @@ def __init__(self, **kwargs): self.data['UseCompression'] = kwargs.get('UseCompression', True) self.data['UseMipMaps'] = kwargs.get('UseMipMaps', True) # True image, False for MASKS and NORMAL - self.data['IsSRGB'] = kwargs.get('IsSRGB', True) + self.data['IsSRGB'] = kwargs.get('IsSRGB', False) self.data['MaterialAlternativeId'] = String(kwargs.get( 'MaterialAlternativeId', ""), 0x10) self.data['TextureAddressMode'] = kwargs.get( diff --git a/NMS/classes/TkMaterialUniform.py b/NMS/classes/TkMaterialUniform_Float.py similarity index 77% rename from NMS/classes/TkMaterialUniform.py rename to NMS/classes/TkMaterialUniform_Float.py index d9a486c..2742942 100644 --- a/NMS/classes/TkMaterialUniform.py +++ b/NMS/classes/TkMaterialUniform_Float.py @@ -1,4 +1,4 @@ -# TkMaterialUniform struct +# TkMaterialUniform_Float struct from .Struct import Struct from .String import String @@ -6,9 +6,9 @@ from .List import List -class TkMaterialUniform(Struct): +class TkMaterialUniform_Float(Struct): def __init__(self, **kwargs): - super(TkMaterialUniform, self).__init__() + super(TkMaterialUniform_Float, self).__init__() """ Contents of the struct """ self.data['Name'] = String(kwargs.get('Name', None), 0x20) diff --git a/NMS/classes/TkMaterialUniform_UInt.py b/NMS/classes/TkMaterialUniform_UInt.py new file mode 100644 index 0000000..4297892 --- /dev/null +++ b/NMS/classes/TkMaterialUniform_UInt.py @@ -0,0 +1,17 @@ +# TkMaterialUniform_UInt struct + +from .Struct import Struct +from .String import String +from .Vector4i import Vector4i +from .List import List + + +class TkMaterialUniform_UInt(Struct): + def __init__(self, **kwargs): + super(TkMaterialUniform_UInt, self).__init__() + + """ Contents of the struct """ + self.data['Name'] = String(kwargs.get('Name', None), 0x20) + self.data['Values'] = kwargs.get('Values', Vector4i()) + self.data['ExtendedValues'] = kwargs.get('ExtendedValues', List()) + """ End of the struct contents""" diff --git a/NMS/classes/Vector4f.py b/NMS/classes/Vector4f.py index 4a141ad..0c18131 100644 --- a/NMS/classes/Vector4f.py +++ b/NMS/classes/Vector4f.py @@ -9,20 +9,20 @@ def __init__(self, **kwargs): super(Vector4f, self).__init__() """ Contents of the struct """ - self.data['x'] = kwargs.get('x', 0.0) - self.data['y'] = kwargs.get('y', 0.0) - self.data['z'] = kwargs.get('z', 0.0) - self.data['t'] = kwargs.get('t', 0.0) + self.data['X'] = kwargs.get('X', 0.0) + self.data['Y'] = kwargs.get('Y', 0.0) + self.data['Z'] = kwargs.get('Z', 0.0) + self.data['W'] = kwargs.get('W', 0.0) """ End of the struct contents""" def __bytes__(self): - return pack('gMaterialParamsVec4.x; # noqa mult_param_x = nodes.new(type="ShaderNodeMath") mult_param_x.operation = 'MULTIPLY' @@ -154,23 +147,12 @@ def create_material_node(mat_path: str, local_root_directory: str): lfRoughness) # If the roughness wasn't ever defined then the default value is 1 # which is what blender has as the default anyway - - # gMaterialParamsVec4.x - # #ifdef _F40_SUBSURFACE_MASK - if 39 in flags: - links.new(principled_BSDF.inputs['Subsurface Weight'], - separate_rgb.outputs['R']) - if 43 in flags: - # lfMetallic = lMasks.b; - links.new(principled_BSDF.inputs['Metallic'], - separate_rgb.outputs['B']) - elif tex_type == NORMAL: # texture _path = realize_path(tex_path, local_root_directory) if _path is not None and op.exists(_path): img = bpy.data.images.load(_path) - img.colorspace_settings.name = 'sRGB' + img.colorspace_settings.name = 'linear' normal_texture = nodes.new(type='ShaderNodeTexImage') normal_texture.name = normal_texture.label = 'Texture Image - Normal' # noqa normal_texture.image = img @@ -199,25 +181,11 @@ def create_material_node(mat_path: str, local_root_directory: str): links.new(principled_BSDF.inputs['Normal'], normal_map.outputs['Normal']) - if 42 in flags: - # lTexCoordsVec4.xy *= lUniforms.mpCustomPerMesh->gCustomParams01Vec4.z; # noqa - normal_scale = nodes.new(type='ShaderNodeMapping') - normal_scale.location = (-1000, -300) - scale = uniforms['gCustomParams01Vec4'].Values[2] - normal_scale.inputs['Scale'].default_value = Vector((scale, scale, scale)) # noqa - tex_coord = nodes.new(type='ShaderNodeTexCoord') - tex_coord.location = (-1200, -300) - tex_coord.object = bpy.context.active_object - links.new(normal_scale.inputs['Vector'], - tex_coord.outputs['Generated']) - links.new(normal_texture.inputs['Vector'], - normal_scale.outputs['Vector']) - # Apply some final transforms to the data before connecting it to the # Material output node if 20 in flags or 28 in flags: - # #ifdef _F21_VERTEXCOLOUR + # #ifdef _F21_VERTEXCUSTOM # lColourVec4 *= IN( mColourVec4 ); col_attribute = nodes.new(type='ShaderNodeAttribute') col_attribute.attribute_name = 'Col' From 6c984b945097d9e883c7ba1b66d25804d30fdc2b Mon Sep 17 00:00:00 2001 From: Gumsk Date: Fri, 22 Aug 2025 12:06:17 +0900 Subject: [PATCH 07/11] Update __init__.py --- __init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__init__.py b/__init__.py index cc39d90..0839c3c 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "No Man's Sky Development Kit", "author": "gregkwaste, monkeyman192", - "version": (0, 9, "28-pre7"), + "version": (0, 9, "28-pre8"), "blender": (4, 2, 0), "location": "File > Export/Import", "description": "Create NMS scene structures and export to NMS File format", From ba6263a1b446f18220209d75c9cd83349ac91587 Mon Sep 17 00:00:00 2001 From: Gumsk Date: Sat, 23 Aug 2025 17:22:46 +0900 Subject: [PATCH 08/11] Update material_node.py Fixed colorspace setting --- NMS/material_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NMS/material_node.py b/NMS/material_node.py index b9a20c7..c3e5b27 100644 --- a/NMS/material_node.py +++ b/NMS/material_node.py @@ -102,7 +102,7 @@ def create_material_node(mat_path: str, local_root_directory: str): _path = realize_path(tex_path, local_root_directory) if _path is not None and op.exists(_path): img = bpy.data.images.load(_path) - img.colorspace_settings.name = 'linear' + img.colorspace_settings.name = 'Linear Rec.2020' mask_texture = nodes.new(type='ShaderNodeTexImage') mask_texture.name = mask_texture.label = 'Texture Image - Mask' mask_texture.image = img @@ -152,7 +152,7 @@ def create_material_node(mat_path: str, local_root_directory: str): _path = realize_path(tex_path, local_root_directory) if _path is not None and op.exists(_path): img = bpy.data.images.load(_path) - img.colorspace_settings.name = 'linear' + img.colorspace_settings.name = 'Linear Rec.2020' normal_texture = nodes.new(type='ShaderNodeTexImage') normal_texture.name = normal_texture.label = 'Texture Image - Normal' # noqa normal_texture.image = img From fbcbf3c701246b505e09f5d64a229ff720300abd Mon Sep 17 00:00:00 2001 From: monkeyman192 Date: Tue, 26 Aug 2025 23:12:29 +1000 Subject: [PATCH 09/11] Improve high vertex mesh serialization --- ModelExporter/export.py | 20 +++++--------------- __init__.py | 2 +- 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/ModelExporter/export.py b/ModelExporter/export.py index 5458b07..cdde485 100644 --- a/ModelExporter/export.py +++ b/ModelExporter/export.py @@ -292,22 +292,12 @@ def serialize_data(self): vertex_sizes.append(v_len) v_pos_len = len(v_pos_data) vertex_pos_sizes.append(v_pos_len) - # new_indexes = self.index_stream[name] - # # TODO: serialize the same way as they are in the actual data. - # # This will also fail I think if there are indexes > 0xFFFF since it will serialize some as H and - # # some as I - # if max(new_indexes) > 2 ** 16: - # indexes = array('I', new_indexes) - # else: - # indexes = array('H', new_indexes) - # i_data = serialize_index_stream(indexes) i_data = self.np_indexes[i] - if (max_idx := max(i_data)) > 0xFFFF: - raise ValueError( - f"The mesh {name} has too many vertexes (max index found = {max_idx}). " - "Please simplify the model to export." - ) - i_data = i_data.astype(np.uint16).tobytes() + # Depending on how many verts there are, we will need to serialize the indexes differently. + if max(i_data) > 0xFFFF: + i_data = i_data.astype(np.uint32).tobytes() + else: + i_data = i_data.astype(np.uint16).tobytes() i_len = len(i_data) index_sizes.append(i_len) md = TkMeshData( diff --git a/__init__.py b/__init__.py index 0839c3c..3c10098 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "No Man's Sky Development Kit", "author": "gregkwaste, monkeyman192", - "version": (0, 9, "28-pre8"), + "version": (0, 9, "28-pre9"), "blender": (4, 2, 0), "location": "File > Export/Import", "description": "Create NMS scene structures and export to NMS File format", From 53bd6cd2c611d511d3937b90ffeb61e2fb6a465f Mon Sep 17 00:00:00 2001 From: Gumsk Date: Sat, 30 Aug 2025 15:27:31 +0900 Subject: [PATCH 10/11] Update material_node.py Fixed metallic node to Blue from Green --- NMS/material_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NMS/material_node.py b/NMS/material_node.py index c3e5b27..0bf2228 100644 --- a/NMS/material_node.py +++ b/NMS/material_node.py @@ -128,9 +128,9 @@ def create_material_node(mat_path: str, local_root_directory: str): # link them up links.new(sub_1.inputs[1], separate_rgb.outputs['R']) - # lfMetallic = lMasks.b; + # lfMetallic = lMasks.g; links.new(principled_BSDF.inputs['Metallic'], - separate_rgb.outputs['B']) + separate_rgb.outputs['G']) else: roughness_value = nodes.new(type='ShaderNodeValue') roughness_value.outputs[0].default_value = 1.0 From c712f816cebb03cc0c28c14a0c521ebf450198b3 Mon Sep 17 00:00:00 2001 From: monkeyman192 Date: Thu, 9 Oct 2025 09:04:52 +1100 Subject: [PATCH 11/11] version bump --- ModelImporter/readers.py | 11 ++++------- __init__.py | 4 ++-- utils/utils.py | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/ModelImporter/readers.py b/ModelImporter/readers.py index 6626e01..ebf208d 100644 --- a/ModelImporter/readers.py +++ b/ModelImporter/readers.py @@ -1,15 +1,12 @@ -from collections import namedtuple import struct from typing import Tuple, NamedTuple import os.path as op # TODO: move to the serialization folder? -from serialization.utils import (read_list_header, read_string, # noqa pylint: disable=relative-beyond-top-level - bytes_to_quat, read_bool, read_uint32, - returned_read) -from serialization.list_header import ListHeader # noqa pylint: disable=relative-beyond-top-level -from utils.utils import mxml_to_dict # noqa pylint: disable=relative-beyond-top-level +from serialization.utils import read_string, bytes_to_quat +from serialization.list_header import ListHeader +from utils.utils import mxml_to_dict from serialization.NMS_Structures import TkMaterialData, MBINHeader, NAMEHASH_MAPPING, TkAnimMetadata @@ -159,7 +156,7 @@ def read_material(fname): return TkMaterialData.read(f) -def read_gstream(fname: str, info: namedtuple) -> Tuple[bytes, bytes]: +def read_gstream(fname: str, info: gstream_info) -> Tuple[bytes, bytes]: """ Read the requested info from the gstream file. Parameters diff --git a/__init__.py b/__init__.py index 3c10098..b143674 100644 --- a/__init__.py +++ b/__init__.py @@ -1,7 +1,7 @@ bl_info = { "name": "No Man's Sky Development Kit", "author": "gregkwaste, monkeyman192", - "version": (0, 9, "28-pre9"), + "version": (0, 9, 28), "blender": (4, 2, 0), "location": "File > Export/Import", "description": "Create NMS scene structures and export to NMS File format", @@ -19,7 +19,7 @@ # become significantly nicer... import sys import os.path as op -sys.path.append(op.dirname(__file__)) +sys.path = [op.dirname(__file__)] + sys.path # External API operators from .NMSDK import ImportSceneOperator, ImportMeshOperator, ExportSceneOperator diff --git a/utils/utils.py b/utils/utils.py index 092de51..219d7b5 100644 --- a/utils/utils.py +++ b/utils/utils.py @@ -1,7 +1,7 @@ import xml.etree.ElementTree as ET -def mxml_to_dict(fpath: str) -> dict: +def mxml_to_dict(fpath) -> dict: tree = ET.parse(fpath) root = tree.getroot() return element_to_dict(root)