emitterShouldTrail fix, review comments, rotateWithEntity

This commit is contained in:
SamGondelman 2018-07-25 19:04:02 -07:00
parent d29332595b
commit 1c1b68ee66
10 changed files with 99 additions and 43 deletions

View file

@ -101,6 +101,10 @@ void ParticleEffectEntityRenderer::doRenderUpdateSynchronousTyped(const ScenePoi
_timeUntilNextEmit = 0;
withWriteLock([&]{
_particleProperties = newParticleProperties;
if (!_prevEmitterShouldTrailInitialized) {
_prevEmitterShouldTrailInitialized = true;
_prevEmitterShouldTrail = _particleProperties.emission.shouldTrail;
}
});
}
_emitting = entity->getIsEmitting();
@ -149,6 +153,7 @@ void ParticleEffectEntityRenderer::doRenderUpdateAsynchronousTyped(const TypedEn
particleUniforms.spin.finish = _particleProperties.spin.range.finish;
particleUniforms.spin.spread = _particleProperties.spin.gradient.spread;
particleUniforms.lifespan = _particleProperties.lifespan;
particleUniforms.rotateWithEntity = _particleProperties.rotateWithEntity ? 1 : 0;
});
// Update particle uniforms
memcpy(&_uniformBuffer.edit<ParticleUniforms>(), &particleUniforms, sizeof(ParticleUniforms));
@ -180,7 +185,7 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa
const auto& azimuthFinish = particleProperties.azimuth.finish;
const auto& emitDimensions = particleProperties.emission.dimensions;
const auto& emitAcceleration = particleProperties.emission.acceleration.target;
auto emitOrientation = particleProperties.emission.orientation;
auto emitOrientation = baseTransform.getRotation() * particleProperties.emission.orientation;
const auto& emitRadiusStart = glm::max(particleProperties.radiusStart, EPSILON); // Avoid math complications at center
const auto& emitSpeed = particleProperties.emission.speed.target;
const auto& speedSpread = particleProperties.emission.speed.spread;
@ -189,10 +194,9 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa
particle.seed = randFloatInRange(-1.0f, 1.0f);
particle.expiration = now + (uint64_t)(particleProperties.lifespan * USECS_PER_SECOND);
if (particleProperties.emission.shouldTrail) {
particle.position = baseTransform.getTranslation();
emitOrientation = baseTransform.getRotation() * emitOrientation;
}
particle.relativePosition = glm::vec3(0.0f);
particle.basePosition = baseTransform.getTranslation();
// Position, velocity, and acceleration
if (polarStart == 0.0f && polarFinish == 0.0f && emitDimensions.z == 0.0f) {
@ -241,7 +245,7 @@ ParticleEffectEntityRenderer::CpuParticle ParticleEffectEntityRenderer::createPa
radii.y > 0.0f ? y / (radii.y * radii.y) : 0.0f,
radii.z > 0.0f ? z / (radii.z * radii.z) : 0.0f
));
particle.position += emitOrientation * emitPosition;
particle.relativePosition += emitOrientation * emitPosition;
}
particle.velocity = (emitSpeed + randFloatInRange(-1.0f, 1.0f) * speedSpread) * (emitOrientation * emitDirection);
@ -266,8 +270,8 @@ void ParticleEffectEntityRenderer::stepSimulation() {
particleProperties = _particleProperties;
});
const auto& modelTransform = getModelTransform();
if (_emitting && particleProperties.emitting()) {
const auto& modelTransform = getModelTransform();
uint64_t emitInterval = particleProperties.emitIntervalUsecs();
if (emitInterval > 0 && interval >= _timeUntilNextEmit) {
auto timeRemaining = interval;
@ -292,15 +296,23 @@ void ParticleEffectEntityRenderer::stepSimulation() {
const float deltaTime = (float)interval / (float)USECS_PER_SECOND;
// update the particles
for (auto& particle : _cpuParticles) {
if (_prevEmitterShouldTrail != particleProperties.emission.shouldTrail) {
if (_prevEmitterShouldTrail) {
particle.relativePosition = particle.relativePosition + particle.basePosition - modelTransform.getTranslation();
}
particle.basePosition = modelTransform.getTranslation();
}
particle.integrate(deltaTime);
}
_prevEmitterShouldTrail = particleProperties.emission.shouldTrail;
// Build particle primitives
static GpuParticles gpuParticles;
gpuParticles.clear();
gpuParticles.reserve(_cpuParticles.size()); // Reserve space
std::transform(_cpuParticles.begin(), _cpuParticles.end(), std::back_inserter(gpuParticles), [](const CpuParticle& particle) {
return GpuParticle(particle.position, glm::vec2(particle.lifetime, particle.seed));
std::transform(_cpuParticles.begin(), _cpuParticles.end(), std::back_inserter(gpuParticles), [&particleProperties, &modelTransform](const CpuParticle& particle) {
glm::vec3 position = particle.relativePosition + (particleProperties.emission.shouldTrail ? particle.basePosition : modelTransform.getTranslation());
return GpuParticle(position, glm::vec2(particle.lifetime, particle.seed));
});
// Update particle buffer
@ -328,15 +340,11 @@ void ParticleEffectEntityRenderer::doRender(RenderArgs* args) {
}
Transform transform;
// In trail mode, the particles are created in world space.
// so we only set a transform if they're not in trail mode
if (!_particleProperties.emission.shouldTrail) {
withReadLock([&] {
transform = _renderTransform;
});
transform.setScale(vec3(1));
}
// The particles are in world space, so the transform is unused, except for the rotation, which we use
// if the particles are marked rotateWithEntity
withReadLock([&] {
transform.setRotation(_renderTransform.getRotation());
});
batch.setModelTransform(transform);
batch.setUniformBuffer(PARTICLE_UNIFORM_SLOT, _uniformBuffer);
batch.setInputFormat(_vertexFormat);
@ -345,5 +353,3 @@ void ParticleEffectEntityRenderer::doRender(RenderArgs* args) {
auto numParticles = _particleBuffer->getSize() / sizeof(GpuParticle);
batch.drawInstanced((gpu::uint32)numParticles, gpu::TRIANGLE_STRIP, (gpu::uint32)VERTEX_PER_PARTICLE);
}

View file

@ -49,13 +49,14 @@ private:
float seed { 0.0f };
uint64_t expiration { 0 };
float lifetime { 0.0f };
glm::vec3 position;
glm::vec3 basePosition;
glm::vec3 relativePosition;
glm::vec3 velocity;
glm::vec3 acceleration;
void integrate(float deltaTime) {
glm::vec3 atSquared = (0.5f * deltaTime * deltaTime) * acceleration;
position += velocity * deltaTime + atSquared;
relativePosition += velocity * deltaTime + atSquared;
velocity += acceleration * deltaTime;
lifetime += deltaTime;
}
@ -76,14 +77,16 @@ private:
InterpolationData<glm::vec4> color; // rgba
InterpolationData<float> spin;
float lifespan;
glm::vec3 spare;
int rotateWithEntity;
glm::vec2 spare;
};
static CpuParticle createParticle(uint64_t now, const Transform& baseTransform, const particle::Properties& particleProperties);
void stepSimulation();
particle::Properties _particleProperties;
bool _prevEmitterShouldTrail;
bool _prevEmitterShouldTrailInitialized { false };
CpuParticles _cpuParticles;
bool _emitting { false };
uint64_t _timeUntilNextEmit { 0 };

View file

@ -38,7 +38,9 @@ struct ParticleUniforms {
Radii radius;
Colors color;
Spin spin;
vec4 lifespan; // x is lifespan, 3 spare floats
float lifespan;
int rotateWithEntity;
vec2 spare;
};
layout(std140) uniform particleBuffer {
@ -120,7 +122,7 @@ void main(void) {
int twoTriID = gl_VertexID - particleID * NUM_VERTICES_PER_PARTICLE;
// Particle properties
float age = inColor.x / particle.lifespan.x;
float age = inColor.x / particle.lifespan;
float seed = inColor.y;
// Pass the texcoord
@ -140,20 +142,24 @@ void main(void) {
float radiusSpread = 2.0 * hifi_hash(seed * 6.0) - 1.0;
radius = max(radius + radiusSpread * particle.radius.spread, 0.0);
vec4 anchorPoint;
vec4 _inPosition = vec4(inPosition, 1.0);
<$transformModelToEyePos(cam, obj, _inPosition, anchorPoint)$>
// inPosition is in world space
vec4 anchorPoint = cam._view * vec4(inPosition, 1.0);
mat3 view3 = mat3(cam._view);
vec3 worldUpEye = normalize(view3 * vec3(0, 1, 0));
vec3 right = cross(vec3(0, 0, -1), worldUpEye);
vec3 up = cross(right, vec3(0, 0, -1));
vec3 UP = vec3(0, 1, 0);
vec3 modelUpWorld;
<$transformModelToWorldDir(cam, obj, UP, modelUpWorld)$>
vec3 upWorld = mix(UP, normalize(modelUpWorld), particle.rotateWithEntity);
vec3 upEye = normalize(view3 * upWorld);
vec3 FORWARD = vec3(0, 0, -1);
vec3 particleRight = normalize(cross(FORWARD, upEye));
vec3 particleUp = cross(particleRight, FORWARD); // don't need to normalize
// This ordering ensures that un-rotated particles render upright in the viewer.
vec3 UNIT_QUAD[NUM_VERTICES_PER_PARTICLE] = vec3[NUM_VERTICES_PER_PARTICLE](
normalize(-right + up),
normalize(-right - up),
normalize(right + up),
normalize(right - up)
normalize(-particleRight + particleUp),
normalize(-particleRight - particleUp),
normalize(particleRight + particleUp),
normalize(particleRight - particleUp)
);
float c = cos(spin);
float s = sin(spin);

View file

@ -373,6 +373,7 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
CHECK_PROPERTY_CHANGE(PROP_SPIN_SPREAD, spinSpread);
CHECK_PROPERTY_CHANGE(PROP_SPIN_START, spinStart);
CHECK_PROPERTY_CHANGE(PROP_SPIN_FINISH, spinFinish);
CHECK_PROPERTY_CHANGE(PROP_PARTICLE_ROTATE_WITH_ENTITY, rotateWithEntity);
// Certifiable Properties
CHECK_PROPERTY_CHANGE(PROP_ITEM_NAME, itemName);
@ -913,12 +914,14 @@ EntityPropertyFlags EntityItemProperties::getChangedProperties() const {
* @property {number} alphaSpread=0 - The spread in alpha that each particle is given. If <code>alpha == 0.5</code>
* and <code>alphaSpread == 0.25</code>, each particle will have an alpha in the range <code>0.25</code> &ndash; <code>0.75</code>.
* @property {number} particleSpin=0 - The spin of each particle at the middle of its life. In the range <code>-2*PI</code> &ndash; <code>2*PI</code>.
* @property {number} spinStart=null - The spin of each particle at the start of its life. In the range <code>-2*PI</code> &ndash; <code>2*PI</code>.
* If <code>null</code>, the <code>particleSpin</code> value is used.
* @property {number} spinFinish=null - The spin of each particle at the end of its life. In the range <code>-2*PI</code> &ndash; <code>2*PI</code>.
* If <code>null</code>, the <code>particleSpin</code> value is used.
* @property {number} spinStart=NaN - The spin of each particle at the start of its life. In the range <code>-2*PI</code> &ndash; <code>2*PI</code>.
* If <code>NaN</code>, the <code>particleSpin</code> value is used.
* @property {number} spinFinish=NaN - The spin of each particle at the end of its life. In the range <code>-2*PI</code> &ndash; <code>2*PI</code>.
* If <code>NaN</code>, the <code>particleSpin</code> value is used.
* @property {number} spinSpread=0 - The spread in spin that each particle is given. In the range <code>0</code> &ndash; <code>2*PI</code>. If <code>particleSpin == PI</code>
* and <code>spinSpread == PI/2</code>, each particle will have a spin in the range <code>PI/2</code> &ndash; <code>3*PI/2</code>.
* @property {boolean} rotateWithEntity=false - Whether or not the particles' spin will rotate with the entity. If false, when <code>particleSpin == 0</code>, the particles will point
* up in the world. If true, they will point towards the entity's up vector, based on its orientation.
*
* @property {ShapeType} shapeType="none" - <em>Currently not used.</em> <em>Read-only.</em>
*
@ -1306,6 +1309,7 @@ QScriptValue EntityItemProperties::copyToScriptValue(QScriptEngine* engine, bool
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SPIN_SPREAD, spinSpread);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SPIN_START, spinStart);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_SPIN_FINISH, spinFinish);
COPY_PROPERTY_TO_QSCRIPTVALUE(PROP_PARTICLE_ROTATE_WITH_ENTITY, rotateWithEntity);
}
// Models only
@ -1602,6 +1606,7 @@ void EntityItemProperties::copyFromScriptValue(const QScriptValue& object, bool
COPY_PROPERTY_FROM_QSCRIPTVALUE(spinSpread, float, setSpinSpread);
COPY_PROPERTY_FROM_QSCRIPTVALUE(spinStart, float, setSpinStart);
COPY_PROPERTY_FROM_QSCRIPTVALUE(spinFinish, float, setSpinFinish);
COPY_PROPERTY_FROM_QSCRIPTVALUE(rotateWithEntity, bool, setRotateWithEntity);
// Certifiable Properties
COPY_PROPERTY_FROM_QSCRIPTVALUE(itemName, QString, setItemName);
@ -1774,6 +1779,7 @@ void EntityItemProperties::merge(const EntityItemProperties& other) {
COPY_PROPERTY_IF_CHANGED(spinSpread);
COPY_PROPERTY_IF_CHANGED(spinStart);
COPY_PROPERTY_IF_CHANGED(spinFinish);
COPY_PROPERTY_IF_CHANGED(rotateWithEntity);
// Certifiable Properties
COPY_PROPERTY_IF_CHANGED(itemName);
@ -1991,6 +1997,7 @@ void EntityItemProperties::entityPropertyFlagsFromScriptValue(const QScriptValue
ADD_PROPERTY_TO_MAP(PROP_SPIN_SPREAD, SpinSpread, spinSpread, float);
ADD_PROPERTY_TO_MAP(PROP_SPIN_START, SpinStart, spinStart, float);
ADD_PROPERTY_TO_MAP(PROP_SPIN_FINISH, SpinFinish, spinFinish, float);
ADD_PROPERTY_TO_MAP(PROP_PARTICLE_ROTATE_WITH_ENTITY, RotateWithEntity, rotateWithEntity, float);
// Certifiable Properties
ADD_PROPERTY_TO_MAP(PROP_ITEM_NAME, ItemName, itemName, QString);
@ -2324,6 +2331,7 @@ OctreeElement::AppendState EntityItemProperties::encodeEntityEditPacket(PacketTy
APPEND_ENTITY_PROPERTY(PROP_SPIN_SPREAD, properties.getSpinSpread());
APPEND_ENTITY_PROPERTY(PROP_SPIN_START, properties.getSpinStart());
APPEND_ENTITY_PROPERTY(PROP_SPIN_FINISH, properties.getSpinFinish());
APPEND_ENTITY_PROPERTY(PROP_PARTICLE_ROTATE_WITH_ENTITY, properties.getRotateWithEntity())
}
if (properties.getType() == EntityTypes::Zone) {
@ -2703,6 +2711,7 @@ bool EntityItemProperties::decodeEntityEditPacket(const unsigned char* data, int
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SPIN_SPREAD, float, setSpinSpread);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SPIN_START, float, setSpinStart);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_SPIN_FINISH, float, setSpinFinish);
READ_ENTITY_PROPERTY_TO_PROPERTIES(PROP_PARTICLE_ROTATE_WITH_ENTITY, bool, setRotateWithEntity);
}
if (properties.getType() == EntityTypes::Zone) {
@ -2972,7 +2981,7 @@ void EntityItemProperties::markAllChanged() {
_shapeTypeChanged = true;
_isEmittingChanged = true;
_emitterShouldTrail = true;
_emitterShouldTrailChanged = true;
_maxParticlesChanged = true;
_lifespanChanged = true;
_emitRateChanged = true;
@ -3001,6 +3010,7 @@ void EntityItemProperties::markAllChanged() {
_spinStartChanged = true;
_spinFinishChanged = true;
_spinSpreadChanged = true;
_rotateWithEntityChanged = true;
_materialURLChanged = true;
_materialMappingModeChanged = true;
@ -3361,6 +3371,9 @@ QList<QString> EntityItemProperties::listChangedProperties() {
if (spinFinishChanged()) {
out += "spinFinish";
}
if (rotateWithEntityChanged()) {
out += "rotateWithEntity";
}
if (materialURLChanged()) {
out += "materialURL";
}

View file

@ -238,7 +238,8 @@ public:
DEFINE_PROPERTY(PROP_PARTICLE_SPIN, ParticleSpin, particleSpin, float, particle::DEFAULT_PARTICLE_SPIN);
DEFINE_PROPERTY(PROP_SPIN_SPREAD, SpinSpread, spinSpread, float, particle::DEFAULT_SPIN_SPREAD);
DEFINE_PROPERTY(PROP_SPIN_START, SpinStart, spinStart, float, particle::DEFAULT_SPIN_START);
DEFINE_PROPERTY(PROP_SPIN_FINISH, SpinFinish, spinFinish, float, particle::DEFAULT_SPIN_FINISH)
DEFINE_PROPERTY(PROP_SPIN_FINISH, SpinFinish, spinFinish, float, particle::DEFAULT_SPIN_FINISH);
DEFINE_PROPERTY(PROP_PARTICLE_ROTATE_WITH_ENTITY, RotateWithEntity, rotateWithEntity, bool, particle::DEFAULT_ROTATE_WITH_ENTITY);
// Certifiable Properties - related to Proof of Purchase certificates
DEFINE_PROPERTY_REF(PROP_ITEM_NAME, ItemName, itemName, QString, ENTITY_ITEM_DEFAULT_ITEM_NAME);

View file

@ -255,6 +255,7 @@ enum EntityPropertyList {
PROP_SPIN_START,
PROP_SPIN_FINISH,
PROP_SPIN_SPREAD,
PROP_PARTICLE_ROTATE_WITH_ENTITY,
////////////////////////////////////////////////////////////////////////////////////////////////////
// ATTENTION: add new properties to end of list just ABOVE this line

View file

@ -92,6 +92,7 @@ bool operator==(const Properties& a, const Properties& b) {
(a.alpha == b.alpha) &&
(a.radius == b.radius) &&
(a.spin == b.spin) &&
(a.rotateWithEntity == b.rotateWithEntity) &&
(a.radiusStart == b.radiusStart) &&
(a.lifespan == b.lifespan) &&
(a.maxParticles == b.maxParticles) &&
@ -444,6 +445,7 @@ EntityItemProperties ParticleEffectEntityItem::getProperties(EntityPropertyFlags
COPY_ENTITY_PROPERTY_TO_PROPERTIES(spinSpread, getSpinSpread);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(spinStart, getSpinStart);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(spinFinish, getSpinFinish);
COPY_ENTITY_PROPERTY_TO_PROPERTIES(rotateWithEntity, getRotateWithEntity);
return properties;
}
@ -485,6 +487,7 @@ bool ParticleEffectEntityItem::setProperties(const EntityItemProperties& propert
SET_ENTITY_PROPERTY_FROM_PROPERTIES(spinSpread, setSpinSpread);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(spinStart, setSpinStart);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(spinFinish, setSpinFinish);
SET_ENTITY_PROPERTY_FROM_PROPERTIES(rotateWithEntity, setRotateWithEntity);
if (somethingChanged) {
bool wantDebug = false;
@ -569,6 +572,7 @@ int ParticleEffectEntityItem::readEntitySubclassDataFromBuffer(const unsigned ch
READ_ENTITY_PROPERTY(PROP_SPIN_SPREAD, float, setSpinSpread);
READ_ENTITY_PROPERTY(PROP_SPIN_START, float, setSpinStart);
READ_ENTITY_PROPERTY(PROP_SPIN_FINISH, float, setSpinFinish);
READ_ENTITY_PROPERTY(PROP_PARTICLE_ROTATE_WITH_ENTITY, bool, setRotateWithEntity);
return bytesRead;
}
@ -610,6 +614,7 @@ EntityPropertyFlags ParticleEffectEntityItem::getEntityProperties(EncodeBitstrea
requestedProperties += PROP_SPIN_SPREAD;
requestedProperties += PROP_SPIN_START;
requestedProperties += PROP_SPIN_FINISH;
requestedProperties += PROP_PARTICLE_ROTATE_WITH_ENTITY;
return requestedProperties;
}
@ -657,6 +662,7 @@ void ParticleEffectEntityItem::appendSubclassData(OctreePacketData* packetData,
APPEND_ENTITY_PROPERTY(PROP_SPIN_SPREAD, getSpinSpread());
APPEND_ENTITY_PROPERTY(PROP_SPIN_START, getSpinStart());
APPEND_ENTITY_PROPERTY(PROP_SPIN_FINISH, getSpinFinish());
APPEND_ENTITY_PROPERTY(PROP_PARTICLE_ROTATE_WITH_ENTITY, getRotateWithEntity());
}
@ -728,6 +734,12 @@ void ParticleEffectEntityItem::setEmitterShouldTrail(bool emitterShouldTrail) {
});
}
void ParticleEffectEntityItem::setRotateWithEntity(bool rotateWithEntity) {
withWriteLock([&] {
_particleProperties.rotateWithEntity = rotateWithEntity;
});
}
particle::Properties ParticleEffectEntityItem::getParticleProperties() const {
particle::Properties result;
withReadLock([&] {

View file

@ -77,6 +77,7 @@ namespace particle {
static const float MAXIMUM_PARTICLE_SPIN = 2.0f * SCRIPT_MAXIMUM_PI;
static const QString DEFAULT_TEXTURES = "";
static const bool DEFAULT_EMITTER_SHOULD_TRAIL = false;
static const bool DEFAULT_ROTATE_WITH_ENTITY = false;
template <typename T>
struct Range {
@ -158,6 +159,7 @@ namespace particle {
float radiusStart { DEFAULT_EMIT_RADIUS_START };
RangeGradient<float> radius { DEFAULT_PARTICLE_RADIUS, DEFAULT_RADIUS_START, DEFAULT_RADIUS_FINISH, DEFAULT_RADIUS_SPREAD };
RangeGradient<float> spin { DEFAULT_PARTICLE_SPIN, DEFAULT_SPIN_START, DEFAULT_SPIN_FINISH, DEFAULT_SPIN_SPREAD };
bool rotateWithEntity { DEFAULT_ROTATE_WITH_ENTITY };
float lifespan { DEFAULT_LIFESPAN };
uint32_t maxParticles { DEFAULT_MAX_PARTICLES };
EmitProperties emission;
@ -176,6 +178,7 @@ namespace particle {
color = other.color;
alpha = other.alpha;
spin = other.spin;
rotateWithEntity = other.rotateWithEntity;
radius = other.radius;
lifespan = other.lifespan;
maxParticles = other.maxParticles;
@ -326,6 +329,9 @@ public:
void setSpinSpread(float spinSpread);
float getSpinSpread() const { return _particleProperties.spin.gradient.spread; }
void setRotateWithEntity(bool rotateWithEntity);
bool getRotateWithEntity() const { return _particleProperties.rotateWithEntity; }
void computeAndUpdateDimensions();
void setTextures(const QString& textures);

View file

@ -238,7 +238,7 @@ TransformObject getTransformObject() {
<@endfunc@>
<@func transformModelToWorldDir(cameraTransform, objectTransform, modelDir, worldDir)@>
{ // transformModelToEyeDir
{ // transformModelToWorldDir
vec3 mr0 = <$objectTransform$>._modelInverse[0].xyz;
vec3 mr1 = <$objectTransform$>._modelInverse[1].xyz;
vec3 mr2 = <$objectTransform$>._modelInverse[2].xyz;

View file

@ -398,6 +398,14 @@
min: -360.0,
max: 360.0
},
{
type: "Row"
},
{
id: "rotateWithEntity",
name: "Rotate with Entity",
type: "Boolean"
},
{
type: "Row"
}