Files
wnsrc/lua/niknaks/modules/sh_bsp_faces.lua
lifestorm 94063e4369 Upload
2024-08-04 22:55:00 +03:00

445 lines
13 KiB
Lua

--[[
| 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<string, LightmapSample[]>?
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