├── .github └── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── addon.json ├── lua ├── autorun │ └── prop_mesh_load.lua ├── entities │ └── prop_mesh │ │ ├── cl_init.lua │ │ ├── init.lua │ │ └── shared.lua └── lib │ ├── cl │ ├── pvs_cache.lua │ ├── queue_sys.lua │ ├── setup.lua │ ├── thumbnail.lua │ └── url_texture.lua │ ├── sh │ ├── mesh_parser.lua │ ├── obj.lua │ ├── setup.lua │ └── util.lua │ └── sv │ ├── registry.lua │ └── setup.lua ├── materials └── vgui │ └── entities │ ├── prop_mesh.vmt │ └── prop_mesh.vtf ├── prop_mesh.code-workspace ├── thumbnail_prop_mesh.jpg └── thumbnail_prop_mesh.psd /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Issue Type 2 | 3 | - [ ] Model .obj loading 4 | - [ ] Model texture loading 5 | 6 | - [ ] Entity related 7 | - [ ] Lua error 8 | 9 | ## Model data (if any) 10 | 11 | - URL : 12 | - TEXTURE : 13 | 14 | ## LUA error (if any) 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2020 Eduardo Fernandes 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # prop_mesh - Custom Model Loader 2 | 3 | prop_mesh allows you to use .obj models as props using box collisions! 4 | Supports multi-textured models! 5 | 6 | ## NOTES 7 | 8 | - Only .obj models are supported! 9 | - You can find prop_mesh on Entities -> Custom Models 10 | - When using prop_mesh make sure you at least have a **Prop Protection ADDON** (else it will use SetOwner to determine the owner, preventing you from grabbing it!) 11 | - If you want to use it **SINGLEPLAYER**, make sure "Local Server" is ticked! **DO NOT START IT IN PURE SINGLEPLAYER** 12 | 13 | ## COMMANDS 14 | 15 | ``` 16 | CLIENT : 17 | prop_mesh_urltexture_timeout - How many seconds before timing out (Default: 30) 18 | ------------- 19 | prop_mesh_queue_interval <0.35 to 1> - How many seconds between prop_mesh mesh rendering (LOW VALUE = More chances of crashing) (Default: 0.35) 20 | ------------- 21 | prop_mesh_urltexture_reload - Reloads all url textures 22 | prop_mesh_urltexture_clear - Clear url texture cache 23 | ``` 24 | 25 | ``` 26 | SERVER : 27 | sbox_maxprop_mesh - Max prop_mesh per players (Default: 10) 28 | 29 | prop_mesh_maxTriangles - Max prop_mesh Obj triangles allowed in TOTAL (Default: 1650) 30 | prop_mesh_maxSubMeshes - Max prop_mesh sub-meshes allowed (HIGH VALUE = More rendering lag) (Default: 5) 31 | prop_mesh_maxOBJ_bytes - Max prop_mesh obj size in BYTES (Default: 2048576) 32 | prop_mesh_maxScaleVolume - Max prop_mesh volume scale (Default: 580) 33 | prop_mesh_minScaleVolume - Min prop_mesh volume scale (Default: 3) 34 | prop_mesh_ignoreContentRange - Ignore Content-Range check, users will be able to force the server to download huge files! (Default: 0) 35 | ``` 36 | 37 | ``` 38 | SHARED : 39 | prop_mesh_objcache_clear - Clear cached models (If ran on server, it will clear clients cache) 40 | ``` 41 | 42 | ## KNOWN ISSUES 43 | 44 | - If your model looks **"weird"** try converting the faces to tris (if you use blender, when exporting the obj, tick **"Triangulate Faces"** 45 | 46 | ## TODO 47 | 48 | ### Mesh 49 | 50 | - [ ] Save parsed mesh on client as cache 51 | - [ ] Save textures on client as cache 52 | - [x] Split the mesh if triangles limit is high 53 | 54 | ### Entity 55 | 56 | - [x] Fix Adv.dup constrains 57 | - [ ] Server / Client code improvements 58 | - [x] Add console commands to limit prop_mesh on client side 59 | - [x] Add console commands server side for admins 60 | - [ ] Handle server failing to parse model? 61 | - [x] Better UI Panel 62 | 63 | ## LINKS 64 | 65 | - [Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=2205982705) 66 | - [Tutorial](https://youtu.be/g1nbhyNAZkU) 67 | 68 | ## SCREENSHOTS 69 | 70 | ![](https://i.imgur.com/5p3USX0.png) 71 | ![](https://i.imgur.com/fc4tl7K.png) 72 | -------------------------------------------------------------------------------- /addon.json: -------------------------------------------------------------------------------- 1 | { 2 | "title" : "prop_mesh", 3 | "type" : "tool", 4 | "tags" : [ "build", "fun" ], 5 | "description": "Custom prop loader", 6 | "ignore" : 7 | [ 8 | ".git*", 9 | ".gitignore", 10 | "*.jpg", 11 | "*.psd", 12 | "prop_mesh.code-workspace", 13 | "LICENSE", 14 | "README.md" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /lua/autorun/prop_mesh_load.lua: -------------------------------------------------------------------------------- 1 | PropMLIB = PropMLIB or {} 2 | 3 | if SERVER then 4 | AddCSLuaFile("autorun/prop_mesh_load.lua") 5 | 6 | -- LIB -- 7 | -- Client side -- 8 | AddCSLuaFile("lib/cl/setup.lua") 9 | AddCSLuaFile("lib/cl/pvs_cache.lua") 10 | AddCSLuaFile("lib/cl/url_texture.lua") 11 | AddCSLuaFile("lib/cl/queue_sys.lua") 12 | AddCSLuaFile("lib/cl/thumbnail.lua") 13 | 14 | -- Shared -- 15 | AddCSLuaFile("lib/sh/setup.lua") 16 | AddCSLuaFile("lib/sh/mesh_parser.lua") 17 | AddCSLuaFile("lib/sh/obj.lua") 18 | AddCSLuaFile("lib/sh/util.lua") 19 | end 20 | 21 | -- SERVER -- 22 | if SERVER then 23 | include("lib/sv/setup.lua") 24 | include("lib/sv/registry.lua") 25 | 26 | resource.AddSingleFile("materials/vgui/entities/prop_mesh.vtf") 27 | resource.AddSingleFile("materials/vgui/entities/prop_mesh.vmt") 28 | end 29 | 30 | -- CLIENT -- 31 | if CLIENT then 32 | -- Folder structure 33 | file.CreateDir( "prop_mesh" ) 34 | file.CreateDir( "prop_mesh/thumbnails" ) 35 | file.CreateDir( "prop_mesh/models" ) 36 | file.CreateDir( "prop_mesh/textures" ) 37 | --- 38 | 39 | include("lib/cl/setup.lua") 40 | include("lib/cl/pvs_cache.lua") 41 | include("lib/cl/url_texture.lua") 42 | include("lib/cl/queue_sys.lua") 43 | include("lib/cl/thumbnail.lua") 44 | end 45 | 46 | -- SHARED -- 47 | include("lib/sh/setup.lua") 48 | include("lib/sh/mesh_parser.lua") 49 | include("lib/sh/obj.lua") 50 | include("lib/sh/util.lua") 51 | 52 | if SERVER then print("[PropMLIB] Startup") end -------------------------------------------------------------------------------- /lua/entities/prop_mesh/cl_init.lua: -------------------------------------------------------------------------------- 1 | include('shared.lua') 2 | 3 | local table_insert = table.insert 4 | local table_remove = table.remove 5 | local table_copy = table.Copy 6 | local table_count = table.Count 7 | 8 | local string_path = string.GetPathFromFilename 9 | local string_trim = string.Trim 10 | 11 | local math_rand = math.Rand 12 | 13 | ---------------- 14 | --- SETTINGS --- 15 | ENT.AutomaticFrameAdvance = true 16 | ENT.DEFAULT_MATERIAL = CreateMaterial( "PROP_MESH_DEFAULT_MATERIAL", "UnlitGeneric", { 17 | ["$basetexture"] = "models/debug/debugwhite", 18 | ["$model"] = "1", 19 | ["$decal"] = "1" 20 | }) 21 | 22 | ENT.DEFAULT_MATERIAL_PHYS = CreateMaterial( "PROP_MESH_DEFAULT_MATERIAL_PHYS", "UnlitGeneric", { 23 | ["$basetexture"] = "models/debug/debugwhite", 24 | ["$model"] = "1", 25 | ["$decal"] = "1" 26 | }) 27 | 28 | ENT.DEFAULT_MATERIAL:SetVector("$color2", Vector(0, 0, 0)) 29 | ENT.DEFAULT_MATERIAL_PHYS:SetVector("$color2", Vector(1, 1, 1)) 30 | 31 | ENT.DEBUG_MATERIAL = CreateMaterial( "PROP_MESH_DEFAULT_MATERIAL_WIREFRAME", "Wireframe", { 32 | ["$basetexture"] = "models/wireframe", 33 | ["$model"] = "1", 34 | ["$vertexalpha"] = "1", 35 | ["$vertexcolor"] = "1", 36 | ["$decal"] = "1" 37 | }) 38 | 39 | ENT.DEBUG_MATERIALS_COLORS = { 40 | {0.90, 0.29, 0.23}, 41 | {0.16, 0.50, 0.72}, 42 | {0.15, 0.68, 0.37}, 43 | {0.10, 0.73, 0.61}, 44 | {0.60, 0.34, 0.71}, 45 | {0.94, 0.76, 0.05} 46 | } 47 | ---------------- 48 | 49 | ENT.MESH_MODELS = {} 50 | ENT.UI = {} 51 | 52 | ENT.HISTORY_MESHES = {} 53 | 54 | ---- INTERNAL CHECKS ---- 55 | ENT.__LOADED_MESH__ = false 56 | ENT.__LOADED_TEXTURES__ = false 57 | ENT.__PHYSICS_BOX__ = nil 58 | ---- 59 | 60 | language.Add( "SBoxLimit_prop_mesh", "You have hit the prop_mesh limit!" ) 61 | 62 | surface.CreateFont( "PROP_MESH_DEBUGFIXED", { 63 | font = "DebugFixedSmall", 64 | size = ScreenScale(6), 65 | weight = 200 66 | }) 67 | 68 | --- SETTINGS --- 69 | ---------------- 70 | 71 | --------------- 72 | --- GENERAL --- 73 | function ENT:LoadTextures(textures) 74 | if not IsValid(self) then return end 75 | 76 | self.MATERIAL_URLS = {} 77 | self.__LOADED_TEXTURES__ = false 78 | 79 | local totalTextures = #textures 80 | local onDone = function() 81 | totalTextures = totalTextures - 1 82 | 83 | if totalTextures <= 0 then 84 | if not IsValid(self) then return end 85 | self.__LOADED_TEXTURES__ = true 86 | 87 | if self.CheckMeshCompletion then 88 | self:CheckMeshCompletion() 89 | end 90 | end 91 | end 92 | 93 | for _, v in pairs(textures) do 94 | if not v or string_trim(v) == "" then 95 | onDone() 96 | continue 97 | end 98 | 99 | PropMLIB.URLMaterial.LoadMaterialURL(PropMLIB.Util.FixUrl(v), function() 100 | return onDone() 101 | end, function() 102 | return onDone() 103 | end) 104 | 105 | table_insert(self.MATERIAL_URLS, v) 106 | end 107 | end 108 | 109 | function ENT:GenerateExtraRandomColors() 110 | for i = 0, 20 do 111 | table_insert(self.DEBUG_MATERIALS_COLORS, {math_rand(0, 1), math_rand(0, 1), math_rand(0, 1)}) 112 | end 113 | end 114 | --- GENERAL --- 115 | --------------- 116 | 117 | ------------ 118 | --- MESH --- 119 | function ENT:BuildIMesh(meshData) 120 | self.__LOADED_MESH__ = false 121 | 122 | -- Prevent crashing players if spammed -- 123 | PropMLIB.QueueSYS.Register({ 124 | callback = function() 125 | if not IsValid(self) then return end 126 | self:ClearMeshes() 127 | 128 | local safeScale = self:VectorToSafe(meshData.scale, meshData.obb) 129 | if not safeScale then safeScale = 1 end 130 | 131 | local minOBB = meshData.obb.minOBB * safeScale 132 | local maxOBB = meshData.obb.maxOBB * safeScale 133 | 134 | self:SetRenderBounds( minOBB, maxOBB ) 135 | for _, v in pairs(meshData.subMeshes) do 136 | local scaledTris = PropMLIB.Obj.GetScaledTris(v, safeScale) 137 | local msh = Mesh() 138 | 139 | msh:BuildFromTriangles(scaledTris) 140 | table_insert(self.MESH_MODELS, msh) 141 | end 142 | 143 | self.__LOADED_MESH__ = true 144 | 145 | self:UpdateTextureName() -- Fix names 146 | self:CheckMeshCompletion() 147 | end 148 | }) 149 | end 150 | 151 | function ENT:CheckMeshCompletion() 152 | if not self.__LOADED_MESH__ or not self.__LOADED_TEXTURES__ then return end 153 | 154 | timer.Simple(1, function() 155 | if not self.MeshComplete then return end 156 | self:MeshComplete() 157 | end) -- Give it some time to fully render 158 | end 159 | 160 | function ENT:MeshComplete() 161 | local owner = self:GetNWEntity("owner") 162 | if self.CPPIGetOwner then owner = self:CPPIGetOwner() end 163 | if LocalPlayer() ~= owner then return end 164 | 165 | self:TakeScreenshot() 166 | end 167 | 168 | function ENT:ClearMeshes() 169 | PropMLIB.MeshParser.ClearMeshes(self.MESH_MODELS) 170 | self.MESH_MODELS = {} 171 | end 172 | 173 | function ENT:LocalLoadMesh(requestData) 174 | -- Cleanup -- 175 | if not requestData.duped then self:Clear() end 176 | -- ------- -- 177 | 178 | self.LAST_REQUESTED_MESH = table_copy(requestData) 179 | self:UpdateMeshSettings() 180 | 181 | self:LoadOBJ(requestData.uri, requestData.isAdmin, function(meshData) 182 | if not IsValid(self) then return end 183 | if not meshData then 184 | print("[prop_mesh] Mesh data is invalid") 185 | 186 | self:SetModelErrored(true) 187 | self:SetStatus("MeshData is invalid") 188 | return 189 | end 190 | 191 | meshData.scale = requestData.scale 192 | meshData.phys = requestData.phys 193 | 194 | self:SetStatus("Done") 195 | self:BuildMeshes(meshData) 196 | end, function(err) 197 | print("[prop_mesh]"..err) 198 | if not IsValid(self) then return end 199 | 200 | self:SetModelErrored(true) 201 | self:SetStatus(err) 202 | end) 203 | end 204 | 205 | function ENT:RetryModelParse() 206 | if not self.LAST_MODEL_ERRORED then return end 207 | 208 | local lastMesh = self.LAST_REQUESTED_MESH 209 | if not lastMesh then return end 210 | 211 | PropMLIB.Obj.UnRegister(lastMesh.uri) -- Uncache it 212 | self:LocalLoadMesh(lastMesh) 213 | end 214 | --- MESH --- 215 | ------------ 216 | 217 | 218 | ------------ 219 | --- UTIL --- 220 | function ENT:GetModelMaterial(index, DebugMode) 221 | if DebugMode then 222 | return self.DEBUG_MATERIAL 223 | end 224 | 225 | local mat = self.DEFAULT_MATERIAL 226 | if self.MATERIAL_URLS and self.MATERIAL_URLS[index] then 227 | if PropMLIB.URLMaterial.Materials[self.MATERIAL_URLS[index]] then 228 | mat = PropMLIB.URLMaterial.Materials[self.MATERIAL_URLS[index]] 229 | end 230 | end 231 | 232 | return mat 233 | end 234 | 235 | function ENT:OnPVSReload() 236 | local meshData = self.LOADED_MESH 237 | if not meshData then return end 238 | 239 | local safeScale = self:VectorToSafe(meshData.scale, meshData.obb) 240 | local minOBB = meshData.obb.minOBB * safeScale 241 | local maxOBB = meshData.obb.maxOBB * safeScale 242 | 243 | self:SetRenderBounds(minOBB, maxOBB) 244 | end 245 | --- UTIL --- 246 | ------------ 247 | 248 | --------------- 249 | --- PHYSICS --- 250 | function ENT:TestCollision( startpos, delta, isbox, extents ) 251 | if not IsValid( self.__PHYSICS_BOX__ ) then 252 | return 253 | end 254 | 255 | -- TraceBox expects the trace to begin at the center of the box, but TestCollision is bad 256 | local max = extents 257 | local min = -extents 258 | max.z = max.z - min.z 259 | min.z = 0 260 | 261 | local hit, norm, frac = self.__PHYSICS_BOX__:TraceBox( self:GetPos(), self:GetAngles(), startpos, startpos + delta, min, max ) 262 | 263 | if not hit then 264 | return 265 | end 266 | 267 | return { 268 | HitPos = hit, 269 | Normal = norm, 270 | Fraction = frac, 271 | } 272 | end 273 | --- PHYSICS --- 274 | --------------- 275 | 276 | --------------- 277 | --- DRAWING --- 278 | function ENT:DrawTranslucent() 279 | local DebugMode = (self.GetDebug and self:GetDebug()) 280 | if PropMLIB.Thumbnail.TakingScreenshot then DebugMode = false end 281 | 282 | if not self.LOADED_MESH then 283 | local minOBB, maxOBB = self:GetRenderBounds() 284 | 285 | render.MaterialOverride( self.DEFAULT_MATERIAL ) 286 | self:DrawModel() 287 | render.DrawWireframeBox( self:GetPos(), self:GetAngles(), minOBB, maxOBB, Color(255, 255, 255), true) 288 | render.MaterialOverride() 289 | 290 | self:DrawLOGO() 291 | else 292 | if DebugMode then self:DrawDEBUGBoxes() end 293 | self:DrawModelMeshes(DebugMode) 294 | end 295 | 296 | if DebugMode then self:DrawDEBUGInfo() end 297 | end 298 | 299 | function ENT:DrawDEBUGBoxes() 300 | local minROBB, maxROBB = self:GetRenderBounds() 301 | local minPOBB, maxPOBB = self:OBBMins(), self:OBBMaxs() 302 | local pos = self:GetPos() 303 | local ang = self:GetAngles() 304 | 305 | render.SetMaterial( self.DEFAULT_MATERIAL ) 306 | render.DrawBox( pos, ang, minROBB, maxROBB, Color(1, 1, 1, 1), true) 307 | 308 | render.DrawWireframeBox( pos, ang, minROBB, maxROBB, Color(255, 255, 255, 255), true) 309 | 310 | render.SetMaterial( self.DEFAULT_MATERIAL_PHYS ) 311 | render.DrawBox( pos, ang, minPOBB, maxPOBB, Color(255, 255, 255, 255), true) 312 | render.DrawWireframeBox( pos, ang, minPOBB, maxPOBB, Color(1, 1, 1, 255), true) 313 | end 314 | 315 | function ENT:DrawDEBUGInfo() 316 | local minROBB, maxROBB = self:GetRenderBounds() 317 | local pos = self:GetPos() 318 | local ang = self:GetAngles() 319 | 320 | local meshData = self.LOADED_MESH 321 | if meshData then 322 | local TexVec, TexAng = LocalToWorld( Vector(maxROBB.x - 0.5, maxROBB.y - 0.5, maxROBB.z), Angle(), pos, ang ) 323 | cam.Start3D2D( TexVec, TexAng, 0.1) 324 | render.PushFilterMag(TEXFILTER.POINT) 325 | render.PushFilterMin(TEXFILTER.POINT) 326 | draw.SimpleTextOutlined( #meshData.subMeshes .. " MESHES", "PROP_MESH_DEBUGFIXED", 0, 0, Color( 255, 255, 255, 255 ), TEXT_ALIGN_RIGHT, TEXT_ALIGN_RIGHT, 1, Color(1,1,1)) 327 | draw.SimpleTextOutlined( meshData.metadata.fileSize , "PROP_MESH_DEBUGFIXED", 0, 20, Color( 255, 255, 255, 255 ), TEXT_ALIGN_RIGHT, TEXT_ALIGN_RIGHT, 1, Color(1,1,1)) 328 | render.PopFilterMag() 329 | render.PopFilterMin() 330 | cam.End3D2D() 331 | 332 | local TexVec2, TexAng2 = LocalToWorld( Vector(minROBB.x + 0.5, minROBB.y, maxROBB.z), Angle(), pos, ang ) 333 | cam.Start3D2D( TexVec2, TexAng2, 0.1) 334 | for k, v in pairs(meshData.subMeshes) do 335 | local color = self.DEBUG_MATERIALS_COLORS[k] or Vector(0, 0, 0) 336 | draw.SimpleTextOutlined( k ..": ".. v.name , "PROP_MESH_DEBUGFIXED", 0, k * 18 - ((#meshData.subMeshes + 1) * 19), Color(color[1] * 255,color[2] * 255, color[3] * 255), TEXT_ALIGN_LEFT, TEXT_ALIGN_LEFT, 1, Color(1, 1, 1)) 337 | end 338 | cam.End3D2D() 339 | end 340 | end 341 | 342 | function ENT:DrawLOGO() 343 | local pos = self:GetPos() + Vector(0, 0, 4) 344 | if self.LAST_STATUS or self.LAST_MODEL_ERRORED then 345 | pos = pos + Vector( 0, 0, 4 ) 346 | end 347 | 348 | local ang = EyeAngles() 349 | ang:RotateAroundAxis(ang:Right(), 90) 350 | ang:RotateAroundAxis(ang:Up(), -90) 351 | 352 | cam.Start3D2D(pos, ang, 0.5) 353 | render.PushFilterMag(TEXFILTER.POINT) 354 | render.PushFilterMin(TEXFILTER.POINT) 355 | draw.DrawText( "PROP", "TargetID", 0, -1, Color( 255, 255, 255, 255 ), TEXT_ALIGN_CENTER ) 356 | render.PopFilterMag() 357 | render.PopFilterMin() 358 | cam.End3D2D() 359 | 360 | self:DrawStatus(pos, ang) 361 | end 362 | 363 | function ENT:DrawStatus(pos, ang) 364 | cam.Start3D2D(pos, ang, 0.18) 365 | render.PushFilterMag(TEXFILTER.POINT) 366 | render.PushFilterMin(TEXFILTER.POINT) 367 | if self.LAST_STATUS then 368 | draw.DrawText(tostring(self.LAST_STATUS), "DebugFixedSmall", 0, 55, Color( 255, 255, 255, 255 ), TEXT_ALIGN_CENTER ) 369 | end 370 | 371 | if self.LAST_MODEL_ERRORED then 372 | draw.DrawText("SHIFT + USE to retry", "DebugFixedSmall", 0, 70, Color( 192, 57, 43, 255 ), TEXT_ALIGN_CENTER ) 373 | end 374 | render.PopFilterMag() 375 | render.PopFilterMin() 376 | cam.End3D2D() 377 | end 378 | 379 | function ENT:DrawModelMeshes(DebugMode) 380 | if not IsValid(self) then return end 381 | if not self.LOADED_MESH then return end 382 | if not self.MESH_MODELS or #self.MESH_MODELS <= 0 then return end 383 | 384 | local Fullbright = self.GetFullbright and self:GetFullbright() 385 | if Fullbright then render.SuppressEngineLighting( true ) end 386 | 387 | self:DrawModel() -- Draw first mesh 388 | if #self.MESH_MODELS > 1 then 389 | local matrix = Matrix() 390 | matrix:SetAngles(self:GetAngles()) 391 | matrix:SetTranslation(self:GetPos()) 392 | 393 | -- Draw rest of meshes 394 | cam.PushModelMatrix( matrix ) 395 | for i = 2, #self.MESH_MODELS do 396 | local v = self.MESH_MODELS[i] 397 | if not v or v == NULL then continue end 398 | 399 | local mat = self:GetModelMaterial(i, DebugMode) 400 | if DebugMode then 401 | local debugColor = self.DEBUG_MATERIALS_COLORS[i] or Vector(0, 0, 0) 402 | mat:SetVector("$color2", Vector(debugColor[1], debugColor[2], debugColor[3])) 403 | end 404 | 405 | render.SetMaterial( mat ) 406 | 407 | if not v:IsValid() then 408 | table_remove(self.MESH_MODELS, i) 409 | else 410 | v:Draw() 411 | end 412 | end 413 | cam.PopModelMatrix() 414 | end 415 | 416 | if Fullbright then render.SuppressEngineLighting( false ) end 417 | end 418 | 419 | function ENT:GetRenderMesh() 420 | if not IsValid(self) then return end 421 | if not self.MESH_MODELS or #self.MESH_MODELS <= 0 then return end 422 | 423 | local initialMesh = self.MESH_MODELS[1] 424 | if not initialMesh or initialMesh == NULL then return end 425 | 426 | local DebugMode = self.GetDebug and self:GetDebug() 427 | if PropMLIB.Thumbnail.TakingScreenshot then DebugMode = false end 428 | 429 | local mat = self:GetModelMaterial(1, DebugMode) 430 | if DebugMode then 431 | local debugColor = self.DEBUG_MATERIALS_COLORS[1] or Vector(0, 0, 0) 432 | mat:SetVector("$color2", Vector(debugColor[1], debugColor[2], debugColor[3])) -- Might be a bad idea, but it looks cool 433 | end 434 | 435 | return { Mesh = self.MESH_MODELS[1], Material = mat } -- Render first mesh 436 | end 437 | 438 | 439 | -- TODO: IMPROVE ANGLE AND POSITIONING -- 440 | function ENT:TakeScreenshot() 441 | local loadedMesh = self.LOADED_MESH 442 | if not loadedMesh then return end 443 | 444 | local maxOBB, minOBB = self:GetRenderBounds() 445 | local OAngle = self:GetAngles() 446 | local OPos = self:GetPos() 447 | 448 | local size = 0 449 | size = math.max( size, math.abs(minOBB.x) + math.abs(maxOBB.x) ) 450 | size = math.max( size, math.abs(minOBB.y) + math.abs(maxOBB.y) ) 451 | size = math.max( size, math.abs(minOBB.z) + math.abs(maxOBB.z) ) 452 | 453 | if ( size < 600 ) then 454 | size = size * (1 - ( size / 254 )) 455 | else 456 | size = size * (1 - ( size / 4096 )) 457 | end 458 | 459 | size = math.Clamp( size, 5, 1000 ) 460 | -- 461 | 462 | local ViewPos, ViewAngle = LocalToWorld(Vector(maxOBB.z - size, (maxOBB.y + minOBB.y) / 2, 0), Angle(0, 0, -90), OPos, OAngle) 463 | 464 | PropMLIB.Thumbnail.TakeThumbnail({ 465 | ent = self, 466 | uri = loadedMesh.uri, 467 | origin = ViewPos, 468 | angles = ViewAngle 469 | }) 470 | 471 | -- Regenerate icons 472 | timer.Simple(0.15, function() 473 | if not IsValid(self) then return end 474 | self:GenerateSpawnIcons() 475 | end) 476 | end 477 | --- DRAWING --- 478 | --------------- 479 | 480 | -------- 481 | -- UI -- 482 | 483 | function ENT:CreateHelpers(props) 484 | --- DEBUG --- 485 | local meshDebug = props:CreateRow( "Helpers", "Debug" ) 486 | meshDebug:Setup( "Boolean" ) 487 | 488 | if self.GetDebug then meshDebug:SetValue(self:GetDebug()) 489 | else meshDebug:SetValue(false) end 490 | 491 | meshDebug.DataChanged = function( _, val ) 492 | net.Start("prop_mesh_command") 493 | net.WriteString("SET_DEBUG") 494 | net.WriteEntity(self) 495 | net.WriteBool((val == 1)) 496 | net.SendToServer() 497 | end 498 | ---- 499 | 500 | ------- 501 | local meshFullbright = props:CreateRow( "Helpers", "Fullbright" ) 502 | meshFullbright:Setup( "Boolean" ) 503 | 504 | if self.GetFullbright then meshFullbright:SetValue(self:GetFullbright()) 505 | else meshFullbright:SetValue(false) end 506 | 507 | meshFullbright.DataChanged = function( _, val ) 508 | net.Start("prop_mesh_command") 509 | net.WriteString("SET_FULLBRIGHT") 510 | net.WriteEntity(self) 511 | net.WriteBool((val == 1)) 512 | net.SendToServer() 513 | end 514 | ----- 515 | end 516 | 517 | function ENT:CreateMeshMenu() 518 | local meshPanel = vgui.Create( "DPanel", self.UI.SHEET ) 519 | self.UI.SHEET:AddSheet( "Prop", meshPanel, "icon16/brick_edit.png" ) 520 | 521 | local props = vgui.Create( "DProperties", meshPanel ) 522 | props:Dock( FILL ) 523 | 524 | ----- 525 | local meshURL = props:CreateRow( "Model", "Url" ) 526 | meshURL:Setup( "Generic" ) 527 | --- 528 | 529 | ---- SCALES --- 530 | local meshSizeX = props:CreateRow( "Mesh Scale", "Scale X" ) 531 | meshSizeX:Setup( "Float", { min = self.MIN_SAFE_SCALE, max = self.MAX_SAFE_SCALE } ) 532 | 533 | local meshSizeY = props:CreateRow( "Mesh Scale", "Scale Y" ) 534 | meshSizeY:Setup( "Float", { min = self.MIN_SAFE_SCALE, max = self.MAX_SAFE_SCALE } ) 535 | 536 | local meshSizeZ = props:CreateRow( "Mesh Scale", "Scale Z" ) 537 | meshSizeZ:Setup( "Float", { min = self.MIN_SAFE_SCALE, max = self.MAX_SAFE_SCALE } ) 538 | ---- 539 | 540 | ---- PHYSICS SCALE --- 541 | local meshPhysReset = props:CreateRow( "Physics Scale - !! Removes constrains if changed !!", "Reset physics to scale" ) 542 | meshPhysReset:Setup( "Boolean" ) 543 | meshPhysReset:SetValue( false ) 544 | 545 | local panelParent = meshPhysReset:GetChildren()[2]:GetChildren()[1] 546 | local checkBox = panelParent:GetChildren()[1] 547 | local showCheckboxText = false 548 | 549 | checkBox:SetPos(0, 1) 550 | checkBox:SetSize(280, 17) 551 | checkBox.Paint = function(self, w, h) 552 | derma.SkinHook( "Paint", "Button", self, w, h ) 553 | if not showCheckboxText then return end 554 | 555 | surface.SetFont("DermaDefaultBold") 556 | local tW, tH = surface.GetTextSize( "Done!" ) 557 | 558 | surface.SetTextColor( Color(39, 174, 96) ) 559 | surface.SetTextPos( ((w - tW) / 2) + 5, 2 ) -- Watever 560 | surface.DrawText( "Done!" ) 561 | end 562 | 563 | local meshPhysX = props:CreateRow( "Physics Scale - !! Removes constrains if changed !!", "Physics X" ) 564 | meshPhysX:Setup( "Float", { min = self.MIN_SAFE_SCALE, max = self.MAX_SAFE_SCALE } ) 565 | 566 | local meshPhysY = props:CreateRow( "Physics Scale - !! Removes constrains if changed !!", "Physics Y" ) 567 | meshPhysY:Setup( "Float", { min = self.MIN_SAFE_SCALE, max = self.MAX_SAFE_SCALE } ) 568 | 569 | local meshPhysZ = props:CreateRow( "Physics Scale - !! Removes constrains if changed !!", "Physics Z" ) 570 | meshPhysZ:Setup( "Float", { min = self.MIN_SAFE_SCALE, max = self.MAX_SAFE_SCALE } ) 571 | 572 | self:CreateHelpers(props) 573 | 574 | -- Garry pls. 575 | local uriElement = meshURL:GetChildren()[2]:GetChildren()[1]:GetChildren()[1] 576 | local SXElement = meshSizeX:GetChildren()[2]:GetChildren()[1]:GetChildren()[1] 577 | local SYElement = meshSizeY:GetChildren()[2]:GetChildren()[1]:GetChildren()[1] 578 | local SZElement = meshSizeZ:GetChildren()[2]:GetChildren()[1]:GetChildren()[1] 579 | 580 | local PXElement = meshPhysX:GetChildren()[2]:GetChildren()[1]:GetChildren()[1] 581 | local PYElement = meshPhysY:GetChildren()[2]:GetChildren()[1]:GetChildren()[1] 582 | local PZElement = meshPhysZ:GetChildren()[2]:GetChildren()[1]:GetChildren()[1] 583 | ---- 584 | 585 | meshPhysReset.DataChanged = function( _, val ) 586 | if not val then return end 587 | meshPhysX:SetValue(0) 588 | meshPhysY:SetValue(0) 589 | meshPhysZ:SetValue(0) 590 | 591 | meshPhysX:SetValue(SXElement:GetValue()) 592 | meshPhysY:SetValue(SYElement:GetValue()) 593 | meshPhysZ:SetValue(SZElement:GetValue()) 594 | 595 | meshPhysReset:SetValue( false ) 596 | surface.PlaySound( "garrysmod/ui_click.wav" ) 597 | 598 | showCheckboxText = true 599 | 600 | timer.Destroy("__prop_mesh_reset_ok__") 601 | timer.Create("__prop_mesh_reset_ok__", 1, 1, function() 602 | showCheckboxText = false 603 | end) 604 | end 605 | ---- 606 | 607 | self.UI.MeshElements = { 608 | uri = meshURL:GetChildren()[2]:GetChildren()[1]:GetChildren()[1], 609 | 610 | scale = {SXElement, SYElement, SZElement}, 611 | phys = {PXElement, PYElement, PZElement} 612 | } 613 | 614 | -- Update mesh -- 615 | self:UpdateMeshSettings() 616 | end 617 | 618 | function ENT:CreateTextureRow(parent, index) 619 | local textureURL = parent:CreateRow( "Urls - Use DEBUG to help map the texture", index ) 620 | textureURL:Setup( "Generic" ) 621 | 622 | local debugColor = self.DEBUG_MATERIALS_COLORS[index] 623 | local labelElement = textureURL.Label 624 | labelElement:SetColor(Color(debugColor[1] * 255, debugColor[2] * 255, debugColor[3] * 255)) 625 | 626 | textureURL.Paint = function(self, w, h) 627 | if ( !IsValid( self.Inner ) ) then return end 628 | 629 | local Skin = self:GetSkin() 630 | local editing = self.Inner:IsEditing() 631 | local disabled = !self.Inner:IsEnabled() || !self:IsEnabled() 632 | 633 | if disabled or editing then 634 | if disabled then 635 | surface.SetDrawColor( Skin.Colours.Properties.Column_Disabled ) 636 | else 637 | surface.SetDrawColor( Color(230, 230, 230) ) 638 | end 639 | 640 | surface.DrawRect( w * 0.45, 0, w, h ) 641 | end 642 | 643 | surface.SetDrawColor( Skin.Colours.Properties.Border ) 644 | surface.DrawRect( w - 1, 0, 1, h ) 645 | surface.DrawRect( w * 0.45, 0, 1, h ) 646 | surface.DrawRect( 0, h - 1, w, 1 ) 647 | 648 | if editing then 649 | surface.SetDrawColor( Color(60, 60, 60) ) 650 | else 651 | surface.SetDrawColor( Color(1, 1, 1) ) 652 | end 653 | surface.DrawRect( 0.1, 0.1, w * 0.45 - 0.1, h - 0.1 ) 654 | end 655 | 656 | return { 657 | uriText = textureURL:GetChildren()[2]:GetChildren()[1]:GetChildren()[1], 658 | rowText = textureURL 659 | } 660 | end 661 | 662 | ------------- 663 | --- MTL --- 664 | function ENT:PreFetchMTL(uri, onComplete) 665 | if self.IGNORE_CONTENT_RANGE:GetInt() ~= 0 then 666 | print("[PropMLIB] Skipping content range check") 667 | return onComplete(nil, tonumber(1)) 668 | end 669 | 670 | HTTP({ 671 | url = uri, 672 | method = "GET", 673 | headers = { 674 | ["Range"] = "bytes=0-1" 675 | }, 676 | success = function(code, body, headers) 677 | if not headers then return onComplete("!! Cannot PRE-FETCH MTL !!") end 678 | local fileSize = nil 679 | local fileRange = headers["Content-Range"] or headers["content-range"] 680 | 681 | if fileRange then 682 | local range = string.Explode('/', fileRange) 683 | if not range or not range[2] then return onComplete("!! Failed to find 'Content-Range' header !!") end 684 | 685 | fileSize = range[2] 686 | else 687 | if string.StartWith(uri, 'https://pastebin.com') then 688 | fileSize = #body 689 | else 690 | return onComplete("!! Failed to find 'Content-Range' header !!") 691 | end 692 | end 693 | 694 | return onComplete(nil, tonumber(fileSize)) 695 | end, 696 | failed = function(err) 697 | return onComplete("!! Cannot PRE-FETCH MTL !!") 698 | end 699 | }) 700 | end 701 | 702 | function ENT:MapMTLTexture(uri, onComplete) 703 | uri = PropMLIB.Util.FixUrl(uri) -- Quick fix 704 | 705 | self:PreFetchMTL(uri, function(err, dataSize) 706 | if err then return onComplete(err) end 707 | if not dataSize then return onComplete("!! Cannot PRE-FETCH MTL !!") end 708 | if dataSize > 20000 then return onComplete("!! MTL file too big (Max: 20kb) !!") end 709 | 710 | local baseUrl = string_path( uri ) 711 | local directLinkSupport = true 712 | if baseUrl:find("drive.google.com", 1, true) or baseUrl:find("dropbox", 1, true) then 713 | directLinkSupport = false 714 | end 715 | 716 | HTTP({ 717 | url = uri, 718 | method = "GET", 719 | success = function(code, body, headers) 720 | if not body or string_trim(body) == "" then return onComplete("!! Invalid MTL url !!") end 721 | 722 | local data = PropMLIB.Obj.ParseMTL(baseUrl, body) 723 | if not data or table_count(data) <= 0 then return onComplete("!! Invalid MTL file !!") end 724 | 725 | local meshData = self.LOADED_MESH 726 | local mapped = 0 727 | 728 | for k, v in pairs(meshData.subMeshes) do 729 | if not v then continue end 730 | if not v.mtl or string_trim(v.mtl) == "" then continue end 731 | if not data[v.mtl] then continue end 732 | 733 | local tRow = self.UI.TextureRows[k] 734 | if not tRow or not tRow.rowText or not tRow.rowText.Label then continue end 735 | 736 | if directLinkSupport then 737 | tRow.uriText:SetText(baseUrl .. data[v.mtl].material) 738 | else 739 | tRow.uriText:SetText("REPLACE WITH GENERATED URL FOR: " .. data[v.mtl].material) -- User needs to generate 740 | end 741 | 742 | mapped = mapped + 1 743 | end 744 | 745 | if mapped <= 0 then 746 | return onComplete("!! Failed to map MTL !!") 747 | else 748 | return onComplete(nil) 749 | end 750 | end, 751 | failed = function(err) 752 | return onComplete("!! Invalid MTL url !!") 753 | end 754 | }) 755 | end) 756 | end 757 | 758 | function ENT:CreateMTLMapper(parent) 759 | local mtlMapper = parent:CreateRow( "MTL Mapper", "Url" ) 760 | mtlMapper:Setup( "Generic" ) 761 | 762 | local lastError = nil 763 | local isLoading = false 764 | local onError = function(err) 765 | print("[prop_mesh]"..err) 766 | 767 | lastError = err 768 | surface.PlaySound( "buttons/button8.wav" ) 769 | 770 | timer.Destroy("__prop_mesh_reset_err__") 771 | timer.Create("__prop_mesh_reset_err__", 2, 1, function() 772 | lastError = nil 773 | end) 774 | end 775 | 776 | local mtlData = mtlMapper:GetChildren()[2]:GetChildren()[1]:GetChildren()[1] 777 | local mtlMapperButton = parent:CreateRow( "MTL Mapper", "Apply MTL" ) 778 | mtlMapperButton:Setup( "Boolean" ) 779 | mtlMapperButton.DataChanged = function( _, val ) 780 | if not val then return end 781 | mtlMapperButton:SetValue( false ) 782 | 783 | if isLoading then return end 784 | if not self.LOADED_MESH then return onError("!! No mesh loaded !!") end 785 | 786 | local mtlUri = mtlData:GetValue() 787 | if mtlUri and string_trim(mtlUri) != "" then 788 | surface.PlaySound( "garrysmod/ui_click.wav" ) 789 | 790 | isLoading = true 791 | lastError = nil 792 | 793 | self:MapMTLTexture(mtlUri, function(err) 794 | if err then onError(err) end 795 | isLoading = false 796 | end) 797 | else 798 | return onError("!! Invalid MTL url !!") 799 | end 800 | end 801 | 802 | local panelParent = mtlMapperButton:GetChildren()[2]:GetChildren()[1] 803 | local checkBox = panelParent:GetChildren()[1] 804 | 805 | checkBox:SetPos(0, 1) 806 | checkBox:SetSize(272, 17) 807 | checkBox.Paint = function(self, w, h) 808 | derma.SkinHook( "Paint", "Button", self, w, h ) 809 | surface.SetFont("DermaDefaultBold") 810 | 811 | if isLoading then 812 | local txt = "Parsing MTL.." 813 | local tW, tH = surface.GetTextSize( txt ) 814 | 815 | surface.SetTextColor( Color(41, 128, 185) ) 816 | surface.SetTextPos( ((w - tW) / 2) + 5, 2 ) -- Watever 817 | surface.DrawText( txt ) 818 | elseif lastError then 819 | local tW, tH = surface.GetTextSize( lastError ) 820 | 821 | surface.SetTextColor( Color(231, 76, 60) ) 822 | surface.SetTextPos( ((w - tW) / 2) + 5, 2 ) -- Watever 823 | surface.DrawText( lastError ) 824 | end 825 | end 826 | end 827 | 828 | 829 | --- MTL --- 830 | ------------- 831 | function ENT:CreateTextureMenu() 832 | local maxMaterials = PropMLIB.Obj.MAX_SUBMESHES:GetInt() 833 | if LocalPlayer():IsAdmin() then 834 | maxMaterials = 20 835 | end 836 | 837 | local texturePanel = vgui.Create( "DPanel", self.UI.SHEET ) 838 | self.UI.SHEET:AddSheet( "Textures", texturePanel, "icon16/images.png" ) 839 | 840 | local props = vgui.Create( "DProperties", texturePanel ) 841 | props:Dock( FILL ) 842 | 843 | --- MAPPER --- 844 | self:CreateMTLMapper(props) 845 | ---- 846 | 847 | self.UI.TextureRows = {} 848 | for i = 1, maxMaterials do 849 | table_insert(self.UI.TextureRows, self:CreateTextureRow(props, i)) 850 | end 851 | 852 | self:UpdateTextureName() 853 | end 854 | 855 | ----- 856 | function ENT:RemoveHistory(uri) 857 | if not self.HISTORY_MESHES or not self.HISTORY_MESHES[uri] then return end 858 | self.HISTORY_MESHES[uri] = nil 859 | 860 | self:SaveHistory() 861 | end 862 | 863 | function ENT:SaveHistory() 864 | file.Write( "prop_mesh/__saved_meshes.json", util.TableToJSON( self.HISTORY_MESHES ) ) 865 | self:GenerateSpawnIcons() -- Re-generate it 866 | end 867 | 868 | function ENT:AddHistory(addData) 869 | if not self.HISTORY_MESHES then self.HISTORY_MESHES = {} end 870 | if not addData or not addData.uri or string_trim(addData.uri) == "" then return end 871 | 872 | self.HISTORY_MESHES[addData.uri] = addData 873 | self:SaveHistory() 874 | end 875 | 876 | function ENT:LoadHistory() 877 | if not file.Exists("prop_mesh/__saved_meshes.json", "DATA") then 878 | return 879 | end 880 | 881 | local rawHistory = file.Read("prop_mesh/__saved_meshes.json") 882 | self.HISTORY_MESHES = util.JSONToTable( rawHistory ) or {} 883 | end 884 | 885 | function ENT:CreateButtonMaterial(path) 886 | local tempMat = Material(path) 887 | tempMat:Recompute() 888 | 889 | return tempMat 890 | end 891 | 892 | function ENT:CreateSpawnIcon(uri, panel, iconLayout, onClick, onRightClick) 893 | local button = vgui.Create( "DImageButton", iconLayout ) 894 | button:SetSize( 128, 128 ) 895 | button:SetTooltip(uri) 896 | 897 | local matTest = self:CreateButtonMaterial("../data/prop_mesh/thumbnails/" .. util.CRC(uri) .. ".jpg") 898 | button:SetMaterial( matTest ) 899 | button.DoClick = function() 900 | return onClick(uri) 901 | end 902 | 903 | button.DoRightClick = function() 904 | local SubMenu = DermaMenu(true, panel) 905 | local deleteBtn = SubMenu:AddOption( "Remove", function() 906 | self:RemoveHistory(uri) 907 | button:Remove() 908 | return 909 | end) 910 | 911 | deleteBtn:SetIcon( "icon16/delete.png" ) 912 | SubMenu:Open() 913 | end 914 | end 915 | 916 | function ENT:CreateExamplesMenu() 917 | self.UI.EXAMPLEPANEL = vgui.Create( "DPanel", self.UI.SHEET ) 918 | self.UI.SHEET:AddSheet( "Examples", self.UI.EXAMPLEPANEL, "icon16/lightbulb.png" ) 919 | local EXAMPLE_LIST = util.JSONToTable('{"1":{"textures":["https://i.imgur.com/Pz7NN5G.png","https://i.imgur.com/Pz7NN5G.png","https://i.imgur.com/Pz7NN5G.png","https://i.imgur.com/Pz7NN5G.png","https://i.imgur.com/Pz7NN5G.png","https://i.imgur.com/Pz7NN5G.png","","","","","","","","","","","","","",""],"scale":"[2 2 2]","uri":"https://pastebin.com/raw.php?i=bk29sfat","phys":"[2 2 2]"},"2":{"textures":["https://i.imgur.com/XgkKhUn.png","https://i.imgur.com/0ub93KD.png","","","","","","","","","","","","","","","","","",""],"scale":"[1 1 1]","uri":"https://pastebin.com/raw.php?i=Kzp4K0DQ","phys":"[1 1 1]"},"3":{"textures":["https://i.imgur.com/jsyUxGy.png","https://i.imgur.com/jsyUxGy.png","https://i.imgur.com/jsyUxGy.png","https://i.imgur.com/jsyUxGy.png","","","","","","","","","","","","","","","",""],"scale":"[20 20 20]","uri":"https://pastebin.com/raw/vxsLQHCL","phys":"[20 10.5 20]"},"4":{"textures":["https://i.rawr.dev/RtXRrOYMK6.png","","","","","","","","","","","","","","","","","","",""],"scale":"[10 10 10]","uri":"https://pastebin.com/raw.php?i=1MZ5nxb4","phys":"[10 10 10]"}}' ) 920 | 921 | local ExampleList = vgui.Create( "DListView", self.UI.EXAMPLEPANEL ) 922 | ExampleList:Dock( FILL ) 923 | ExampleList:SetMultiSelect( false ) 924 | ExampleList:AddColumn("Name (Double click to load)") 925 | 926 | ExampleList:AddLine("Puppet Axyl") 927 | ExampleList:AddLine("Lasagna") 928 | ExampleList:AddLine("Couch") 929 | ExampleList:AddLine("Spyro") 930 | 931 | ExampleList.DoDoubleClick = function(panel, index) 932 | if not EXAMPLE_LIST[index] then return end 933 | 934 | local savedData = EXAMPLE_LIST[index] 935 | if savedData.textures then 936 | savedData.textures = self:SanitizeTextures(savedData.textures) 937 | end 938 | 939 | self:UILoadData(savedData) 940 | self:UpdateTextureName(savedData) 941 | self:UpdateMeshSettings(savedData) 942 | end 943 | end 944 | 945 | function ENT:CreateHistoryMenu() 946 | self:LoadHistory() -- Load history 947 | 948 | self.UI.HISTORYPANEL = vgui.Create( "DPanel", self.UI.SHEET ) 949 | self.UI.SHEET:AddSheet( "Saved Props", self.UI.HISTORYPANEL, "icon16/book_addresses.png" ) 950 | 951 | local scroll = vgui.Create( "DScrollPanel", self.UI.HISTORYPANEL) -- Create the Scroll panel 952 | scroll.Paint = function(self, w, h) 953 | surface.SetDrawColor( Color(1, 1, 1) ) 954 | surface.DrawRect( 0, 0, w , h ) 955 | end 956 | scroll:Dock( FILL ) 957 | 958 | self.UI.ICONLIST = vgui.Create( "DIconLayout", scroll ) 959 | self.UI.ICONLIST:Dock( FILL ) 960 | self.UI.ICONLIST:SetSpaceY( 5 ) 961 | self.UI.ICONLIST:SetSpaceX( 5 ) 962 | self.UI.ICONLIST:Layout() 963 | 964 | self:GenerateSpawnIcons() 965 | end 966 | 967 | ---- 968 | 969 | function ENT:SanitizeTextures(textures) 970 | local cleanTextures = {} 971 | if not textures or #textures <= 0 then return cleanTextures end 972 | 973 | for _, text in pairs(textures) do 974 | if not text then continue end 975 | table_insert(cleanTextures, text) 976 | end 977 | 978 | return cleanTextures 979 | end 980 | 981 | function ENT:UILoadData(data) 982 | surface.PlaySound( "garrysmod/ui_click.wav" ) 983 | 984 | net.Start("prop_mesh_command") 985 | net.WriteString("UPDATE_MESH") 986 | net.WriteEntity(self) 987 | net.WriteTable(data) 988 | net.SendToServer() 989 | end 990 | 991 | function ENT:GenerateSpawnIcons() 992 | if not self.UI or not IsValid(self.UI.PANEL) or not self.UI.ICONLIST then return end 993 | self.UI.ICONLIST:Clear() 994 | 995 | local onClick = function(clickedURL) 996 | local savedData = self.HISTORY_MESHES[clickedURL] 997 | if not self.HISTORY_MESHES or not savedData then return end 998 | if savedData.textures then 999 | savedData.textures = self:SanitizeTextures(savedData.textures) 1000 | end 1001 | 1002 | self:UILoadData(savedData) 1003 | self:UpdateTextureName(savedData) 1004 | self:UpdateMeshSettings(savedData) 1005 | end 1006 | 1007 | for _, v in pairs(self.HISTORY_MESHES) do 1008 | self:CreateSpawnIcon(v.uri, self.UI.HISTORYPANEL, self.UI.ICONLIST, function(clickedURL) 1009 | return onClick(clickedURL) 1010 | end) 1011 | end 1012 | end 1013 | 1014 | function ENT:UpdateMeshSettings(savedData) 1015 | if not self.UI or not IsValid(self.UI.PANEL) or not self.UI.MeshElements then return end 1016 | local elements = self.UI.MeshElements 1017 | local currentData = {} 1018 | 1019 | if savedData then 1020 | currentData = savedData 1021 | elseif self.LAST_REQUESTED_MESH then 1022 | currentData = table_copy(self.LAST_REQUESTED_MESH) 1023 | end 1024 | 1025 | elements.uri:SetValue( currentData.uri or "" ) 1026 | 1027 | local scale = currentData.scale or Vector(1, 1, 1) 1028 | elements.scale[1]:SetValue(scale.x) 1029 | elements.scale[2]:SetValue(scale.y) 1030 | elements.scale[3]:SetValue(scale.z) 1031 | 1032 | local phys = currentData.phys or Vector(1, 1, 1) 1033 | elements.phys[1]:SetValue(phys.x) 1034 | elements.phys[2]:SetValue(phys.y) 1035 | elements.phys[3]:SetValue(phys.z) 1036 | end 1037 | 1038 | function ENT:UpdateTextureName(texturesData) 1039 | if not self.UI or not IsValid(self.UI.PANEL) or not self.UI.TextureRows then return end 1040 | 1041 | local materials = table_copy(self.MATERIAL_URLS) or {} 1042 | if texturesData and texturesData.textures then 1043 | materials = texturesData.textures 1044 | end 1045 | 1046 | local loadedMesh = self.LOADED_MESH 1047 | for i = 1, #self.UI.TextureRows do 1048 | local tRow = self.UI.TextureRows[i] 1049 | if not tRow or not tRow.rowText or not tRow.rowText.Label then continue end 1050 | 1051 | local name = nil 1052 | if loadedMesh and loadedMesh.subMeshes[i] then 1053 | name = loadedMesh.subMeshes[i].name 1054 | end 1055 | 1056 | tRow.rowText.Label:SetText(name or "Texture_" .. i) 1057 | tRow.uriText:SetText(materials[i] or "") 1058 | end 1059 | end 1060 | 1061 | function ENT:CreateMenu() 1062 | if self.UI then 1063 | if IsValid(self.UI.PANEL) then 1064 | self.UI.PANEL:Remove() 1065 | end 1066 | else 1067 | self.UI = {} 1068 | end 1069 | 1070 | self.UI.PANEL = vgui.Create( "DFrame" ) 1071 | self.UI.PANEL:SetSize( 568, 400 ) 1072 | self.UI.PANEL:SetTitle( "prop_mesh - Settings Menu" ) 1073 | self.UI.PANEL:SetDraggable( true ) 1074 | self.UI.PANEL:Center() 1075 | self.UI.PANEL:MakePopup() 1076 | 1077 | self.UI.PANEL.btnMinim:SetVisible( false ) 1078 | self.UI.PANEL.btnMaxim:SetVisible( false ) 1079 | 1080 | 1081 | ---- SECTIONS --- 1082 | self.UI.SHEET = vgui.Create( "DPropertySheet", self.UI.PANEL ) 1083 | self.UI.SHEET:Dock( FILL ) 1084 | 1085 | --- MESH --- 1086 | self:CreateMeshMenu() 1087 | --- TEXTURE --- 1088 | self:CreateTextureMenu() 1089 | --- HISTORY --- 1090 | self:CreateHistoryMenu() 1091 | --- EXAMPLES --- 1092 | self:CreateExamplesMenu() 1093 | --------------- 1094 | 1095 | local updateBtn = vgui.Create( "DButton", self.UI.PANEL ) 1096 | updateBtn:SetText( "Update prop" ) 1097 | updateBtn:Dock( BOTTOM ) 1098 | updateBtn.DoClick = function() 1099 | local elements = self.UI.MeshElements 1100 | if not elements then return end 1101 | 1102 | local texts = {} 1103 | for _, v in pairs(self.UI.TextureRows) do 1104 | if not v or not v.uriText then continue end 1105 | table_insert(texts, PropMLIB.Util.FixUrl(v.uriText:GetValue())) 1106 | end 1107 | 1108 | local data = { 1109 | uri = PropMLIB.Util.FixUrl(elements.uri:GetValue()), 1110 | scale = Vector(elements.scale[1]:GetValue(), elements.scale[2]:GetValue(), elements.scale[3]:GetValue()), 1111 | phys = Vector(elements.phys[1]:GetValue(), elements.phys[2]:GetValue(), elements.phys[3]:GetValue()), 1112 | textures = self:SanitizeTextures(texts) 1113 | } 1114 | 1115 | self:AddHistory(data) 1116 | self:UILoadData(data) 1117 | end 1118 | end 1119 | -- UI -- 1120 | -------- -------------------------------------------------------------------------------- /lua/entities/prop_mesh/init.lua: -------------------------------------------------------------------------------- 1 | local string_trim = string.Trim 2 | local string_find = string.find 3 | local string_replace = string.Replace 4 | 5 | AddCSLuaFile( "cl_init.lua" ) -- Make sure clientside 6 | AddCSLuaFile( "shared.lua" ) -- and shared scripts are sent. 7 | 8 | include('shared.lua') 9 | 10 | ENT.SAVE_DATA = {} 11 | --- INIT --- 12 | 13 | -------------- 14 | --- Spawn ---- 15 | local function MakePMESHEnt(ply, data) 16 | if IsValid(ply) and not ply:CheckLimit("prop_mesh") then return nil end 17 | 18 | local ent = ents.Create("prop_mesh") 19 | if not ent:IsValid() then return nil end 20 | 21 | ent:SetPos(data.Pos) 22 | 23 | if ent.CPPISetOwner then 24 | ent:CPPISetOwner(ply) 25 | else 26 | ent:SetNWEntity("owner", ply) 27 | end 28 | 29 | ent:Spawn() 30 | ent:Activate() 31 | 32 | if IsValid(ply) then 33 | ply:AddCount("prop_mesh", ent) 34 | ply:AddCleanup("prop_mesh", ent) 35 | end 36 | 37 | return ent 38 | end 39 | 40 | function ENT:SpawnFunction( ply, tr ) 41 | if (not tr.Hit) then return end 42 | 43 | local SpawnPos = tr.HitPos + tr.HitNormal * 16 44 | return MakePMESHEnt(ply, {Pos = SpawnPos}) 45 | end 46 | --- Spawn ---- 47 | -------------- 48 | 49 | ------------- 50 | --- SEND ---- 51 | function ENT:SendLoadedMeshToNewPlayer(ply) 52 | local lastMesh = self.LAST_REQUESTED_MESH 53 | if not lastMesh or not lastMesh.uri then return end 54 | 55 | -- SEND TEXTURES AND MESH -- 56 | self:SendTextures(self.MATERIAL_URLS, ply) 57 | self:SendLoadMesh(lastMesh, ply) 58 | end 59 | 60 | function ENT:SendLoadMesh(data, ply) 61 | net.Start("prop_mesh_command") 62 | net.WriteInt(self:EntIndex(), 32) 63 | net.WriteString("MESH_LOAD") 64 | net.WriteTable(data) 65 | if ply then 66 | net.Send(ply) 67 | else 68 | net.Broadcast() 69 | end 70 | end 71 | 72 | function ENT:SetTextures(textures) 73 | if not textures or #textures <= 0 then return end 74 | self.MATERIAL_URLS = textures 75 | 76 | self.SAVE_DATA.textures = textures -- Save 77 | self:SaveDupeData() 78 | 79 | self:SendTextures(textures) 80 | end 81 | 82 | function ENT:SendTextures(textures, ply) 83 | net.Start("prop_mesh_command") 84 | net.WriteInt(self:EntIndex(), 32) 85 | net.WriteString("TEXTURE_LOAD") 86 | net.WriteTable(textures) 87 | 88 | if ply then 89 | net.Send(ply) 90 | else 91 | net.Broadcast() 92 | end 93 | end 94 | --- SEND ---- 95 | ------------- 96 | 97 | ---------------- 98 | --- GENERAL ---- 99 | function ENT:OnNewPlayerJoin(newPly) 100 | self:SendLoadedMeshToNewPlayer(newPly) 101 | end 102 | 103 | function ENT:Use(ply, caller) 104 | if not IsValid(ply) then return end 105 | 106 | net.Start("prop_mesh_command") 107 | net.WriteInt(self:EntIndex(), 32) 108 | net.WriteString("ON_USE_PRESS") 109 | net.Send(ply) 110 | end 111 | 112 | function ENT:SaveDupeData() 113 | if not IsValid(self) or not self.SAVE_DATA then return end 114 | duplicator.StoreEntityModifier(self, "SAVE_DATA", self.SAVE_DATA) 115 | end 116 | 117 | function ENT:Load(uri, textures, scale, phys, duped) 118 | if not uri or string_trim(uri) == "" then return end 119 | local owner = self:GetNWEntity("owner") 120 | if self.CPPIGetOwner then 121 | owner = self:CPPIGetOwner() 122 | end 123 | 124 | local isAdmin = owner:IsAdmin() 125 | 126 | -- FIX INPUT --- 127 | scale = PropMLIB.Util.ClampVector(scale or Vector(1, 1, 1), self.MIN_SAFE_SCALE, self.MAX_SAFE_SCALE) 128 | phys = PropMLIB.Util.ClampVector(phys or Vector(1, 1, 1), self.MIN_SAFE_SCALE, self.MAX_SAFE_SCALE) 129 | uri = PropMLIB.Util.FixUrl(uri) 130 | ---------- 131 | 132 | -- Adv dupe saving 133 | self.SAVE_DATA = {meshURL = uri, textures = textures, scale = scale, phys = phys} 134 | self:SaveDupeData() 135 | ------- 136 | 137 | -- Clear -- 138 | if not duped then self:Clear() end 139 | ----------- 140 | 141 | self:SetTextures(textures) 142 | 143 | self.LAST_REQUESTED_MESH = {uri = uri, scale = scale, phys = phys, isAdmin = isAdmin, duped = duped} 144 | self:SendLoadMesh(self.LAST_REQUESTED_MESH) -- Start client load 145 | 146 | -- Server load -- 147 | self:LoadOBJ(uri, isAdmin, function(meshData) 148 | if not meshData then 149 | print("MeshData is invalid") 150 | return 151 | end 152 | 153 | meshData.scale = scale 154 | meshData.phys = phys 155 | 156 | -- Adv dupe save OBB 157 | self.SAVE_DATA.obb = meshData.obb 158 | self:SaveDupeData() 159 | ------- 160 | 161 | self:BuildMeshes(meshData) 162 | end, function(err) 163 | -- ERR 164 | print("server side failed :<", err) 165 | end) 166 | end 167 | --- GENERAL ---- 168 | ---------------- 169 | -------------------------------------------------------------------------------- /lua/entities/prop_mesh/shared.lua: -------------------------------------------------------------------------------- 1 | 2 | ENT.Type = "anim" 3 | ENT.Base = "base_gmodentity" 4 | 5 | ENT.PrintName = "prop_mesh" 6 | ENT.Author = "FailCake" 7 | ENT.RenderGroup = RENDERGROUP_TRANSLUCENT 8 | ENT.AdminOnly = false 9 | ENT.Category = "Custom Props" 10 | ENT.Contact = "https://github.com/edunad/prop_mesh" 11 | ENT.Spawnable = true 12 | 13 | local math_clamp_ = math.Clamp 14 | local math_abs = math.abs 15 | local table_copy = table.Copy 16 | local string_find = string.find 17 | 18 | -- Default SETTINGS --------- 19 | ENT.MAX_SAFE_VOLUME = GetConVar( "prop_mesh_maxScaleVolume" ) 20 | ENT.MIN_SAFE_VOLUME = GetConVar( "prop_mesh_minScaleVolume" ) 21 | ENT.IGNORE_CONTENT_RANGE = GetConVar( "prop_mesh_ignoreContentRange" ) 22 | 23 | ENT.MIN_SAFE_SCALE = 0.01 24 | ENT.MAX_SAFE_SCALE = 100 25 | 26 | ENT.MAX_OBJ_SIZE_BYTES = GetConVar( "prop_mesh_maxOBJ_bytes" ) 27 | ----------------------------- 28 | 29 | --- LOADED MODEL --- 30 | ENT.LOADED_MESH = nil 31 | ENT.LAST_REQUESTED_MESH = nil 32 | ENT.LAST_PHYSICS_OBB = nil 33 | 34 | ENT.LAST_MODEL_ERRORED = false 35 | ENT.MATERIAL_URLS = {} 36 | ------------------- 37 | 38 | --- OTHERS --- 39 | ENT.LAST_STATUS = nil 40 | -------------- 41 | 42 | ---------------- 43 | --- GENERAL ---- 44 | function ENT:Initialize() 45 | self:SetDefaultPhysics() 46 | 47 | if CLIENT then 48 | self:GenerateExtraRandomColors() 49 | 50 | timer.Simple(0.01, function() 51 | if not IsValid(self) then return end 52 | self.DEFAULT_MATERIAL:SetVector("$color2", Vector(0, 0, 0)) 53 | self.DEFAULT_MATERIAL_PHYS:SetVector("$color2", Vector(1, 1, 1)) 54 | end) 55 | return 56 | end 57 | 58 | self:SetModel("models/hunter/blocks/cube05x05x05.mdl") 59 | self:SetRenderMode( RENDERMODE_TRANSTEXTURE ) 60 | self:SetMoveType( MOVETYPE_VPHYSICS ) 61 | self:SetUseType( SIMPLE_USE ) 62 | self:DrawShadow( false ) 63 | 64 | duplicator.StoreEntityModifier(self, "SAVE_DATA", self.SAVE_DATA) 65 | PropMLIB.Registry.RegisterPMesh(self) 66 | end 67 | 68 | function ENT:SetupDataTables() 69 | self:NetworkVar( "Bool", 0, "Debug" ) 70 | self:NetworkVar( "Bool", 1, "Fullbright" ) 71 | end 72 | 73 | function ENT:OnRemove() 74 | if CLIENT then 75 | local entIndex = self:EntIndex() 76 | local meshes = self.MESH_MODELS 77 | local panel = self.PANEL 78 | 79 | timer.Simple(0.1, function() 80 | if IsValid(self) then return end 81 | if self.PANEL then self.PANEL:Remove() end 82 | 83 | if IsValid( self.__PHYSICS_BOX__ ) then 84 | self.__PHYSICS_BOX__:Destroy() 85 | end 86 | 87 | PropMLIB.PVSCache.Remove(entIndex) 88 | PropMLIB.MeshParser.ClearMeshes(meshes) 89 | PropMLIB.MeshParser.UnRegister(self) 90 | end) 91 | else 92 | PropMLIB.Registry.UnRegisterPMesh(self) 93 | PropMLIB.MeshParser.UnRegister(self) 94 | end 95 | end 96 | 97 | function ENT:Think() 98 | self:EnableCustomCollisions(true) -- Gravity gun likes to mess with it 99 | 100 | if SERVER then 101 | self:NextThink( CurTime() ) 102 | elseif CLIENT then 103 | self:SetNextClientThink( CurTime() ) 104 | end 105 | 106 | return true 107 | end 108 | --- GENERAL ---- 109 | ---------------- 110 | 111 | ------------- 112 | --- UTIL ---- 113 | function ENT:GetOBBSize(obb) 114 | local minOBB = obb.minOBB 115 | local maxOBB = obb.maxOBB 116 | 117 | local width = maxOBB.x - minOBB.x 118 | local lenght = maxOBB.y - minOBB.y 119 | local height = maxOBB.z - minOBB.z 120 | 121 | return Vector(width, lenght, height) 122 | end 123 | 124 | function ENT:VectorToSafe(scale, obb) 125 | local fixedScale = PropMLIB.Util.ClampVector(Vector(scale.x, scale.y, scale.z) or Vector(1, 1, 1), self.MIN_SAFE_SCALE, self.MAX_SAFE_SCALE) 126 | local minVol = self.MIN_SAFE_VOLUME:GetInt() 127 | local maxVol = self.MAX_SAFE_VOLUME:GetInt() 128 | 129 | local OBB = self:GetOBBSize(obb) 130 | for i = 1, 3 do 131 | local size = OBB[i] 132 | local scaler = fixedScale[i] 133 | local size_actual = size * scaler 134 | local size_clamped = math_clamp_(size_actual, minVol, maxVol) 135 | local new = size_clamped / size 136 | 137 | if not PropMLIB.Util.IsFinite(new) or math_abs(new) < 0.00000001 then return end 138 | fixedScale[i] = new 139 | end 140 | 141 | return fixedScale 142 | end 143 | 144 | function ENT:Clear() 145 | -- Clear currently loaded -- 146 | self.LOADED_MESH = nil 147 | 148 | -- Clear last requested -- 149 | self.LAST_REQUESTED_MESH = nil 150 | 151 | -- Clear status / error --- 152 | self.LAST_STATUS = nil 153 | self.LAST_MODEL_ERRORED = false 154 | 155 | -- Clear physics -- 156 | self.LAST_PHYSICS_OBB = nil 157 | self:SetDefaultPhysics() 158 | 159 | -- Clear meshes -- 160 | if CLIENT then self:ClearMeshes() end 161 | end 162 | --- UTIL ---- 163 | ------------- 164 | 165 | ---------------- 166 | --- Physics ---- 167 | function ENT:CreateOBBPhysics(minOBB, maxOBB, forced) 168 | if not IsValid(self) then return end 169 | 170 | minOBB = PropMLIB.Util.SafeVector(minOBB, true) 171 | maxOBB = PropMLIB.Util.SafeVector(maxOBB, false) 172 | 173 | if not forced then 174 | if self.LAST_PHYSICS_OBB and 175 | self.LAST_PHYSICS_OBB.minOBB:IsEqualTol(minOBB,0) and 176 | self.LAST_PHYSICS_OBB.maxOBB:IsEqualTol(maxOBB,0) then 177 | return 178 | end 179 | 180 | self.LAST_PHYSICS_OBB = {minOBB = minOBB, maxOBB = maxOBB} 181 | end 182 | 183 | if CLIENT then 184 | if IsValid( self.__PHYSICS_BOX__ ) then 185 | self.__PHYSICS_BOX__:Destroy() 186 | end 187 | end 188 | 189 | -- Create OBB physics -- 190 | if SERVER then 191 | self:PhysicsInitBox( minOBB, maxOBB ) 192 | self:SetSolid( SOLID_VPHYSICS ) 193 | 194 | local phys = self:GetPhysicsObject() 195 | if IsValid(phys) then 196 | phys:EnableMotion( false ) 197 | phys:Sleep() 198 | end 199 | else 200 | self.__PHYSICS_BOX__ = CreatePhysCollideBox( minOBB, maxOBB ) 201 | end 202 | 203 | self:SetCollisionBounds( minOBB, maxOBB ) 204 | end 205 | 206 | function ENT:BuildPhysics(phys, obb) 207 | local safeScale = self:VectorToSafe(phys, obb) 208 | if not safeScale then safeScale = 1 end 209 | 210 | local minOBB = obb.minOBB * safeScale 211 | local maxOBB = obb.maxOBB * safeScale 212 | 213 | self:CreateOBBPhysics(minOBB, maxOBB) 214 | end 215 | 216 | function ENT:SetDefaultPhysics() 217 | local minOBB = Vector(-12, -12, -12) 218 | local maxOBB = Vector(12, 12, 12) 219 | 220 | -- Ignore physics check -- 221 | self:CreateOBBPhysics(minOBB, maxOBB, true) 222 | 223 | if CLIENT then 224 | self:SetRenderBounds(minOBB, maxOBB) 225 | end 226 | end 227 | --- Physics ---- 228 | ---------------- 229 | 230 | ------------ 231 | --- SETS --- 232 | function ENT:SetStatus(newStatus) 233 | if SERVER then return end -- Ignore server for now? 234 | 235 | if self.LAST_STATUS == newStatus then return end 236 | self.LAST_STATUS = newStatus 237 | end 238 | 239 | function ENT:SetModelErrored(errored) 240 | self.LAST_MODEL_ERRORED = errored 241 | 242 | if SERVER then 243 | net.Start("prop_mesh_command") 244 | net.WriteInt(self:EntIndex(), 32) 245 | net.WriteString("MODEL_FAILED") 246 | net.WriteBool(errored) 247 | net.Broadcast() 248 | end 249 | end 250 | 251 | function ENT:SetScale(scale) 252 | if not self.LOADED_MESH then return end 253 | 254 | self.LOADED_MESH.scale = scale 255 | self.LAST_REQUESTED_MESH.scale = scale 256 | 257 | if SERVER then 258 | self.SAVE_DATA.scale = scale -- Update scale and save it 259 | self:SaveDupeData() 260 | 261 | net.Start("prop_mesh_command") 262 | net.WriteInt(self:EntIndex(), 32) 263 | net.WriteString("MESH_SCALE") 264 | net.WriteVector(scale) 265 | net.Broadcast() 266 | elseif CLIENT then 267 | self:BuildIMesh( self.LOADED_MESH ) -- Rebuild the mesh 268 | end 269 | end 270 | 271 | function ENT:SetPhysScale(phys, obb) 272 | if self.LOADED_MESH then 273 | self.LOADED_MESH.phys = phys 274 | self.LOADED_MESH.obb = obb 275 | end 276 | 277 | if self.LAST_REQUESTED_MESH then 278 | self.LAST_REQUESTED_MESH.phys = phys 279 | end 280 | 281 | -- Rebuild collisions 282 | self:BuildPhysics( phys, obb ) 283 | 284 | if SERVER then 285 | self.SAVE_DATA.obb = obb -- Update obb and save it 286 | self.SAVE_DATA.phys = phys -- Update phys and save it 287 | self:SaveDupeData() 288 | 289 | net.Start("prop_mesh_command") 290 | net.WriteInt(self:EntIndex(), 32) 291 | net.WriteString("MESH_PHYS_SCALE") 292 | net.WriteVector(phys) 293 | net.WriteTable(obb) 294 | net.Broadcast() 295 | end 296 | end 297 | --- SETS --- 298 | ------------ 299 | 300 | ------------- 301 | --- OBJ --- 302 | function ENT:CheckOBJUri(uri, onComplete) 303 | if self.IGNORE_CONTENT_RANGE:GetInt() ~= 0 then 304 | print("[PropMLIB] Skipping content range check") 305 | 306 | return onComplete(nil, { 307 | fileSize = 1, 308 | fileType = "text/plain" 309 | }) 310 | end 311 | 312 | local allowedTypes = {"text/plain", "application/octet%-stream", "application/x%-tgif"} 313 | HTTP({ 314 | url = uri, 315 | method = "GET", 316 | headers = { 317 | ["Range"] = "bytes=0-1", 318 | --["Accept-Encoding"] = "none" 319 | }, 320 | success = function(code, body, headers) 321 | if not headers then return onComplete("!! Cannot PRE-FETCH model !!") end 322 | 323 | local fileSize = nil 324 | local fileRange = headers["Content-Range"] or headers["content-range"] 325 | 326 | if fileRange then 327 | local range = string.Explode('/', fileRange) 328 | if not range or not range[2] then return onComplete("!! Failed to find 'Content-Range' header !!") end 329 | 330 | fileSize = range[2] 331 | else 332 | if string.StartWith(uri, 'https://pastebin.com') then 333 | fileSize = #body 334 | else 335 | return onComplete("!! Failed to find 'Content-Range' header !!") 336 | end 337 | end 338 | 339 | local fileType = headers["Content-Type"] 340 | if not fileType then return onComplete("!! Failed to find 'Content-Type' header !!") end 341 | 342 | local foundType = false 343 | for _, v in pairs(allowedTypes) do 344 | if string_find(fileType, v) then 345 | foundType = true 346 | break 347 | end 348 | end 349 | 350 | if not foundType then 351 | print("[PropMLIB] Allowed content-types: ") 352 | PrintTable(allowedTypes) 353 | 354 | return onComplete("!! Content-Type '" .. fileType .. "' not allowed !!") 355 | end 356 | 357 | return onComplete(nil, { 358 | fileSize = tonumber(fileSize), 359 | fileType = fileType 360 | }) 361 | end, 362 | failed = function(err) 363 | return onComplete("!! Cannot PRE-FETCH model !!") 364 | end 365 | }) 366 | end 367 | 368 | function ENT:LoadOBJ(uri, isAdmin, onSuccess, onFail) 369 | local fetchBody = nil 370 | local bodySize = nil 371 | local maxBytes = self.MAX_OBJ_SIZE_BYTES:GetInt() 372 | 373 | PropMLIB.MeshParser.Register(self, { 374 | onInitialize = function(onInitialized) 375 | -- Entity died 376 | if not IsValid(self) then 377 | return PropMLIB.MeshParser.QueueDone() 378 | end 379 | 380 | -- Being solved, send texture! 381 | if PropMLIB.Obj.IsCached(uri) then 382 | self:SetStatus("Loading cached model") 383 | 384 | onSuccess(table_copy(PropMLIB.Obj.Cache[uri])) 385 | return PropMLIB.MeshParser.QueueDone() 386 | end 387 | 388 | self:SetStatus("Pre-fetching model") 389 | self:CheckOBJUri(uri, function(err, data) 390 | if err then 391 | PropMLIB.MeshParser.QueueDone() 392 | return onFail(err) 393 | end 394 | 395 | if not data then 396 | PropMLIB.MeshParser.QueueDone() 397 | return onFail("Failed to parse model") 398 | end 399 | 400 | local niceSize = PropMLIB.Util.NiceSize(data.fileSize) 401 | if not isAdmin then 402 | if data.fileSize > maxBytes then 403 | PropMLIB.MeshParser.QueueDone() 404 | return onFail("!! Model too big (".. niceSize ..") !!") 405 | end 406 | end 407 | 408 | self:SetStatus("Fetching model") 409 | 410 | HTTP({ 411 | url = uri, 412 | method = "GET", 413 | success = function(code, body, headers) 414 | if code ~= 200 then 415 | PropMLIB.MeshParser.QueueDone() 416 | return onFail("!! Failed to fetch model: "..code.." !!") 417 | end 418 | 419 | fetchBody = body 420 | bodySize = niceSize 421 | 422 | return onInitialized() 423 | end, 424 | failed = function(err) 425 | PropMLIB.MeshParser.QueueDone() 426 | return onFail("!! Invalid url !!") 427 | end 428 | }) 429 | end) 430 | end, 431 | 432 | onStatusUpdate = function(message) 433 | if not IsValid(self) or not message then return end 434 | self:SetStatus(message) 435 | end, 436 | 437 | onComplete = function(meshData) 438 | PropMLIB.MeshParser.QueueDone() 439 | 440 | if not IsValid(self) then return end 441 | return onSuccess(table_copy(meshData)) 442 | end, 443 | 444 | onFailed = function() 445 | PropMLIB.MeshParser.QueueDone() 446 | if not IsValid(self) then return end 447 | 448 | local status = self.LAST_STATUS or "UNKNOWN" 449 | return onFail("!! FAILED: " .. status .. " !!") 450 | end, 451 | 452 | co = coroutine.create(function () 453 | if not IsValid(self) then 454 | return coroutine.yield(true, "") 455 | end 456 | 457 | local meshData = PropMLIB.Obj.Parse(isAdmin, fetchBody, true) 458 | meshData.uri = uri 459 | meshData.metadata = { 460 | fileSize = bodySize 461 | } 462 | 463 | -- Cache it! -- 464 | PropMLIB.Obj.Register(uri, meshData) 465 | 466 | -- Finish it -- 467 | return coroutine.yield(true, "Done parsing", meshData) 468 | end) 469 | }) 470 | end 471 | 472 | function ENT:BuildMeshes(meshData) 473 | self.LOADED_MESH = table_copy(meshData) 474 | 475 | if CLIENT then self:BuildIMesh(meshData) end 476 | self:BuildPhysics(meshData.phys, meshData.obb) 477 | end 478 | 479 | --- OBJ --- 480 | ------------- -------------------------------------------------------------------------------- /lua/lib/cl/pvs_cache.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return error("[PropMLIB]Tried to load 'PVSCache.lua' on SERVER") end 2 | 3 | PropMLIB = PropMLIB or {} 4 | PropMLIB.PVSCache = PropMLIB.PVSCache or {} 5 | PropMLIB.PVSCache.Cache = {} 6 | PropMLIB.PVSCache.Message_Delay_Mult = 0.4 7 | 8 | PropMLIB.PVSCache.ResolveNetCache = function(ent) 9 | local id = ent:EntIndex() 10 | if not PropMLIB.PVSCache.Cache[id] then return end 11 | 12 | local t = 0 13 | for k, v in pairs(PropMLIB.PVSCache.Cache[id]) do 14 | if not v then continue end 15 | 16 | timer.Simple(PropMLIB.PVSCache.Message_Delay_Mult * t, function() 17 | if not IsValid(ent) then return end 18 | v(ent) 19 | end) 20 | 21 | t = t + 1 22 | end 23 | 24 | PropMLIB.PVSCache.Remove(id) -- Clear all messages 25 | end 26 | 27 | PropMLIB.PVSCache.Remove = function(id) 28 | PropMLIB.PVSCache.Cache[id] = {} 29 | end 30 | 31 | PropMLIB.PVSCache.CacheNetMessage = function(id, key, onPVS) 32 | if not PropMLIB.PVSCache.Cache[id] then 33 | PropMLIB.PVSCache.Cache[id] = {} 34 | end 35 | 36 | PropMLIB.PVSCache.Cache[id][key] = onPVS -- Cache latest message 37 | end 38 | 39 | 40 | hook.Add("NetworkEntityCreated", "__loadmodel_prop_mesh__", function(ent) 41 | if not IsValid(ent) or not IsValid(LocalPlayer()) then return end 42 | if ent:GetClass() ~= "prop_mesh" then 43 | PropMLIB.PVSCache.Remove(ent:EntIndex()) 44 | return 45 | end 46 | 47 | if ent.OnPVSReload then ent:OnPVSReload()end 48 | PropMLIB.PVSCache.ResolveNetCache(ent) 49 | end) -------------------------------------------------------------------------------- /lua/lib/cl/queue_sys.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return error("[PropMLIB]Tried to load 'queue_sys.lua' on SERVER") end 2 | 3 | local table_insert = table.insert 4 | local table_remove = table.remove 5 | 6 | PropMLIB = PropMLIB or {} 7 | PropMLIB.QueueSYS = PropMLIB.QueueSYS or {} 8 | PropMLIB.QueueSYS.Queue = {} 9 | PropMLIB.QueueSYS.ParseTime = CreateClientConVar("prop_mesh_queue_interval", 0.5, true, false, "How many seconds between prop_mesh mesh rendering (LOW VALUE = More chances of crashing) (Default: 0.5)", 0.30, 1) 10 | 11 | PropMLIB.QueueSYS.Register = function(queueItem) 12 | table_insert(PropMLIB.QueueSYS.Queue, queueItem) 13 | end 14 | 15 | PropMLIB.QueueSYS.Initialize = function() 16 | timer.Remove("__prop_mesh_queuesys__") 17 | timer.Create("__prop_mesh_queuesys__", PropMLIB.QueueSYS.ParseTime:GetFloat(), 0, function() 18 | if #PropMLIB.QueueSYS.Queue <= 0 then return end 19 | 20 | local callbackData = table_remove(PropMLIB.QueueSYS.Queue, 1) 21 | callbackData.callback() 22 | end) 23 | end 24 | 25 | cvars.RemoveChangeCallback("prop_mesh_queue_interval", "__prop_mesh_queuesys__" ) 26 | cvars.AddChangeCallback("prop_mesh_queue_interval", function() 27 | print("[PropMLIB] 'prop_mesh_queue_interval' value changed, restarting queue") 28 | PropMLIB.QueueSYS.Initialize() 29 | end, "__prop_mesh_queuesys__" ) 30 | 31 | -- Start queue -- 32 | PropMLIB.QueueSYS.Initialize() -------------------------------------------------------------------------------- /lua/lib/cl/setup.lua: -------------------------------------------------------------------------------- 1 | net.Receive("prop_mesh_lib", function() 2 | local command = net.ReadString() 3 | if command == "OBJ_CACHE_CLEANUP" then 4 | PropMLIB.Obj.Clear() 5 | end 6 | end) 7 | 8 | net.Receive("prop_mesh_command", function() 9 | local indx = net.ReadInt(32) 10 | local ent = Entity(indx) 11 | local command = net.ReadString() 12 | 13 | if command == "MESH_LOAD" then 14 | local data = net.ReadTable() 15 | 16 | if not IsValid(ent) then 17 | return PropMLIB.PVSCache.CacheNetMessage(indx, command, function(newEnt) 18 | if not newEnt.LocalLoadMesh then return end 19 | newEnt:LocalLoadMesh(data) 20 | end) 21 | else 22 | if not ent.LocalLoadMesh then return end 23 | ent:LocalLoadMesh(data) 24 | end 25 | elseif command == "TEXTURE_LOAD" then 26 | local textures = net.ReadTable() 27 | 28 | if not IsValid(ent) then 29 | return PropMLIB.PVSCache.CacheNetMessage(indx, command, function(newEnt) 30 | if not newEnt.LoadTextures then return end 31 | newEnt:LoadTextures(textures) 32 | end) 33 | else 34 | if not ent.LoadTextures then return end 35 | ent:LoadTextures(textures) 36 | end 37 | elseif command == "MESH_PHYS_SCALE" then 38 | local phys = net.ReadVector() 39 | local obb = net.ReadTable() 40 | 41 | if not IsValid(ent) then 42 | return PropMLIB.PVSCache.CacheNetMessage(indx, command, function(newEnt) 43 | if not newEnt.SetPhysScale then return end 44 | newEnt:SetPhysScale(phys, obb) 45 | end) 46 | else 47 | if not ent.SetPhysScale then return end 48 | ent:SetPhysScale(phys, obb) 49 | end 50 | elseif command == "MODEL_FAILED" then 51 | local errored = net.ReadBool() 52 | 53 | if not IsValid(ent) then 54 | return PropMLIB.PVSCache.CacheNetMessage(indx, command, function(newEnt) 55 | if not newEnt.SetModelErrored then return end 56 | newEnt:SetModelErrored(errored) 57 | end) 58 | else 59 | if not ent.SetModelErrored then return end 60 | ent:SetModelErrored(errored) 61 | end 62 | elseif command == "MESH_SCALE" then 63 | local scale = net.ReadVector() 64 | 65 | if not IsValid(ent) then 66 | return PropMLIB.PVSCache.CacheNetMessage(indx, command, function(newEnt) 67 | if not newEnt.SetScale then return end 68 | newEnt:SetScale(scale) 69 | end) 70 | else 71 | if not ent.SetScale then return end 72 | ent:SetScale(scale) 73 | end 74 | elseif command == "ON_USE_PRESS" then 75 | if not IsValid(ent) then return end 76 | if input.IsKeyDown(KEY_LALT) or input.IsKeyDown(KEY_RALT) then return end -- Allow Sitting 77 | if input.IsKeyDown(KEY_LSHIFT) or input.IsKeyDown(KEY_RSHIFT) then 78 | if not ent.LAST_MODEL_ERRORED then return end 79 | ent:RetryModelParse() 80 | return 81 | end 82 | 83 | local owner = ent:GetNWEntity("owner") 84 | if ent.CPPIGetOwner then 85 | owner = ent:CPPIGetOwner() 86 | end 87 | 88 | if LocalPlayer() == owner then 89 | ent:CreateMenu() 90 | end 91 | end 92 | end) -------------------------------------------------------------------------------- /lua/lib/cl/thumbnail.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return error("[PropMLIB]Tried to load 'thumbnail.lua' on SERVER") end 2 | 3 | PropMLIB = PropMLIB or {} 4 | PropMLIB.Thumbnail = PropMLIB.Thumbnail or {} 5 | PropMLIB.Thumbnail.ThumbnailsCache = {} 6 | PropMLIB.Thumbnail.TakingScreenshot = false 7 | PropMLIB.Thumbnail.RTTexture = GetRenderTarget( "prop_mesh_rttexture_", ScrW(), ScrH(), true ) 8 | 9 | PropMLIB.Thumbnail.Clear = function() 10 | PropMLIB.Thumbnail.ThumbnailsCache = {} 11 | end 12 | 13 | PropMLIB.Thumbnail.TakeThumbnail = function(data) 14 | if not data or not data.uri then return end 15 | -- if PropMLIB.Thumbnail.HasThumbnail(data.uri) then return end TODO: Figure out if textures changed / model? 16 | 17 | PropMLIB.Thumbnail.TakingScreenshot = true 18 | PropMLIB.Thumbnail.ScreenshotDrawHook(data) 19 | end 20 | 21 | PropMLIB.Thumbnail.SaveThumbnail = function(name, data) 22 | local fileName = util.CRC(name) .. ".jpg" 23 | local path = "prop_mesh/thumbnails/" .. fileName 24 | 25 | local f = file.Open(path, "wb", "DATA" ) 26 | if not f then return print("[PropMLIB] Failed to save mesh thumbnail") end 27 | 28 | f:Write( data ) 29 | f:Close() 30 | 31 | PropMLIB.Thumbnail.ThumbnailsCache[path] = nil 32 | end 33 | 34 | PropMLIB.Thumbnail.Initialize = function() 35 | local files, directories = file.Find("prop_mesh/thumbnails/*.jpg", "DATA") 36 | 37 | PropMLIB.Thumbnail.ThumbnailsCache = {} 38 | for _, v in pairs(files) do 39 | PropMLIB.Thumbnail.ThumbnailsCache["prop_mesh/thumbnails/" .. v] = true 40 | end 41 | end 42 | 43 | PropMLIB.Thumbnail.HasThumbnail = function(name) 44 | local fileName = util.CRC(name) .. ".jpg" 45 | return PropMLIB.Thumbnail.ThumbnailsCache[fileName] 46 | end 47 | 48 | PropMLIB.Thumbnail.DeleteThumbnail = function(name) 49 | local fileName = util.CRC(name) .. ".jpg" 50 | local path = "prop_mesh/thumbnails/" .. fileName 51 | if not file.Exists(path, "DATA" ) then return end 52 | 53 | file.Delete(path) 54 | PropMLIB.Thumbnail.ThumbnailsCache[path] = nil 55 | end 56 | 57 | PropMLIB.Thumbnail.RemoveHook = function() 58 | hook.Remove("PostDrawViewModel", "__prop_mesh_screenshot__") 59 | end 60 | 61 | PropMLIB.Thumbnail.ClearHook = function() 62 | PropMLIB.Thumbnail.TakingScreenshot = false 63 | PropMLIB.Thumbnail.RemoveHook() 64 | end 65 | 66 | PropMLIB.Thumbnail.ScreenshotDrawHook = function(data) 67 | hook.Add("PostDrawViewModel", "__prop_mesh_screenshot__", function() 68 | if not PropMLIB.Thumbnail.TakingScreenshot then return PropMLIB.Thumbnail.ClearHook() end 69 | if not data or not IsValid(data.ent) then return PropMLIB.Thumbnail.ClearHook() end 70 | 71 | local thumbnailData = nil 72 | render.PushRenderTarget( PropMLIB.Thumbnail.RTTexture ) 73 | cam.Start3D( data.origin, data.angles, data.fov ) 74 | render.Clear( 35, 35, 35, 255, true ) 75 | 76 | render.SuppressEngineLighting( true ) 77 | data.ent:DrawTranslucent() 78 | render.SuppressEngineLighting( false ) 79 | 80 | local startX = (ScrW() - 1024) / 2 81 | local endX = 1024 82 | if startX <= 0 then 83 | endX = ScrW() 84 | startX = 0 85 | end 86 | 87 | 88 | local startY = (ScrH() - 1024) / 2 89 | local endY = 1024 90 | if startY <= 0 then 91 | endY = ScrH() 92 | startY = 0 93 | end 94 | 95 | thumbnailData = render.Capture({ 96 | format = "jpeg", 97 | quality = 70, 98 | x = startX, 99 | y = startY, 100 | h = endY, 101 | w = endX 102 | }) 103 | cam.End3D() 104 | render.PopRenderTarget() 105 | 106 | if thumbnailData then 107 | PropMLIB.Thumbnail.SaveThumbnail(data.uri, thumbnailData) 108 | end 109 | 110 | PropMLIB.Thumbnail.ClearHook() 111 | end) 112 | end 113 | 114 | PropMLIB.Thumbnail.Initialize() 115 | 116 | concommand.Add( "prop_mesh_thumbnail_clear", function() 117 | PropMLIB.Thumbnail.Clear() 118 | end, nil, "Clears thumbnail cache") 119 | -------------------------------------------------------------------------------- /lua/lib/cl/url_texture.lua: -------------------------------------------------------------------------------- 1 | if SERVER then return error("[PropMLIB]Tried to load 'URLTexture.lua' on SERVER") end 2 | 3 | local table_insert = table.insert 4 | local table_removeByValue = table.RemoveByValue 5 | local table_remove = table.remove 6 | local table_count = table.Count 7 | 8 | local string_trim = string.Trim 9 | local string_find = string.find 10 | local string_split = string.Split 11 | 12 | PropMLIB = PropMLIB or {} 13 | PropMLIB.URLMaterial = PropMLIB.URLMaterial or {} 14 | 15 | PropMLIB.URLMaterial.MAX_TIMEOUT = CreateClientConVar("prop_mesh_urltexture_timeout", 30, true, false, "How many seconds before timing out (Default: 30)") 16 | PropMLIB.URLMaterial.RequestedTextures = PropMLIB.URLMaterial.RequestedTextures or {} 17 | PropMLIB.URLMaterial.Materials = PropMLIB.URLMaterial.Materials or {} 18 | PropMLIB.URLMaterial.Panels = PropMLIB.URLMaterial.Panels or {} 19 | PropMLIB.URLMaterial.Queue = {} 20 | 21 | PropMLIB.URLMaterial.Clear = function() 22 | PropMLIB.URLMaterial.Materials = {} 23 | print("[PropMLIB] Cleared all loaded materials") 24 | end 25 | 26 | PropMLIB.URLMaterial.ReloadTextures = function() 27 | PropMLIB.URLMaterial.Clear() -- Clear all materials first 28 | 29 | for uri, _ in pairs(PropMLIB.URLMaterial.RequestedTextures) do 30 | print("[PropMLIB] Reloading texture ".. uri) 31 | PropMLIB.URLMaterial.LoadMaterialURL(uri) 32 | end 33 | 34 | print("[PropMLIB] Reloaded " .. tostring(table_count(PropMLIB.URLMaterial.RequestedTextures)) .. " textures!") 35 | end 36 | 37 | PropMLIB.URLMaterial.LoadMaterialURL = function(uri, success, failure) 38 | if uri == "" then return end 39 | 40 | if PropMLIB.URLMaterial.Materials[uri] then 41 | if success then success(PropMLIB.URLMaterial.Materials[uri]) end 42 | return 43 | end 44 | 45 | local imgURL = uri:gsub("&", "&"):gsub("<", "<"):gsub(">", ">"):gsub('"', """) 46 | local PANEL = vgui.Create("DHTML") 47 | local onFail = function(msg) 48 | if PANEL then PANEL:Remove() end 49 | PropMLIB.URLMaterial.RequestedTextures[uri] = nil 50 | 51 | print("[PropMLIB] Texture failed: " .. imgURL .. " -> " .. msg) 52 | if failure then failure() end 53 | end 54 | 55 | PANEL:SetAlpha( 0 ) 56 | PANEL:SetMouseInputEnabled( false ) 57 | PANEL:SetPos(0, 0) 58 | 59 | PANEL.ConsoleMessage = function(panel, data) 60 | if not data or string_trim(data) == "" then return end 61 | 62 | if string_find(data, "DATA:") then 63 | data = data:gsub("DATA:","") 64 | 65 | local args = string_split(data, ",") 66 | if not args or #args <= 0 then 67 | return onFail("Invalid texture") 68 | end 69 | 70 | local width = tonumber(args[1]) or 0 71 | local height = tonumber(args[2]) or 0 72 | 73 | if width <= 0 or height <= 0 then return onFail("Invalid texture") end 74 | PANEL:SetSize(width, height) 75 | 76 | timer.Simple(1, function() 77 | PANEL:UpdateHTMLTexture() 78 | 79 | table_removeByValue(PropMLIB.URLMaterial.Panels, PANEL) 80 | table_insert(PropMLIB.URLMaterial.Queue, { 81 | panel = PANEL, 82 | uri = uri, 83 | cooldown = CurTime() + PropMLIB.URLMaterial.MAX_TIMEOUT:GetInt(), 84 | success = success, 85 | failure = failure 86 | }) 87 | end) 88 | elseif string_find(data, "FAIL::") then 89 | return onFail('Failed to load texture') 90 | end 91 | end 92 | 93 | 94 | PANEL:SetHTML([[ 95 | 96 | 97 | 112 | 113 | 114 | 124 | 125 | 126 | 127 | ]]) 128 | 129 | PropMLIB.URLMaterial.RequestedTextures[uri] = true -- Used on texture reload 130 | table_insert(PropMLIB.URLMaterial.Panels, PANEL) 131 | end 132 | 133 | PropMLIB.URLMaterial.ClearPanels = function() 134 | local panels = PropMLIB.URLMaterial.Panels 135 | if not panels or #panels <= 0 then return end 136 | 137 | for _, v in pairs(panels) do 138 | if not IsValid(v) then continue end 139 | v:Remove() 140 | end 141 | 142 | PropMLIB.URLMaterial.Panels = {} 143 | end 144 | 145 | PropMLIB.URLMaterial.CreateMaterial = function(name, baseTexture) 146 | return CreateMaterial(name, "VertexLitGeneric", { 147 | ["$basetexture"] = baseTexture, 148 | 149 | ["$alphatest"] = "1", 150 | ["$allowalphatocoverage"] = "1", 151 | 152 | ["$distancealpha"] = "1", 153 | 154 | ["$vertexcolor"] = "1", 155 | 156 | ["$model"] = "1", 157 | ["$nocull"] = "1", 158 | ["$nomip"] = "1", 159 | ["$nolod"] = "1", 160 | ["$nocompress"] = "1", 161 | }) 162 | end 163 | 164 | hook.Add("Think", "__loadtexture_prop_mesh__", function() 165 | if #PropMLIB.URLMaterial.Queue <= 0 then return end 166 | 167 | for k, v in pairs( PropMLIB.URLMaterial.Queue ) do 168 | if not IsValid(v.panel) then continue end 169 | 170 | if v.panel:GetHTMLMaterial() and not v.panel:IsLoading() then 171 | local material = v.panel:GetHTMLMaterial() 172 | local matName = material:GetName() 173 | 174 | local Mat = PropMLIB.URLMaterial.CreateMaterial(matName .. CurTime(), matName) 175 | if not Mat then 176 | if v.failure then v.failure() end 177 | return 178 | end 179 | 180 | PropMLIB.URLMaterial.Materials[v.uri] = Mat 181 | v.panel:Remove() 182 | 183 | if v.success then v.success(Mat) end 184 | 185 | table_remove( PropMLIB.URLMaterial.Queue, k ) 186 | table_removeByValue(PropMLIB.URLMaterial.Panels, v.panel) 187 | elseif CurTime() > v.cooldown then 188 | if v.failure then v.failure() end 189 | 190 | table_remove( PropMLIB.URLMaterial.Queue, k ) 191 | table_removeByValue(PropMLIB.URLMaterial.Panels, v.panel) 192 | end 193 | end 194 | end) 195 | 196 | concommand.Add( "prop_mesh_urltexture_reload", function() 197 | PropMLIB.URLMaterial.ReloadTextures() 198 | end, nil, "Reloads all url textures") 199 | 200 | concommand.Add( "prop_mesh_urltexture_clear", function() 201 | PropMLIB.URLMaterial.Clear() 202 | end, nil, "Clear url texture cache") 203 | 204 | PropMLIB.URLMaterial.ClearPanels() -------------------------------------------------------------------------------- /lua/lib/sh/mesh_parser.lua: -------------------------------------------------------------------------------- 1 | local table_remove = table.remove 2 | local table_insert = table.insert 3 | local table_count = table.Count 4 | local table_keys = table.GetKeys 5 | 6 | PropMLIB = PropMLIB or {} 7 | PropMLIB.MeshParser = PropMLIB.MeshParser or {} 8 | PropMLIB.MeshParser.CurThread = nil 9 | PropMLIB.MeshParser.Threads = {} 10 | 11 | PropMLIB.MeshParser.ClearMeshes = function (imeshes) 12 | if not imeshes or #imeshes <= 0 then return end 13 | for _, v in pairs(imeshes) do 14 | if not v or v == NULL or not pcall( v.Draw, v ) then continue end 15 | v:Destroy() 16 | end 17 | end 18 | 19 | PropMLIB.MeshParser.Register = function(ent, tblData) 20 | if not IsValid(ent) then return end 21 | local indx = ent:EntIndex() 22 | 23 | PropMLIB.MeshParser.CancelThread(indx) -- Cancel previous thread 24 | PropMLIB.MeshParser.Threads[indx] = tblData 25 | end 26 | 27 | PropMLIB.MeshParser.CancelThread = function(indx) 28 | if not PropMLIB.MeshParser.Threads[indx] then return end 29 | if PropMLIB.MeshParser.CurThread ~= PropMLIB.MeshParser.Threads[indx] then return end 30 | 31 | PropMLIB.MeshParser.CurThread = nil 32 | end 33 | 34 | PropMLIB.MeshParser.UnRegister = function(ent) 35 | if not IsValid(ent) then return end 36 | local indx = ent:EntIndex() 37 | 38 | PropMLIB.MeshParser.CancelThread(indx) 39 | PropMLIB.MeshParser.Remove(indx) 40 | end 41 | 42 | PropMLIB.MeshParser.QueueDone = function () 43 | PropMLIB.MeshParser.CurThread = nil 44 | end 45 | 46 | PropMLIB.MeshParser.Remove = function(index) 47 | local data = PropMLIB.MeshParser.Threads[index] 48 | PropMLIB.MeshParser.Threads[index] = nil 49 | return data 50 | end 51 | 52 | 53 | PropMLIB.MeshParser.QueueThink = function () 54 | if not PropMLIB.MeshParser.CurThread then 55 | local tblKey = table_keys(PropMLIB.MeshParser.Threads)[1] 56 | 57 | PropMLIB.MeshParser.CurThread = PropMLIB.MeshParser.Remove(tblKey) 58 | PropMLIB.MeshParser.CurThread.onInitialize(function() 59 | PropMLIB.MeshParser.CurThread.__INIT__ = true 60 | end) 61 | else 62 | local currThread = PropMLIB.MeshParser.CurThread 63 | if not currThread or not currThread.__INIT__ then 64 | return 65 | end 66 | 67 | -- START RENDERING 68 | local PARSING_THERSOLD = 0.005 69 | local t0 = SysTime () 70 | local success, finished, statusMessage, meshData 71 | 72 | -- COROUTINE 73 | while SysTime () - t0 < PARSING_THERSOLD do 74 | if not currThread then break end 75 | success, finished, statusMessage, meshData = coroutine.resume(currThread.co) 76 | 77 | if statusMessage then currThread.onStatusUpdate(statusMessage)end 78 | if (not success or finished) then break end 79 | end 80 | 81 | --- CHECK 82 | if currThread then 83 | if not success then 84 | local error_message = finished or "???" 85 | print("MeshParser Failed: " ,debug.traceback(currThread.co,error_message)) 86 | currThread.onFailed() 87 | return 88 | end 89 | 90 | if finished then 91 | currThread.onComplete(meshData) 92 | return 93 | end 94 | end 95 | end 96 | end 97 | 98 | hook.Add("Think", "__loadmodel_prop_mesh__", function() 99 | if table_count(PropMLIB.MeshParser.Threads) <= 0 and not PropMLIB.MeshParser.CurThread then return end 100 | PropMLIB.MeshParser.QueueThink() 101 | end) 102 | -------------------------------------------------------------------------------- /lua/lib/sh/obj.lua: -------------------------------------------------------------------------------- 1 | local table_copy = table.Copy 2 | local table_insert = table.insert 3 | 4 | local math_min = math.min 5 | local math_max = math.max 6 | local math_sqrt = math.sqrt 7 | 8 | local string_gmatch = string.gmatch 9 | local string_match = string.match 10 | local string_split = string.Split 11 | local string_trim = string.Trim 12 | local string_find = string.find 13 | local string_explode = string.Explode 14 | local string_replace = string.Replace 15 | 16 | PropMLIB = PropMLIB or {} 17 | PropMLIB.Obj = PropMLIB.Obj or {} 18 | PropMLIB.Obj.Cache = PropMLIB.Obj.Cache or {} 19 | PropMLIB.Obj.MAX_SUBMESHES = GetConVar( "prop_mesh_maxSubMeshes" ) 20 | PropMLIB.Obj.MAX_SAFE_TRIANGLES = GetConVar( "prop_mesh_maxTriangles" ) 21 | 22 | PropMLIB.Obj.IsCached = function(uri) 23 | local cache = PropMLIB.Obj.Cache[uri] 24 | return (cache and cache ~= nil) 25 | end 26 | 27 | PropMLIB.Obj.Clear = function() 28 | PropMLIB.Obj.Cache = {} 29 | 30 | if SERVER then 31 | print("[PropMLIB][Server] Cleared obj model cache, sending to clients") 32 | net.Start("prop_mesh_lib") 33 | net.WriteString("OBJ_CACHE_CLEANUP") 34 | net.Broadcast() 35 | else 36 | print("[PropMLIB] Cleared obj model cache") 37 | end 38 | end 39 | 40 | PropMLIB.Obj.UnRegister = function(uri) 41 | PropMLIB.Obj.Cache[uri] = nil 42 | end 43 | 44 | PropMLIB.Obj.Register = function(uri, meshData) 45 | PropMLIB.Obj.Cache[uri] = table_copy(meshData) 46 | end 47 | 48 | PropMLIB.Obj.GetScaledTris = function(subMeshData, scale) 49 | local triCopy = table_copy(subMeshData) 50 | 51 | for i = 1, #triCopy do 52 | triCopy[i]["pos"] = triCopy[i]["pos"] * scale 53 | end 54 | 55 | return triCopy 56 | end 57 | 58 | if CLIENT then 59 | -- Based on PAC 60 | PropMLIB.Obj.CalculateNormals = function(triangleList) 61 | local coroutine_yield = coroutine.running() and coroutine.yield or function() end 62 | 63 | local vertexNormals = {} 64 | local triangleCount = #triangleList / 3 65 | local inverseTriangleCount = 1 / triangleCount 66 | local defaultNormal = Vector(0, 0, -1) 67 | 68 | for i = 1, triangleCount do 69 | local a, b, c = triangleList[1+(i-1)*3+0], triangleList[1+(i-1)*3+1], triangleList[1+(i-1)*3+2] 70 | 71 | local normal = defaultNormal 72 | if a.pos and b.pos and c.pos then 73 | normal = (c.pos - a.pos):Cross(b.pos - a.pos):GetNormalized() 74 | end 75 | 76 | vertexNormals[a.pos_index] = vertexNormals[a.pos_index] or Vector() 77 | vertexNormals[a.pos_index] = (vertexNormals[a.pos_index] + normal) 78 | 79 | vertexNormals[b.pos_index] = vertexNormals[b.pos_index] or Vector() 80 | vertexNormals[b.pos_index] = (vertexNormals[b.pos_index] + normal) 81 | 82 | vertexNormals[c.pos_index] = vertexNormals[c.pos_index] or Vector() 83 | vertexNormals[c.pos_index] = (vertexNormals[c.pos_index] + normal) 84 | 85 | coroutine_yield(false, "Parsing normals") 86 | end 87 | 88 | local vertexCount = #triangleList 89 | local inverseVertexCount = 1 / vertexCount 90 | for i = 1, vertexCount do 91 | local normal = vertexNormals[triangleList[i].pos_index] or defaultNormal 92 | normal:Normalize() 93 | 94 | triangleList[i].normal = normal 95 | coroutine_yield(false, "Normalizing normals") 96 | end 97 | end 98 | 99 | -- Based on PAC 100 | PropMLIB.Obj.CalculateTangents = function(triangleList) 101 | local coroutine_yield = coroutine.running() and coroutine.yield or function() end 102 | 103 | do 104 | -- Lengyel, Eric. “Computing Tangent Space Basis Vectors for an Arbitrary Mesh”. Terathon Software, 2001. http://terathon.com/code/tangent.html 105 | local tan1 = {} 106 | local tan2 = {} 107 | local vertexCount = #triangleList 108 | 109 | for i = 1, vertexCount do 110 | tan1[i] = Vector(0, 0, 0) 111 | tan2[i] = Vector(0, 0, 0) 112 | end 113 | 114 | for i = 1, vertexCount - 2, 3 do 115 | local vert1, vert2, vert3 = triangleList[i], triangleList[i+1], triangleList[i+2] 116 | if not vert1 or not vert2 or not vert3 then continue end 117 | 118 | local p1, p2, p3 = vert1.pos, vert2.pos, vert3.pos 119 | local u1, u2, u3 = vert1.u, vert2.u, vert3.u 120 | local v1, v2, v3 = vert1.v, vert2.v, vert3.v 121 | 122 | local x1 = p2.x - p1.x; 123 | local x2 = p3.x - p1.x; 124 | local y1 = p2.y - p1.y; 125 | local y2 = p3.y - p1.y; 126 | local z1 = p2.z - p1.z; 127 | local z2 = p3.z - p1.z; 128 | 129 | local s1 = u2 - u1; 130 | local s2 = u3 - u1; 131 | local t1 = v2 - v1; 132 | local t2 = v3 - v1; 133 | 134 | local r = 1 / (s1 * t2 - s2 * t1) 135 | local sdir = Vector((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r); 136 | local tdir = Vector((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r); 137 | 138 | tan1[i]:Add(sdir) 139 | tan1[i+1]:Add(sdir) 140 | tan1[i+2]:Add(sdir) 141 | 142 | tan2[i]:Add(tdir) 143 | tan2[i+1]:Add(tdir) 144 | tan2[i+2]:Add(tdir) 145 | 146 | coroutine_yield(false, "Parsing tangents") 147 | end 148 | 149 | local tangent = {} 150 | for i = 1, vertexCount do 151 | local n = triangleList[i].normal 152 | local t = tan1[i] 153 | 154 | local tan = (t - n * n:Dot(t)) 155 | tan:Normalize() 156 | 157 | local w = (n:Cross(t)):Dot(tan2[i]) < 0 and -1 or 1 158 | 159 | local tn1 = tan[1] ~= tan[1] and 0 or tan[1] 160 | local tn2 = tan[2] ~= tan[2] and 0 or tan[2] 161 | local tn3 = tan[3] ~= tan[3] and 0 or tan[3] 162 | 163 | triangleList[i].userdata = {tn1, tn2, tn3, w} 164 | 165 | coroutine_yield(false, "Parsing tangents") 166 | end 167 | end 168 | end 169 | 170 | -- Based on PAC 171 | PropMLIB.Obj.CalculateFaces = function(faceLines, globalMesh) 172 | local coroutine_yield = coroutine.running() and coroutine.yield or function () end 173 | 174 | local faceLineCount = #faceLines 175 | local inverseFaceLineCount = 1 / faceLineCount 176 | local facesMapper = "([0-9]+)/?([0-9]*)/?([0-9]*)" 177 | local triangleList = {} 178 | local defaultNormal = Vector(0, 0, -1) 179 | 180 | for i = 1, #faceLines do 181 | local parts = faceLines[i] 182 | if #parts < 3 then continue end 183 | 184 | local v1PositionIndex, v1TexCoordIndex, v1NormalIndex = string_match(parts[1], facesMapper) 185 | local v3PositionIndex, v3TexCoordIndex, v3NormalIndex = string_match(parts[2], facesMapper) 186 | 187 | v1PositionIndex, v1TexCoordIndex, v1NormalIndex = tonumber(v1PositionIndex), tonumber(v1TexCoordIndex), tonumber(v1NormalIndex) 188 | v3PositionIndex, v3TexCoordIndex, v3NormalIndex = tonumber(v3PositionIndex), tonumber(v3TexCoordIndex), tonumber(v3NormalIndex) 189 | 190 | for i = 3, #parts do 191 | local v2PositionIndex, v2TexCoordIndex, v2NormalIndex = string_match(parts[i], facesMapper) 192 | v2PositionIndex, v2TexCoordIndex, v2NormalIndex = tonumber(v2PositionIndex), tonumber(v2TexCoordIndex), tonumber(v2NormalIndex) 193 | 194 | local v1 = { pos_index = nil, pos = nil, u = nil, v = nil, normal = nil, userdata = nil } 195 | local v2 = { pos_index = nil, pos = nil, u = nil, v = nil, normal = nil, userdata = nil } 196 | local v3 = { pos_index = nil, pos = nil, u = nil, v = nil, normal = nil, userdata = nil } 197 | 198 | v1.pos_index = v1PositionIndex 199 | v2.pos_index = v2PositionIndex 200 | v3.pos_index = v3PositionIndex 201 | 202 | v1.pos = globalMesh.positions[v1PositionIndex] 203 | v2.pos = globalMesh.positions[v2PositionIndex] 204 | v3.pos = globalMesh.positions[v3PositionIndex] 205 | 206 | if #globalMesh.texCoordsU > 0 then 207 | v1.u = globalMesh.texCoordsU[v1TexCoordIndex] 208 | v2.u = globalMesh.texCoordsU[v2TexCoordIndex] 209 | v3.u = globalMesh.texCoordsU[v3TexCoordIndex] 210 | else 211 | v1.u = 0 212 | v2.u = 0 213 | v3.u = 0 214 | end 215 | 216 | if #globalMesh.texCoordsV > 0 then 217 | v1.v = globalMesh.texCoordsV[v1TexCoordIndex] 218 | v2.v = globalMesh.texCoordsV[v2TexCoordIndex] 219 | v3.v = globalMesh.texCoordsV[v3TexCoordIndex] 220 | else 221 | v1.v = 0 222 | v2.v = 0 223 | v3.v = 0 224 | end 225 | 226 | if #globalMesh.normals > 0 then 227 | v1.normal = globalMesh.normals[v1NormalIndex] 228 | v2.normal = globalMesh.normals[v2NormalIndex] 229 | v3.normal = globalMesh.normals[v3NormalIndex] 230 | else 231 | v1.normal = defaultNormal 232 | v2.normal = defaultNormal 233 | v3.normal = defaultNormal 234 | end 235 | 236 | triangleList[#triangleList + 1] = v1 237 | triangleList[#triangleList + 1] = v2 238 | triangleList[#triangleList + 1] = v3 239 | 240 | v3PositionIndex, v3TexCoordIndex, v3NormalIndex = v2PositionIndex, v2TexCoordIndex, v2NormalIndex 241 | end 242 | 243 | coroutine_yield(false, "Parsing triangles") 244 | end 245 | 246 | return triangleList 247 | end 248 | 249 | PropMLIB.Obj.NewSubMesh = function(name) 250 | return { 251 | mtl = 'default', 252 | 253 | positionsCount = 0, 254 | 255 | faceLines = {}, 256 | faceCount = 0, 257 | 258 | name = name 259 | } 260 | end 261 | end 262 | 263 | -- ID: newmtl None 264 | -- TEXTURE: map_Kd Sheep_VertColor.png 265 | PropMLIB.Obj.ParseMTL = function(baseURL, body) 266 | local parsedData = {} 267 | local rawData = string_split(body, "\n") 268 | 269 | local currentMTL = {} 270 | for _, v in pairs(rawData) do 271 | local data = string_explode("%s+", v, true) 272 | local mode = tostring(data[1]) 273 | 274 | if mode == "newmtl" then 275 | currentMTL.id = tostring(data[2]) 276 | elseif mode == "map_Kd" then 277 | local filePath = tostring(data[#data]) 278 | filePath = string_replace(filePath, '\\', '/') 279 | filePath = string_replace(filePath, '//', '/') 280 | 281 | currentMTL.material = filePath 282 | end 283 | 284 | -- GROUP DONE -- 285 | if currentMTL.id and currentMTL.material then 286 | parsedData[currentMTL.id] = table_copy(currentMTL) 287 | currentMTL = {} 288 | end 289 | end 290 | 291 | return parsedData 292 | end 293 | 294 | PropMLIB.Obj.Parse = function(isAdmin, body, fixNormals) 295 | local coroutine_yield = coroutine.running() and coroutine.yield or function () end 296 | local fixNormals = (fixNormals ~= nil and fixNormals or true) 297 | 298 | if not body or string_trim(body) == "" then return coroutine_yield(true, "Invalid model") end 299 | if not string_find(body, "\no ") then -- Add a default object 300 | body = "o default\n" .. body 301 | end 302 | 303 | local rawData = string_split(body, "\n") 304 | local minOBB = Vector(100000, 100000, 100000) 305 | local maxOBB = Vector(-100000, -100000, -100000) 306 | 307 | local maxTriangles = PropMLIB.Obj.MAX_SAFE_TRIANGLES:GetInt() 308 | local maxSubMeshes = PropMLIB.Obj.MAX_SUBMESHES:GetInt() 309 | 310 | local subMeshes = {} 311 | local globalMesh = { 312 | positions = {}, 313 | texCoordsU = {}, 314 | texCoordsV = {}, 315 | normals = {} 316 | } 317 | 318 | for _, v in pairs(rawData) do 319 | local data = string_explode("%s+", v, true) 320 | local mode = tostring(data[1]) 321 | 322 | if mode == "v" then -- POSITION 323 | local x = tonumber(data[2]) or 1 324 | local y = tonumber(data[3]) or 1 325 | local z = tonumber(data[4]) or 1 326 | local pos = Vector(x, y, z) 327 | 328 | -- We don't really care about it on server :< 329 | if CLIENT then 330 | globalMesh.positions[#globalMesh.positions + 1] = pos 331 | subMeshes[#subMeshes].positionsCount = subMeshes[#subMeshes].positionsCount + 1 332 | end 333 | 334 | -- OBB parsing -- 335 | minOBB.x = math_min(x, minOBB.x) 336 | minOBB.y = math_min(y, minOBB.y) 337 | minOBB.z = math_min(z, minOBB.z) 338 | 339 | maxOBB.x = math_max(x, maxOBB.x) 340 | maxOBB.y = math_max(y, maxOBB.y) 341 | maxOBB.z = math_max(z, maxOBB.z) 342 | --------- 343 | elseif CLIENT then 344 | if mode == "vt" then -- UV 345 | local rawU = tonumber(data[2]) or 0 346 | local rawV = tonumber(data[3]) or 0 347 | 348 | globalMesh.texCoordsU[#globalMesh.texCoordsU + 1] = rawU % 1 349 | globalMesh.texCoordsV[#globalMesh.texCoordsV + 1] = (1 - rawV) % 1 350 | elseif mode == "vn" then -- NORMALS 351 | local nx = tonumber(data[2]) or 0 352 | local ny = tonumber(data[3]) or 0 353 | local nz = tonumber(data[4]) or 0 354 | 355 | local inverseLength = 1 / math_sqrt(nx * nx + ny * ny + nz * nz) 356 | nx, ny, nz = nx * inverseLength, ny * inverseLength, nz * inverseLength 357 | 358 | globalMesh.normals[#globalMesh.normals + 1] = Vector(nx, ny, nz) 359 | elseif mode == "f" then -- FACES 360 | local parts = {} 361 | local matchLine = string_match(v, "^ *f +(.*)") 362 | for part in string_gmatch(matchLine, "[^ ]+") do 363 | parts[#parts + 1] = part 364 | end 365 | 366 | if subMeshes[#subMeshes].faceCount >= maxTriangles then 367 | if #subMeshes < maxSubMeshes or isAdmin then 368 | print("[PropMLIB][Client] Triangle limit, splitting model..") 369 | 370 | local splitName = subMeshes[#subMeshes].name or "obj" 371 | if string.find(splitName, "_split") then 372 | table_insert(subMeshes, PropMLIB.Obj.NewSubMesh(splitName)) 373 | else 374 | table_insert(subMeshes, PropMLIB.Obj.NewSubMesh(splitName.."_split")) 375 | end 376 | end 377 | else 378 | subMeshes[#subMeshes].faceLines[#subMeshes[#subMeshes].faceLines + 1] = parts 379 | subMeshes[#subMeshes].faceCount = subMeshes[#subMeshes].faceCount + 1 380 | end 381 | elseif mode == "o" then -- OBJECT 382 | local name = tostring(data[2]) or ("obj_" .. #subMeshes) 383 | if #subMeshes < maxSubMeshes or isAdmin then 384 | table_insert(subMeshes, PropMLIB.Obj.NewSubMesh(name)) 385 | end 386 | elseif mode == "usemtl" then -- MATERIAL ID 387 | subMeshes[#subMeshes].mtl = tostring(data[2]) 388 | end 389 | end 390 | 391 | coroutine_yield(false, "Parsing raw data") 392 | end 393 | 394 | if minOBB.x == 0 then minOBB.x = -1 end 395 | if minOBB.y == 0 then minOBB.y = -1 end 396 | if minOBB.z == 0 then minOBB.z = -1 end 397 | 398 | if maxOBB.x == 0 then maxOBB.x = 1 end 399 | if maxOBB.y == 0 then maxOBB.y = 1 end 400 | if maxOBB.z == 0 then maxOBB.z = 1 end 401 | 402 | -- 403 | if SERVER then 404 | local width = maxOBB.x - minOBB.x 405 | local lenght = maxOBB.y - minOBB.y 406 | local height = maxOBB.z - minOBB.z 407 | local volumeOBB = width * lenght * height 408 | 409 | return { 410 | obb = { 411 | minOBB = minOBB, 412 | maxOBB = maxOBB, 413 | }, 414 | volumeOBB = width * lenght * height 415 | } 416 | elseif CLIENT then 417 | if #subMeshes <= 0 then 418 | return coroutine_yield(false, "Invalid model") 419 | end 420 | 421 | local parsedSubMeshes = {} 422 | for _, objMesh in pairs(subMeshes) do 423 | if not isAdmin then 424 | if objMesh.positionsCount <= 0 or objMesh.positionsCount > maxTriangles*3 then 425 | continue 426 | end 427 | end 428 | 429 | local tris = PropMLIB.Obj.CalculateFaces(objMesh.faceLines, globalMesh) 430 | 431 | if fixNormals then 432 | PropMLIB.Obj.CalculateNormals(tris) 433 | PropMLIB.Obj.CalculateTangents(tris) 434 | end 435 | 436 | tris.name = objMesh.name 437 | tris.mtl = objMesh.mtl 438 | 439 | table_insert(parsedSubMeshes, tris) 440 | coroutine_yield(false, "Parsing Sub-Mesh") 441 | end 442 | 443 | if #parsedSubMeshes <= 0 then 444 | return coroutine_yield(false, "Invalid model") 445 | end 446 | 447 | local width = maxOBB.x - minOBB.x 448 | local lenght = maxOBB.y - minOBB.y 449 | local height = maxOBB.z - minOBB.z 450 | local volumeOBB = width * lenght * height 451 | 452 | return { 453 | subMeshes = parsedSubMeshes, 454 | obb = { 455 | minOBB = minOBB, 456 | maxOBB = maxOBB, 457 | }, 458 | volumeOBB = width * lenght * height 459 | } 460 | end 461 | end 462 | 463 | concommand.Add( "prop_mesh_objcache_clear", function() 464 | PropMLIB.Obj.Clear() 465 | end, nil, "Clears all cached models") 466 | -------------------------------------------------------------------------------- /lua/lib/sh/setup.lua: -------------------------------------------------------------------------------- 1 | -- CLEANUP -- 2 | cleanup.Register("prop_mesh") 3 | 4 | -- Registry.PMESH CONVARS -- 5 | CreateConVar( "sbox_maxprop_mesh", 10, {FCVAR_SERVER_CAN_EXECUTE, FCVAR_REPLICATED, FCVAR_ARCHIVE, FCVAR_NOTIFY}, "Max prop_mesh entities allowed (Default: 10)" ) 6 | 7 | CreateConVar( "prop_mesh_maxTriangles", 1650, {FCVAR_SERVER_CAN_EXECUTE, FCVAR_REPLICATED, FCVAR_ARCHIVE, FCVAR_NOTIFY}, "Max prop_mesh Obj triangles allowed in TOTAL (Default: 1650)" ) 8 | CreateConVar( "prop_mesh_maxSubMeshes", 5, {FCVAR_SERVER_CAN_EXECUTE, FCVAR_REPLICATED, FCVAR_ARCHIVE, FCVAR_NOTIFY}, "Max prop_mesh sub-meshes allowed (HIGH VALUE = More rendering lag) (Default: 5)" ) 9 | CreateConVar( "prop_mesh_maxOBJ_bytes", 2048576, {FCVAR_SERVER_CAN_EXECUTE, FCVAR_REPLICATED, FCVAR_ARCHIVE, FCVAR_NOTIFY}, "Max prop_mesh obj size in BYTES (Default: 2048576)" ) 10 | CreateConVar( "prop_mesh_maxScaleVolume", 580, {FCVAR_SERVER_CAN_EXECUTE, FCVAR_REPLICATED, FCVAR_ARCHIVE, FCVAR_NOTIFY}, "Max prop_mesh volume scale (Default: 580)" ) 11 | CreateConVar( "prop_mesh_minScaleVolume", 3, {FCVAR_SERVER_CAN_EXECUTE, FCVAR_REPLICATED, FCVAR_ARCHIVE, FCVAR_NOTIFY}, "Min prop_mesh volume scale (Default: 3)" ) 12 | 13 | CreateConVar( "prop_mesh_ignoreContentRange", 0, {FCVAR_SERVER_CAN_EXECUTE, FCVAR_REPLICATED, FCVAR_ARCHIVE, FCVAR_NOTIFY}, "Ignore Content-Range check, users will be able to force the server to download huge files!" ) 14 | 15 | -------------------------------------------------------------------------------- /lua/lib/sh/util.lua: -------------------------------------------------------------------------------- 1 | local math_round = math.Round 2 | local math_clamp_ = math.Clamp 3 | local math_huge = math.huge 4 | 5 | PropMLIB = PropMLIB or {} 6 | PropMLIB.Util = PropMLIB.Util or {} 7 | 8 | -- Taken from metastruct, cus im lazy -- 9 | PropMLIB.Util.NiceSize = function(size) 10 | size = tonumber( size ) 11 | 12 | if ( size <= 0 ) then return "0" end 13 | if ( size < 1000 ) then return size .. " Bytes" end 14 | if ( size < 1000 * 1000 ) then return math_round( size / 1000, 2 ) .. " KB" end 15 | if ( size < 1000 * 1000 * 1000 ) then return math_round( size / ( 1000 * 1000 ), 2 ) .. " MB" end 16 | 17 | return math_round( size / ( 1000 * 1000 * 1000 ), 2 ) .. " GB" 18 | end 19 | 20 | PropMLIB.Util.SafeVector = function(vec, negative) 21 | if not vec then 22 | if negative then return Vector(-1, -1, -1) 23 | else return Vector(1, 1, 1) end 24 | end 25 | 26 | if not PropMLIB.Util.IsFinite(vec.x) then 27 | if negative then vec.x = -1 else vec.x = 1 end 28 | end 29 | 30 | if not PropMLIB.Util.IsFinite(vec.y) then 31 | if negative then vec.y = -1 else vec.y = 1 end 32 | end 33 | 34 | if not PropMLIB.Util.IsFinite(vec.z) then 35 | if negative then vec.z = -1 else vec.z = 1 end 36 | end 37 | 38 | return vec 39 | end 40 | 41 | PropMLIB.Util.ClampVector = function(vec, min, max) 42 | if not vec then vec = Vector(0, 0, 0) end 43 | 44 | if not min then min = 0 end 45 | if not max then max = 1 end 46 | 47 | vec.x = math_clamp_(vec.x, min, max) 48 | vec.y = math_clamp_(vec.y, min, max) 49 | vec.z = math_clamp_(vec.z, min, max) 50 | 51 | return vec 52 | end 53 | 54 | PropMLIB.Util.IsFinite = function(x) 55 | if not x or PropMLIB.Util.IsNan(x) then return false end 56 | if x == math_huge then return false end 57 | if x == -math_huge then return false end 58 | 59 | return true 60 | end 61 | 62 | PropMLIB.Util.IsNan = function(x) 63 | return x ~= x 64 | end 65 | 66 | --- From PAC3 (Thanks! :D) --- 67 | --- https://github.com/CapsAdmin/pac3/blob/97ab99e9e8f5f16063ee5480ee33d21970822b8c/lua/pac3/core/shared/http.lua 68 | PropMLIB.Util.FixUrl = function(url) 69 | url = url:Trim() 70 | 71 | if url:find("dropbox", 1, true) then 72 | url = url:gsub([[^http%://dl%.dropboxusercontent%.com/]], [[https://dl.dropboxusercontent.com/]]) 73 | url = url:gsub([[^https?://dl.dropbox.com/]], [[https://www.dropbox.com/]]) 74 | url = url:gsub([[^https?://www.dropbox.com/s/(.+)%?dl%=[01]$]], [[https://dl.dropboxusercontent.com/s/%1]]) 75 | url = url:gsub([[^https?://www.dropbox.com/s/(.+)$]], [[https://dl.dropboxusercontent.com/s/%1]]) 76 | return url 77 | end 78 | 79 | if url:find("drive.google.com", 1, true) and not url:find("export=download", 1, true) then 80 | local id = 81 | url:match("https://drive.google.com/file/d/(.-)/") or 82 | url:match("https://drive.google.com/file/d/(.-)$") or 83 | url:match("https://drive.google.com/open%?id=(.-)$") 84 | 85 | if id then 86 | return "https://drive.google.com/uc?export=download&id=" .. id 87 | end 88 | return url 89 | end 90 | 91 | if url:find("gitlab.com", 1, true) then 92 | return url:gsub("^(https?://.-/.-/.-/)blob", "%1raw") 93 | end 94 | 95 | if url:find("github.com", 1, true) and url:find("/blob/", 1, true) then 96 | local id = url:match("https://github.com/(.-)$") 97 | if id then 98 | return "https://raw.githubusercontent.com/" .. string.Replace(string.Replace(id, '/raw/', '/'),'/blob/', '/') 99 | end 100 | 101 | return url:gsub("github.com/([a-zA-Z0-9_-]+)/([a-zA-Z0-9_]+)/blob/", "github.com/%1/%2/raw/") 102 | end 103 | 104 | url = url:gsub([[^http%://onedrive%.live%.com/redir?]],[[https://onedrive.live.com/download?]]) 105 | url = url:gsub("pastebin.com/([a-zA-Z0-9]*)$", "pastebin.com/raw.php?i=%1") 106 | return url 107 | end -------------------------------------------------------------------------------- /lua/lib/sv/registry.lua: -------------------------------------------------------------------------------- 1 | if CLIENT then return error("[PropMLIB]Tried to load 'registry.lua' on CLIENT") end 2 | 3 | local table_insert = table.insert 4 | local table_remove = table.remove 5 | local table_removeByValue = table.RemoveByValue 6 | 7 | PropMLIB = PropMLIB or {} 8 | PropMLIB.Registry = PropMLIB.Registry or {} 9 | PropMLIB.Registry.PMESH = PropMLIB.Registry.PMESH or {} 10 | 11 | PropMLIB.Registry.RegisterPMesh = function(ent) 12 | table_insert(PropMLIB.Registry.PMESH, ent) 13 | end 14 | 15 | PropMLIB.Registry.UnRegisterPMesh = function(ent) 16 | table_removeByValue(PropMLIB.Registry.PMESH, ent) 17 | end 18 | 19 | PropMLIB.Registry.Init = function() 20 | if not PropMLIB.Registry.PMESH or #PropMLIB.Registry.PMESH <= 0 then return end 21 | for k, v in pairs(PropMLIB.Registry.PMESH) do 22 | if IsValid(v) then continue end 23 | table_remove(PropMLIB.Registry.PMESH, k) 24 | end 25 | end 26 | 27 | PropMLIB.Registry.NewPlayer = function(newPly) 28 | for k, v in pairs(PropMLIB.Registry.PMESH) do 29 | if not IsValid(v) then continue end 30 | v:OnNewPlayerJoin(newPly) 31 | end 32 | end 33 | 34 | hook.Add("PlayerInitialSpawn", "__playerspawn_prop_mesh__", function(newPly) 35 | timer.Simple(10, function() 36 | PropMLIB.Registry.NewPlayer(newPly) 37 | end) 38 | end) 39 | 40 | PropMLIB.Registry.Init() -------------------------------------------------------------------------------- /lua/lib/sv/setup.lua: -------------------------------------------------------------------------------- 1 | util.AddNetworkString("prop_mesh_command" ) 2 | util.AddNetworkString("prop_mesh_lib" ) 3 | 4 | duplicator.RegisterEntityModifier( "SAVE_DATA", function(ply, ent, data) 5 | if not IsValid(ply) or not ply:CheckLimit("prop_mesh") then return ent:Remove() end 6 | ply:AddCount("prop_mesh", ent) 7 | ply:AddCleanup("prop_mesh", ent) 8 | 9 | if not IsValid(ent) or not data.meshURL then return end 10 | if not ent.Load then return end 11 | ent.SAVE_DATA = data 12 | 13 | if data.phys and data.obb then 14 | ent:SetPhysScale(data.phys, data.obb) 15 | end 16 | 17 | ent:Load(data.meshURL, data.textures, data.scale, data.phys, true) 18 | end) 19 | 20 | net.Receive("prop_mesh_command", function( len, ply ) 21 | if not IsValid(ply) or not ply:IsPlayer() then return end 22 | local command = net.ReadString() 23 | 24 | local ent = net.ReadEntity() 25 | if not IsValid(ent) then return end 26 | 27 | local isowner = false 28 | if ent.CPPIGetOwner then 29 | isowner = ent:CPPIGetOwner() == ply 30 | else 31 | isowner = ent:GetNWEntity("owner") == ply 32 | end 33 | 34 | if not isowner then return end 35 | 36 | if command == "SET_DEBUG" then 37 | if not ent.SetDebug then return end 38 | ent:SetDebug(net.ReadBool()) 39 | elseif command == "SET_FULLBRIGHT" then 40 | if not ent.SetFullbright then return end 41 | ent:SetFullbright(net.ReadBool()) 42 | elseif command == "UPDATE_MESH" then 43 | local newData = net.ReadTable() 44 | if not newData then return end 45 | 46 | local currMesh = ent.LOADED_MESH 47 | if not currMesh then 48 | ent:Load(newData.uri, newData.textures, newData.scale, newData.phys) 49 | else 50 | if currMesh.uri ~= newData.uri then 51 | ent:Load(newData.uri, newData.textures, newData.scale, newData.phys) 52 | return 53 | end 54 | 55 | ent:SetTextures(newData.textures) 56 | ent:SetScale(newData.scale) 57 | ent:SetPhysScale(newData.phys, currMesh.obb) 58 | end 59 | end 60 | end) -------------------------------------------------------------------------------- /materials/vgui/entities/prop_mesh.vmt: -------------------------------------------------------------------------------- 1 | "UnlitGeneric" 2 | { 3 | "$basetexture" "vgui/entities/prop_mesh" 4 | "$nolod" 1 5 | "$vertexalpha" 1 6 | "$vertexcolor" 1 7 | } -------------------------------------------------------------------------------- /materials/vgui/entities/prop_mesh.vtf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edunad/prop_mesh/42a45ba091ebc54a00678ca1c5c4ea5de1589840/materials/vgui/entities/prop_mesh.vtf -------------------------------------------------------------------------------- /prop_mesh.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".", 5 | "name": "prop_mesh" 6 | } 7 | ], 8 | "settings": {} 9 | } 10 | -------------------------------------------------------------------------------- /thumbnail_prop_mesh.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edunad/prop_mesh/42a45ba091ebc54a00678ca1c5c4ea5de1589840/thumbnail_prop_mesh.jpg -------------------------------------------------------------------------------- /thumbnail_prop_mesh.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/edunad/prop_mesh/42a45ba091ebc54a00678ca1c5c4ea5de1589840/thumbnail_prop_mesh.psd --------------------------------------------------------------------------------