diff --git a/light/trace_embree.cc b/light/trace_embree.cc index a5fd2e4b..fc74f6c5 100644 --- a/light/trace_embree.cc +++ b/light/trace_embree.cc @@ -158,8 +158,10 @@ CreateGeometryFromWindings(RTCScene scene, const std::vector &windi RTCDevice device; RTCScene scene; -/* global shadow casters */ -sceneinfo skygeom, solidgeom, fencegeom, selfshadowgeom, switchableshadowgeom; + +sceneinfo skygeom; // sky. always occludes. +sceneinfo solidgeom; // solids. always occludes. +sceneinfo filtergeom; // conditional occluders.. needs to run ray intersection filter static const bsp2_t *bsp_static; @@ -175,12 +177,8 @@ Embree_SceneinfoForGeomID(unsigned int geomID) return skygeom; } else if (geomID == solidgeom.geomID) { return solidgeom; - } else if (geomID == fencegeom.geomID) { - return fencegeom; - } else if (geomID == selfshadowgeom.geomID) { - return selfshadowgeom; - } else if (geomID == switchableshadowgeom.geomID) { - return switchableshadowgeom; + } else if (geomID == filtergeom.geomID) { + return filtergeom; } else { Error("unexpected geomID"); } @@ -227,6 +225,15 @@ void AddGlassToRay(const RTCIntersectContext* context, unsigned rayIndex, float void AddDynamicOccluderToRay(const RTCIntersectContext* context, unsigned rayIndex, int style); +static const unsigned RAYMASK_HASMODEL_SHIFT = 0; +static const unsigned RAYMASK_HASMODEL_MASK = (1 << RAYMASK_HASMODEL_SHIFT); + +static const unsigned RAYMASK_MODELINDEX_SHIFT = 1; +static const unsigned RAYMASK_MODELINDEX_MASK = (0xffff << RAYMASK_MODELINDEX_SHIFT); + +static const unsigned RAYMASK_RAYINDEX_SHIFT = 17; +static const unsigned RAYMASK_RAYINDEX_MASK = (0x7fff << RAYMASK_RAYINDEX_SHIFT); + // called to evaluate transparency template static void @@ -251,76 +258,94 @@ Embree_FilterFuncN(int* valid, const unsigned &primID = RTCHitN_primID(potentialHit, N, i); // unpack ray index - const unsigned rayIndex = (mask >> 1); + const bool hasmodel = static_cast((mask & RAYMASK_HASMODEL_MASK) >> RAYMASK_HASMODEL_SHIFT); + const unsigned raySourceModelindex = (mask & RAYMASK_MODELINDEX_MASK) >> RAYMASK_MODELINDEX_SHIFT; + const unsigned rayIndex = (mask & RAYMASK_RAYINDEX_MASK) >> RAYMASK_RAYINDEX_SHIFT; - // bail if we hit a selfshadow face, but the ray is not coming from within that model - if (geomID == selfshadowgeom.geomID) { - const bool from_selfshadow = ((mask & 1) == 1); - if (!from_selfshadow) { + const modelinfo_t *source_modelinfo = hasmodel ? ModelInfoForModel(bsp_static, raySourceModelindex) : nullptr; + const modelinfo_t *hit_modelinfo = Embree_LookupModelinfo(geomID, primID); + Q_assert(hit_modelinfo != nullptr); + + if (hit_modelinfo->shadowworldonly.boolValue()) { + // we hit "_shadowworldonly" "1" geometry. Ignore the hit unless we are from world. + if (!source_modelinfo || !source_modelinfo->isWorld()) { // reject hit valid[i] = INVALID; continue; } - } else if (geomID == switchableshadowgeom.geomID) { + } + + if (hit_modelinfo->shadowself.boolValue()) { + // only casts shadows on itself + if (source_modelinfo != hit_modelinfo) { + // reject hit + valid[i] = INVALID; + continue; + } + } + + if (hit_modelinfo->switchableshadow.boolValue()) { // we hit a dynamic shadow caster. reject the hit, but store the // info about what we hit. - const modelinfo_t *modelinfo = Embree_LookupModelinfo(geomID, primID); - int style = modelinfo->switchshadstyle.intValue(); + + int style = hit_modelinfo->switchshadstyle.intValue(); AddDynamicOccluderToRay(context, rayIndex, style); // reject hit valid[i] = INVALID; continue; - } else { - // test fence textures and glass - const bsp2_dface_t *face = Embree_LookupFace(geomID, primID); - const modelinfo_t *modelinfo = Embree_LookupModelinfo(geomID, primID); - + } + + // test fence textures and glass + const bsp2_dface_t *face = Embree_LookupFace(geomID, primID); + const char *name = Face_TextureName(bsp_static, face); + + const float alpha = hit_modelinfo->alpha.floatValue(); + const bool isFence = (name[0] == '{'); + const bool isGlass = (alpha < 1.0f); + + if (isFence || isGlass) { vec3_t hitpoint; Embree_RayEndpoint(ray, potentialHit, N, i, hitpoint); const int sample = SampleTexture(face, bsp_static, hitpoint); - - float alpha = 1.0f; - if (modelinfo != nullptr) { - alpha = modelinfo->alpha.floatValue(); - if (alpha < 1.0f) { - // hit glass... + + if (isGlass) { + // hit glass... + + vec3_t rayDir = { + RTCRayN_dir_x(ray, N, i), + RTCRayN_dir_y(ray, N, i), + RTCRayN_dir_z(ray, N, i) + }; + vec3_t potentialHitGeometryNormal = { + RTCHitN_Ng_x(potentialHit, N, i), + RTCHitN_Ng_y(potentialHit, N, i), + RTCHitN_Ng_z(potentialHit, N, i) + }; + + VectorNormalize(rayDir); + VectorNormalize(potentialHitGeometryNormal); + + const vec_t raySurfaceCosAngle = DotProduct(rayDir, potentialHitGeometryNormal); + + // only pick up the color of the glass on the _exiting_ side of the glass. + // (we currently trace "backwards", from surface point --> light source) + if (raySurfaceCosAngle < 0) { + vec3_t samplecolor; + glm_to_vec3_t(Palette_GetColor(sample), samplecolor); + VectorScale(samplecolor, 1/255.0, samplecolor); - vec3_t rayDir = { - RTCRayN_dir_x(ray, N, i), - RTCRayN_dir_y(ray, N, i), - RTCRayN_dir_z(ray, N, i) - }; - vec3_t potentialHitGeometryNormal = { - RTCHitN_Ng_x(potentialHit, N, i), - RTCHitN_Ng_y(potentialHit, N, i), - RTCHitN_Ng_z(potentialHit, N, i) - }; - - VectorNormalize(rayDir); - VectorNormalize(potentialHitGeometryNormal); - - const vec_t raySurfaceCosAngle = DotProduct(rayDir, potentialHitGeometryNormal); - - // only pick up the color of the glass on the _exiting_ side of the glass. - // (we currently trace "backwards", from surface point --> light source) - if (raySurfaceCosAngle < 0) { - vec3_t samplecolor; - glm_to_vec3_t(Palette_GetColor(sample), samplecolor); - VectorScale(samplecolor, 1/255.0, samplecolor); - - AddGlassToRay(context, rayIndex, alpha, samplecolor); - } - - // reject hit - valid[i] = INVALID; - continue; + AddGlassToRay(context, rayIndex, alpha, samplecolor); } + + // reject hit + valid[i] = INVALID; + continue; } - const char *name = Face_TextureName(bsp_static, face); - if (name[0] == '{') { + + if (isFence) { if (sample == 255) { // reject hit valid[i] = INVALID; @@ -526,53 +551,72 @@ Embree_TraceInit(const bsp2_t *bsp) bsp_static = bsp; Q_assert(device == nullptr); - std::vector skyfaces, solidfaces, fencefaces, selfshadowfaces, switchableshadowfaces; + std::vector skyfaces, solidfaces, filterfaces; - /* Check against the list of global shadow casters */ - for (const modelinfo_t *model : tracelist) { - // TODO: factor out - const bool isWorld = (model->model == &bsp->dmodels[0]); + // check all modelinfos + for (int mi = 0; minummodels; mi++) { + const modelinfo_t *model = ModelInfoForModel(bsp, mi); + + const bool isWorld = model->isWorld(); + const bool shadow = model->shadow.boolValue(); + const bool shadowself = model->shadowself.boolValue(); + const bool shadowworldonly = model->shadowworldonly.boolValue(); + const bool switchableshadow = model->switchableshadow.boolValue(); + + if (!(isWorld || shadow || shadowself || shadowworldonly || switchableshadow)) + continue; for (int i=0; imodel->numfaces; i++) { const bsp2_dface_t *face = BSP_GetFace(bsp, model->model->firstface + i); + const char *texname = Face_TextureName(bsp, face); // check for TEX_NOSHADOW const uint64_t extended_flags = extended_texinfo_flags[face->texinfo]; if (extended_flags & TEX_NOSHADOW) continue; - const char *texname = Face_TextureName(bsp, face); - + // handle switchableshadow + if (switchableshadow) { + filterfaces.push_back(face); + continue; + } + + // handle glass if (model->alpha.floatValue() < 1.0f) { - fencefaces.push_back(face); - } else if (!Q_strncasecmp("sky", texname, 3)) { + filterfaces.push_back(face); + continue; + } + + // fence + if (texname[0] == '{') { + filterfaces.push_back(face); + continue; + } + + // handle sky + if (!Q_strncasecmp("sky", texname, 3)) { skyfaces.push_back(face); - } else if (texname[0] == '{') { - fencefaces.push_back(face); - } else if (texname[0] == '*') { + continue; + } + + // liquids + if (texname[0] == '*') { if (!isWorld) { // world liquids never cast shadows; shadow casting bmodel liquids do solidfaces.push_back(face); } - } else { - solidfaces.push_back(face); + continue; + } + + // solid faces + + if (isWorld || shadow){ + solidfaces.push_back(face); + } else { + // shadowself or shadowworldonly + Q_assert(shadowself || shadowworldonly); + filterfaces.push_back(face); } - } - } - - /* Self-shadow models */ - for (const modelinfo_t *model : selfshadowlist) { - for (int i=0; imodel->numfaces; i++) { - const bsp2_dface_t *face = BSP_GetFace(bsp, model->model->firstface + i); - selfshadowfaces.push_back(face); - } - } - - /* Dynamic-shadow models */ - for (const modelinfo_t *model : switchableshadowlist) { - for (int i=0; imodel->numfaces; i++) { - const bsp2_dface_t *face = BSP_GetFace(bsp, model->model->firstface + i); - switchableshadowfaces.push_back(face); } } @@ -605,29 +649,19 @@ Embree_TraceInit(const bsp2_t *bsp) scene = rtcDeviceNewScene(device, RTC_SCENE_STATIC | RTC_SCENE_COHERENT, RTC_INTERSECT1 | RTC_INTERSECT_STREAM); skygeom = CreateGeometry(bsp, scene, skyfaces); solidgeom = CreateGeometry(bsp, scene, solidfaces); - fencegeom = CreateGeometry(bsp, scene, fencefaces); - selfshadowgeom = CreateGeometry(bsp, scene, selfshadowfaces); - switchableshadowgeom = CreateGeometry(bsp, scene, switchableshadowfaces); + filtergeom = CreateGeometry(bsp, scene, filterfaces); CreateGeometryFromWindings(scene, skipwindings); - rtcSetIntersectionFilterFunctionN(scene, fencegeom.geomID, Embree_FilterFuncN); - rtcSetOcclusionFilterFunctionN(scene, fencegeom.geomID, Embree_FilterFuncN); - - rtcSetIntersectionFilterFunctionN(scene, selfshadowgeom.geomID, Embree_FilterFuncN); - rtcSetOcclusionFilterFunctionN(scene, selfshadowgeom.geomID, Embree_FilterFuncN); - - rtcSetIntersectionFilterFunctionN(scene, switchableshadowgeom.geomID, Embree_FilterFuncN); - rtcSetOcclusionFilterFunctionN(scene, switchableshadowgeom.geomID, Embree_FilterFuncN); + rtcSetIntersectionFilterFunctionN(scene, filtergeom.geomID, Embree_FilterFuncN); + rtcSetOcclusionFilterFunctionN(scene, filtergeom.geomID, Embree_FilterFuncN); rtcCommit (scene); - logprint("Embree_TraceInit: %d skyfaces %d solidfaces %d fencefaces %d selfshadowfaces %d switchableshadowfaces %d skipwindings\n", - (int)skyfaces.size(), - (int)solidfaces.size(), - (int)fencefaces.size(), - (int)selfshadowfaces.size(), - (int)switchableshadowfaces.size(), - (int)skipwindings.size()); + logprint("Embree_TraceInit:\n"); + logprint("\t%d sky faces\n", (int)skyfaces.size()); + logprint("\t%d solid faces\n", (int)solidfaces.size()); + logprint("\t%d filtered faces\n", (int)filterfaces.size()); + logprint("\t%d shadow-casting skip faces\n", (int)skipwindings.size()); FreeWindings(skipwindings); } @@ -646,12 +680,21 @@ static RTCRay SetupRay(unsigned rayindex, const vec3_t start, const vec3_t dir, // NOTE: we are not using the ray masking feature of embree, but just using // this field to store whether the ray is coming from self-shadow geometry ray.mask = 0; - if (modelinfo && modelinfo->shadowself.boolValue()) { - ray.mask |= 1; + + if (modelinfo) { + ray.mask |= RAYMASK_HASMODEL_MASK; + + // Hacky.. + const int modelindex = (modelinfo->model - bsp_static->dmodels); + Q_assert(modelindex >= 0 && modelindex < bsp_static->nummodels); + Q_assert(modelindex <= 65535); + + ray.mask |= (static_cast(modelindex) << RAYMASK_MODELINDEX_SHIFT); } - + // pack the ray index into the rest of the mask - ray.mask |= (rayindex << 1); + Q_assert(rayindex <= 32767); + ray.mask |= (rayindex << RAYMASK_RAYINDEX_SHIFT); ray.time = 0.f; return ray; diff --git a/testmaps/shadowself_shadow.map b/testmaps/shadowself_shadow.map index 01e25524..aa92d3b0 100644 --- a/testmaps/shadowself_shadow.map +++ b/testmaps/shadowself_shadow.map @@ -63,7 +63,7 @@ // entity 1 { "classname" "light" -"origin" "-40 8 104" +"origin" "-40 8 152" "angle" "-0" "delay" "2" "light" "500" @@ -80,45 +80,53 @@ "_shadowself" "1" // brush 0 { -( -72 -136 96 ) ( 24 -136 112 ) ( 24 -136 96 ) narrow -8 80 -0 1 1 -( -40 -152 96 ) ( -40 -88 112 ) ( -40 -152 112 ) narrow -8 80 -0 1 1 -( -72 -152 48 ) ( 24 -88 48 ) ( -72 -88 48 ) narrow -8 8 -0 1 1 -( -72 -152 112 ) ( 24 -88 112 ) ( 24 -152 112 ) narrow -8 8 -0 1 1 -( -72 -120 96 ) ( 24 -120 112 ) ( -72 -120 112 ) narrow -8 80 -0 1 1 -( 48 -152 96 ) ( 48 -88 112 ) ( 48 -88 96 ) narrow -8 80 -0 1 1 +( -64 -176 72 ) ( -64 -112 72 ) ( -80 -192 72 ) narrow -0 -0 -0 1 1 +( -80 -96 88 ) ( -80 -192 88 ) ( -80 -96 72 ) narrow -0 56 -0 1 1 +( -80 -192 88 ) ( -64 -176 88 ) ( -80 -192 72 ) narrow 0 0 0 1 1 +( -80 -96 88 ) ( -64 -112 88 ) ( -80 -192 88 ) narrow -0 -0 -0 1 1 +( -64 -176 88 ) ( -64 -112 88 ) ( -64 -176 72 ) narrow 16 56 -0 1 1 +( -64 -112 72 ) ( -64 -112 88 ) ( -80 -96 72 ) narrow 0 0 0 1 1 +} +// brush 1 +{ +( -80 -192 72 ) ( 80 -192 72 ) ( -64 -176 72 ) narrow -0 -0 -0 1 1 +( -64 -176 88 ) ( -80 -192 88 ) ( -64 -176 72 ) narrow 0 0 0 1 1 +( 64 -176 72 ) ( 64 -176 88 ) ( -64 -176 72 ) narrow -16 56 -0 1 1 +( -80 -192 88 ) ( 80 -192 88 ) ( -80 -192 72 ) narrow -0 56 -0 1 1 +( 80 -192 72 ) ( 80 -192 88 ) ( 64 -176 72 ) narrow 0 0 0 1 1 +( 64 -176 88 ) ( 80 -192 88 ) ( -64 -176 88 ) narrow -0 -0 -0 1 1 +} +// brush 2 +{ +( -80 -96 72 ) ( -80 -96 88 ) ( -64 -112 72 ) narrow 0 0 0 1 1 +( -64 -112 88 ) ( 64 -112 88 ) ( -64 -112 72 ) narrow -16 56 -0 1 1 +( 64 -112 72 ) ( 80 -96 72 ) ( -64 -112 72 ) narrow -0 -0 -0 1 1 +( -80 -96 88 ) ( 80 -96 88 ) ( -64 -112 88 ) narrow -0 -0 -0 1 1 +( 64 -112 88 ) ( 80 -96 88 ) ( 64 -112 72 ) narrow 0 0 0 1 1 +( 80 -96 72 ) ( 80 -96 88 ) ( -80 -96 72 ) narrow -0 56 -0 1 1 +} +// brush 3 +{ +( 80 -192 72 ) ( 80 -96 72 ) ( 64 -176 72 ) narrow -0 -0 -0 1 1 +( 64 -112 88 ) ( 64 -176 88 ) ( 64 -112 72 ) narrow 16 56 -0 1 1 +( 64 -176 88 ) ( 80 -192 88 ) ( 64 -176 72 ) narrow 0 0 0 1 1 +( 64 -112 88 ) ( 80 -96 88 ) ( 64 -176 88 ) narrow -0 -0 -0 1 1 +( 80 -192 88 ) ( 80 -96 88 ) ( 80 -192 72 ) narrow -0 56 -0 1 1 +( 80 -96 72 ) ( 80 -96 88 ) ( 64 -112 72 ) narrow 0 0 0 1 1 +} +// brush 4 +{ +( -64 -176 64 ) ( 64 -176 80 ) ( 64 -176 64 ) narrow 0 0 0 1 1 +( -64 -176 64 ) ( -64 -112 80 ) ( -64 -176 80 ) narrow 0 0 0 1 1 +( -64 -176 64 ) ( 64 -112 64 ) ( -64 -112 64 ) narrow 0 0 0 1 1 +( -64 -176 80 ) ( 64 -112 80 ) ( 64 -176 80 ) narrow 0 0 0 1 1 +( -64 -112 64 ) ( 64 -112 80 ) ( -64 -112 80 ) narrow 0 0 0 1 1 +( 64 -176 64 ) ( 64 -112 80 ) ( 64 -112 64 ) narrow 0 0 0 1 1 } } // entity 4 { "classname" "func_wall" -"_shadowself" "1" -// brush 0 -{ -( -80 -144 88 ) ( 16 -144 104 ) ( 16 -144 88 ) narrow -0 72 -0 1 1 -( -64 -160 88 ) ( -64 -96 104 ) ( -64 -160 104 ) narrow -0 72 -0 1 1 -( -80 -160 56 ) ( 16 -96 56 ) ( -80 -96 56 ) narrow -0 -0 -0 1 1 -( -80 -160 104 ) ( 16 -96 104 ) ( 16 -160 104 ) narrow -0 -0 -0 1 1 -( -80 -112 88 ) ( 16 -112 104 ) ( -80 -112 104 ) narrow -0 72 -0 1 1 -( 64 -160 88 ) ( 64 -96 104 ) ( 64 -96 88 ) narrow -0 72 -0 1 1 -} -} -// entity 5 -{ -"classname" "func_wall" -"_shadowself" "1" -// brush 0 -{ -( -80 -160 72 ) ( 16 -160 88 ) ( 16 -160 72 ) narrow -0 56 -0 1 1 -( -80 -160 72 ) ( -80 -96 88 ) ( -80 -160 88 ) narrow -0 56 -0 1 1 -( -80 -160 72 ) ( 16 -96 72 ) ( -80 -96 72 ) narrow -0 -0 -0 1 1 -( -80 -160 88 ) ( 16 -96 88 ) ( 16 -160 88 ) narrow -0 -0 -0 1 1 -( -80 -96 72 ) ( 16 -96 88 ) ( -80 -96 88 ) narrow -0 56 -0 1 1 -( 80 -160 72 ) ( 80 -96 88 ) ( 80 -96 72 ) narrow -0 56 -0 1 1 -} -} -// entity 6 -{ -"classname" "func_wall" "_shadow" "1" // brush 0 { @@ -130,7 +138,7 @@ ( -32 -80 16 ) ( -32 -64 32 ) ( -32 -64 16 ) narrow -112 -0 -0 1 1 } } -// entity 7 +// entity 5 { "classname" "func_illusionary" // brush 0 @@ -143,7 +151,7 @@ ( 144 -320 16 ) ( 144 -256 80 ) ( 144 -256 16 ) narrow -0 16 -0 1 1 } } -// entity 8 +// entity 6 { "classname" "func_wall" "_shadowworldonly" "1" @@ -157,3 +165,53 @@ ( -0 -80 16 ) ( -0 -64 32 ) ( -0 -64 16 ) narrow -112 -0 -0 1 1 } } +// entity 7 +{ +"classname" "func_wall" +"_shadowself" "1" +// brush 0 +{ +( -32 -152 144 ) ( -96 -152 144 ) ( -16 -152 160 ) narrow 80 80 270 1 -1 +( -112 -136 160 ) ( -16 -136 160 ) ( -112 -152 160 ) narrow -80 -40 180 1 -1 +( -16 -136 160 ) ( -32 -136 144 ) ( -16 -152 160 ) narrow 96 -96 270 1 1 +( -112 -136 160 ) ( -96 -136 144 ) ( -16 -136 160 ) narrow 80 80 270 1 -1 +( -32 -136 144 ) ( -96 -136 144 ) ( -32 -152 144 ) narrow -64 -40 180 1 -1 +( -96 -152 144 ) ( -96 -136 144 ) ( -112 -152 160 ) narrow -0 -96 90 1 -1 +} +// brush 1 +{ +( -16 -152 160 ) ( -16 -152 -0 ) ( -32 -152 144 ) narrow 80 80 270 1 -1 +( -32 -136 144 ) ( -16 -136 160 ) ( -32 -152 144 ) narrow 96 -96 270 1 1 +( -32 -152 16 ) ( -32 -136 16 ) ( -32 -152 144 ) narrow 64 -40 270 1 1 +( -16 -136 160 ) ( -16 -136 -0 ) ( -16 -152 160 ) narrow 80 -40 270 1 1 +( -16 -152 -0 ) ( -16 -136 -0 ) ( -32 -152 16 ) narrow -64 -96 90 1 -1 +( -32 -136 16 ) ( -16 -136 -0 ) ( -32 -136 144 ) narrow 80 80 270 1 -1 +} +// brush 2 +{ +( -112 -152 160 ) ( -112 -136 160 ) ( -96 -152 144 ) narrow -0 -96 90 1 -1 +( -96 -136 144 ) ( -96 -136 16 ) ( -96 -152 144 ) narrow 64 -40 270 1 1 +( -96 -152 16 ) ( -112 -152 -0 ) ( -96 -152 144 ) narrow 80 80 270 1 -1 +( -112 -136 160 ) ( -112 -136 -0 ) ( -96 -136 144 ) narrow 80 80 270 1 -1 +( -96 -136 16 ) ( -112 -136 -0 ) ( -96 -152 16 ) narrow 32 -96 270 1 1 +( -112 -152 -0 ) ( -112 -136 -0 ) ( -112 -152 160 ) narrow 80 -40 270 1 1 +} +// brush 3 +{ +( -16 -152 -0 ) ( -112 -152 -0 ) ( -32 -152 16 ) narrow 80 80 270 1 -1 +( -96 -136 16 ) ( -32 -136 16 ) ( -96 -152 16 ) narrow -64 -40 180 1 -1 +( -32 -136 16 ) ( -16 -136 -0 ) ( -32 -152 16 ) narrow -64 -96 90 1 -1 +( -96 -136 16 ) ( -112 -136 -0 ) ( -32 -136 16 ) narrow 80 80 270 1 -1 +( -16 -136 -0 ) ( -112 -136 -0 ) ( -16 -152 -0 ) narrow -80 -40 180 1 -1 +( -112 -152 -0 ) ( -112 -136 -0 ) ( -96 -152 16 ) narrow 32 -96 270 1 1 +} +// brush 4 +{ +( -32 -160 144 ) ( -32 -144 16 ) ( -32 -160 16 ) narrow 80 -96 270 1 1 +( -32 -160 144 ) ( -96 -144 144 ) ( -32 -144 144 ) narrow -80 -96 180 1 -1 +( -32 -160 144 ) ( -96 -160 16 ) ( -96 -160 144 ) narrow 80 80 270 1 -1 +( -32 -144 144 ) ( -96 -144 16 ) ( -32 -144 16 ) narrow 80 80 270 1 -1 +( -96 -160 144 ) ( -96 -144 16 ) ( -96 -144 144 ) narrow 80 -96 270 1 1 +( -32 -160 16 ) ( -96 -144 16 ) ( -96 -160 16 ) narrow -80 -96 180 1 -1 +} +}