--[[ | This file was obtained through the combined efforts | of Madbluntz & Plymouth Antiquarian Society. | | Credits: lifestorm, Gregory Wayne Rossel JR., | Maloy, DrPepper10 @ RIP, Atle! | | Visit for more: https://plymouth.thetwilightzone.ru/ --]] -- Copyright © 2022-2072, Nak, https://steamcommunity.com/id/Nak2/ -- All Rights Reserved. Not allowed to be reuploaded. -- License: https://github.com/Nak2/NikNaks/blob/main/LICENSE local obj_tostring = "BSP %s [ %s ]" local format = string.format --- @class BSPObject local meta = NikNaks.__metatables["BSP"] --- @class FaceObject local meta_face = {} meta_face.__index = meta_face meta_face.__tostring = function( self ) return format( obj_tostring, "Faces", self.__id ) end meta_face.MetaName = "BSP Faces" NikNaks.__metatables["BSP Faces"] = meta_face local MAX_MAP_FACES = 65536 --- Returns all faces. ( Warning, uses a lot of memory ) --- @return FaceObject[] function meta:GetFaces() if self._faces then return self._faces end self._faces = {} local data = self:GetLump( 7 ) for i = 0, math.min( data:Size() / 448, MAX_MAP_FACES ) - 1 do --- @class FaceObject local t = {} t.planenum = data:ReadUShort() t.plane = self:GetPlanes()[ t.planenum ] t.side = data:ReadByte() -- 1 = same direciton as face t.onNode = data:ReadByte() -- 1 if on node, 0 if in leaf t.firstedge = data:ReadLong() t.numedges = data:ReadShort() t.texinfo = data:ReadShort() -- Texture info t.dispinfo = data:ReadShort() -- Displacement info t.surfaceFogVolumeID = data:ReadShort() t.styles = { data:ReadByte(), data:ReadByte(), data:ReadByte(), data:ReadByte() } t.lightofs = data:ReadLong() t.area = data:ReadFloat() t.LightmapTextureMinsInLuxels = { data:ReadLong(), data:ReadLong() } t.LightmapTextureSizeInLuxels = { data:ReadLong(), data:ReadLong() } t.origFace = data:ReadLong() t.numPrims = data:ReadUShort() t.firstPrimID = data:ReadUShort() t.smoothingGroups = data:ReadULong() t.__bmodel = self:FindBModelIDByFaceIndex( i ) t.__map = self t.__id = i setmetatable( t, meta_face ) self._faces[i] = t end self:ClearLump( 7 ) return self._faces end -- Returns the original face function meta:GetOriginalFace() return self.__map:GetOriginalFaces()[self.origFace] end -- We make a small hack to cache and get the entities using brush-models. local function __findEntityUsingBrush( self ) if self.__funcBrush then return self.__funcBrush end local entities = self:GetEntities() self.__funcBrush = { [0] = entities[0] } for _, v in pairs( entities ) do local numMdl = string.match( v.model or "", "*([%d]+)" ) if numMdl then self.__funcBrush[tonumber( numMdl )] = v end end return self.__funcBrush end --- Parses a Color object from the given BitBuffer --- @param data BitBuffer --- @return Color local function __readColorRGBExp32 ( data ) return NikNaks.ColorRGBExp32ToColor( { r = data:ReadByte(), g = data:ReadByte(), b = data:ReadByte(), exponent = data:ReadSignedByte() } ) end --- Returns the lightmap samples for the face. --- @return table? function meta_face:GetLightmapSamples() local lightofs = self.lightofs if lightofs == -1 then return end if self._lightmap_samples then return self._lightmap_samples end --- @class LightmapSample --- @field color Color --- @field exponent number --- @type LightmapSample[] local full = {} --- @type LightmapSample[] local average = {} local samples = { average = average, full = full } self._lightmap_samples = samples local has_bumpmap = self:GetMaterial():GetString( "$bumpmap" ) ~= nil local luxel_count = ( self.LightmapTextureSizeInLuxels[1] + 1 ) * ( self.LightmapTextureSizeInLuxels[2] + 1 ) local lightstyle_count = 0 for _, v in ipairs( self.styles ) do if v ~= 255 then lightstyle_count = lightstyle_count + 1 end end -- "For faces with bumpmapped textures, there are four times the usual number of lightmap samples" local sample_count = lightstyle_count * luxel_count if has_bumpmap then sample_count = sample_count * 4 end local data = self.__map:GetLump( 8 ) -- Get the average samples -- "Immediately preceeding the lightofs-referenced sample group, -- there are single samples containing the average lighting on the face, one for each lightstyle, -- in reverse order from that given in the styles[] array." local color, exponent data:Seek( ( lightofs * 8 ) - ( 32 * lightstyle_count ) ) for _ = 1, lightstyle_count do color, exponent = __readColorRGBExp32( data ) table.insert( average, 1, { color = color, exponent = exponent } ) end -- Get the full samples for _ = 1, sample_count do color, exponent = __readColorRGBExp32( data ) table.insert( full, { color = color, exponent = exponent } ) end return samples end --- Returns the face-index. --- @return number function meta_face:GetIndex() return self.__id or -1 end --- Returns the normal for the face --- @return Vector function meta_face:GetNormal() return self.plane.normal end --- Returns the texture info for the face. --- @return table function meta_face:GetTexInfo() return self.__map:GetTexInfo()[self.texinfo] end --- Returns the texture data for the face. --- @return table function meta_face:GetTexData() return self.__map:GetTexData()[ self:GetTexInfo().texdata ] end --- Returns the texture for the face. function meta_face:GetTexture() return self:GetTexData().nameStringTableID end --- Returns the material the face use. Note: Materials within the BSP is not loaded. --- @return IMaterial function meta_face:GetMaterial() if self._mat then return self._mat end self._mat = Material( self:GetTexture() or "__error" ) return self._mat end --- Returns true if the face should render. --- @return boolean function meta_face:ShouldRender() local texinfo = self:GetTexInfo() local flags = texinfo and texinfo.flags or 0 return bit.band( flags, 0x80 ) == 0 and bit.band( flags, 0x200 ) == 0 end --- Returns true if the face-texture is translucent --- @return boolean function meta_face:IsTranslucent() local texinfo = self:GetTexInfo() or 0 return bit.band( texinfo.flags, 0x10 ) ~= 0 end --- Returns true if the face is part of 2D skybox. --- @return boolean function meta_face:IsSkyBox() local texinfo = self:GetTexInfo() or 0 return bit.band( texinfo.flags, 0x2 ) ~= 0 end --- Returns true if the face is part of 3D skybox. --- @return boolean function meta_face:IsSkyBox3D() local texinfo = self:GetTexInfo() or 0 return bit.band( texinfo.flags, 0x4 ) ~= 0 end --- Returns true if the face's texinfo has said flag. --- @return boolean function meta_face:HasTexInfoFlag( flag ) local texinfo = self:GetTexInfo() or 0 return bit.band( texinfo.flags, flag ) ~= 0 end --- Returns true if the face is part of the world and not another entity. --- @return boolean function meta_face:IsWorld() return self.__bmodel == 0 end --- Returns the BModel the face has. 0 if it is part of the world. --- @return number function meta_face:GetBModel() return self.__bmodel end --- Returns the entity-object-data that is part of this face. --- @return string EntityData function meta_face:GetEntity() return __findEntityUsingBrush( self.__map )[self.__bmodel] end -- Displacments TODO: Fix Displacment Position and Data --- Returns true if the face is part of Displacment --- @return boolean function meta_face:IsDisplacment() return self.dispinfo > -1 end --- Returns the vertex positions for the face. [Not Cached] --- Note this will ignore BModel-positions! --- @return table function meta_face:GetVertexs() if self._vertex then return self._vertex end local t = {} if not self:IsDisplacment() then for i = 0, self.numedges - 1 do t[i + 1] = self.__map:GetSurfEdgesIndex( self.firstedge + i ) end return t end -- This is a displacment -- TODO: Calculate the displacment mesh and return it here --local dispVertStart = end --- Returns a table in form of a polygon-mesh. [Not Cached] --- @return PolygonMeshVertex[] function meta_face:GenerateVertexData() --- @type PolygonMeshVertex[] local t = {} local tv = self:GetTexInfo().textureVects local lv = self:GetTexInfo().lightmapVecs local texdata = self:GetTexData() local mat_w, mat_h = texdata.view_width, texdata.view_height local n = self:GetNormal() -- Move the faces to match func_brushes (If any) local bNum = self.__bmodel local exPos, exAng if bNum > 0 then -- Get funch_brushes and their location local func_brush = __findEntityUsingBrush( self.__map )[bNum] if func_brush then exPos = func_brush.origin exAng = func_brush.angles end end local luxelW = self.LightmapTextureSizeInLuxels[1] + 1 local luxelH = self.LightmapTextureSizeInLuxels[2] + 1 for i = 0, self.numedges - 1 do --- @class PolygonMeshVertex local vert = {} local a = self.__map:GetSurfEdgesIndex( self.firstedge + i ) vert.pos = a if bNum > 0 then -- WorldPos -> Entity Brush a = WorldToLocal( a, Angle( 0, 0, 0 ), Vector( 0, 0, 0 ), exAng ) vert.pos = a + exPos end vert.normal = n -- UV & LV vert.u = ( tv[0][0] * a.x + tv[0][1] * a.y + tv[0][2] * a.z + tv[0][3] ) / mat_w vert.v = ( tv[1][0] * a.x + tv[1][1] * a.y + tv[1][2] * a.z + tv[1][3] ) / mat_h vert.lu = ( ( lv[0][0] * a.x + lv[0][1] * a.y + lv[0][2] * a.z + lv[0][3] ) - self.LightmapTextureMinsInLuxels[1] ) / luxelW vert.lv = ( ( lv[1][0] * a.x + lv[1][1] * a.y + lv[1][2] * a.z + lv[1][3] ) - self.LightmapTextureMinsInLuxels[2] ) / luxelH vert.userdata = { 0, 0, 0, 0 } -- Todo: Calculate this? t[i + 1] = vert end return t end --- @return PolygonMeshVertex[]? local function PolyChop( o_vert ) local vert = {} if #o_vert < 3 then return end local n = 1 local triCount = #o_vert - 2 for i = 1, triCount do vert[n] = o_vert[1] vert[n + 1] = o_vert[i + 1] vert[n + 2] = o_vert[i + 2] n = n + 3 end return vert end ---Returns a table in form of a polygon-mesh for triangles. [Not Cached] ---@return PolygonMeshVertex[]? function meta_face:GenerateVertexTriangleData() if self._vertTriangleData then return self._vertTriangleData end self._vertTriangleData = PolyChop( self:GenerateVertexData() ) return self._vertTriangleData end --- All mesh-data regarding said face. Should use face:GenerateVertexTriangleData intead! --- @return table function meta_face:GenerateMeshData() --- @class PolygonMeshData local t = {} t.verticies = self:GenerateVertexData() t.triangles = PolyChop( t.verticies ) t.material = self:GetTexture() return {t} end if CLIENT then --- @type IMesh[] NIKNAKS_TABOMESH = NIKNAKS_TABOMESH or {} --- Builds the mesh if face has none. --- @return IMesh|boolean? function meta_face:BuildMesh() if SERVER then return end if self._mesh then return self._mesh end -- Tex local texinfo = self:GetTexInfo() if bit.band( texinfo.flags, 0x80 ) ~= 0 or bit.band( texinfo.flags, 0x200 ) ~= 0 then self._mesh = false return self._mesh end local meshData = self:GenerateVertexTriangleData() if not meshData then return self._mesh end self._mesh = Mesh( self:GetMaterial() ) -- Vert mesh.Begin( self._mesh, MATERIAL_TRIANGLES, #meshData ) for i = 1, #meshData do local vert = meshData[i] -- > Mesh mesh.Normal( vert.normal ) mesh.Position( vert.pos ) -- Set the position mesh.Color(col.r, col.g, col.b, col.a) mesh.TexCoord( 0, vert.u, vert.v ) -- Set the texture UV coordinates mesh.TexCoord( 1, vert.lu, vert.lv ) -- Set the lightmap UV coordinates mesh.TexCoord( 2, vert.lu, vert.lv ) -- Set the lightmap UV coordinates --mesh.TexCoord( 2, self.LightmapTextureSizeInLuxels[1], self.LightmapTextureSizeInLuxels[2] ) -- Set the texture UV coordinates --mesh.TexCoord( 2, self.LightmapTextureMinsInLuxels[1], self.LightmapTextureMinsInLuxels[2] ) -- Set the texture UV coordinates mesh.AdvanceVertex() end mesh.End() table.insert( NIKNAKS_TABOMESH, self._mesh ) return self._mesh end --- Returns the mesh generated for the face. --- Note. Need to call face:BuildMesh first. --- @return IMesh|boolean? function meta_face:GetMesh() return self._mesh end --- Deletes the mesh generated for the face. --- @return self function meta_face:DeleteMesh() if not self._mesh then return end self._mesh:Destroy() self._mesh = nil return self end --- Generates a mesh for the face and renders it. --- @param dontGenerate boolean --- @return boolean? didGenerateMesh function meta_face:DebugRender( dontGenerate ) local _mesh = self:GetMesh() if not _mesh and dontGenerate then return false end _mesh = _mesh or self:BuildMesh() if not IsValid( _mesh ) then return end render.SetMaterial( self:GetMaterial() ) _mesh:Draw() return true end for _, _mesh in pairs( NIKNAKS_TABOMESH ) do if IsValid( _mesh ) then _mesh:Destroy() end end end