--[[ | This file was obtained through the combined efforts | of Madbluntz & Plymouth Antiquarian Society. | | Credits: lifestorm, Gregory Wayne Rossel JR., | Maloy, DrPepper10 @ RIP, Atle! | | Visit for more: https://plymouth.thetwilightzone.ru/ --]] local urlobj = {} _G.pac_urlobj = urlobj local CreateCache = include("pac3/libraries/urlobj/cache.lua") local CreateQueueItem = include("pac3/libraries/urlobj/queueitem.lua") _G.pac_urlobj = nil urlobj.DataCache = CreateCache("objcache") local maxAgeConvar = CreateConVar("pac_obj_cache_maxage", "604800", FCVAR_ARCHIVE, "Maximum age of cache entries in seconds, default is 1 week.") urlobj.DataCache:ClearBefore(os.time() - maxAgeConvar:GetFloat()) concommand.Add("pac_urlobj_clear_disk", function() urlobj.DataCache:Clear() pac.Message("Disk cache cleared") end, nil, "Clears obj file cache on disk") local SIMULATENOUS_DOWNLOADS = CreateConVar("pac_objdl_streams", "4", {FCVAR_ARCHIVE}, "OBJ files download streams") local CURRENTLY_DOWNLOADING = 0 urlobj.Cache = {} urlobj.CacheCount = 0 urlobj.Queue = {} urlobj.QueueCount = 0 urlobj.DownloadQueue = {} urlobj.DownloadQueueCount = 0 local pac_enable_urlobj = CreateClientConVar("pac_enable_urlobj", "1", true) concommand.Add("pac_urlobj_clear_cache", function () urlobj.ClearCache() urlobj.ClearQueue() end ) function urlobj.Reload() urlobj.ClearCache() end function urlobj.ClearCache() urlobj.Cache = {} urlobj.CacheCount = 0 end function urlobj.ClearQueue() urlobj.Queue = {} urlobj.QueueCount = 0 urlobj.DownloadQueue = {} urlobj.DownloadQueueCount = 0 CURRENTLY_DOWNLOADING = 0 end function urlobj.GetObjFromURL(url, forceReload, generateNormals, callback, statusCallback) if not pac_enable_urlobj:GetBool() then return end -- if it"s already downloaded just return it if callback and not forceReload and urlobj.Cache[url] then callback(urlobj.Cache[url]) return end -- Add item to queue if not urlobj.Queue[url] then local queueItem = CreateQueueItem(url) urlobj.Queue[url] = queueItem urlobj.QueueCount = urlobj.QueueCount + 1 urlobj.DownloadQueue[url] = queueItem urlobj.DownloadQueueCount = urlobj.DownloadQueueCount + 1 queueItem:BeginCacheRetrieval() queueItem:AddStatusCallback( function(finished, statusMessage) if not finished then return end urlobj.Queue[url] = nil urlobj.QueueCount = urlobj.QueueCount - 1 urlobj.Cache[url] = queueItem:GetModel() urlobj.CacheCount = urlobj.CacheCount + 1 end ) end -- Add callbacks if callback then urlobj.Queue[url]:AddCallback (callback ) end if statusCallback then urlobj.Queue[url]:AddStatusCallback(function(isFinished, mStatus) statusCallback(isFinished, mStatus ~= "" and mStatus or "Queued for processing") end) end end local thinkThreads = {} local PARSING_THERSOLD = CreateConVar("pac_obj_runtime", "0.002", {FCVAR_ARCHIVE}, "Maximal parse runtime in seconds") local PARSE_CHECK_LINES = 30 local function Think() local PARSING_THERSOLD = PARSING_THERSOLD:GetFloat() for i, threadData in ipairs(thinkThreads) do local statusCallback, co = threadData.statusCallback, threadData.co local t0 = SysTime () local success, finished, statusMessage, msg while SysTime () - t0 < PARSING_THERSOLD do success, finished, statusMessage, msg = coroutine.resume(co) if not success then break end if finished then break end end if not success then table.remove(thinkThreads, i) error(finished) statusCallback(true, "Decoding error") elseif finished then statusCallback(true, "Finished") table.remove(thinkThreads, i) else if statusMessage == "Preprocessing lines" then statusCallback(false, statusMessage .. " " .. msg) elseif msg then statusCallback(false, statusMessage .. " " .. math.Round(msg*100) .. " %") else statusCallback(false, statusMessage) end end break end end pac.AddHook("Think", "parse_obj", Think) local nextParsingHookId = 0 function urlobj.CreateModelFromObjData(objData, generateNormals, statusCallback) local mesh = Mesh() local co = coroutine.create ( function () local meshData = urlobj.ParseObj(objData, generateNormals) mesh:BuildFromTriangles (meshData) coroutine.yield (true) end ) table.insert(thinkThreads, { objData = objData, generateNormals = generateNormals, statusCallback = statusCallback, co = co, mesh = mesh }) statusCallback(false, "Queued") return { mesh } end -- =========================================================================== -- Everything below is internal and should only be called by code in this file -- =========================================================================== -- parser made by animorten -- modified slightly by capsadmin local ipairs = ipairs local pairs = pairs local tonumber = tonumber local math_sqrt = math.sqrt local string_gmatch = string.gmatch local string_gsub = string.gsub local string_match = string.match local string_sub = string.sub local string_Split = string.Split local string_Trim = string.Trim local table_concat = table.concat local table_insert = table.insert local Vector = Vector local facesMapper = "([0-9]+)/?([0-9]*)/?([0-9]*)" local numberMatch = "(-?[0-9.+-e0-9]+)" local vMatch = "^ *v *" .. numberMatch .. " +" .. numberMatch .. " +" .. numberMatch local vtMatch = "^ *vt *" .. numberMatch .. " +" .. numberMatch local vnMatch = "^ *vn *" .. numberMatch .. " +" .. numberMatch .. " +" .. numberMatch function urlobj.ParseObj(data, generateNormals) local coroutine_yield = coroutine.running () and coroutine.yield or function () end local positions = {} local texCoordsU = {} local texCoordsV = {} local normals = {} local triangleList = {} local lines = {} local faceLines = {} local vLines = {} local vtLines = {} local vnLines = {} local facesPreprocess = {} local i = 1 local inContinuation = false local continuationLines = nil local defaultNormal = Vector(0, 0, -1) for line in string_gmatch (data, "(.-)\r?\n") do if #line > 3 then local first = string_sub(line, 1, 1) if first ~= "#" and first ~= "l" and first ~= "g" and first ~= "u" then if string_sub(line, #line) == "\\" then line = string_sub (line, 1, #line - 1) if inContinuation then continuationLines[#continuationLines + 1] = line else inContinuation = true continuationLines = { line } end else local currLine if inContinuation then continuationLines[#continuationLines + 1] = line currLine = table_concat (continuationLines) first = string_sub(currLine, 1, 1) inContinuation = false continuationLines = nil else currLine = line end local second = string_sub(currLine, 1, 2) if second == "vt" then vtLines[#vtLines + 1] = currLine elseif second == "vn" then vnLines[#vnLines + 1] = currLine elseif first == "v" then vLines[#vLines + 1] = currLine elseif first == "f" then facesPreprocess[#facesPreprocess + 1] = currLine else lines[#lines + 1] = currLine end end if i % PARSE_CHECK_LINES == 0 then coroutine_yield(false, "Preprocessing lines", i) end i = i + 1 end end end if inContinuation then continuationLines[#continuationLines + 1] = line lines[#lines + 1] = table.concat (continuationLines) inContinuation = false continuationLines = nil end coroutine_yield(false, "Preprocessing lines", i) local lineCount = #vtLines + #vnLines + #vLines + #facesPreprocess local inverseLineCount = 1 / lineCount local lineProcessed = 0 for i, line in ipairs(vLines) do local x, y, z = string_match(line, vMatch) x, y, z = tonumber(x) or 0, tonumber(y) or 0, tonumber(z) or 0 positions[#positions + 1] = Vector(x, y, z) if i % PARSE_CHECK_LINES == 0 then coroutine_yield(false, "Processing vertices", i * inverseLineCount) end end lineProcessed = #vLines for i, line in ipairs(vtLines) do local u, v = string_match(line, vtMatch) u, v = tonumber(u) or 0, tonumber(v) or 0 local texCoordIndex = #texCoordsU + 1 texCoordsU[texCoordIndex] = u % 1 texCoordsV[texCoordIndex] = (1 - v) % 1 if i % PARSE_CHECK_LINES == 0 then coroutine_yield(false, "Processing vertices", (i + lineProcessed) * inverseLineCount) end end lineProcessed = #vLines + #vtLines if not generateNormals then for i, line in ipairs(vnLines) do local nx, ny, nz = string_match(line, vnMatch) if nx and ny and nz then nx, ny, nz = tonumber(nx) or 0, tonumber(ny) or 0, tonumber(nz) or 0 -- possible / by zero local inverseLength = 1 / math_sqrt(nx * nx + ny * ny + nz * nz) nx, ny, nz = nx * inverseLength, ny * inverseLength, nz * inverseLength local normal = Vector(nx, ny, nz) normals[#normals + 1] = normal end if i % PARSE_CHECK_LINES == 0 then coroutine_yield(false, "Processing vertices", (i + lineProcessed) * inverseLineCount) end end end lineProcessed = #vLines + #vtLines + #vnLines for i, line in ipairs(facesPreprocess) do local matchLine = string_match(line, "^ *f +(.*)") if matchLine then -- Explode line local parts = {} for part in string_gmatch(matchLine, "[^ ]+") do parts[#parts + 1] = part end faceLines[#faceLines + 1] = parts if i % PARSE_CHECK_LINES == 0 then coroutine_yield(false, "Processing vertices", (i + lineProcessed) * inverseLineCount) end end end local lineCount = #lines local inverseLineCount = 1 / lineCount local i = 1 while i <= lineCount do local processedLine = false -- Positions: v %f %f %f [%f] while i <= lineCount do local line = lines[i] local x, y, z = string_match(line, vMatch) if not x then break end processedLine = true x, y, z = tonumber(x) or 0, tonumber(y) or 0, tonumber(z) or 0 positions[#positions + 1] = Vector(x, y, z) if i % PARSE_CHECK_LINES == 0 then coroutine_yield(false, "Processing vertices", i * inverseLineCount) end i = i + 1 end if processedLine then coroutine_yield(false, "Processing vertices", i * inverseLineCount) end -- Texture coordinates: vt %f %f while i <= lineCount do local line = lines[i] local u, v = string_match(line, vtMatch) if not u then break end processedLine = true u, v = tonumber(u) or 0, tonumber(v) or 0 local texCoordIndex = #texCoordsU + 1 texCoordsU[texCoordIndex] = u % 1 texCoordsV[texCoordIndex] = (1 - v) % 1 if i % PARSE_CHECK_LINES == 0 then coroutine_yield(false, "Processing vertices", i * inverseLineCount) end i = i + 1 end if processedLine then coroutine_yield(false, "Processing vertices", i * inverseLineCount) end -- Normals: vn %f %f %f while i <= lineCount do local line = lines[i] local nx, ny, nz = string_match(line, vnMatch) if not nx then break end processedLine = true if not generateNormals then nx, ny, nz = tonumber(nx) or 0, tonumber(ny) or 0, tonumber(nz) or 0 local inverseLength = 1 / math_sqrt(nx * nx + ny * ny + nz * nz) nx, ny, nz = nx * inverseLength, ny * inverseLength, nz * inverseLength local normal = Vector(nx, ny, nz) normals[#normals + 1] = normal end if i % PARSE_CHECK_LINES == 0 then coroutine_yield(false, "Processing vertices", i * inverseLineCount) end i = i + 1 end if processedLine then coroutine_yield(false, "Processing vertices", i * inverseLineCount) end -- Faces: f %f %f %f+ while i <= lineCount do local line = lines[i] local matchLine = string_match(line, "^ *f +(.*)") if not matchLine then break end processedLine = true -- Explode line local parts = {} for part in string_gmatch(matchLine, "[^ ]+") do parts[#parts + 1] = part end faceLines[#faceLines + 1] = parts if i % PARSE_CHECK_LINES == 0 then coroutine_yield(false, "Processing vertices", i * inverseLineCount) end i = i + 1 end if processedLine then coroutine_yield(false, "Processing vertices", i * inverseLineCount) end -- Something else if not processedLine then i = i + 1 end end local faceLineCount = #faceLines local inverseFaceLineCount = 1 / faceLineCount for i = 1, #faceLines do local parts = faceLines [i] if #parts >= 3 then -- are they always integers? local v1PositionIndex, v1TexCoordIndex, v1NormalIndex = string_match(parts[1], facesMapper) local v3PositionIndex, v3TexCoordIndex, v3NormalIndex = string_match(parts[2], facesMapper) v1PositionIndex, v1TexCoordIndex, v1NormalIndex = tonumber(v1PositionIndex), tonumber(v1TexCoordIndex), tonumber(v1NormalIndex) v3PositionIndex, v3TexCoordIndex, v3NormalIndex = tonumber(v3PositionIndex), tonumber(v3TexCoordIndex), tonumber(v3NormalIndex) for i = 3, #parts do local v2PositionIndex, v2TexCoordIndex, v2NormalIndex = string_match(parts[i], facesMapper) v2PositionIndex, v2TexCoordIndex, v2NormalIndex = tonumber(v2PositionIndex), tonumber(v2TexCoordIndex), tonumber(v2NormalIndex) local v1 = { pos_index = nil, pos = nil, u = nil, v = nil, normal = nil, userdata = nil } local v2 = { pos_index = nil, pos = nil, u = nil, v = nil, normal = nil, userdata = nil } local v3 = { pos_index = nil, pos = nil, u = nil, v = nil, normal = nil, userdata = nil } v1.pos_index = v1PositionIndex v2.pos_index = v2PositionIndex v3.pos_index = v3PositionIndex v1.pos = positions[v1PositionIndex] v2.pos = positions[v2PositionIndex] v3.pos = positions[v3PositionIndex] if #texCoordsU > 0 then v1.u = texCoordsU[v1TexCoordIndex] or 0 v1.v = texCoordsV[v1TexCoordIndex] or 0 v2.u = texCoordsU[v2TexCoordIndex] or 0 v2.v = texCoordsV[v2TexCoordIndex] or 0 v3.u = texCoordsU[v3TexCoordIndex] or 0 v3.v = texCoordsV[v3TexCoordIndex] or 0 else v1.u, v1.v = 0, 0 v2.u, v2.v = 0, 0 v3.u, v3.v = 0, 0 end if #normals > 0 then v1.normal = normals[v1NormalIndex] v2.normal = normals[v2NormalIndex] v3.normal = normals[v3NormalIndex] else v1.normal = defaultNormal v2.normal = defaultNormal v3.normal = defaultNormal end triangleList [#triangleList + 1] = v1 triangleList [#triangleList + 1] = v2 triangleList [#triangleList + 1] = v3 v3PositionIndex, v3TexCoordIndex, v3NormalIndex = v2PositionIndex, v2TexCoordIndex, v2NormalIndex end end if i % PARSE_CHECK_LINES == 0 then coroutine_yield(false, "Processing faces", i * inverseFaceLineCount) end end coroutine_yield(false, "Processing faces", faceLineCount) if generateNormals then local vertexNormals = {} local triangleCount = #triangleList / 3 local inverseTriangleCount = 1 / triangleCount for i = 1, triangleCount do local a, b, c = triangleList[1+(i-1)*3+0], triangleList[1+(i-1)*3+1], triangleList[1+(i-1)*3+2] local normal = (c.pos - a.pos):Cross(b.pos - a.pos):GetNormalized() vertexNormals[a.pos_index] = vertexNormals[a.pos_index] or Vector() vertexNormals[a.pos_index] = (vertexNormals[a.pos_index] + normal) vertexNormals[b.pos_index] = vertexNormals[b.pos_index] or Vector() vertexNormals[b.pos_index] = (vertexNormals[b.pos_index] + normal) vertexNormals[c.pos_index] = vertexNormals[c.pos_index] or Vector() vertexNormals[c.pos_index] = (vertexNormals[c.pos_index] + normal) if i % PARSE_CHECK_LINES == 0 then coroutine_yield(false, "Generating normals", i * inverseTriangleCount) end end coroutine_yield(false, "Generating normals", triangleCount) local vertexCount = #triangleList local inverseVertexCount = 1 / vertexCount for i = 1, vertexCount do local normal = vertexNormals[triangleList[i].pos_index] or defaultNormal normal:Normalize() normals[i] = normal triangleList[i].normal = normal coroutine_yield(false, "Normalizing normals", i * inverseVertexCount) end end do -- Lengyel, Eric. “Computing Tangent Space Basis Vectors for an Arbitrary Mesh”. Terathon Software, 2001. http://terathon.com/code/tangent.html local tan1 = {} local tan2 = {} local vertexCount = #triangleList for i = 1, vertexCount do tan1[i] = Vector(0, 0, 0) tan2[i] = Vector(0, 0, 0) end for i = 1, vertexCount - 2, 3 do local vert1, vert2, vert3 = triangleList[i], triangleList[i+1], triangleList[i+2] local p1, p2, p3 = vert1.pos, vert2.pos, vert3.pos local u1, u2, u3 = vert1.u, vert2.u, vert3.u local v1, v2, v3 = vert1.v, vert2.v, vert3.v local x1 = p2.x - p1.x; local x2 = p3.x - p1.x; local y1 = p2.y - p1.y; local y2 = p3.y - p1.y; local z1 = p2.z - p1.z; local z2 = p3.z - p1.z; local s1 = u2 - u1; local s2 = u3 - u1; local t1 = v2 - v1; local t2 = v3 - v1; local r = 1 / (s1 * t2 - s2 * t1) local sdir = Vector((t2 * x1 - t1 * x2) * r, (t2 * y1 - t1 * y2) * r, (t2 * z1 - t1 * z2) * r); local tdir = Vector((s1 * x2 - s2 * x1) * r, (s1 * y2 - s2 * y1) * r, (s1 * z2 - s2 * z1) * r); tan1[i]:Add(sdir) tan1[i+1]:Add(sdir) tan1[i+2]:Add(sdir) tan2[i]:Add(tdir) tan2[i+1]:Add(tdir) tan2[i+2]:Add(tdir) end local tangent = {} for i = 1, vertexCount do local n = triangleList[i].normal local t = tan1[i] local tan = (t - n * n:Dot(t)) tan:Normalize() local w = (n:Cross(t)):Dot(tan2[i]) < 0 and -1 or 1 triangleList[i].userdata = {tan[1], tan[2], tan[3], w} end end return triangleList end -- Download queuing function urlobj.DownloadQueueThink() if pac.urltex and pac.urltex.Busy then return end for url, queueItem in pairs(urlobj.DownloadQueue) do if not queueItem:IsDownloading() and not queueItem:IsCacheDecodeFinished () then queueItem:SetStatus("Queued for download (" .. urlobj.DownloadQueueCount .. " items in queue)") end -- Check for download timeout if queueItem:IsDownloading() and queueItem:HasDownloadTimedOut() then pac.dprint("model download timed out for the %s time %q", queueItem:GetDownloadAttemptCount(), queueItem:GetUrl()) queueItem:AbortDownload() if queueItem:GetDownloadAttemptCount() > 3 then -- Give up urlobj.Queue[url] = nil urlobj.QueueCount = urlobj.QueueCount - 1 urlobj.DownloadQueue[url] = nil urlobj.DownloadQueueCount = urlobj.DownloadQueueCount - 1 CURRENTLY_DOWNLOADING = CURRENTLY_DOWNLOADING - 1 pac.dprint("model download timed out for good %q", url) else -- Reattempt download queueItem:BeginDownload() end end end urlobj.Busy = next (urlobj.Queue) ~= nil if CURRENTLY_DOWNLOADING >= SIMULATENOUS_DOWNLOADS:GetInt() then return end -- Start download of next item in queue if next(urlobj.DownloadQueue) then for url, queueItem in pairs(urlobj.DownloadQueue) do if not queueItem:IsDownloading() then queueItem:BeginDownload() queueItem:AddDownloadCallback( function() urlobj.DownloadQueue[url] = nil urlobj.DownloadQueueCount = urlobj.DownloadQueueCount - 1 CURRENTLY_DOWNLOADING = CURRENTLY_DOWNLOADING - 1 end ) CURRENTLY_DOWNLOADING = CURRENTLY_DOWNLOADING + 1 pac.dprint("requesting model download %q", url) if CURRENTLY_DOWNLOADING >= SIMULATENOUS_DOWNLOADS:GetInt() then return end end end end end timer.Create("urlobj_download_queue", 0.1, 0, urlobj.DownloadQueueThink) return urlobj