3D Polyobject specification for k8vavoom engine, v0.99

This is description of k8vavoom 3d polyobjects: how to create them, how to use them, and, most important, how to NOT use them.

Think of 3D polyobject as if it is a moving solid sector. Normal Doom sectors are empty inside, i.e. they have empty space between their floors and ceilings. 3D polyobject is the inverse of that: it has solid space between its floor and ceiling, and you can stand on its ceiling (which will look like platform floor for the player).

TOC
What's new since the previous version
How to draw 3d polyobject
How to spawn 3d polyobject
How to set 3d polyobject height (and texture parameters meaning)
Linked 3d polyobjects
Line blocking flags
Notes about movement
New ACS API Overview
ACS API Explanation
Current Limitations

How to create 3d polyobject

Drawing 3d polyobjects is very easy, it is not much different from drawing a standard polyobject. Please, refer to any polyobject documentation. I will only describe the differences here.

Each 3d polyobject should consist of one sector, created with closed contours. The shape and the number of the contours can be arbitrary, as long as all contours are closed, and there are no self-intersections. All lines should point outside of that "inner" sector, and should be marked as "impassable". Of course, those lines should be two-sided. ;-)

Please, note that breaking any requirement may lead to undefined result. I.e. your invalid 3d polyobject may work, and you may even get some fancy effects, but there are no guarantees that it will keep working in any following engine build.

PLEASE, DON'T DO ANYTHING THAT IS NOT EXPLICITLY DESCRIBED AS ALLOWED IN THIS DOCUMENT!

Here is the picture for you.

There are two sectors on the picture. "Inner sector" is 3d polyobject sector. As you can see, its shape is not convex, but it is closed, and all lines points outside of it. The outer sector is just a "container" for polyobject. It will not be visible in-game.

You can place normal polyobject anchor thing to mark the anchor point of your 3d polyobject. If you will not do it, the engine will select some point on its own. It will try to select a point close to polyobject center, but it is not guaranteed. The algorithm to calculate center point may change in the future, so you should ALWAYS use anchor things.

Inner sector height will be used as 3d polyobject height. But please, note that maximum height is limited by middle texture height, if you won't set your middle texture as wrapping. Inner sector floor texture will be the bottom 3d polyobject texture, and ceiling texture will become the top one.

To align floor and ceiling textures, just align your inner sector as you want to. The inner sector will be "cut out" as it is, and will retain its flat align no matter how you'll move it.

You can set top and bottom textures for 3d polyobject lines. Top texture can be made impassable by setting "clip midtexture" line flag. Impassable top textures should be assigned to the both line sides, and should be of the same height, or the engine will refuse loading a map. Bottom texture is just a decoration and will not block movement.

Note that top texture is always mapped from 3d polyobject ceiling up, and bottom texture is always mapped from 3d polyobject floor down.

You can use "midtexture is wrapped" flag to tell the engine that middle texture should be wrapped. Note that all middle textures should be high enough (or wrapped) to fill the whole side. If this rule is violated, the engine will refuse loading a map.

Line alpha and additivenes can be used to control top/bottom texture rendering. Middle texture is always non-alphablended. Do not assign translucent textures to midtex, or the engine will refuse loading a map.

Currently you cannot assign "masked" middle texture (i.e. things like grates and such). This limitation may be removed in the future.

3D polyobject light level is determined by the light level of its inner sector. Currently this is the only thing that controls 3d polyobject ambient lighting.

3D polyobject sound sequence origin will move with the corresponding 3d polyobject anchor. I.e. for things like moving trains, engine sounds will move with them instead of staying at the starting spot. This is done only for 3d polyobjects, old-style polyobject sounds will not move.


How to spawn 3d polyobject

To spawn your 3d polyobject, you should use new thing with editor number 9369. It's height will specify initial z offset from the inner sector. NOT from the destination one! The angle is the polyobject tag, as in other polyobject spawners.
arg1 is the sum of 3d pobj flags (see `POBJ_FLAG_xxx` below).
Note: if `POBJ_FLAG_SIDE_CRUSH` is set, entity will be destroyed if it was touched by a moving 3d pobj side. This is useful to create vertical crushers, because otherwise crushing 3d pobj can try to push an entity away if it is close to the edge. Also, this flag does nothing if `POBJ_FLAG_CRUSH` is not set.
WARNING! Other things args must be zero! They may be used later, and if you will not set them to zeroes, your map may break in the future!


How to set 3d polyobject height

3D polyobject height is determined by two parameters: its inner sector, and middle texture of its first line (at the front side).

First, 3d polyobject inner sector sets hard limits: 3d pobj cannot be higher than its inner sector.

Second, if the middle texture of the 3d polyobject first line is not wrapped, then the height is clamped by that texture scaled height (i.e. UDMF Y scale value is in effect). if ML_DONTPEGBOTTOM flag is set, then the texture goes from inner sector floor up, otherwise it goes from inner sector ceiling down (i.e. that flag has the same meaning as for other two-sided lines).


Linked 3d polyobjects

There is a new feature you can use to create complex structures out of simple polyobjects: polyobject links. It is somewhat similar to linked sectors you could use to create complex lifts.

Linked polyobjects will behave as one object, and all operations on the first polyojbect in the link will affect all linked "children". All operations on the link chain are atomic. I.e. if moving/rotating is failed, no objects in chain will be modified. Just think about the link chain as one big object.

To link polyobjects, use things with editor id 9368. Its first argument is the master polyobject id, its second argument is the secondary polyobject id. It will link the secondary polyobject to the master. Note that you cannot link two or more polyobject to one master, so to create complex structures you have to link all polyobjects one to another, creating a "link chain". Linking all polyobjects to a single master will not work.

There is a simplier way to create a link chain of sequential polyobjects, with thing 9367. Its first argument is the first polyobject id, and its second argument is the last polyobject id. The engine will link each polyobject to the next one (in backwards direction if the starting id is higher then the ending id). The sequence can have "holes" (i.e. you can setup link from 1 to 6, but only have polyobjects with ids 2 and 4, for example). The usual link chain restrictions still should be obeyed.


Line blocking flags

Each 3d polyobject line should have ML_BLOCKING flag set. This controls midtexture blocking, and cannot be changed (i.e. midtexture is always blocking).

Other blocking flags controls entity blocking with top texture.
ML_BLOCKEVERYTHING will block... well, every entity.
ML_BLOCKPLAYERS will block players.
ML_BLOCKMONSTERS will block monsters.
ML_BLOCKPROJECTILE will block projectiles.
ML_BLOCK_FLOATERS will block floaters.

You can also block hitscan attacks with ML_BLOCKHITSCAN.
Note that ML_BLOCKEVERYTHING will not block hitscans.

Blocking monster sight with ML_BLOCKSIGHT currently doesn't work, but this WILL be fixed.
WARNING! Setting ML_BLOCKSIGHT flag currently may not work, but don't rely on that! This is a known bug, and it will be fixed!
Note that ML_BLOCKEVERYTHING will not block sight.


Notes about movement

You can move your 3d polyobject by all three axes, and it will carry all things standing on it. Currently it works like a platform in arcade games: it carries things, but doesn't modify their velocities. Yet jumping (and only jumping, not walking!) from moving 3d polyobject will add its velocity to the player velocity. That velocity will be subtracted if the player will land on the same 3d polyobject. This allows more-or-less proper jumping on moving platforms. Note that for chained 3d polyobjects, the velocity will be taken from the object you moved via ACS (i.e. most of the time this will properly work with chain links).
Rotating 3d polyobjects will not add any rotation impulse, though.

Rotating 3d polyobject is allowed too. As you may expect, all things standing on such 3d polyobject will be properly rotated. Floor and ceiling textures will be rotated too (around polyobject anchor point). You can rotate 3d pobj only around Z axis (i.e. the same way non-3d pobjs are rotated). This limitation won't be removed.

Currently, you cannot stack several entities on top of each other and make them move with the 3d polyobject. I.e. some solid entity that is standing on some other solid entity will block the whole movement, and may be crushed (because it is considered as "blocking" one).


New ACS API

Assuming that you are using ZDoom ACC compiler fork, paste the following into your "zspecial.acs".

  // k8vavoom polyobjects
  // note that angles are in degrees
  // speed is in units per second
  // bool Polyobj_MoveEx (int po, int hspeed, int yawangle, int dist, int vspeed, int vdist, int moveflags)
  -800:Polyobj_MoveEx(7),
  // bool Polyobj_MoveToEx (int po, int speed, int x, int y, int z, int moveflags)
  -801:Polyobj_MoveToEx(6),
  // bool Polyobj_MoveToSpotEx (int po, int speed, int targettid, int moveflags) -- this uses target height too
  -802:Polyobj_MoveToSpotEx(4),
  // fixed GetPolyobjZ (int po)
  -803:GetPolyobjZ(1),
  // int Polyobj_GetFlagsEx (int po) -- -1 means "no pobj"
  -804:Polyobj_GetFlagsEx(1),
  // int Polyobj_SetFlagsEx (int po, int flags, int oper) -- oper: 0 means "clear", 1 means "set", -1 means "replace"
  -805:Polyobj_SetFlagsEx(3),
  // int Polyobj_IsBusy (int po) -- returns -1 if there is no such pobj
  -806:Polyobj_IsBusy(1),

  // fixed Polyobj_GetAngle (int po) -- returns current pobj yaw angle, in degrees
  -807:Polyobj_GetAngle(1),

  // bool Polyobj_MoveRotateEx (int po, int hspeed, int yawangle, int dist, int vspeed, int vdist, fixed deltaangle, int moveflags)
  -808:Polyobj_MoveRotateEx(8),
  // bool Polyobj_MoveToRotateEx (int po, int speed, int x, int y, int z, fixed deltaangle, int moveflags)
  -809:Polyobj_MoveToRotateEx(7),
  // bool Polyobj_MoveToSpotRotateEx (int po, int speed, int targettid, fixed deltaangle, int moveflags) -- this uses target height too
  -810:Polyobj_MoveToSpotRotateEx(5),
  // bool Polyobj_RotateEx (int po, int speed, fixed deltaangle, int moveflags)
  -811:Polyobj_RotateEx(4),

Assuming that you are using ZDoom ACC compiler fork, paste the following into your "zdefs.acs".

// for Polyobj_GetFlagsEx and Polyobj_SetFlagsEx
#define POBJ_FLAG_CRUSH            (1 << 0) /* 1 */
#define POBJ_FLAG_HURT_ON_TOUCH    (1 << 1) /* 2 */
#define POBJ_FLAG_NO_CARRY_THINGS  (1 << 2) /* 4 */
#define POBJ_FLAG_NO_ANGLE_CHANGE  (1 << 3) /* 8 */
#define POBJ_FLAG_SIDE_CRUSH       (1 << 4) /* 16 */

// for Polyobj_SetFlagsEx
#define POBJ_FLAGS_CLEAR    0
#define POBJ_FLAGS_SET      1
#define POBJ_FLAGS_REPLACE  -1

// for `moveflags`
#define POBJ_MOVE_NORMAL    0
#define POBJ_MOVE_OVERRIDE  (1 << 0)
#define POBJ_MOVE_NOLINK    (1 << 1)
/* for linked polyobjects, don't use master pobj center, but rotate each pobj around it's own center */
#define POBJ_MOVE_INDROT    (1 << 2)
/* use polyobject angle instead of `yawangle` in `Polyobj_MoveRotateEx()` */
#define POBJ_MOVE_POANGLE   (1 << 3)
/* used only in `Polyobj_RotateEx()`, to create rotating doors */
#define POBJ_MOVE_MIRRORED  (1 << 4)
/* used only in `Polyobj_RotateEx()`, to create perpetual rotation */
#define POBJ_MOVE_PERPETUAL (1 << 5)

ACS API Explanation

Polyobj_MoveEx

bool Polyobj_MoveEx (int po, int hspeed, int yawangle, int dist, int vspeed, int vdist, int moveflags);

This is extension of 3D movement function. Note that `vdist` should always be positive, the actual direction is specified by `vspeed` (positive is up, negative is down).

The `hspeed` value should be positive. The direction is determined by the `yawangle`.

Speed is in map units per 1/8 of second.


Polyobj_MoveToEx

bool Polyobj_MoveToEx (int po, int speed, int x, int y, int z, int moveflags);

This moves the polyobject to the specified world coordinates.

Speed is in map units per 1/8 of second.


Polyobj_MoveToSpotEx

bool Polyobj_MoveToSpotEx (int po, int speed, int targettid, int moveflags);

This is 3D extension of `Polyobj_MoveToSpot()`.

Speed is in map units per 1/8 of second.


Polyobj_IsBusy

int Polyobj_IsBusy (int po);

This checks if polyobject is currently busy with movement or rotation. Contrary to `PolyWait()`, this call is not blocking.
Return values:

Basically, any negative value means "no such polyobject", and any positive value means "it is busy".


Polyobj_GetAngle

fixed Polyobj_GetAngle (int po);

This returns current polyobject yaw angle, in degrees, as fixed-point value (NOT INTEGER!). If `po` doesn't specify a valid polyobject, return value is negative.


Polyobj_MoveRotateEx

bool Polyobj_MoveRotateEx (int po, int hspeed, int yawangle, int dist, int vspeed, int vdist, fixed deltaangle, int moveflags);

This is extension of 3D movement function. Note that `vdist` should always be positive, the actual direction is specified by `vspeed` (positive is up, negative is down).

The `hspeed` value should be positive. The direction is determined by the `yawangle`.

Speed is in map units per 1/8 of second.

`deltaangle` specifies the angle change, in degrees. Polyobject will slowly rotate while moving, and when it will arrive at the destination point, it will be rotated by `deltaangle`. Positive value means "rotate right", negative value means "rotate left".


Polyobj_MoveToRotateEx

bool Polyobj_MoveToRotateEx (int po, int speed, int x, int y, int z, fixed deltaangle, int moveflags);

This moves the polyobject to the specified world coordinates.

Speed is in map units per 1/8 of second.

`deltaangle` specifies the angle change, in degrees. Polyobject will slowly rotate while moving, and when it will arrive at the destination point, it will be rotated by `deltaangle`. Positive value means "rotate right", negative value means "rotate left".


Polyobj_MoveToSpotRotateEx

bool Polyobj_MoveToSpotRotateEx (int po, int speed, int targettid, fixed deltaangle, int moveflags);

This is 3D extension of `Polyobj_MoveToSpot()`.

Speed is in map units per 1/8 of second.

`deltaangle` specifies the additional angle change, in degrees. Polyobject will slowly rotate while moving, and when it will arrive at the destination point, it will be rotated by `deltaangle`. Positive value means "rotate right", negative value means "rotate left".

PLEASE, NOTE that polyobject will rotate to the spot thing angle plus `deltaangle`. Most of the time `deltaangle` will be zero.


POBJ_FLAG_SIDE_CRUSH

This flag is required due to the way the movement of polyobjects is implemented. Basically, if entity is collided with polyobject linedef, it is impossible to determine if it is due to polyobject is "crushing" the entity from above, or due to side movement. So even crushing polyobjects may push entities away. With this flag set, any collision with moving polyobject is fatal.

The one quirk here is that even if polyobject is horizontally moving, and touched some entity, that entity will be crushed. So use this flag with care. It is mostly useful for "crusher" polyobjects, not for platforms.

NOTE: In the future polyobject crushing detection may be improved, and new flag will be introduced for "proper" crushing platforms.


Polyobj_RotateEx

bool Polyobj_RotateEx (int po, int speed, fixed deltaangle, int moveflags);

This rotates the polyobject by the specified angle.

Speed is in map units per 1/8 of second.

It is possible to use `POBJ_MOVE_MIRRORED` flag here to rotate "mirror" polyobject with negative angle.


Current Limitations

Riding on the moving 3d polyobject is done by teleporting. It means that entities are not moved by velocities or proper box tracing, they simply teleported to their destination. If the destination is occupied, and it is impossible to make a valid step up (not a stair, or stairs are too high), the entity will be returned to its original place. If returned entity blocks the polyobject, either the polyobject will stop, or the entity will be squashed.

Due to the tepelorting, vertical entity "stacking" is not supported. I.e. you cannot put one entity on top of another and make them both ride on a 3d polyobject: if an entity is moved to the place which is already occupied by another entity, it will be processed as a normal blocking (see above).

Impassable top texture currently doesn't block sight, and may fail to block hitscan attacks. This will be improved in the future.

Also, please, use only the flags that are defined in this spec on 3d polyobject linedefs. Other flags may or may not work properly, and nothing is guaranteed. Even if it seems to work now (or do nothing), the engine may refuse to load such maps in the future, or flag meaning may change.

Currently you cannot assign "masked" or translucent middle texture (i.e. things like grates and such). This limitation may be removed in the future.

Also, currently you cannot use "masked" or translucent floor/ceiling textures. This limitation may be removed in the future.

All middle textures should cover the full side (i.e. be either wrapped, or high enough).

Lightmapped lighting may glitch on 3d polyobjects. This will be fixed in the future.

Ambient lighting of 3d polyobject is controlled only by its inner sector light level. Moving 3d polyobject will not "inherit" light level of the sector (or sectors) it is currently in. Dynamic light level change can be implemented in the future, but it will be opt-in (i.e. there will be a special flag to turn it on).


What's new since the previous version

since v0.99.1