light: implement _shadowworldonly, fix _shadowself which was broken

This commit is contained in:
Eric Wasylishen 2017-06-27 14:48:27 -06:00
parent 62db977309
commit 6f07303504
2 changed files with 248 additions and 147 deletions

View File

@ -158,8 +158,10 @@ CreateGeometryFromWindings(RTCScene scene, const std::vector<winding_t *> &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<filtertype_t filtertype>
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<bool>((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<const bsp2_dface_t *> skyfaces, solidfaces, fencefaces, selfshadowfaces, switchableshadowfaces;
std::vector<const bsp2_dface_t *> 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; mi<bsp->nummodels; 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; i<model->model->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; i<model->model->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; i<model->model->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<filtertype_t::INTERSECTION>);
rtcSetOcclusionFilterFunctionN(scene, fencegeom.geomID, Embree_FilterFuncN<filtertype_t::OCCLUSION>);
rtcSetIntersectionFilterFunctionN(scene, selfshadowgeom.geomID, Embree_FilterFuncN<filtertype_t::INTERSECTION>);
rtcSetOcclusionFilterFunctionN(scene, selfshadowgeom.geomID, Embree_FilterFuncN<filtertype_t::OCCLUSION>);
rtcSetIntersectionFilterFunctionN(scene, switchableshadowgeom.geomID, Embree_FilterFuncN<filtertype_t::INTERSECTION>);
rtcSetOcclusionFilterFunctionN(scene, switchableshadowgeom.geomID, Embree_FilterFuncN<filtertype_t::OCCLUSION>);
rtcSetIntersectionFilterFunctionN(scene, filtergeom.geomID, Embree_FilterFuncN<filtertype_t::INTERSECTION>);
rtcSetOcclusionFilterFunctionN(scene, filtergeom.geomID, Embree_FilterFuncN<filtertype_t::OCCLUSION>);
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<unsigned>(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;

View File

@ -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
}
}