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; RTCDevice device;
RTCScene scene; 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; static const bsp2_t *bsp_static;
@ -175,12 +177,8 @@ Embree_SceneinfoForGeomID(unsigned int geomID)
return skygeom; return skygeom;
} else if (geomID == solidgeom.geomID) { } else if (geomID == solidgeom.geomID) {
return solidgeom; return solidgeom;
} else if (geomID == fencegeom.geomID) { } else if (geomID == filtergeom.geomID) {
return fencegeom; return filtergeom;
} else if (geomID == selfshadowgeom.geomID) {
return selfshadowgeom;
} else if (geomID == switchableshadowgeom.geomID) {
return switchableshadowgeom;
} else { } else {
Error("unexpected geomID"); Error("unexpected geomID");
} }
@ -227,6 +225,15 @@ void AddGlassToRay(const RTCIntersectContext* context, unsigned rayIndex, float
void AddDynamicOccluderToRay(const RTCIntersectContext* context, unsigned rayIndex, int style); 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 // called to evaluate transparency
template<filtertype_t filtertype> template<filtertype_t filtertype>
static void static void
@ -251,40 +258,59 @@ Embree_FilterFuncN(int* valid,
const unsigned &primID = RTCHitN_primID(potentialHit, N, i); const unsigned &primID = RTCHitN_primID(potentialHit, N, i);
// unpack ray index // 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 const modelinfo_t *source_modelinfo = hasmodel ? ModelInfoForModel(bsp_static, raySourceModelindex) : nullptr;
if (geomID == selfshadowgeom.geomID) { const modelinfo_t *hit_modelinfo = Embree_LookupModelinfo(geomID, primID);
const bool from_selfshadow = ((mask & 1) == 1); Q_assert(hit_modelinfo != nullptr);
if (!from_selfshadow) {
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 // reject hit
valid[i] = INVALID; valid[i] = INVALID;
continue; 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 // we hit a dynamic shadow caster. reject the hit, but store the
// info about what we hit. // 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); AddDynamicOccluderToRay(context, rayIndex, style);
// reject hit // reject hit
valid[i] = INVALID; valid[i] = INVALID;
continue; continue;
} else { }
// test fence textures and glass // test fence textures and glass
const bsp2_dface_t *face = Embree_LookupFace(geomID, primID); const bsp2_dface_t *face = Embree_LookupFace(geomID, primID);
const modelinfo_t *modelinfo = Embree_LookupModelinfo(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; vec3_t hitpoint;
Embree_RayEndpoint(ray, potentialHit, N, i, hitpoint); Embree_RayEndpoint(ray, potentialHit, N, i, hitpoint);
const int sample = SampleTexture(face, bsp_static, hitpoint); const int sample = SampleTexture(face, bsp_static, hitpoint);
float alpha = 1.0f; if (isGlass) {
if (modelinfo != nullptr) {
alpha = modelinfo->alpha.floatValue();
if (alpha < 1.0f) {
// hit glass... // hit glass...
vec3_t rayDir = { vec3_t rayDir = {
@ -317,10 +343,9 @@ Embree_FilterFuncN(int* valid,
valid[i] = INVALID; valid[i] = INVALID;
continue; continue;
} }
}
const char *name = Face_TextureName(bsp_static, face);
if (name[0] == '{') { if (isFence) {
if (sample == 255) { if (sample == 255) {
// reject hit // reject hit
valid[i] = INVALID; valid[i] = INVALID;
@ -526,56 +551,75 @@ Embree_TraceInit(const bsp2_t *bsp)
bsp_static = bsp; bsp_static = bsp;
Q_assert(device == nullptr); 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 */ // check all modelinfos
for (const modelinfo_t *model : tracelist) { for (int mi = 0; mi<bsp->nummodels; mi++) {
// TODO: factor out const modelinfo_t *model = ModelInfoForModel(bsp, mi);
const bool isWorld = (model->model == &bsp->dmodels[0]);
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++) { for (int i=0; i<model->model->numfaces; i++) {
const bsp2_dface_t *face = BSP_GetFace(bsp, model->model->firstface + i); const bsp2_dface_t *face = BSP_GetFace(bsp, model->model->firstface + i);
const char *texname = Face_TextureName(bsp, face);
// check for TEX_NOSHADOW // check for TEX_NOSHADOW
const uint64_t extended_flags = extended_texinfo_flags[face->texinfo]; const uint64_t extended_flags = extended_texinfo_flags[face->texinfo];
if (extended_flags & TEX_NOSHADOW) if (extended_flags & TEX_NOSHADOW)
continue; 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) { if (model->alpha.floatValue() < 1.0f) {
fencefaces.push_back(face); filterfaces.push_back(face);
} else if (!Q_strncasecmp("sky", texname, 3)) { continue;
}
// fence
if (texname[0] == '{') {
filterfaces.push_back(face);
continue;
}
// handle sky
if (!Q_strncasecmp("sky", texname, 3)) {
skyfaces.push_back(face); skyfaces.push_back(face);
} else if (texname[0] == '{') { continue;
fencefaces.push_back(face); }
} else if (texname[0] == '*') {
// liquids
if (texname[0] == '*') {
if (!isWorld) { if (!isWorld) {
// world liquids never cast shadows; shadow casting bmodel liquids do // world liquids never cast shadows; shadow casting bmodel liquids do
solidfaces.push_back(face); solidfaces.push_back(face);
} }
} else { continue;
}
// solid faces
if (isWorld || shadow){
solidfaces.push_back(face); 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);
}
}
/* Special handling of skip-textured bmodels */ /* Special handling of skip-textured bmodels */
std::vector<winding_t *> skipwindings; std::vector<winding_t *> skipwindings;
for (const modelinfo_t *model : tracelist) { for (const modelinfo_t *model : tracelist) {
@ -605,29 +649,19 @@ Embree_TraceInit(const bsp2_t *bsp)
scene = rtcDeviceNewScene(device, RTC_SCENE_STATIC | RTC_SCENE_COHERENT, RTC_INTERSECT1 | RTC_INTERSECT_STREAM); scene = rtcDeviceNewScene(device, RTC_SCENE_STATIC | RTC_SCENE_COHERENT, RTC_INTERSECT1 | RTC_INTERSECT_STREAM);
skygeom = CreateGeometry(bsp, scene, skyfaces); skygeom = CreateGeometry(bsp, scene, skyfaces);
solidgeom = CreateGeometry(bsp, scene, solidfaces); solidgeom = CreateGeometry(bsp, scene, solidfaces);
fencegeom = CreateGeometry(bsp, scene, fencefaces); filtergeom = CreateGeometry(bsp, scene, filterfaces);
selfshadowgeom = CreateGeometry(bsp, scene, selfshadowfaces);
switchableshadowgeom = CreateGeometry(bsp, scene, switchableshadowfaces);
CreateGeometryFromWindings(scene, skipwindings); CreateGeometryFromWindings(scene, skipwindings);
rtcSetIntersectionFilterFunctionN(scene, fencegeom.geomID, Embree_FilterFuncN<filtertype_t::INTERSECTION>); rtcSetIntersectionFilterFunctionN(scene, filtergeom.geomID, Embree_FilterFuncN<filtertype_t::INTERSECTION>);
rtcSetOcclusionFilterFunctionN(scene, fencegeom.geomID, Embree_FilterFuncN<filtertype_t::OCCLUSION>); rtcSetOcclusionFilterFunctionN(scene, filtergeom.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>);
rtcCommit (scene); rtcCommit (scene);
logprint("Embree_TraceInit: %d skyfaces %d solidfaces %d fencefaces %d selfshadowfaces %d switchableshadowfaces %d skipwindings\n", logprint("Embree_TraceInit:\n");
(int)skyfaces.size(), logprint("\t%d sky faces\n", (int)skyfaces.size());
(int)solidfaces.size(), logprint("\t%d solid faces\n", (int)solidfaces.size());
(int)fencefaces.size(), logprint("\t%d filtered faces\n", (int)filterfaces.size());
(int)selfshadowfaces.size(), logprint("\t%d shadow-casting skip faces\n", (int)skipwindings.size());
(int)switchableshadowfaces.size(),
(int)skipwindings.size());
FreeWindings(skipwindings); 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 // 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 // this field to store whether the ray is coming from self-shadow geometry
ray.mask = 0; 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 // 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; ray.time = 0.f;
return ray; return ray;

View File

@ -63,7 +63,7 @@
// entity 1 // entity 1
{ {
"classname" "light" "classname" "light"
"origin" "-40 8 104" "origin" "-40 8 152"
"angle" "-0" "angle" "-0"
"delay" "2" "delay" "2"
"light" "500" "light" "500"
@ -80,45 +80,53 @@
"_shadowself" "1" "_shadowself" "1"
// brush 0 // brush 0
{ {
( -72 -136 96 ) ( 24 -136 112 ) ( 24 -136 96 ) narrow -8 80 -0 1 1 ( -64 -176 72 ) ( -64 -112 72 ) ( -80 -192 72 ) narrow -0 -0 -0 1 1
( -40 -152 96 ) ( -40 -88 112 ) ( -40 -152 112 ) narrow -8 80 -0 1 1 ( -80 -96 88 ) ( -80 -192 88 ) ( -80 -96 72 ) narrow -0 56 -0 1 1
( -72 -152 48 ) ( 24 -88 48 ) ( -72 -88 48 ) narrow -8 8 -0 1 1 ( -80 -192 88 ) ( -64 -176 88 ) ( -80 -192 72 ) narrow 0 0 0 1 1
( -72 -152 112 ) ( 24 -88 112 ) ( 24 -152 112 ) narrow -8 8 -0 1 1 ( -80 -96 88 ) ( -64 -112 88 ) ( -80 -192 88 ) narrow -0 -0 -0 1 1
( -72 -120 96 ) ( 24 -120 112 ) ( -72 -120 112 ) narrow -8 80 -0 1 1 ( -64 -176 88 ) ( -64 -112 88 ) ( -64 -176 72 ) narrow 16 56 -0 1 1
( 48 -152 96 ) ( 48 -88 112 ) ( 48 -88 96 ) narrow -8 80 -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 // entity 4
{ {
"classname" "func_wall" "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" "_shadow" "1"
// brush 0 // brush 0
{ {
@ -130,7 +138,7 @@
( -32 -80 16 ) ( -32 -64 32 ) ( -32 -64 16 ) narrow -112 -0 -0 1 1 ( -32 -80 16 ) ( -32 -64 32 ) ( -32 -64 16 ) narrow -112 -0 -0 1 1
} }
} }
// entity 7 // entity 5
{ {
"classname" "func_illusionary" "classname" "func_illusionary"
// brush 0 // brush 0
@ -143,7 +151,7 @@
( 144 -320 16 ) ( 144 -256 80 ) ( 144 -256 16 ) narrow -0 16 -0 1 1 ( 144 -320 16 ) ( 144 -256 80 ) ( 144 -256 16 ) narrow -0 16 -0 1 1
} }
} }
// entity 8 // entity 6
{ {
"classname" "func_wall" "classname" "func_wall"
"_shadowworldonly" "1" "_shadowworldonly" "1"
@ -157,3 +165,53 @@
( -0 -80 16 ) ( -0 -64 32 ) ( -0 -64 16 ) narrow -112 -0 -0 1 1 ( -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
}
}