// Copyright (c) 2019 Lawnjelly // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // SOFTWARE. #include "lroom_converter.h" #include "lroom_manager.h" #include "lportal.h" #include "scene/3d/mesh_instance.h" #include "core/math/quick_hull.h" #include "ldebug.h" // save typing, I am lazy #define LMAN m_pManager void LRoomConverter::Convert(LRoomManager &manager) { // This just is simply used to set how much debugging output .. more during conversion, less during running // except when requested by explicitly clearing this flag. Lawn::LDebug::m_bRunning = false; LPRINT(5, "running convert"); LMAN = &manager; int count = CountRooms(); LMAN->m_SOBs.clear(); LMAN->m_ShadowCasters_SOB.clear(); int num_global_lights = LMAN->m_Lights.size(); // make sure bitfield is right size for number of rooms LMAN->m_BF_visible_rooms.Create(count); LMAN->m_Rooms.clear(true); LMAN->m_Rooms.resize(count); m_TempRooms.clear(true); m_TempRooms.resize(count); Convert_Rooms(); Convert_Portals(); Convert_Bounds(); // make sure manager bitfields are the correct size for number of objects int num_sobs = LMAN->m_SOBs.size(); LPRINT(5,"Total SOBs " + itos(num_sobs)); LMAN->m_BF_caster_SOBs.Create(num_sobs); LMAN->m_BF_visible_SOBs.Create(num_sobs); LMAN->m_BF_master_SOBs.Create(num_sobs); LMAN->m_BF_master_SOBs_prev.Create(num_sobs); // must be done after the bitfields Convert_ShadowCasters(); // hide all in preparation for first frame Convert_HideAll(); // temp rooms no longer needed m_TempRooms.clear(true); // clear out the local room lights, leave only global lights LMAN->m_Lights.resize(num_global_lights); Lawn::LDebug::m_bRunning = true; } void LRoomConverter::Convert_Rooms() { LPRINT(5,"Convert_Rooms"); // first find all room empties and convert to LRooms int count = 0; for (int n=0; nget_child_count(); n++) { Node * pChild = LMAN->get_child(n); if (!Node_IsRoom(pChild)) continue; Spatial * pSpat = Object::cast_to(pChild); assert (pSpat); Convert_Room(pSpat, count++); } } int LRoomConverter::FindRoom_ByName(String szName) const { for (int n=0; nm_Rooms.size(); n++) { if (LMAN->m_Rooms[n].m_szName == szName) return n; } return -1; } void LRoomConverter::Convert_Room_FindObjects_Recursive(Node * pParent, LRoom &lroom, LAABB &bb_room) { int nChildren = pParent->get_child_count(); for (int n=0; nget_child(n); // we are not interested in portal meshes, as they will be deleted later in conversion if (Node_IsPortal(pChild)) continue; // we can optionally ignore nodes (they will still be shown / hidden with the room though) if (Node_IsIgnore(pChild)) continue; // not interested in bounds if (Node_IsBound(pChild)) continue; VisualInstance * pVI = Object::cast_to(pChild); if (pVI) { LPRINT(2, "\t\tFound VI : " + pVI->get_name()); // update bound to find centre of room roughly AABB bb = pVI->get_transformed_aabb(); bb_room.ExpandToEnclose(bb); // store some info about the static object for use at runtime LSob sob; sob.m_ID = pVI->get_instance_id(); sob.m_aabb = bb; //lroom.m_SOBs.push_back(sob); LRoom_PushBackSOB(lroom, sob); } else { // not visual instances } // does it have further children? Convert_Room_FindObjects_Recursive(pChild, lroom, bb_room); } } bool LRoomConverter::Convert_Room(Spatial * pNode, int lroomID) { // get the room part of the name String szFullName = pNode->get_name(); String szRoom = LPortal::FindNameAfter(pNode, "room_"); LPRINT(4, "Convert_Room : " + szFullName); // get a reference to the lroom we are writing to LRoom &lroom = LMAN->m_Rooms[lroomID]; // store the godot room lroom.m_GodotID = pNode->get_instance_id(); lroom.m_RoomID = lroomID; // save the room ID on the godot room metadata // This is used when registering DOBs and teleporting them with hints // i.e. the Godot room is used to lookup the room ID of the startroom. LMAN->Obj_SetRoomNum(pNode, lroomID); // create a new LRoom to exchange the children over to, and delete the original empty lroom.m_szName = szRoom; // keep a running bounding volume as we go through the visual instances // to determine the overall bound of the room LAABB bb_room; bb_room.SetToMaxOpposite(); // recursively find statics Convert_Room_FindObjects_Recursive(pNode, lroom, bb_room); // store the lroom centre and bound lroom.m_ptCentre = bb_room.FindCentre(); // bound (untested) lroom.m_AABB.position = bb_room.m_ptMins; lroom.m_AABB.size = bb_room.m_ptMaxs - bb_room.m_ptMins; LPRINT(2, "\t\t\t" + String(lroom.m_szName) + " centre " + lroom.m_ptCentre); return true; } bool LRoomConverter::Bound_AddPlaneIfUnique(LVector &planes, const Plane &p) { for (int n=0; n d) continue; float dot = p.normal.dot(o.normal); if (dot < 0.98f) continue; // match! return false; } // test // Vector3 va(1, 0, 0); // Vector3 vb(1, 0.2, 0); // vb.normalize(); // float dot = va.dot(vb); // print("va dot vb is " + String(Variant(dot))); // is unique // print("\t\t\t\tAdding bound plane : " + p); planes.push_back(p); return true; } bool LRoomConverter::Convert_Bound(LRoom &lroom, MeshInstance * pMI) { LPRINT(2, "\tCONVERT_BOUND : '" + pMI->get_name() + "' for room '" + lroom.get_name() + "'"); // some godot jiggery pokery to get the mesh verts in local space Ref rmesh = pMI->get_mesh(); Array arrays = rmesh->surface_get_arrays(0); PoolVector p_vertices = arrays[VS::ARRAY_VERTEX]; // convert to world space Transform trans = pMI->get_global_transform(); Vector points; for (int n=0; n 3) { Geometry::MeshData md; Error err = QuickHull::build(points, md); if (err == OK) { // get the planes for (int n=0; nm_SOBs.size(); n++) { LSob &sob = LMAN->m_SOBs[n]; sob.Show(false); } } void LRoomConverter::Convert_ShadowCasters() { LPRINT(5,"Convert_ShadowCasters ... numlights " + itos (LMAN->m_Lights.size())); for (int n=0; nm_Rooms.size(); n++) { LPRINT(2,"\tRoom " + itos(n)); LRoom &lroom = LMAN->m_Rooms[n]; LRoom_FindShadowCasters(lroom); } } void LRoomConverter::Convert_Bounds() { for (int n=0; nm_Rooms.size(); n++) { LRoom &lroom = LMAN->m_Rooms[n]; //print("DetectBounds from room " + lroom.get_name()); Spatial * pGRoom = lroom.GetGodotRoom(); assert (pGRoom); for (int n=0; nget_child_count(); n++) { Node * pChild = pGRoom->get_child(n); if (Node_IsBound(pChild)) { MeshInstance * pMesh = Object::cast_to(pChild); assert (pMesh); Convert_Bound(lroom, pMesh); // delete the mesh pGRoom->remove_child(pChild); pChild->queue_delete(); } } } } void LRoomConverter::Convert_Portals() { for (int pass=0; pass<3; pass++) { LPRINT(2, "Convert_Portals pass " + itos(pass)); LPRINT(2, ""); for (int n=0; nm_Rooms.size(); n++) { LRoom &lroom = LMAN->m_Rooms[n]; LTempRoom &troom = m_TempRooms[n]; switch (pass) { case 0: LRoom_DetectPortalMeshes(lroom, troom); break; case 1: LRoom_MakePortalsTwoWay(lroom, troom, n); break; case 2: LRoom_MakePortalFinalList(lroom, troom); break; } } } } int LRoomConverter::CountRooms() { int nChildren = LMAN->get_child_count(); int count = 0; for (int n=0; nget_child(n))) count++; } return count; } // find all objects that cast shadows onto the objects in this room void LRoomConverter::LRoom_FindShadowCasters(LRoom &lroom) { // each global light, and each light affecting this room for (int n=0; nm_Lights.size(); n++) { LRoom_FindShadowCasters_FromLight(lroom, LMAN->m_Lights[n]); } return; } void LRoomConverter::LRoom_AddShadowCaster_SOB(LRoom &lroom, int sobID) { // we will reuse the rendering bitflags for shadow casters for this ... to check for double entries (fnaa fnaa) if (LMAN->m_BF_caster_SOBs.GetBit(sobID)) return; LMAN->m_BF_caster_SOBs.SetBit(sobID, true); // first? if (!lroom.m_iNumShadowCasters_SOB) lroom.m_iFirstShadowCaster_SOB = LMAN->m_ShadowCasters_SOB.size(); LMAN->m_ShadowCasters_SOB.push_back(sobID); lroom.m_iNumShadowCasters_SOB++; } void LRoomConverter::LRoom_FindShadowCasters_FromLight(LRoom &lroom, const LLight &light) { // blank this each time as it is used to create the list of casters LMAN->m_BF_caster_SOBs.Blank(); // first add all objects in this room as casters // int last_sob = lroom.m_iFirstSOB + lroom.m_iNumSOBs; // for (int n=lroom.m_iFirstSOB; nm_Pool.Reset(); // the first set of planes are blank unsigned int pool_member = LMAN->m_Pool.Request(); assert (pool_member != -1); LVector &planes = LMAN->m_Pool.Get(pool_member); planes.clear(); Lawn::LDebug::m_iTabDepth = 0; LRoom_FindShadowCasters_Recursive(lroom, 0, lroom, light, planes); } void LRoomConverter::LRoom_FindShadowCasters_Recursive(LRoom &source_lroom, int depth, LRoom &lroom, const LLight &light, const LVector &planes) { // prevent too much depth if (depth > 8) { LPRINT_RUN(2, "\t\t\tLRoom_FindShadowCasters_Recursive DEPTH LIMIT REACHED"); // WARN_PRINT_ONCE("LPortal Depth Limit reached (seeing through > 8 portals)"); return; } Lawn::LDebug::m_iTabDepth = depth; LPRINT_RUN(2, "ROOM " + lroom.get_name()); // every object in this room is added that is within the planes int last_sob = lroom.m_iFirstSOB + lroom.m_iNumSOBs; for (int n=lroom.m_iFirstSOB; nm_SOBs[n]; // not a shadow caster? don't add to the list if (!sob.IsShadowCaster()) continue; bool bShow = true; const AABB &bb = sob.m_aabb; // print("\t\t\tculling object " + pObj->get_name()); for (int p=0; p 0.0f) { bShow = false; break; } } if (bShow) { LPRINT_RUN(2, "\tcaster " + itos(n) + ", " + sob.GetSpatial()->get_name()); LRoom_AddShadowCaster_SOB(source_lroom, n); } } // look through every portal out for (int n=0; nm_Portals[portalID]; LPRINT_RUN(2, "\tPORTAL " + itos (n) + " (" + itos(portalID) + ") " + port.get_name() + " normal " + port.m_Plane.normal); // cull with light direction float dot = port.m_Plane.normal.dot(light.m_ptDir); if (dot <= 0.0f) { LPRINT_RUN(2, "\t\tCULLED (wrong direction)"); continue; } // is it culled by the planes? LPortal::eClipResult overall_res = LPortal::eClipResult::CLIP_INSIDE; // cull portal with planes for (int l=0; lPortal_GetLinkedRoom(port); // recurse into that portal unsigned int uiPoolMem = LMAN->m_Pool.Request(); if (uiPoolMem != -1) { // get a vector of planes from the pool LVector &new_planes = LMAN->m_Pool.Get(uiPoolMem); // copy the existing planes new_planes.copy_from(planes); // add the planes for the portal port.AddLightPlanes(light, new_planes); LRoom_FindShadowCasters_Recursive(source_lroom, depth + 1, linked_room, light, new_planes); // for debugging need to reset tab depth Lawn::LDebug::m_iTabDepth = depth; // we no longer need these planes LMAN->m_Pool.Free(uiPoolMem); } else { // planes pool is empty! // This will happen if the view goes through shedloads of portals // The solution is either to increase the plane pool size, or build levels // with views through multiple portals. Looking through multiple portals is likely to be // slow anyway because of the number of planes to test. WARN_PRINT_ONCE("LRoom_FindShadowCasters_Recursive : Planes pool is empty"); } } } // go through the nodes hanging off the room looking for those that are meshes to mark portal locations void LRoomConverter::LRoom_DetectPortalMeshes(LRoom &lroom, LTempRoom &troom) { LPRINT(2, "DETECT_PORTALS from room " + lroom.get_name()); Spatial * pGRoom = lroom.GetGodotRoom(); assert (pGRoom); for (int n=0; nget_child_count(); n++) { Node * pChild = pGRoom->get_child(n); if (Node_IsPortal(pChild)) { MeshInstance * pMesh = Object::cast_to(pChild); assert (pMesh); // name must start with 'portal_' // and ends with the name of the room we want to link to (without the 'room_') String szLinkRoom = LPortal::FindNameAfter(pMesh, "portal_"); LRoom_DetectedPortalMesh(lroom, troom, pMesh, szLinkRoom); } } // we need an enclosing while loop because we might be deleting children and mucking up the iterator bool bDetectedOne = true; while (bDetectedOne) { bDetectedOne = false; for (int n=0; nget_child_count(); n++) { Node * pChild = pGRoom->get_child(n); if (Node_IsPortal(pChild)) { // delete the original child, as it is no longer needed at runtime (except maybe for debugging .. NYI?) // pMeshInstance->hide(); pChild->get_parent()->remove_child(pChild); pChild->queue_delete(); bDetectedOne = true; } if (bDetectedOne) break; } // for loop } // while } void LRoomConverter::LRoom_PushBackSOB(LRoom &lroom, const LSob &sob) { // first added for this room? if (lroom.m_iNumSOBs == 0) lroom.m_iFirstSOB = LMAN->m_SOBs.size(); LMAN->m_SOBs.push_back(sob); lroom.m_iNumSOBs++; } // handles the slight faff involved in getting a new portal in the manager contiguous list of portals LPortal * LRoomConverter::LRoom_RequestNewPortal(LRoom &lroom) { // is this the first portal? if (lroom.m_iNumPortals == 0) lroom.m_iFirstPortal = LMAN->m_Portals.size(); lroom.m_iNumPortals++; return LMAN->m_Portals.request(); } // convert the list on each room to a single contiguous list in the manager void LRoomConverter::LRoom_MakePortalFinalList(LRoom &lroom, LTempRoom &troom) { for (int n=0; n rmesh = pMeshInstance->get_mesh(); Array arrays = rmesh->surface_get_arrays(0); PoolVector p_vertices = arrays[VS::ARRAY_VERTEX]; // create a new LPortal to fill with this wonderful info LPortal &lport = *troom.m_Portals.request(); lport.m_szName = szLinkRoom; lport.m_iRoomNum = iLinkRoom; // create the portal geometry lport.CreateGeometry(p_vertices, pMeshInstance->get_global_transform()); // LPRINT(2, "\t\t\tnum portals now " + itos(troom.m_Portals.size())); } // This aims to make life easier for level designers. They only need to make a portal facing one way and LPortal // will automatically create a mirror portal the other way. void LRoomConverter::LRoom_MakePortalsTwoWay(LRoom &lroom, LTempRoom &troom, int iRoomNum) { LPRINT(2, "MAKE_PORTALS_TWOWAY from room " + lroom.get_name()); LPRINT(2, "\tcontains " + itos (troom.m_Portals.size()) + " portals"); for (int n=0; nm_Rooms[iRoomOrig]; // the new portal should have the name of the room the original came from LPortal &new_port = *nroom.m_Portals.request(); new_port.m_szName = orig_lroom.m_szName; new_port.m_iRoomNum = iRoomOrig; new_port.m_bMirror = true; // the portal vertices should be the same but reversed (to flip the normal) new_port.CopyReversedGeometry(port); } /////////////////////////////////////////////////// // helper bool LRoomConverter::Node_IsRoom(Node * pNode) const { Spatial * pSpat = Object::cast_to(pNode); if (!pSpat) return false; if (LPortal::NameStartsWith(pSpat, "room_")) return true; return false; } bool LRoomConverter::Node_IsIgnore(Node * pNode) const { if (LPortal::NameStartsWith(pNode, "ignore_")) return true; return false; } bool LRoomConverter::Node_IsBound(Node * pNode) const { MeshInstance * pMI = Object::cast_to(pNode); if (!pMI) return false; if (LPortal::NameStartsWith(pMI, "bound_")) return true; return false; } bool LRoomConverter::Node_IsPortal(Node * pNode) const { MeshInstance * pMI = Object::cast_to(pNode); if (!pMI) return false; if (LPortal::NameStartsWith(pMI, "portal_")) return true; return false; } // keep the global namespace clean #undef LMAN