v1.4.4
This commit is contained in:
commit
9c94d113d3
10260 changed files with 1237388 additions and 0 deletions
37
source/base/CMakeLists.txt
Normal file
37
source/base/CMakeLists.txt
Normal file
|
@ -0,0 +1,37 @@
|
|||
INCLUDE_DIRECTORIES (
|
||||
${STAR_EXTERN_INCLUDES}
|
||||
${STAR_CORE_INCLUDES}
|
||||
${STAR_BASE_INCLUDES}
|
||||
)
|
||||
|
||||
SET (star_base_HEADERS
|
||||
StarAnimatedPartSet.hpp
|
||||
StarAssets.hpp
|
||||
StarAssetSource.hpp
|
||||
StarBlocksAlongLine.hpp
|
||||
StarCellularLightArray.hpp
|
||||
StarCellularLighting.hpp
|
||||
StarCellularLiquid.hpp
|
||||
StarConfiguration.hpp
|
||||
StarDirectoryAssetSource.hpp
|
||||
StarMixer.hpp
|
||||
StarPackedAssetSource.hpp
|
||||
StarVersion.hpp
|
||||
StarVersionOptionParser.hpp
|
||||
StarWorldGeometry.hpp
|
||||
)
|
||||
|
||||
SET (star_base_SOURCES
|
||||
StarAnimatedPartSet.cpp
|
||||
StarAssets.cpp
|
||||
StarCellularLighting.cpp
|
||||
StarConfiguration.cpp
|
||||
StarDirectoryAssetSource.cpp
|
||||
StarMixer.cpp
|
||||
StarPackedAssetSource.cpp
|
||||
StarVersionOptionParser.cpp
|
||||
StarWorldGeometry.cpp
|
||||
)
|
||||
|
||||
CONFIGURE_FILE (StarVersion.cpp.in ${CMAKE_CURRENT_BINARY_DIR}/StarVersion.cpp)
|
||||
ADD_LIBRARY (star_base OBJECT ${star_base_SOURCES} ${star_base_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/StarVersion.cpp)
|
305
source/base/StarAnimatedPartSet.cpp
Normal file
305
source/base/StarAnimatedPartSet.cpp
Normal file
|
@ -0,0 +1,305 @@
|
|||
#include "StarAnimatedPartSet.hpp"
|
||||
#include "StarMathCommon.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
AnimatedPartSet::AnimatedPartSet() {}
|
||||
|
||||
AnimatedPartSet::AnimatedPartSet(Json config) {
|
||||
for (auto const& stateTypePair : config.get("stateTypes", JsonObject()).iterateObject()) {
|
||||
auto const& stateTypeName = stateTypePair.first;
|
||||
auto const& stateTypeConfig = stateTypePair.second;
|
||||
|
||||
StateType newStateType;
|
||||
newStateType.priority = stateTypeConfig.getFloat("priority", 0.0f);
|
||||
newStateType.enabled = stateTypeConfig.getBool("enabled", true);
|
||||
newStateType.defaultState = stateTypeConfig.getString("default", "");
|
||||
newStateType.stateTypeProperties = stateTypeConfig.getObject("properties", {});
|
||||
|
||||
for (auto const& statePair : stateTypeConfig.get("states", JsonObject()).iterateObject()) {
|
||||
auto const& stateName = statePair.first;
|
||||
auto const& stateConfig = statePair.second;
|
||||
|
||||
auto newState = make_shared<State>();
|
||||
newState->frames = stateConfig.getInt("frames", 1);
|
||||
newState->cycle = stateConfig.getFloat("cycle", 1.0f);
|
||||
newState->animationMode = stringToAnimationMode(stateConfig.getString("mode", "end"));
|
||||
newState->transitionState = stateConfig.getString("transition", "");
|
||||
newState->stateProperties = stateConfig.getObject("properties", {});
|
||||
newState->stateFrameProperties = stateConfig.getObject("frameProperties", {});
|
||||
newStateType.states[stateName] = move(newState);
|
||||
}
|
||||
|
||||
newStateType.states.sortByKey();
|
||||
|
||||
newStateType.activeState.stateTypeName = stateTypeName;
|
||||
newStateType.activeStateDirty = true;
|
||||
|
||||
if (newStateType.defaultState.empty() && !newStateType.states.empty())
|
||||
newStateType.defaultState = newStateType.states.firstKey();
|
||||
|
||||
m_stateTypes[stateTypeName] = move(newStateType);
|
||||
}
|
||||
|
||||
// Sort state types by decreasing priority.
|
||||
m_stateTypes.sort([](pair<String, StateType> const& a, pair<String, StateType> const& b) {
|
||||
return b.second.priority < a.second.priority;
|
||||
});
|
||||
|
||||
for (auto const& partPair : config.get("parts", JsonObject()).iterateObject()) {
|
||||
auto const& partName = partPair.first;
|
||||
auto const& partConfig = partPair.second;
|
||||
|
||||
Part newPart;
|
||||
newPart.partProperties = partConfig.getObject("properties", {});
|
||||
|
||||
for (auto const& partStateTypePair : partConfig.get("partStates", JsonObject()).iterateObject()) {
|
||||
auto const& stateTypeName = partStateTypePair.first;
|
||||
|
||||
for (auto const& partStatePair : partStateTypePair.second.toObject()) {
|
||||
auto const& stateName = partStatePair.first;
|
||||
auto const& stateConfig = partStatePair.second;
|
||||
|
||||
PartState partState = {stateConfig.getObject("properties", {}), stateConfig.getObject("frameProperties", {})};
|
||||
newPart.partStates[stateTypeName][stateName] = move(partState);
|
||||
}
|
||||
}
|
||||
newPart.activePart.partName = partPair.first;
|
||||
newPart.activePartDirty = true;
|
||||
|
||||
m_parts[partName] = move(newPart);
|
||||
}
|
||||
|
||||
for (auto const& pair : m_stateTypes)
|
||||
setActiveState(pair.first, pair.second.defaultState, true);
|
||||
}
|
||||
|
||||
StringList AnimatedPartSet::stateTypes() const {
|
||||
return m_stateTypes.keys();
|
||||
}
|
||||
|
||||
void AnimatedPartSet::setStateTypeEnabled(String const& stateTypeName, bool enabled) {
|
||||
auto& stateType = m_stateTypes.get(stateTypeName);
|
||||
if (stateType.enabled != enabled) {
|
||||
stateType.enabled = enabled;
|
||||
for (auto& pair : m_parts)
|
||||
pair.second.activePartDirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
void AnimatedPartSet::setEnabledStateTypes(StringList const& stateTypeNames) {
|
||||
for (auto& pair : m_stateTypes)
|
||||
pair.second.enabled = false;
|
||||
|
||||
for (auto const& stateTypeName : stateTypeNames)
|
||||
m_stateTypes.get(stateTypeName).enabled = true;
|
||||
|
||||
for (auto& pair : m_parts)
|
||||
pair.second.activePartDirty = true;
|
||||
}
|
||||
|
||||
bool AnimatedPartSet::stateTypeEnabled(String const& stateTypeName) const {
|
||||
return m_stateTypes.get(stateTypeName).enabled;
|
||||
}
|
||||
|
||||
StringList AnimatedPartSet::states(String const& stateTypeName) const {
|
||||
return m_stateTypes.get(stateTypeName).states.keys();
|
||||
}
|
||||
|
||||
StringList AnimatedPartSet::parts() const {
|
||||
return m_parts.keys();
|
||||
}
|
||||
|
||||
bool AnimatedPartSet::setActiveState(String const& stateTypeName, String const& stateName, bool alwaysStart) {
|
||||
auto& stateType = m_stateTypes.get(stateTypeName);
|
||||
if (stateType.activeState.stateName != stateName || alwaysStart) {
|
||||
stateType.activeState.stateName = stateName;
|
||||
stateType.activeState.timer = 0.0f;
|
||||
stateType.activeStatePointer = stateType.states.get(stateName).get();
|
||||
|
||||
stateType.activeStateDirty = true;
|
||||
for (auto& pair : m_parts)
|
||||
pair.second.activePartDirty = true;
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void AnimatedPartSet::restartState(String const& stateTypeName) {
|
||||
auto& stateType = m_stateTypes.get(stateTypeName);
|
||||
stateType.activeState.timer = 0.0f;
|
||||
|
||||
stateType.activeStateDirty = true;
|
||||
for (auto& pair : m_parts)
|
||||
pair.second.activePartDirty = true;
|
||||
}
|
||||
|
||||
AnimatedPartSet::ActiveStateInformation const& AnimatedPartSet::activeState(String const& stateTypeName) const {
|
||||
auto& stateType = const_cast<StateType&>(m_stateTypes.get(stateTypeName));
|
||||
const_cast<AnimatedPartSet*>(this)->freshenActiveState(stateType);
|
||||
return stateType.activeState;
|
||||
}
|
||||
|
||||
AnimatedPartSet::ActivePartInformation const& AnimatedPartSet::activePart(String const& partName) const {
|
||||
auto& part = const_cast<Part&>(m_parts.get(partName));
|
||||
const_cast<AnimatedPartSet*>(this)->freshenActivePart(part);
|
||||
return part.activePart;
|
||||
}
|
||||
|
||||
void AnimatedPartSet::forEachActiveState(function<void(String const&, ActiveStateInformation const&)> callback) const {
|
||||
for (auto const& p : m_stateTypes) {
|
||||
const_cast<AnimatedPartSet*>(this)->freshenActiveState(const_cast<StateType&>(p.second));
|
||||
callback(p.first, p.second.activeState);
|
||||
}
|
||||
}
|
||||
|
||||
void AnimatedPartSet::forEachActivePart(function<void(String const&, ActivePartInformation const&)> callback) const {
|
||||
for (auto const& p : m_parts) {
|
||||
const_cast<AnimatedPartSet*>(this)->freshenActivePart(const_cast<Part&>(p.second));
|
||||
callback(p.first, p.second.activePart);
|
||||
}
|
||||
}
|
||||
|
||||
size_t AnimatedPartSet::activeStateIndex(String const& stateTypeName) const {
|
||||
auto const& stateType = m_stateTypes.get(stateTypeName);
|
||||
return *stateType.states.indexOf(stateType.activeState.stateName);
|
||||
}
|
||||
|
||||
bool AnimatedPartSet::setActiveStateIndex(String const& stateTypeName, size_t stateIndex, bool alwaysStart) {
|
||||
auto const& stateType = m_stateTypes.get(stateTypeName);
|
||||
String const& stateName = stateType.states.keyAt(stateIndex);
|
||||
return setActiveState(stateTypeName, stateName, alwaysStart);
|
||||
}
|
||||
|
||||
void AnimatedPartSet::update(float dt) {
|
||||
for (auto& pair : m_stateTypes) {
|
||||
auto& stateType = pair.second;
|
||||
auto const& state = *stateType.activeStatePointer;
|
||||
|
||||
stateType.activeState.timer += dt;
|
||||
if (stateType.activeState.timer > state.cycle) {
|
||||
if (state.animationMode == End) {
|
||||
stateType.activeState.timer = state.cycle;
|
||||
} else if (state.animationMode == Loop) {
|
||||
stateType.activeState.timer = std::fmod(stateType.activeState.timer, state.cycle);
|
||||
} else if (state.animationMode == Transition) {
|
||||
stateType.activeState.stateName = state.transitionState;
|
||||
stateType.activeState.timer = 0.0f;
|
||||
stateType.activeStatePointer = stateType.states.get(state.transitionState).get();
|
||||
}
|
||||
}
|
||||
|
||||
stateType.activeStateDirty = true;
|
||||
}
|
||||
|
||||
for (auto& pair : m_parts)
|
||||
pair.second.activePartDirty = true;
|
||||
}
|
||||
|
||||
void AnimatedPartSet::finishAnimations() {
|
||||
for (auto& pair : m_stateTypes) {
|
||||
auto& stateType = pair.second;
|
||||
|
||||
while (true) {
|
||||
auto const& state = *stateType.activeStatePointer;
|
||||
|
||||
if (state.animationMode == End) {
|
||||
stateType.activeState.timer = state.cycle;
|
||||
} else if (state.animationMode == Transition) {
|
||||
stateType.activeState.stateName = state.transitionState;
|
||||
stateType.activeState.timer = 0.0f;
|
||||
stateType.activeStatePointer = stateType.states.get(state.transitionState).get();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
stateType.activeStateDirty = true;
|
||||
}
|
||||
|
||||
for (auto& pair : m_parts)
|
||||
pair.second.activePartDirty = true;
|
||||
}
|
||||
|
||||
AnimatedPartSet::AnimationMode AnimatedPartSet::stringToAnimationMode(String const& string) {
|
||||
if (string.equals("end", String::CaseInsensitive)) {
|
||||
return End;
|
||||
} else if (string.equals("loop", String::CaseInsensitive)) {
|
||||
return Loop;
|
||||
} else if (string.equals("transition", String::CaseInsensitive)) {
|
||||
return Transition;
|
||||
} else {
|
||||
throw AnimatedPartSetException(strf("No such AnimationMode '%s'", string));
|
||||
}
|
||||
}
|
||||
|
||||
void AnimatedPartSet::freshenActiveState(StateType& stateType) {
|
||||
if (stateType.activeStateDirty) {
|
||||
auto const& state = *stateType.activeStatePointer;
|
||||
auto& activeState = stateType.activeState;
|
||||
activeState.frame = clamp<int>(activeState.timer / state.cycle * state.frames, 0, state.frames - 1);
|
||||
|
||||
activeState.properties = stateType.stateTypeProperties;
|
||||
activeState.properties.merge(state.stateProperties, true);
|
||||
|
||||
for (auto const& pair : state.stateFrameProperties) {
|
||||
if (activeState.frame < pair.second.size())
|
||||
activeState.properties[pair.first] = pair.second.get(activeState.frame);
|
||||
}
|
||||
|
||||
stateType.activeStateDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
void AnimatedPartSet::freshenActivePart(Part& part) {
|
||||
if (part.activePartDirty) {
|
||||
// First reset all the active part information assuming that no state type
|
||||
// x state match exists.
|
||||
auto& activePart = part.activePart;
|
||||
activePart.activeState = {};
|
||||
activePart.properties = part.partProperties;
|
||||
|
||||
// Then go through each of the state types and states and look for a part
|
||||
// state match in order of priority.
|
||||
for (auto& stateTypePair : m_stateTypes) {
|
||||
auto const& stateTypeName = stateTypePair.first;
|
||||
auto& stateType = stateTypePair.second;
|
||||
|
||||
// Skip disabled state types
|
||||
if (!stateType.enabled)
|
||||
continue;
|
||||
|
||||
auto partStateType = part.partStates.ptr(stateTypeName);
|
||||
if (!partStateType)
|
||||
continue;
|
||||
|
||||
auto const& stateName = stateType.activeState.stateName;
|
||||
auto partState = partStateType->ptr(stateName);
|
||||
if (!partState)
|
||||
continue;
|
||||
|
||||
// If we have a partState match, then set the active state information.
|
||||
freshenActiveState(stateType);
|
||||
activePart.activeState = stateType.activeState;
|
||||
unsigned frame = stateType.activeState.frame;
|
||||
|
||||
// Then set the part state data, as well as any part state frame data if
|
||||
// the current frame is within the list size.
|
||||
activePart.properties.merge(partState->partStateProperties, true);
|
||||
|
||||
for (auto const& pair : partState->partStateFrameProperties) {
|
||||
if (frame < pair.second.size())
|
||||
activePart.properties[pair.first] = pair.second.get(frame);
|
||||
}
|
||||
|
||||
// Each part can only have one state type x state match, so we are done.
|
||||
break;
|
||||
}
|
||||
|
||||
part.activePartDirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
163
source/base/StarAnimatedPartSet.hpp
Normal file
163
source/base/StarAnimatedPartSet.hpp
Normal file
|
@ -0,0 +1,163 @@
|
|||
#ifndef STAR_ANIMATED_PART_SET_HPP
|
||||
#define STAR_ANIMATED_PART_SET_HPP
|
||||
|
||||
#include "StarOrderedMap.hpp"
|
||||
#include "StarJson.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_EXCEPTION(AnimatedPartSetException, StarException);
|
||||
|
||||
// Defines a "animated" data set constructed in such a way that it is very
|
||||
// useful for doing generic animations with lots of additional animation data.
|
||||
// It is made up of two concepts, "states" and "parts".
|
||||
//
|
||||
// States:
|
||||
//
|
||||
// There are N "state types" defined, which each defines a set of mutually
|
||||
// exclusive states that each "state type" can be in. For example, one state
|
||||
// type might be "movement", and the "movement" states might be "idle", "walk",
|
||||
// and "run. Another state type might be "attack" which could have as its
|
||||
// states "idle", and "melee". Each state type will have exactly one currently
|
||||
// active state, so this class may, for example, be in the total state of
|
||||
// "movement:idle" and "attack:melee". Each state within each state type is
|
||||
// animated, so that over time the state frame increases and may loop around,
|
||||
// or transition into another state so that that state type without interaction
|
||||
// may go from "melee" to "idle" when the "melee" state animation is finished.
|
||||
// This is defined by the individual state config in the configuration passed
|
||||
// into the constructor.
|
||||
//
|
||||
// Parts:
|
||||
//
|
||||
// Each instance of this class also can have N "Parts" defined, which are
|
||||
// groups of properties that "listen" to active states. Each part can "listen"
|
||||
// to one or more state types, and the first matching state x state type pair
|
||||
// (in order of state type priority which is specified in the config) is
|
||||
// chosen, and the properties from that state type and state are merged into
|
||||
// the part to produce the final active part information. Rather than having a
|
||||
// single image or image set for each part, since this class is intended to be
|
||||
// as generic as possible, all of this data is assumed to be queried from the
|
||||
// part properties, so that things such as image data as well as other things
|
||||
// like damage or collision polys can be stored along with the animation
|
||||
// frames, the part state, the base part, whichever is most applicable.
|
||||
class AnimatedPartSet {
|
||||
public:
|
||||
struct ActiveStateInformation {
|
||||
String stateTypeName;
|
||||
String stateName;
|
||||
float timer;
|
||||
unsigned frame;
|
||||
JsonObject properties;
|
||||
};
|
||||
|
||||
struct ActivePartInformation {
|
||||
String partName;
|
||||
// If a state match is found, this will be set.
|
||||
Maybe<ActiveStateInformation> activeState;
|
||||
JsonObject properties;
|
||||
};
|
||||
|
||||
AnimatedPartSet();
|
||||
AnimatedPartSet(Json config);
|
||||
|
||||
// Returns the available state types.
|
||||
StringList stateTypes() const;
|
||||
|
||||
// If a state type is disabled, no parts will match against it even
|
||||
// if they have entries for that state type.
|
||||
void setStateTypeEnabled(String const& stateTypeName, bool enabled);
|
||||
void setEnabledStateTypes(StringList const& stateTypeNames);
|
||||
bool stateTypeEnabled(String const& stateTypeName) const;
|
||||
|
||||
// Returns the available states for the given state type.
|
||||
StringList states(String const& stateTypeName) const;
|
||||
|
||||
StringList parts() const;
|
||||
|
||||
// Sets the active state for this state type. If the state is different than
|
||||
// the previously set state, will start the new states animation off at the
|
||||
// beginning. If alwaysStart is true, then starts the state animation off at
|
||||
// the beginning even if no state change has occurred. Returns true if a
|
||||
// state animation reset was done.
|
||||
bool setActiveState(String const& stateTypeName, String const& stateName, bool alwaysStart = false);
|
||||
|
||||
// Restart this given state type's timer off at the beginning.
|
||||
void restartState(String const& stateTypeName);
|
||||
|
||||
ActiveStateInformation const& activeState(String const& stateTypeName) const;
|
||||
ActivePartInformation const& activePart(String const& partName) const;
|
||||
|
||||
// Function will be given the name of each state type, and the
|
||||
// ActiveStateInformation for the active state for that state type.
|
||||
void forEachActiveState(function<void(String const&, ActiveStateInformation const&)> callback) const;
|
||||
|
||||
// Function will be given the name of each part, and the
|
||||
// ActivePartInformation for the active part.
|
||||
void forEachActivePart(function<void(String const&, ActivePartInformation const&)> callback) const;
|
||||
|
||||
// Useful for serializing state changes. Since each set of states for a
|
||||
// state type is ordered, it is possible to simply serialize and deserialize
|
||||
// the state index for that state type.
|
||||
size_t activeStateIndex(String const& stateTypeName) const;
|
||||
bool setActiveStateIndex(String const& stateTypeName, size_t stateIndex, bool alwaysStart = false);
|
||||
|
||||
// Animate each state type forward 'dt' time, and either change state frames
|
||||
// or transition to new states, depending on the config.
|
||||
void update(float dt);
|
||||
|
||||
// Pushes all the animations into their final state
|
||||
void finishAnimations();
|
||||
|
||||
private:
|
||||
enum AnimationMode {
|
||||
End,
|
||||
Loop,
|
||||
Transition
|
||||
};
|
||||
|
||||
struct State {
|
||||
unsigned frames;
|
||||
float cycle;
|
||||
AnimationMode animationMode;
|
||||
String transitionState;
|
||||
JsonObject stateProperties;
|
||||
JsonObject stateFrameProperties;
|
||||
};
|
||||
|
||||
struct StateType {
|
||||
float priority;
|
||||
bool enabled;
|
||||
String defaultState;
|
||||
JsonObject stateTypeProperties;
|
||||
OrderedHashMap<String, shared_ptr<State const>> states;
|
||||
|
||||
ActiveStateInformation activeState;
|
||||
State const* activeStatePointer;
|
||||
bool activeStateDirty;
|
||||
};
|
||||
|
||||
struct PartState {
|
||||
JsonObject partStateProperties;
|
||||
JsonObject partStateFrameProperties;
|
||||
};
|
||||
|
||||
struct Part {
|
||||
JsonObject partProperties;
|
||||
StringMap<StringMap<PartState>> partStates;
|
||||
|
||||
ActivePartInformation activePart;
|
||||
bool activePartDirty;
|
||||
};
|
||||
|
||||
static AnimationMode stringToAnimationMode(String const& string);
|
||||
|
||||
void freshenActiveState(StateType& stateType);
|
||||
void freshenActivePart(Part& part);
|
||||
|
||||
OrderedHashMap<String, StateType> m_stateTypes;
|
||||
StringMap<Part> m_parts;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
35
source/base/StarAssetSource.hpp
Normal file
35
source/base/StarAssetSource.hpp
Normal file
|
@ -0,0 +1,35 @@
|
|||
#ifndef STAR_ASSET_SOURCE_HPP
|
||||
#define STAR_ASSET_SOURCE_HPP
|
||||
|
||||
#include "StarIODevice.hpp"
|
||||
#include "StarJson.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(AssetSource);
|
||||
|
||||
STAR_EXCEPTION(AssetSourceException, StarException);
|
||||
|
||||
// An asset source could be a directory on a filesystem, where assets are
|
||||
// pulled directly from files, or a single pak-like file containing all assets,
|
||||
// where assets are pulled from the correct region of the pak-like file.
|
||||
class AssetSource {
|
||||
public:
|
||||
virtual ~AssetSource() = default;
|
||||
|
||||
// An asset source can have arbitrary metadata attached.
|
||||
virtual JsonObject metadata() const = 0;
|
||||
|
||||
// Should return all the available assets in this source
|
||||
virtual StringList assetPaths() const = 0;
|
||||
|
||||
// Open the given path in this source and return an IODevicePtr to it.
|
||||
virtual IODevicePtr open(String const& path) = 0;
|
||||
|
||||
// Read the entirety of the given path into a buffer.
|
||||
virtual ByteArray read(String const& path) = 0;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
1113
source/base/StarAssets.cpp
Normal file
1113
source/base/StarAssets.cpp
Normal file
File diff suppressed because it is too large
Load diff
380
source/base/StarAssets.hpp
Normal file
380
source/base/StarAssets.hpp
Normal file
|
@ -0,0 +1,380 @@
|
|||
#ifndef STAR_ASSETS_HPP
|
||||
#define STAR_ASSETS_HPP
|
||||
|
||||
#include "StarJson.hpp"
|
||||
#include "StarOrderedMap.hpp"
|
||||
#include "StarRect.hpp"
|
||||
#include "StarBiMap.hpp"
|
||||
#include "StarThread.hpp"
|
||||
#include "StarAssetSource.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Font);
|
||||
STAR_CLASS(Audio);
|
||||
STAR_CLASS(Image);
|
||||
STAR_STRUCT(FramesSpecification);
|
||||
STAR_CLASS(Assets);
|
||||
|
||||
STAR_EXCEPTION(AssetException, StarException);
|
||||
|
||||
// Asset paths are not filesystem paths. '/' is always the directory separator,
|
||||
// and it is not possible to escape any asset source directory. '\' is never a
|
||||
// valid directory separator. All asset paths are considered case-insensitive.
|
||||
//
|
||||
// In addition to the path portion of the asset path, some asset types may also
|
||||
// have a sub-path, which is always separated from the path portion of the asset
|
||||
// by ':'. There can be at most 1 sub-path component.
|
||||
//
|
||||
// Image paths may also have a directives portion of the full asset path, which
|
||||
// must come after the path and optional sub-path comopnent. The directives
|
||||
// portion of the path starts with a '?', and '?' separates each subsquent
|
||||
// directive.
|
||||
struct AssetPath {
|
||||
static AssetPath split(String const& path);
|
||||
static String join(AssetPath const& path);
|
||||
|
||||
// Get / modify sub-path directly on a joined path string
|
||||
static String setSubPath(String const& joinedPath, String const& subPath);
|
||||
static String removeSubPath(String const& joinedPath);
|
||||
|
||||
// Get / modify directives directly on a joined path string
|
||||
static String getDirectives(String const& joinedPath);
|
||||
static String addDirectives(String const& joinedPath, String const& directives);
|
||||
static String removeDirectives(String const& joinedPath);
|
||||
|
||||
// The base directory name for any given path, including the trailing '/'.
|
||||
// Ignores sub-path and directives.
|
||||
static String directory(String const& path);
|
||||
|
||||
// The file part of any given path, ignoring sub-path and directives. Path
|
||||
// must be a file not a directory.
|
||||
static String filename(String const& path);
|
||||
|
||||
// The file extension of a given file path, ignoring directives and
|
||||
// sub-paths.
|
||||
static String extension(String const& path);
|
||||
|
||||
// Computes an absolute asset path from a relative path relative to another
|
||||
// asset. The sourcePath must be an absolute path (may point to a directory
|
||||
// or an asset in a directory, and ignores ':' sub-path or ? directives),
|
||||
// and the givenPath may be either an absolute *or* a relative path. If it
|
||||
// is an absolute path, it is returned unchanged. If it is a relative path,
|
||||
// then it is computed as relative to the directory component of the
|
||||
// sourcePath.
|
||||
static String relativeTo(String const& sourcePath, String const& givenPath);
|
||||
|
||||
String basePath;
|
||||
Maybe<String> subPath;
|
||||
StringList directives;
|
||||
|
||||
bool operator==(AssetPath const& rhs) const;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, AssetPath const& rhs);
|
||||
|
||||
// The contents of an assets .frames file, which can be associated with one or
|
||||
// more images, and specifies named sub-rects of those images.
|
||||
struct FramesSpecification {
|
||||
// Get the target sub-rect of a given frame name (which can be an alias).
|
||||
// Returns nothing if the frame name is not found.
|
||||
Maybe<RectU> getRect(String const& frame) const;
|
||||
|
||||
// The full path to the .frames file from which this was loaded.
|
||||
String framesFile;
|
||||
// Named sub-frames
|
||||
StringMap<RectU> frames;
|
||||
// Aliases for named sub-frames, always points to a valid frame name in the
|
||||
// 'frames' map.
|
||||
StringMap<String> aliases;
|
||||
};
|
||||
|
||||
// The assets system can load image, font, json, and data assets from a set of
|
||||
// sources. Each source is either a directory on the filesystem or a single
|
||||
// packed asset file.
|
||||
//
|
||||
// Assets is thread safe and performs TTL caching.
|
||||
class Assets {
|
||||
public:
|
||||
struct Settings {
|
||||
// TTL for cached assets
|
||||
float assetTimeToLive;
|
||||
|
||||
// Audio under this length will be automatically decompressed
|
||||
float audioDecompressLimit;
|
||||
|
||||
// Number of background worker threads
|
||||
unsigned workerPoolSize;
|
||||
|
||||
// If given, if an image is unable to load, will log the error and load
|
||||
// this path instead
|
||||
Maybe<String> missingImage;
|
||||
|
||||
// Same, but for audio
|
||||
Maybe<String> missingAudio;
|
||||
|
||||
// When loading assets from a directory, will automatically ignore any
|
||||
// files whose asset paths matching any of the given patterns.
|
||||
StringList pathIgnore;
|
||||
|
||||
// Same, but only ignores the file for the purposes of calculating the
|
||||
// digest.
|
||||
StringList digestIgnore;
|
||||
};
|
||||
|
||||
Assets(Settings settings, StringList assetSources);
|
||||
~Assets();
|
||||
|
||||
// Returns a list of all the asset source paths used by Assets in load order.
|
||||
StringList assetSources() const;
|
||||
|
||||
// Return metadata for the given loaded asset source path
|
||||
JsonObject assetSourceMetadata(String const& sourcePath) const;
|
||||
|
||||
// An imperfect sha256 digest of the contents of all combined asset sources.
|
||||
// Useful for detecting if there are mismatched assets between a client and
|
||||
// server or if assets sources have changed from a previous load.
|
||||
ByteArray digest() const;
|
||||
|
||||
// Is there an asset associated with the given path? Path must not contain
|
||||
// sub-paths or directives.
|
||||
bool assetExists(String const& path) const;
|
||||
|
||||
// The name of the asset source within which the path exists.
|
||||
String assetSource(String const& path) const;
|
||||
|
||||
// Scans for all assets with the given suffix in any directory.
|
||||
StringList scan(String const& suffix) const;
|
||||
// Scans for all assets matching both prefix and suffix (prefix may be, for
|
||||
// example, a directory)
|
||||
StringList scan(String const& prefix, String const& suffix) const;
|
||||
// Scans all assets for files with the given extension, which is specially
|
||||
// indexed and much faster than a normal scan. Extension may contain leading
|
||||
// '.' character or it may be omitted.
|
||||
StringList scanExtension(String const& extension) const;
|
||||
|
||||
// Get json asset with an optional sub-path. The sub-path portion of the
|
||||
// path refers to a key in the top-level object, and may use dot notation
|
||||
// for deeper field access and [] notation for array access. Example:
|
||||
// "/path/to/json:key1.key2.key3[4]".
|
||||
Json json(String const& path) const;
|
||||
|
||||
// Either returns the json v, or, if v is a string type, returns the json
|
||||
// pointed to by interpreting v as a string path.
|
||||
Json fetchJson(Json const& v, String const& dir = "/") const;
|
||||
|
||||
// Load all the given jsons using background processing.
|
||||
void queueJsons(StringList const& paths) const;
|
||||
|
||||
// Returns *either* an image asset or a sub-frame. Frame files are JSON
|
||||
// descriptor files that reference a particular image and label separate
|
||||
// sub-rects of the image. If the given path has a ':' sub-path, then the
|
||||
// assets system will look for an associated .frames named either
|
||||
// <full-path-minus-extension>.frames or default.frames, going up to assets
|
||||
// root. May return the same ImageConstPtr for different paths if the paths
|
||||
// are equivalent or they are aliases of other image paths.
|
||||
ImageConstPtr image(String const& path) const;
|
||||
// Load images using background processing
|
||||
void queueImages(StringList const& paths) const;
|
||||
// Return the given image *if* it is already loaded, otherwise queue it for
|
||||
// loading.
|
||||
ImageConstPtr tryImage(String const& path) const;
|
||||
|
||||
// Returns the best associated FramesSpecification for a given image path, if
|
||||
// it exists. The given path must not contain sub-paths or directives, and
|
||||
// this function may return nullptr if no frames file is associated with the
|
||||
// given image path.
|
||||
FramesSpecificationConstPtr imageFrames(String const& path) const;
|
||||
|
||||
// Returns a pointer to a shared audio asset;
|
||||
AudioConstPtr audio(String const& path) const;
|
||||
// Load audios using background processing
|
||||
void queueAudios(StringList const& paths) const;
|
||||
// Return the given audio *if* it is already loaded, otherwise queue it for
|
||||
// loading.
|
||||
AudioConstPtr tryAudio(String const& path) const;
|
||||
|
||||
// Returns pointer to shared font asset
|
||||
FontConstPtr font(String const& path) const;
|
||||
|
||||
// Returns a bytes asset (Reads asset as an opaque binary blob)
|
||||
ByteArrayConstPtr bytes(String const& path) const;
|
||||
|
||||
// Bypass asset caching and open an asset file directly.
|
||||
IODevicePtr openFile(String const& basePath) const;
|
||||
|
||||
// Clear all cached assets that are not queued, persistent, or broken.
|
||||
void clearCache();
|
||||
|
||||
// Run a cleanup pass and remove any assets past their time to live.
|
||||
void cleanup();
|
||||
|
||||
private:
|
||||
enum class AssetType {
|
||||
Json,
|
||||
Image,
|
||||
Audio,
|
||||
Font,
|
||||
Bytes
|
||||
};
|
||||
|
||||
enum class QueuePriority {
|
||||
None,
|
||||
Working,
|
||||
PostProcess,
|
||||
Load
|
||||
};
|
||||
|
||||
struct AssetId {
|
||||
AssetType type;
|
||||
AssetPath path;
|
||||
|
||||
bool operator==(AssetId const& assetId) const;
|
||||
};
|
||||
|
||||
struct AssetIdHash {
|
||||
size_t operator()(AssetId const& id) const;
|
||||
};
|
||||
|
||||
struct AssetData {
|
||||
virtual ~AssetData() = default;
|
||||
|
||||
// Should return true if this asset is shared and still in use, so freeing
|
||||
// it from cache will not really free the resource, so it should persist in
|
||||
// the cache.
|
||||
virtual bool shouldPersist() const = 0;
|
||||
|
||||
double time = 0.0;
|
||||
bool needsPostProcessing = false;
|
||||
};
|
||||
|
||||
struct JsonData : AssetData {
|
||||
bool shouldPersist() const override;
|
||||
|
||||
Json json;
|
||||
};
|
||||
|
||||
// Image data for an image, sub-frame, or post-processed image.
|
||||
struct ImageData : AssetData {
|
||||
bool shouldPersist() const override;
|
||||
|
||||
ImageConstPtr image;
|
||||
|
||||
// *Optional* sub-frames data for this image, only will exist when the
|
||||
// image is a top-level image and has an associated frames file.
|
||||
FramesSpecificationConstPtr frames;
|
||||
|
||||
// If this image aliases another asset entry, this will be true and
|
||||
// shouldPersist will never be true (to ensure that this alias and its
|
||||
// target can be removed from the cache).
|
||||
bool alias = false;
|
||||
};
|
||||
|
||||
struct AudioData : AssetData {
|
||||
bool shouldPersist() const override;
|
||||
|
||||
AudioConstPtr audio;
|
||||
};
|
||||
|
||||
struct FontData : AssetData {
|
||||
bool shouldPersist() const override;
|
||||
|
||||
FontConstPtr font;
|
||||
};
|
||||
|
||||
struct BytesData : AssetData {
|
||||
bool shouldPersist() const override;
|
||||
|
||||
ByteArrayConstPtr bytes;
|
||||
};
|
||||
|
||||
struct AssetFileDescriptor {
|
||||
// The mixed case original source name;
|
||||
String sourceName;
|
||||
// The source that has the primary asset copy
|
||||
AssetSourcePtr source;
|
||||
// List of source names and sources for patches to this file.
|
||||
List<pair<String, AssetSourcePtr>> patchSources;
|
||||
};
|
||||
|
||||
static FramesSpecification parseFramesSpecification(Json const& frameConfig, String path);
|
||||
|
||||
void queueAssets(List<AssetId> const& assetIds) const;
|
||||
shared_ptr<AssetData> tryAsset(AssetId const& id) const;
|
||||
shared_ptr<AssetData> getAsset(AssetId const& id) const;
|
||||
|
||||
void workerMain();
|
||||
|
||||
// All methods below assume that the asset mutex is locked when calling.
|
||||
|
||||
// Do some processing that might take a long time and should not hold the
|
||||
// assets mutex during it. Unlocks the assets mutex while the function is in
|
||||
// progress and re-locks it on return or before exception is thrown.
|
||||
template <typename Function>
|
||||
decltype(auto) unlockDuring(Function f) const;
|
||||
|
||||
// Returns the best frames specification for the given image path, if it exists.
|
||||
FramesSpecificationConstPtr bestFramesSpecification(String const& basePath) const;
|
||||
|
||||
IODevicePtr open(String const& basePath) const;
|
||||
ByteArray read(String const& basePath) const;
|
||||
|
||||
Json readJson(String const& basePath) const;
|
||||
|
||||
// Load / post process an asset and log any exception. Returns true if the
|
||||
// work was performed (whether successful or not), false if the work is
|
||||
// blocking on something.
|
||||
bool doLoad(AssetId const& id) const;
|
||||
bool doPost(AssetId const& id) const;
|
||||
|
||||
// Assets can recursively depend on other assets, so the main entry point for
|
||||
// loading assets is in this separate method, and is safe for other loading
|
||||
// methods to call recursively. If there is an error loading the asset, this
|
||||
// method will throw. If, and only if, the asset is blocking on another busy
|
||||
// asset, this method will return null.
|
||||
shared_ptr<AssetData> loadAsset(AssetId const& id) const;
|
||||
|
||||
shared_ptr<AssetData> loadJson(AssetPath const& path) const;
|
||||
shared_ptr<AssetData> loadImage(AssetPath const& path) const;
|
||||
shared_ptr<AssetData> loadAudio(AssetPath const& path) const;
|
||||
shared_ptr<AssetData> loadFont(AssetPath const& path) const;
|
||||
shared_ptr<AssetData> loadBytes(AssetPath const& path) const;
|
||||
|
||||
shared_ptr<AssetData> postProcessAudio(shared_ptr<AssetData> const& original) const;
|
||||
|
||||
// Updates time on the given asset (with smearing).
|
||||
void freshen(shared_ptr<AssetData> const& asset) const;
|
||||
|
||||
Settings m_settings;
|
||||
|
||||
mutable Mutex m_assetsMutex;
|
||||
|
||||
mutable ConditionVariable m_assetsQueued;
|
||||
mutable OrderedHashMap<AssetId, QueuePriority, AssetIdHash> m_queue;
|
||||
|
||||
mutable ConditionVariable m_assetsDone;
|
||||
mutable HashMap<AssetId, shared_ptr<AssetData>, AssetIdHash> m_assetsCache;
|
||||
|
||||
mutable StringMap<String> m_bestFramesFiles;
|
||||
mutable StringMap<FramesSpecificationConstPtr> m_framesSpecifications;
|
||||
|
||||
// Paths of all used asset sources, in load order.
|
||||
StringList m_assetSources;
|
||||
|
||||
// Maps an asset path to the loaded asset source and vice versa
|
||||
BiMap<String, AssetSourcePtr> m_assetSourcePaths;
|
||||
|
||||
// Maps the source asset name to the source containing it
|
||||
CaseInsensitiveStringMap<AssetFileDescriptor> m_files;
|
||||
// Maps an extension to the files with that extension
|
||||
CaseInsensitiveStringMap<StringList> m_filesByExtension;
|
||||
|
||||
ByteArray m_digest;
|
||||
|
||||
List<ThreadFunction<void>> m_workerThreads;
|
||||
atomic<bool> m_stopThreads;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
108
source/base/StarBlocksAlongLine.hpp
Normal file
108
source/base/StarBlocksAlongLine.hpp
Normal file
|
@ -0,0 +1,108 @@
|
|||
#ifndef STAR_BLOCKS_ALONG_LINE_HPP
|
||||
#define STAR_BLOCKS_ALONG_LINE_HPP
|
||||
|
||||
#include "StarVector.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
// Iterate over integral cells based on Bresenham's line drawing algorithm.
|
||||
// Returns false immediately when the callback returns false for any cell,
|
||||
// returns true after iterating through every cell otherwise.
|
||||
template <typename Scalar>
|
||||
bool forBlocksAlongLine(Vector<Scalar, 2> origin, Vector<Scalar, 2> const& dxdy, function<bool(int, int)> callback) {
|
||||
Vector<Scalar, 2> remote = origin + dxdy;
|
||||
|
||||
double dx = dxdy[0];
|
||||
if (dx < 0)
|
||||
dx *= -1;
|
||||
|
||||
double dy = dxdy[1];
|
||||
if (dy < 0)
|
||||
dy *= -1;
|
||||
|
||||
double oxfloor = floor(origin[0]);
|
||||
double oyfloor = floor(origin[1]);
|
||||
double rxfloor = floor(remote[0]);
|
||||
double ryfloor = floor(remote[1]);
|
||||
|
||||
if (dx == 0) {
|
||||
if (oyfloor < ryfloor) {
|
||||
for (int i = oyfloor; i <= ryfloor; ++i) {
|
||||
if (!callback(oxfloor, i))
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (int i = oyfloor; i >= ryfloor; --i) {
|
||||
if (!callback(oxfloor, i))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
} else if (dy == 0) {
|
||||
if (oxfloor < rxfloor) {
|
||||
for (int i = oxfloor; i <= rxfloor; ++i) {
|
||||
if (!callback(i, oyfloor))
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
for (int i = oxfloor; i >= rxfloor; --i) {
|
||||
if (!callback(i, oyfloor))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
} else {
|
||||
int x = oxfloor;
|
||||
int y = oyfloor;
|
||||
|
||||
int n = 1;
|
||||
int x_inc, y_inc;
|
||||
double error;
|
||||
|
||||
if (dxdy[0] > 0) {
|
||||
x_inc = 1;
|
||||
n += int(rxfloor) - x;
|
||||
error = (oxfloor + 1 - origin[0]) * dy;
|
||||
} else {
|
||||
x_inc = -1;
|
||||
n += x - int(rxfloor);
|
||||
error = (origin[0] - oxfloor) * dy;
|
||||
}
|
||||
|
||||
if (dxdy[1] > 0) {
|
||||
y_inc = 1;
|
||||
n += int(ryfloor) - y;
|
||||
error -= (oyfloor + 1 - origin[1]) * dx;
|
||||
} else {
|
||||
y_inc = -1;
|
||||
n += y - int(ryfloor);
|
||||
error -= (origin[1] - oyfloor) * dx;
|
||||
}
|
||||
|
||||
for (; n > 0; --n) {
|
||||
if (!callback(x, y))
|
||||
return false;
|
||||
|
||||
if (error > 0) {
|
||||
y += y_inc;
|
||||
error -= dx;
|
||||
} else if (error < 0) {
|
||||
x += x_inc;
|
||||
error += dy;
|
||||
} else {
|
||||
--n;
|
||||
y += y_inc;
|
||||
x += x_inc;
|
||||
error += dy;
|
||||
error -= dx;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
590
source/base/StarCellularLightArray.hpp
Normal file
590
source/base/StarCellularLightArray.hpp
Normal file
|
@ -0,0 +1,590 @@
|
|||
#ifndef STAR_CELLULAR_LIGHT_ARRAY_HPP
|
||||
#define STAR_CELLULAR_LIGHT_ARRAY_HPP
|
||||
|
||||
#include "StarList.hpp"
|
||||
#include "StarVector.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
// Operations for simple scalar lighting.
|
||||
struct ScalarLightTraits {
|
||||
typedef float Value;
|
||||
|
||||
static float spread(float source, float dest, float drop);
|
||||
static float subtract(float value, float drop);
|
||||
|
||||
static float maxIntensity(float value);
|
||||
static float minIntensity(float value);
|
||||
|
||||
static float max(float v1, float v2);
|
||||
};
|
||||
|
||||
// Operations for 3 component (colored) lighting. Spread and subtract are
|
||||
// applied proportionally, so that color ratios stay the same, to prevent hues
|
||||
// changing as light spreads.
|
||||
struct ColoredLightTraits {
|
||||
typedef Vec3F Value;
|
||||
|
||||
static Vec3F spread(Vec3F const& source, Vec3F const& dest, float drop);
|
||||
static Vec3F subtract(Vec3F value, float drop);
|
||||
|
||||
static float maxIntensity(Vec3F const& value);
|
||||
static float minIntensity(Vec3F const& value);
|
||||
|
||||
static Vec3F max(Vec3F const& v1, Vec3F const& v2);
|
||||
};
|
||||
|
||||
template <typename LightTraits>
|
||||
class CellularLightArray {
|
||||
public:
|
||||
typedef typename LightTraits::Value LightValue;
|
||||
|
||||
struct Cell {
|
||||
LightValue light;
|
||||
bool obstacle;
|
||||
};
|
||||
|
||||
struct SpreadLight {
|
||||
Vec2F position;
|
||||
LightValue value;
|
||||
};
|
||||
|
||||
struct PointLight {
|
||||
Vec2F position;
|
||||
LightValue value;
|
||||
float beam;
|
||||
float beamAngle;
|
||||
float beamAmbience;
|
||||
};
|
||||
|
||||
void setParameters(unsigned spreadPasses, float spreadMaxAir, float spreadMaxObstacle,
|
||||
float pointMaxAir, float pointMaxObstacle, float pointObstacleBoost);
|
||||
|
||||
// The border around the target lighting array where initial lighting / light
|
||||
// source data is required. Based on parameters.
|
||||
size_t borderCells() const;
|
||||
|
||||
// Begin a new calculation, setting internal storage to new width and height
|
||||
// (if these are the same as last time this is cheap). Always clears all
|
||||
// existing light and collision data.
|
||||
void begin(size_t newWidth, size_t newHeight);
|
||||
|
||||
// Position is in index space, spread lights will have no effect if they are
|
||||
// outside of the array. Integer points are assumed to be on the corners of
|
||||
// the grid (not the center)
|
||||
void addSpreadLight(SpreadLight const& spreadLight);
|
||||
void addPointLight(PointLight const& pointLight);
|
||||
|
||||
// Directly set the lighting values for this position.
|
||||
void setLight(size_t x, size_t y, LightValue const& light);
|
||||
|
||||
// Get current light value. Call after calling calculate() to pull final
|
||||
// data out.
|
||||
LightValue getLight(size_t x, size_t y) const;
|
||||
|
||||
// Set obstacle values for this position
|
||||
void setObstacle(size_t x, size_t y, bool obstacle);
|
||||
bool getObstacle(size_t x, size_t y) const;
|
||||
|
||||
Cell const& cell(size_t x, size_t y) const;
|
||||
Cell& cell(size_t x, size_t y);
|
||||
|
||||
Cell const& cellAtIndex(size_t index) const;
|
||||
Cell& cellAtIndex(size_t index);
|
||||
|
||||
// Calculate lighting in the given sub-rect, in order to properly do spread
|
||||
// lighting, and initial lighting must be given for the ambient border this
|
||||
// given rect, and the array size must be at least that large. xMax / yMax
|
||||
// are not inclusive, the range is [xMin, xMax) and [yMin, yMax).
|
||||
void calculate(size_t xMin, size_t yMin, size_t xMax, size_t yMax);
|
||||
|
||||
private:
|
||||
// Set 4 points based on interpolated light position and free space
|
||||
// attenuation.
|
||||
void setSpreadLightingPoints();
|
||||
|
||||
// Spreads light out in an octagonal based cellular automata
|
||||
void calculateLightSpread(size_t xmin, size_t ymin, size_t xmax, size_t ymax);
|
||||
|
||||
// Loops through each light and adds light strength based on distance and
|
||||
// obstacle attenuation. Calculates within the given sub-rect
|
||||
void calculatePointLighting(size_t xmin, size_t ymin, size_t xmax, size_t ymax);
|
||||
|
||||
// Run Xiaolin Wu's anti-aliased line drawing algorithm from start to end,
|
||||
// summing each block that would be drawn to to produce an attenuation. Not
|
||||
// circularized.
|
||||
float lineAttenuation(Vec2F const& start, Vec2F const& end, float perObstacleAttenuation, float maxAttenuation);
|
||||
|
||||
size_t m_width;
|
||||
size_t m_height;
|
||||
unique_ptr<Cell[]> m_cells;
|
||||
List<SpreadLight> m_spreadLights;
|
||||
List<PointLight> m_pointLights;
|
||||
|
||||
unsigned m_spreadPasses;
|
||||
float m_spreadMaxAir;
|
||||
float m_spreadMaxObstacle;
|
||||
float m_pointMaxAir;
|
||||
float m_pointMaxObstacle;
|
||||
float m_pointObstacleBoost;
|
||||
};
|
||||
|
||||
typedef CellularLightArray<ColoredLightTraits> ColoredCellularLightArray;
|
||||
typedef CellularLightArray<ScalarLightTraits> ScalarCellularLightArray;
|
||||
|
||||
inline float ScalarLightTraits::spread(float source, float dest, float drop) {
|
||||
return std::max(source - drop, dest);
|
||||
}
|
||||
|
||||
inline float ScalarLightTraits::subtract(float c, float drop) {
|
||||
return std::max(c - drop, 0.0f);
|
||||
}
|
||||
|
||||
inline float ScalarLightTraits::maxIntensity(float value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
inline float ScalarLightTraits::minIntensity(float value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
inline float ScalarLightTraits::max(float v1, float v2) {
|
||||
return std::max(v1, v2);
|
||||
}
|
||||
|
||||
inline Vec3F ColoredLightTraits::spread(Vec3F const& source, Vec3F const& dest, float drop) {
|
||||
float maxChannel = std::max(source[0], std::max(source[1], source[2]));
|
||||
if (maxChannel <= 0.0f)
|
||||
return dest;
|
||||
|
||||
drop /= maxChannel;
|
||||
return Vec3F(
|
||||
std::max(source[0] - source[0] * drop, dest[0]),
|
||||
std::max(source[1] - source[1] * drop, dest[1]),
|
||||
std::max(source[2] - source[2] * drop, dest[2])
|
||||
);
|
||||
}
|
||||
|
||||
inline Vec3F ColoredLightTraits::subtract(Vec3F c, float drop) {
|
||||
float max = std::max(std::max(c[0], c[1]), c[2]);
|
||||
if (max <= 0.0f)
|
||||
return c;
|
||||
|
||||
for (size_t i = 0; i < 3; ++i) {
|
||||
float pdrop = (drop * c[i]) / max;
|
||||
if (c[i] > pdrop)
|
||||
c[i] -= pdrop;
|
||||
else
|
||||
c[i] = 0;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
inline float ColoredLightTraits::maxIntensity(Vec3F const& value) {
|
||||
return value.max();
|
||||
}
|
||||
|
||||
inline float ColoredLightTraits::minIntensity(Vec3F const& value) {
|
||||
return value.min();
|
||||
}
|
||||
|
||||
inline Vec3F ColoredLightTraits::max(Vec3F const& v1, Vec3F const& v2) {
|
||||
return vmax(v1, v2);
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
void CellularLightArray<LightTraits>::setParameters(unsigned spreadPasses, float spreadMaxAir, float spreadMaxObstacle,
|
||||
float pointMaxAir, float pointMaxObstacle, float pointObstacleBoost) {
|
||||
m_spreadPasses = spreadPasses;
|
||||
m_spreadMaxAir = spreadMaxAir;
|
||||
m_spreadMaxObstacle = spreadMaxObstacle;
|
||||
m_pointMaxAir = pointMaxAir;
|
||||
m_pointMaxObstacle = pointMaxObstacle;
|
||||
m_pointObstacleBoost = pointObstacleBoost;
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
size_t CellularLightArray<LightTraits>::borderCells() const {
|
||||
return (size_t)ceil(max(0.0f, max(m_spreadMaxAir, m_pointMaxAir)));
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
void CellularLightArray<LightTraits>::begin(size_t newWidth, size_t newHeight) {
|
||||
m_spreadLights.clear();
|
||||
m_pointLights.clear();
|
||||
starAssert(newWidth > 0 && newHeight > 0);
|
||||
|
||||
if (!m_cells || newWidth != m_width || newHeight != m_height) {
|
||||
m_width = newWidth;
|
||||
m_height = newHeight;
|
||||
|
||||
m_cells.reset(new Cell[m_width * m_height]());
|
||||
|
||||
} else {
|
||||
std::fill(m_cells.get(), m_cells.get() + m_width * m_height, Cell{LightValue{}, false});
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
void CellularLightArray<LightTraits>::addSpreadLight(SpreadLight const& spreadLight) {
|
||||
m_spreadLights.append(spreadLight);
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
void CellularLightArray<LightTraits>::addPointLight(PointLight const& pointLight) {
|
||||
m_pointLights.append(pointLight);
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
void CellularLightArray<LightTraits>::setLight(size_t x, size_t y, LightValue const& lightValue) {
|
||||
cell(x, y).light = lightValue;
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
void CellularLightArray<LightTraits>::setObstacle(size_t x, size_t y, bool obstacle) {
|
||||
cell(x, y).obstacle = obstacle;
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
auto CellularLightArray<LightTraits>::getLight(size_t x, size_t y) const -> LightValue {
|
||||
return cell(x, y).light;
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
bool CellularLightArray<LightTraits>::getObstacle(size_t x, size_t y) const {
|
||||
return cell(x, y).obstacle;
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
auto CellularLightArray<LightTraits>::cell(size_t x, size_t y) const -> Cell const & {
|
||||
starAssert(x < m_width && y < m_height);
|
||||
return m_cells[x * m_height + y];
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
auto CellularLightArray<LightTraits>::cell(size_t x, size_t y) -> Cell & {
|
||||
starAssert(x < m_width && y < m_height);
|
||||
return m_cells[x * m_height + y];
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
auto CellularLightArray<LightTraits>::cellAtIndex(size_t index) const -> Cell const & {
|
||||
starAssert(index < m_width * m_height);
|
||||
return m_cells[index];
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
auto CellularLightArray<LightTraits>::cellAtIndex(size_t index) -> Cell & {
|
||||
starAssert(index < m_width * m_height);
|
||||
return m_cells[index];
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
void CellularLightArray<LightTraits>::calculate(size_t xMin, size_t yMin, size_t xMax, size_t yMax) {
|
||||
setSpreadLightingPoints();
|
||||
calculateLightSpread(xMin, yMin, xMax, yMax);
|
||||
calculatePointLighting(xMin, yMin, xMax, yMax);
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
void CellularLightArray<LightTraits>::setSpreadLightingPoints() {
|
||||
for (SpreadLight const& light : m_spreadLights) {
|
||||
// - 0.5f to correct for lights being on the grid corners and not center
|
||||
int minX = floor(light.position[0] - 0.5f);
|
||||
int minY = floor(light.position[1] - 0.5f);
|
||||
int maxX = minX + 1;
|
||||
int maxY = minY + 1;
|
||||
|
||||
float xdist = light.position[0] - minX - 0.5f;
|
||||
float ydist = light.position[1] - minY - 0.5f;
|
||||
|
||||
// Pick falloff here based on closest block obstacle value (probably not
|
||||
// best)
|
||||
Vec2I pos(light.position.floor());
|
||||
float oneBlockAtt;
|
||||
if (pos[0] >= 0 && pos[0] < (int)m_width && pos[1] >= 0 && pos[1] < (int)m_height && getObstacle(pos[0], pos[1]))
|
||||
oneBlockAtt = 1.0f / m_spreadMaxObstacle;
|
||||
else
|
||||
oneBlockAtt = 1.0f / m_spreadMaxAir;
|
||||
|
||||
// "pre fall-off" a 2x2 area of blocks to smooth out floating point
|
||||
// positions using the cellular algorithm
|
||||
|
||||
if (minX >= 0 && minX < (int)m_width && minY >= 0 && minY < (int)m_height)
|
||||
setLight(minX, minY, LightTraits::max(getLight(minX, minY), LightTraits::subtract(light.value, oneBlockAtt * (2.0f - (1.0f - xdist) - (1.0f - ydist)))));
|
||||
|
||||
if (minX >= 0 && minX < (int)m_width && maxY >= 0 && maxY < (int)m_height)
|
||||
setLight(minX, maxY, LightTraits::max(getLight(minX, maxY), LightTraits::subtract(light.value, oneBlockAtt * (2.0f - (1.0f - xdist) - (ydist)))));
|
||||
|
||||
if (maxX >= 0 && maxX < (int)m_width && minY >= 0 && minY < (int)m_height)
|
||||
setLight(maxX, minY, LightTraits::max(getLight(maxX, minY), LightTraits::subtract(light.value, oneBlockAtt * (2.0f - (xdist) - (1.0f - ydist)))));
|
||||
|
||||
if (maxX >= 0 && maxX < (int)m_width && maxY >= 0 && maxY < (int)m_height)
|
||||
setLight(maxX, maxY, LightTraits::max(getLight(maxX, maxY), LightTraits::subtract(light.value, oneBlockAtt * (2.0f - (xdist) - (ydist)))));
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
void CellularLightArray<LightTraits>::calculateLightSpread(size_t xMin, size_t yMin, size_t xMax, size_t yMax) {
|
||||
starAssert(m_width > 0 && m_height > 0);
|
||||
|
||||
float dropoffAir = 1.0f / m_spreadMaxAir;
|
||||
float dropoffObstacle = 1.0f / m_spreadMaxObstacle;
|
||||
float dropoffAirDiag = 1.0f / m_spreadMaxAir * Constants::sqrt2;
|
||||
float dropoffObstacleDiag = 1.0f / m_spreadMaxObstacle * Constants::sqrt2;
|
||||
|
||||
// enlarge x/y min/max taking into ambient spread of light
|
||||
xMin = xMin - min(xMin, (size_t)ceil(m_spreadMaxAir));
|
||||
yMin = yMin - min(yMin, (size_t)ceil(m_spreadMaxAir));
|
||||
xMax = min(m_width, xMax + (size_t)ceil(m_spreadMaxAir));
|
||||
yMax = min(m_height, yMax + (size_t)ceil(m_spreadMaxAir));
|
||||
|
||||
for (unsigned p = 0; p < m_spreadPasses; ++p) {
|
||||
// Spread right and up and diag up right / diag down right
|
||||
for (size_t x = xMin + 1; x < xMax - 1; ++x) {
|
||||
size_t xCellOffset = x * m_height;
|
||||
size_t xRightCellOffset = (x + 1) * m_height;
|
||||
|
||||
for (size_t y = yMin + 1; y < yMax - 1; ++y) {
|
||||
auto cell = cellAtIndex(xCellOffset + y);
|
||||
auto& cellRight = cellAtIndex(xRightCellOffset + y);
|
||||
auto& cellUp = cellAtIndex(xCellOffset + y + 1);
|
||||
auto& cellRightUp = cellAtIndex(xRightCellOffset + y + 1);
|
||||
auto& cellRightDown = cellAtIndex(xRightCellOffset + y - 1);
|
||||
|
||||
float straightDropoff = cell.obstacle ? dropoffObstacle : dropoffAir;
|
||||
float diagDropoff = cell.obstacle ? dropoffObstacleDiag : dropoffAirDiag;
|
||||
|
||||
cellRight.light = LightTraits::spread(cell.light, cellRight.light, straightDropoff);
|
||||
cellUp.light = LightTraits::spread(cell.light, cellUp.light, straightDropoff);
|
||||
|
||||
cellRightUp.light = LightTraits::spread(cell.light, cellRightUp.light, diagDropoff);
|
||||
cellRightDown.light = LightTraits::spread(cell.light, cellRightDown.light, diagDropoff);
|
||||
}
|
||||
}
|
||||
|
||||
// Spread left and down and diag up left / diag down left
|
||||
for (size_t x = xMax - 2; x > xMin; --x) {
|
||||
size_t xCellOffset = x * m_height;
|
||||
size_t xLeftCellOffset = (x - 1) * m_height;
|
||||
|
||||
for (size_t y = yMax - 2; y > yMin; --y) {
|
||||
auto cell = cellAtIndex(xCellOffset + y);
|
||||
auto& cellLeft = cellAtIndex(xLeftCellOffset + y);
|
||||
auto& cellDown = cellAtIndex(xCellOffset + y - 1);
|
||||
auto& cellLeftUp = cellAtIndex(xLeftCellOffset + y + 1);
|
||||
auto& cellLeftDown = cellAtIndex(xLeftCellOffset + y - 1);
|
||||
|
||||
float straightDropoff = cell.obstacle ? dropoffObstacle : dropoffAir;
|
||||
float diagDropoff = cell.obstacle ? dropoffObstacleDiag : dropoffAirDiag;
|
||||
|
||||
cellLeft.light = LightTraits::spread(cell.light, cellLeft.light, straightDropoff);
|
||||
cellDown.light = LightTraits::spread(cell.light, cellDown.light, straightDropoff);
|
||||
|
||||
cellLeftUp.light = LightTraits::spread(cell.light, cellLeftUp.light, diagDropoff);
|
||||
cellLeftDown.light = LightTraits::spread(cell.light, cellLeftDown.light, diagDropoff);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
void CellularLightArray<LightTraits>::calculatePointLighting(size_t xmin, size_t ymin, size_t xmax, size_t ymax) {
|
||||
float perBlockObstacleAttenuation = 1.0f / m_pointMaxObstacle;
|
||||
float perBlockAirAttenuation = 1.0f / m_pointMaxAir;
|
||||
|
||||
for (PointLight light : m_pointLights) {
|
||||
if (light.position[0] < 0 || light.position[0] > m_width - 1 || light.position[1] < 0 || light.position[1] > m_height - 1)
|
||||
continue;
|
||||
|
||||
float maxIntensity = LightTraits::maxIntensity(light.value);
|
||||
Vec2F beamDirection = Vec2F(1, 0).rotate(light.beamAngle);
|
||||
|
||||
float maxRange = maxIntensity * m_pointMaxAir;
|
||||
// The min / max considering the radius of the light
|
||||
size_t lxmin = std::floor(std::max<float>(xmin, light.position[0] - maxRange));
|
||||
size_t lymin = std::floor(std::max<float>(ymin, light.position[1] - maxRange));
|
||||
size_t lxmax = std::ceil(std::min<float>(xmax, light.position[0] + maxRange));
|
||||
size_t lymax = std::ceil(std::min<float>(ymax, light.position[1] + maxRange));
|
||||
|
||||
for (size_t x = lxmin; x < lxmax; ++x) {
|
||||
for (size_t y = lymin; y < lymax; ++y) {
|
||||
LightValue lvalue = getLight(x, y);
|
||||
// + 0.5f to correct block position to center
|
||||
Vec2F blockPos = Vec2F(x + 0.5f, y + 0.5f);
|
||||
|
||||
Vec2F relativeLightPosition = blockPos - light.position;
|
||||
float distance = relativeLightPosition.magnitude();
|
||||
if (distance == 0.0f) {
|
||||
setLight(x, y, LightTraits::max(light.value, lvalue));
|
||||
continue;
|
||||
}
|
||||
|
||||
float attenuation = distance * perBlockAirAttenuation;
|
||||
if (attenuation >= 1.0f)
|
||||
continue;
|
||||
|
||||
Vec2F direction = relativeLightPosition / distance;
|
||||
if (light.beam > 0.0f) {
|
||||
attenuation += (1.0f - light.beamAmbience) * clamp(light.beam * (1.0f - direction * beamDirection), 0.0f, 1.0f);
|
||||
if (attenuation >= 1.0f)
|
||||
continue;
|
||||
}
|
||||
|
||||
float remainingAttenuation = maxIntensity - LightTraits::minIntensity(lvalue) - attenuation;
|
||||
if (remainingAttenuation <= 0.0f)
|
||||
continue;
|
||||
|
||||
// Need to circularize manhattan attenuation here
|
||||
float circularizedPerBlockObstacleAttenuation = perBlockObstacleAttenuation / max(fabs(direction[0]), fabs(direction[1]));
|
||||
float blockAttenuation = lineAttenuation(blockPos, light.position, circularizedPerBlockObstacleAttenuation, remainingAttenuation);
|
||||
|
||||
// Apply single obstacle boost (determine single obstacle by one
|
||||
// block unit of attenuation).
|
||||
attenuation += blockAttenuation + min(blockAttenuation, circularizedPerBlockObstacleAttenuation) * m_pointObstacleBoost;
|
||||
|
||||
if (attenuation < 1.0f)
|
||||
setLight(x, y, LightTraits::max(LightTraits::subtract(light.value, attenuation), lvalue));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LightTraits>
|
||||
float CellularLightArray<LightTraits>::lineAttenuation(Vec2F const& start, Vec2F const& end,
|
||||
float perObstacleAttenuation, float maxAttenuation) {
|
||||
// Run Xiaolin Wu's line algorithm from start to end, summing over colliding
|
||||
// blocks using perObstacleAttenuation.
|
||||
float obstacleAttenuation = 0.0;
|
||||
|
||||
// Apply correction because integer coordinates are lower left corner.
|
||||
float x1 = start[0] - 0.5;
|
||||
float y1 = start[1] - 0.5;
|
||||
float x2 = end[0] - 0.5;
|
||||
float y2 = end[1] - 0.5;
|
||||
|
||||
float dx = x2 - x1;
|
||||
float dy = y2 - y1;
|
||||
|
||||
if (fabs(dx) < fabs(dy)) {
|
||||
if (y2 < y1) {
|
||||
swap(y1, y2);
|
||||
swap(x1, x2);
|
||||
}
|
||||
|
||||
float gradient = dx / dy;
|
||||
|
||||
// first end point
|
||||
float yend = round(y1);
|
||||
float xend = x1 + gradient * (yend - y1);
|
||||
float ygap = rfpart(y1 + 0.5);
|
||||
int ypxl1 = yend;
|
||||
int xpxl1 = ipart(xend);
|
||||
|
||||
if (cell(xpxl1, ypxl1).obstacle)
|
||||
obstacleAttenuation += rfpart(xend) * ygap * perObstacleAttenuation;
|
||||
|
||||
if (cell(xpxl1 + 1, ypxl1).obstacle)
|
||||
obstacleAttenuation += fpart(xend) * ygap * perObstacleAttenuation;
|
||||
|
||||
if (obstacleAttenuation >= maxAttenuation)
|
||||
return maxAttenuation;
|
||||
|
||||
float interx = xend + gradient;
|
||||
|
||||
// second end point
|
||||
yend = round(y2);
|
||||
xend = x2 + gradient * (yend - y2);
|
||||
ygap = fpart(y2 + 0.5);
|
||||
int ypxl2 = yend;
|
||||
int xpxl2 = ipart(xend);
|
||||
|
||||
if (cell(xpxl2, ypxl2).obstacle)
|
||||
obstacleAttenuation += rfpart(xend) * ygap * perObstacleAttenuation;
|
||||
|
||||
if (cell(xpxl2 + 1, ypxl2).obstacle)
|
||||
obstacleAttenuation += fpart(xend) * ygap * perObstacleAttenuation;
|
||||
|
||||
if (obstacleAttenuation >= maxAttenuation)
|
||||
return maxAttenuation;
|
||||
|
||||
for (int y = ypxl1 + 1; y < ypxl2; ++y) {
|
||||
int interxIpart = ipart(interx);
|
||||
float interxFpart = interx - interxIpart;
|
||||
float interxRFpart = 1.0 - interxFpart;
|
||||
|
||||
if (cell(interxIpart, y).obstacle)
|
||||
obstacleAttenuation += interxRFpart * perObstacleAttenuation;
|
||||
if (cell(interxIpart + 1, y).obstacle)
|
||||
obstacleAttenuation += interxFpart * perObstacleAttenuation;
|
||||
|
||||
if (obstacleAttenuation >= maxAttenuation)
|
||||
return maxAttenuation;
|
||||
|
||||
interx += gradient;
|
||||
}
|
||||
} else {
|
||||
if (x2 < x1) {
|
||||
swap(x1, x2);
|
||||
swap(y1, y2);
|
||||
}
|
||||
|
||||
float gradient = dy / dx;
|
||||
|
||||
// first end point
|
||||
float xend = round(x1);
|
||||
float yend = y1 + gradient * (xend - x1);
|
||||
float xgap = rfpart(x1 + 0.5);
|
||||
int xpxl1 = xend;
|
||||
int ypxl1 = ipart(yend);
|
||||
|
||||
if (cell(xpxl1, ypxl1).obstacle)
|
||||
obstacleAttenuation += rfpart(yend) * xgap * perObstacleAttenuation;
|
||||
|
||||
if (cell(xpxl1, ypxl1 + 1).obstacle)
|
||||
obstacleAttenuation += fpart(yend) * xgap * perObstacleAttenuation;
|
||||
|
||||
if (obstacleAttenuation >= maxAttenuation)
|
||||
return maxAttenuation;
|
||||
|
||||
float intery = yend + gradient;
|
||||
|
||||
// second end point
|
||||
xend = round(x2);
|
||||
yend = y2 + gradient * (xend - x2);
|
||||
xgap = fpart(x2 + 0.5);
|
||||
int xpxl2 = xend;
|
||||
int ypxl2 = ipart(yend);
|
||||
|
||||
if (cell(xpxl2, ypxl2).obstacle)
|
||||
obstacleAttenuation += rfpart(yend) * xgap * perObstacleAttenuation;
|
||||
|
||||
if (cell(xpxl2, ypxl2 + 1).obstacle)
|
||||
obstacleAttenuation += fpart(yend) * xgap * perObstacleAttenuation;
|
||||
|
||||
if (obstacleAttenuation >= maxAttenuation)
|
||||
return maxAttenuation;
|
||||
|
||||
for (int x = xpxl1 + 1; x < xpxl2; ++x) {
|
||||
int interyIpart = ipart(intery);
|
||||
float interyFpart = intery - interyIpart;
|
||||
float interyRFpart = 1.0 - interyFpart;
|
||||
|
||||
if (cell(x, interyIpart).obstacle)
|
||||
obstacleAttenuation += interyRFpart * perObstacleAttenuation;
|
||||
if (cell(x, interyIpart + 1).obstacle)
|
||||
obstacleAttenuation += interyFpart * perObstacleAttenuation;
|
||||
|
||||
if (obstacleAttenuation >= maxAttenuation)
|
||||
return maxAttenuation;
|
||||
|
||||
intery += gradient;
|
||||
}
|
||||
}
|
||||
|
||||
return min(obstacleAttenuation, maxAttenuation);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
160
source/base/StarCellularLighting.cpp
Normal file
160
source/base/StarCellularLighting.cpp
Normal file
|
@ -0,0 +1,160 @@
|
|||
#include "StarCellularLighting.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
CellularLightingCalculator::CellularLightingCalculator(bool monochrome) {
|
||||
setMonochrome(monochrome);
|
||||
}
|
||||
|
||||
void CellularLightingCalculator::setMonochrome(bool monochrome) {
|
||||
if (monochrome == m_monochrome)
|
||||
return;
|
||||
|
||||
m_monochrome = monochrome;
|
||||
if (monochrome)
|
||||
m_lightArray.setRight(ScalarCellularLightArray());
|
||||
else
|
||||
m_lightArray.setLeft(ColoredCellularLightArray());
|
||||
|
||||
if (m_config)
|
||||
setParameters(m_config);
|
||||
}
|
||||
|
||||
void CellularLightingCalculator::setParameters(Json const& config) {
|
||||
m_config = config;
|
||||
if (m_monochrome)
|
||||
m_lightArray.right().setParameters(
|
||||
config.getInt("spreadPasses"),
|
||||
config.getFloat("spreadMaxAir"),
|
||||
config.getFloat("spreadMaxObstacle"),
|
||||
config.getFloat("pointMaxAir"),
|
||||
config.getFloat("pointMaxObstacle"),
|
||||
config.getFloat("pointObstacleBoost")
|
||||
);
|
||||
else
|
||||
m_lightArray.left().setParameters(
|
||||
config.getInt("spreadPasses"),
|
||||
config.getFloat("spreadMaxAir"),
|
||||
config.getFloat("spreadMaxObstacle"),
|
||||
config.getFloat("pointMaxAir"),
|
||||
config.getFloat("pointMaxObstacle"),
|
||||
config.getFloat("pointObstacleBoost")
|
||||
);
|
||||
}
|
||||
|
||||
void CellularLightingCalculator::begin(RectI const& queryRegion) {
|
||||
m_queryRegion = queryRegion;
|
||||
if (m_monochrome) {
|
||||
m_calculationRegion = RectI(queryRegion).padded((int)m_lightArray.right().borderCells());
|
||||
m_lightArray.right().begin(m_calculationRegion.width(), m_calculationRegion.height());
|
||||
} else {
|
||||
m_calculationRegion = RectI(queryRegion).padded((int)m_lightArray.left().borderCells());
|
||||
m_lightArray.left().begin(m_calculationRegion.width(), m_calculationRegion.height());
|
||||
}
|
||||
}
|
||||
|
||||
RectI CellularLightingCalculator::calculationRegion() const {
|
||||
return m_calculationRegion;
|
||||
}
|
||||
|
||||
void CellularLightingCalculator::addSpreadLight(Vec2F const& position, Vec3F const& light) {
|
||||
Vec2F arrayPosition = position - Vec2F(m_calculationRegion.min());
|
||||
if (m_monochrome)
|
||||
m_lightArray.right().addSpreadLight({arrayPosition, light.max()});
|
||||
else
|
||||
m_lightArray.left().addSpreadLight({arrayPosition, light});
|
||||
}
|
||||
|
||||
void CellularLightingCalculator::addPointLight(Vec2F const& position, Vec3F const& light, float beam, float beamAngle, float beamAmbience) {
|
||||
Vec2F arrayPosition = position - Vec2F(m_calculationRegion.min());
|
||||
if (m_monochrome)
|
||||
m_lightArray.right().addPointLight({arrayPosition, light.max(), beam, beamAngle, beamAmbience});
|
||||
else
|
||||
m_lightArray.left().addPointLight({arrayPosition, light, beam, beamAngle, beamAmbience});
|
||||
}
|
||||
|
||||
void CellularLightingCalculator::calculate(Image& output) {
|
||||
Vec2S arrayMin = Vec2S(m_queryRegion.min() - m_calculationRegion.min());
|
||||
Vec2S arrayMax = Vec2S(m_queryRegion.max() - m_calculationRegion.min());
|
||||
|
||||
if (m_monochrome)
|
||||
m_lightArray.right().calculate(arrayMin[0], arrayMin[1], arrayMax[0], arrayMax[1]);
|
||||
else
|
||||
m_lightArray.left().calculate(arrayMin[0], arrayMin[1], arrayMax[0], arrayMax[1]);
|
||||
|
||||
output.reset(arrayMax[0] - arrayMin[0], arrayMax[1] - arrayMin[1], PixelFormat::RGB24);
|
||||
|
||||
for (size_t x = arrayMin[0]; x < arrayMax[0]; ++x) {
|
||||
for (size_t y = arrayMin[1]; y < arrayMax[1]; ++y) {
|
||||
if (m_monochrome)
|
||||
output.set24(x - arrayMin[0], y - arrayMin[1], Color::grayf(m_lightArray.right().getLight(x, y)).toRgb());
|
||||
else
|
||||
output.set24(x - arrayMin[0], y - arrayMin[1], Color::v3fToByte(m_lightArray.left().getLight(x, y)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CellularLightIntensityCalculator::setParameters(Json const& config) {
|
||||
m_lightArray.setParameters(
|
||||
config.getInt("spreadPasses"),
|
||||
config.getFloat("spreadMaxAir"),
|
||||
config.getFloat("spreadMaxObstacle"),
|
||||
config.getFloat("pointMaxAir"),
|
||||
config.getFloat("pointMaxObstacle"),
|
||||
config.getFloat("pointObstacleBoost")
|
||||
);
|
||||
}
|
||||
|
||||
void CellularLightIntensityCalculator::begin(Vec2F const& queryPosition) {
|
||||
m_queryPosition = queryPosition;
|
||||
m_queryRegion = RectI::withSize(Vec2I::floor(queryPosition - Vec2F::filled(0.5f)), Vec2I(2, 2));
|
||||
m_calculationRegion = RectI(m_queryRegion).padded((int)m_lightArray.borderCells());
|
||||
|
||||
m_lightArray.begin(m_calculationRegion.width(), m_calculationRegion.height());
|
||||
}
|
||||
|
||||
RectI CellularLightIntensityCalculator::calculationRegion() const {
|
||||
return m_calculationRegion;
|
||||
}
|
||||
|
||||
void CellularLightIntensityCalculator::setCell(Vec2I const& position, Cell const& cell) {
|
||||
setCellColumn(position, &cell, 1);
|
||||
}
|
||||
|
||||
void CellularLightIntensityCalculator::setCellColumn(Vec2I const& position, Cell const* cells, size_t count) {
|
||||
size_t baseIndex = (position[0] - m_calculationRegion.xMin()) * m_calculationRegion.height() + position[1] - m_calculationRegion.yMin();
|
||||
for (size_t i = 0; i < count; ++i)
|
||||
m_lightArray.cellAtIndex(baseIndex + i) = cells[i];
|
||||
}
|
||||
|
||||
void CellularLightIntensityCalculator::addSpreadLight(Vec2F const& position, float light) {
|
||||
Vec2F arrayPosition = position - Vec2F(m_calculationRegion.min());
|
||||
m_lightArray.addSpreadLight({arrayPosition, light});
|
||||
}
|
||||
|
||||
void CellularLightIntensityCalculator::addPointLight(Vec2F const& position, float light, float beam, float beamAngle, float beamAmbience) {
|
||||
Vec2F arrayPosition = position - Vec2F(m_calculationRegion.min());
|
||||
m_lightArray.addPointLight({arrayPosition, light, beam, beamAngle, beamAmbience});
|
||||
}
|
||||
|
||||
|
||||
float CellularLightIntensityCalculator::calculate() {
|
||||
Vec2S arrayMin = Vec2S(m_queryRegion.min() - m_calculationRegion.min());
|
||||
Vec2S arrayMax = Vec2S(m_queryRegion.max() - m_calculationRegion.min());
|
||||
|
||||
m_lightArray.calculate(arrayMin[0], arrayMin[1], arrayMax[0], arrayMax[1]);
|
||||
|
||||
// Do 2d lerp to find lighting intensity
|
||||
|
||||
float ll = m_lightArray.getLight(arrayMin[0], arrayMin[1]);
|
||||
float lr = m_lightArray.getLight(arrayMin[0] + 1, arrayMin[1]);
|
||||
float ul = m_lightArray.getLight(arrayMin[0], arrayMin[1] + 1);
|
||||
float ur = m_lightArray.getLight(arrayMin[0] + 1, arrayMin[1] + 1);
|
||||
|
||||
float xl = m_queryPosition[0] - 0.5f - m_queryRegion.xMin();
|
||||
float yl = m_queryPosition[1] - 0.5f - m_queryRegion.yMin();
|
||||
|
||||
return lerp(yl, lerp(xl, ll, lr), lerp(xl, ul, ur));
|
||||
}
|
||||
|
||||
}
|
96
source/base/StarCellularLighting.hpp
Normal file
96
source/base/StarCellularLighting.hpp
Normal file
|
@ -0,0 +1,96 @@
|
|||
#ifndef STAR_CELLULAR_LIGHTING_HPP
|
||||
#define STAR_CELLULAR_LIGHTING_HPP
|
||||
|
||||
#include "StarEither.hpp"
|
||||
#include "StarRect.hpp"
|
||||
#include "StarImage.hpp"
|
||||
#include "StarJson.hpp"
|
||||
#include "StarColor.hpp"
|
||||
#include "StarInterpolation.hpp"
|
||||
#include "StarCellularLightArray.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
// Produce lighting values from an integral cellular grid. Allows for floating
|
||||
// positional point and cellular light sources, as well as pre-lighting cells
|
||||
// individually.
|
||||
class CellularLightingCalculator {
|
||||
public:
|
||||
CellularLightingCalculator(bool monochrome = false);
|
||||
|
||||
typedef ColoredCellularLightArray::Cell Cell;
|
||||
|
||||
void setMonochrome(bool monochrome);
|
||||
|
||||
void setParameters(Json const& config);
|
||||
|
||||
// Call 'begin' to start a calculation for the given region
|
||||
void begin(RectI const& queryRegion);
|
||||
|
||||
// Once begin is called, this will return the region that could possibly
|
||||
// affect the target calculation region. All lighting values should be set
|
||||
// for the given calculation region before calling 'calculate'.
|
||||
RectI calculationRegion() const;
|
||||
|
||||
size_t baseIndexFor(Vec2I const& position);
|
||||
|
||||
void setCellIndex(size_t cellIndex, Vec3F const& light, bool obstacle);
|
||||
|
||||
void addSpreadLight(Vec2F const& position, Vec3F const& light);
|
||||
void addPointLight(Vec2F const& position, Vec3F const& light, float beam, float beamAngle, float beamAmbience);
|
||||
|
||||
// Finish the calculation, and put the resulting color data in the given
|
||||
// output image. The image will be reset to the size of the region given in
|
||||
// the call to 'begin', and formatted as RGB24.
|
||||
void calculate(Image& output);
|
||||
|
||||
private:
|
||||
Json m_config;
|
||||
bool m_monochrome;
|
||||
Either<ColoredCellularLightArray, ScalarCellularLightArray> m_lightArray;
|
||||
RectI m_queryRegion;
|
||||
RectI m_calculationRegion;
|
||||
};
|
||||
|
||||
// Produce light intensity values using the same algorithm as
|
||||
// CellularLightingCalculator. Only calculates a single point at a time, and
|
||||
// uses scalar lights with no color calculation.
|
||||
class CellularLightIntensityCalculator {
|
||||
public:
|
||||
typedef ScalarCellularLightArray::Cell Cell;
|
||||
|
||||
void setParameters(Json const& config);
|
||||
|
||||
void begin(Vec2F const& queryPosition);
|
||||
|
||||
RectI calculationRegion() const;
|
||||
|
||||
void setCell(Vec2I const& position, Cell const& cell);
|
||||
void setCellColumn(Vec2I const& position, Cell const* cells, size_t count);
|
||||
|
||||
void addSpreadLight(Vec2F const& position, float light);
|
||||
void addPointLight(Vec2F const& position, float light, float beam, float beamAngle, float beamAmbience);
|
||||
|
||||
float calculate();
|
||||
|
||||
private:
|
||||
ScalarCellularLightArray m_lightArray;
|
||||
Vec2F m_queryPosition;
|
||||
RectI m_queryRegion;;
|
||||
RectI m_calculationRegion;
|
||||
};
|
||||
|
||||
inline size_t CellularLightingCalculator::baseIndexFor(Vec2I const& position) {
|
||||
return (position[0] - m_calculationRegion.xMin()) * m_calculationRegion.height() + position[1] - m_calculationRegion.yMin();
|
||||
}
|
||||
|
||||
inline void CellularLightingCalculator::setCellIndex(size_t cellIndex, Vec3F const& light, bool obstacle) {
|
||||
if (m_monochrome)
|
||||
m_lightArray.right().cellAtIndex(cellIndex) = ScalarCellularLightArray::Cell{light.sum() / 3, obstacle};
|
||||
else
|
||||
m_lightArray.left().cellAtIndex(cellIndex) = ColoredCellularLightArray::Cell{light, obstacle};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
658
source/base/StarCellularLiquid.hpp
Normal file
658
source/base/StarCellularLiquid.hpp
Normal file
|
@ -0,0 +1,658 @@
|
|||
#ifndef STAR_CELLULAR_LIQUID_HPP
|
||||
#define STAR_CELLULAR_LIQUID_HPP
|
||||
|
||||
#include "StarVariant.hpp"
|
||||
#include "StarRect.hpp"
|
||||
#include "StarMultiArray.hpp"
|
||||
#include "StarMap.hpp"
|
||||
#include "StarOrderedSet.hpp"
|
||||
#include "StarRandom.hpp"
|
||||
#include "StarBlockAllocator.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
struct CellularLiquidCollisionCell {};
|
||||
|
||||
template <typename LiquidId>
|
||||
struct CellularLiquidFlowCell {
|
||||
Maybe<LiquidId> liquid;
|
||||
float level;
|
||||
float pressure;
|
||||
};
|
||||
|
||||
template <typename LiquidId>
|
||||
struct CellularLiquidSourceCell {
|
||||
LiquidId liquid;
|
||||
float pressure;
|
||||
};
|
||||
|
||||
template <typename LiquidId>
|
||||
using CellularLiquidCell = Variant<CellularLiquidCollisionCell, CellularLiquidFlowCell<LiquidId>, CellularLiquidSourceCell<LiquidId>>;
|
||||
|
||||
template <typename LiquidId>
|
||||
struct CellularLiquidWorld {
|
||||
virtual ~CellularLiquidWorld();
|
||||
|
||||
virtual Vec2I uniqueLocation(Vec2I const& location) const;
|
||||
|
||||
virtual CellularLiquidCell<LiquidId> cell(Vec2I const& location) const = 0;
|
||||
|
||||
// Should return an amount between 0.0 and 1.0 as a percentage of liquid
|
||||
// drain at this position
|
||||
virtual float drainLevel(Vec2I const& location) const;
|
||||
|
||||
// Will be called only on cells which for which the cell method returned a
|
||||
// flow cell, to update the flow cell.
|
||||
virtual void setFlow(Vec2I const& location, CellularLiquidFlowCell<LiquidId> const& flow) = 0;
|
||||
|
||||
// Called once for every active liquid <-> liquid interaction of different
|
||||
// liquid types each update. Will be called AFTER pushing all the flow
|
||||
// values back out so modifications to liquids are sensible.
|
||||
virtual void liquidInteraction(Vec2I const& a, LiquidId aLiquid, Vec2I const& b, LiquidId bLiquid);
|
||||
// Called once for every liquid collision each update. Also called after
|
||||
// pushing all the flow values out, so changes to liquids can sensibly be
|
||||
// performed here.
|
||||
virtual void liquidCollision(Vec2I const& pos, LiquidId liquid, Vec2I const& collisionPos);
|
||||
};
|
||||
|
||||
struct LiquidCellEngineParameters {
|
||||
float lateralMoveFactor;
|
||||
float spreadOverfillUpFactor;
|
||||
float spreadOverfillLateralFactor;
|
||||
float spreadOverfillDownFactor;
|
||||
float pressureEqualizeFactor;
|
||||
float pressureMoveFactor;
|
||||
float maximumPressureLevelImbalance;
|
||||
float minimumLivenPressureChange;
|
||||
float minimumLivenLevelChange;
|
||||
float minimumLiquidLevel;
|
||||
float interactTransformationLevel;
|
||||
};
|
||||
|
||||
template <typename LiquidId>
|
||||
class LiquidCellEngine {
|
||||
public:
|
||||
typedef shared_ptr<CellularLiquidWorld<LiquidId>> CellularLiquidWorldPtr;
|
||||
|
||||
LiquidCellEngine(LiquidCellEngineParameters parameters, CellularLiquidWorldPtr cellWorld);
|
||||
|
||||
unsigned liquidTickDelta(LiquidId liquid);
|
||||
void setLiquidTickDelta(LiquidId liquid, unsigned tickDelta);
|
||||
|
||||
void setProcessingLimit(Maybe<unsigned> processingLimit);
|
||||
|
||||
List<RectI> noProcessingLimitRegions() const;
|
||||
void setNoProcessingLimitRegions(List<RectI> noProcessingLimitRegions);
|
||||
|
||||
void visitLocation(Vec2I const& location);
|
||||
void visitRegion(RectI const& region);
|
||||
|
||||
void update();
|
||||
|
||||
size_t activeCells() const;
|
||||
size_t activeCells(LiquidId liquid) const;
|
||||
bool isActive(Vec2I const& pos) const;
|
||||
|
||||
private:
|
||||
enum class Adjacency {
|
||||
Left,
|
||||
Right,
|
||||
Bottom,
|
||||
Top
|
||||
};
|
||||
|
||||
struct WorkingCell {
|
||||
Vec2I position;
|
||||
Maybe<LiquidId> liquid;
|
||||
bool sourceCell;
|
||||
float level;
|
||||
float pressure;
|
||||
|
||||
WorkingCell* leftCell;
|
||||
WorkingCell* rightCell;
|
||||
WorkingCell* topCell;
|
||||
WorkingCell* bottomCell;
|
||||
};
|
||||
|
||||
template <typename Key, typename Value>
|
||||
using BAHashMap = StableHashMap<Key, Value, hash<Key>, std::equal_to<Key>, BlockAllocator<pair<Key const, Value>, 4096>>;
|
||||
|
||||
template <typename Value>
|
||||
using BAHashSet = HashSet<Value, hash<Value>, std::equal_to<Value>>;
|
||||
|
||||
template <typename Value>
|
||||
using BAOrderedHashSet = OrderedHashSet<Value, hash<Value>, std::equal_to<Value>, BlockAllocator<Value, 4096>>;
|
||||
|
||||
void setup();
|
||||
void applyPressure();
|
||||
void spreadPressure();
|
||||
void limitPressure();
|
||||
void pressureMove();
|
||||
void spreadOverfill();
|
||||
void levelMove();
|
||||
void findInteractions();
|
||||
void finish();
|
||||
|
||||
WorkingCell* workingCell(Vec2I p);
|
||||
WorkingCell* adjacentCell(WorkingCell* cell, Adjacency adjacency);
|
||||
|
||||
void setPressure(float pressure, WorkingCell& cell);
|
||||
void transferPressure(float amount, WorkingCell& source, WorkingCell& dest, bool allowReverse);
|
||||
void transferLevel(float amount, WorkingCell& source, WorkingCell& dest, bool allowReverse);
|
||||
void setLevel(float level, WorkingCell& cell);
|
||||
|
||||
RandomSource m_random;
|
||||
LiquidCellEngineParameters m_engineParameters;
|
||||
CellularLiquidWorldPtr m_cellWorld;
|
||||
|
||||
BAHashMap<LiquidId, BAOrderedHashSet<Vec2I>> m_activeCells;
|
||||
BAHashMap<LiquidId, unsigned> m_liquidTickDeltas;
|
||||
Maybe<unsigned> m_processingLimit;
|
||||
List<RectI> m_noProcessingLimitRegions;
|
||||
uint64_t m_step;
|
||||
|
||||
BAHashMap<Vec2I, Maybe<WorkingCell>> m_workingCells;
|
||||
List<WorkingCell*> m_currentActiveCells;
|
||||
BAHashSet<Vec2I> m_nextActiveCells;
|
||||
BAHashSet<tuple<Vec2I, LiquidId, Vec2I, LiquidId>> m_liquidInteractions;
|
||||
BAHashSet<tuple<Vec2I, LiquidId, Vec2I>> m_liquidCollisions;
|
||||
};
|
||||
|
||||
template <typename LiquidId>
|
||||
CellularLiquidWorld<LiquidId>::~CellularLiquidWorld() {}
|
||||
|
||||
template <typename LiquidId>
|
||||
Vec2I CellularLiquidWorld<LiquidId>::uniqueLocation(Vec2I const& location) const {
|
||||
return location;
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
float CellularLiquidWorld<LiquidId>::drainLevel(Vec2I const&) const {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void CellularLiquidWorld<LiquidId>::liquidInteraction(Vec2I const&, LiquidId, Vec2I const&, LiquidId) {}
|
||||
|
||||
template <typename LiquidId>
|
||||
void CellularLiquidWorld<LiquidId>::liquidCollision(Vec2I const&, LiquidId, Vec2I const&) {}
|
||||
|
||||
template <typename LiquidId>
|
||||
LiquidCellEngine<LiquidId>::LiquidCellEngine(LiquidCellEngineParameters parameters, CellularLiquidWorldPtr cellWorld)
|
||||
: m_engineParameters(parameters), m_cellWorld(cellWorld), m_step(0) {}
|
||||
|
||||
template <typename LiquidId>
|
||||
unsigned LiquidCellEngine<LiquidId>::liquidTickDelta(LiquidId liquid) {
|
||||
return m_liquidTickDeltas.value(liquid, 1);
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::setLiquidTickDelta(LiquidId liquid, unsigned tickDelta) {
|
||||
m_liquidTickDeltas[liquid] = tickDelta;
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::setProcessingLimit(Maybe<unsigned> processingLimit) {
|
||||
m_processingLimit = processingLimit;
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
List<RectI> LiquidCellEngine<LiquidId>::noProcessingLimitRegions() const {
|
||||
return m_noProcessingLimitRegions;
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::setNoProcessingLimitRegions(List<RectI> noProcessingLimitRegions) {
|
||||
m_noProcessingLimitRegions = noProcessingLimitRegions;
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::visitLocation(Vec2I const& p) {
|
||||
m_nextActiveCells.add(p);
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::visitRegion(RectI const& region) {
|
||||
for (int x = region.xMin(); x < region.xMax(); ++x) {
|
||||
for (int y = region.yMin(); y < region.yMax(); ++y)
|
||||
m_nextActiveCells.add({x, y});
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::update() {
|
||||
setup();
|
||||
applyPressure();
|
||||
spreadPressure();
|
||||
limitPressure();
|
||||
pressureMove();
|
||||
spreadOverfill();
|
||||
levelMove();
|
||||
findInteractions();
|
||||
finish();
|
||||
|
||||
++m_step;
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
size_t LiquidCellEngine<LiquidId>::activeCells() const {
|
||||
size_t totalSize = 0;
|
||||
for (auto const& p : m_activeCells)
|
||||
totalSize += p.second.size();
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
size_t LiquidCellEngine<LiquidId>::activeCells(LiquidId liquid) const {
|
||||
return m_activeCells.value(liquid).size();
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
bool LiquidCellEngine<LiquidId>::isActive(Vec2I const& pos) const {
|
||||
for (auto const& p : m_activeCells) {
|
||||
if (p.second.contains(pos))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::setup() {
|
||||
// In case an exception occurred during the last update, clear potentially
|
||||
// stale data here
|
||||
m_workingCells.clear();
|
||||
m_currentActiveCells.clear();
|
||||
|
||||
for (auto& activeCellsPair : m_activeCells) {
|
||||
unsigned tickDelta = liquidTickDelta(activeCellsPair.first);
|
||||
if (tickDelta == 0 || m_step % tickDelta != 0)
|
||||
continue;
|
||||
|
||||
size_t limitedCellNumber = 0;
|
||||
for (auto const& pos : activeCellsPair.second.values()) {
|
||||
if (m_processingLimit) {
|
||||
bool foundInUnlimitedRegion = false;
|
||||
for (auto const& region : m_noProcessingLimitRegions) {
|
||||
if (region.contains(pos)) {
|
||||
foundInUnlimitedRegion = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundInUnlimitedRegion) {
|
||||
if (limitedCellNumber < *m_processingLimit)
|
||||
++limitedCellNumber;
|
||||
else
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
auto cell = workingCell(pos);
|
||||
if (!cell || cell->liquid != activeCellsPair.first) {
|
||||
activeCellsPair.second.remove(pos);
|
||||
} else {
|
||||
m_currentActiveCells.append(cell);
|
||||
activeCellsPair.second.remove(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort(m_currentActiveCells, [](WorkingCell* lhs, WorkingCell* rhs) {
|
||||
return lhs->position[1] < rhs->position[1];
|
||||
});
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::applyPressure() {
|
||||
for (auto const& selfCell : m_currentActiveCells) {
|
||||
if (!selfCell->liquid || selfCell->sourceCell)
|
||||
continue;
|
||||
|
||||
auto topCell = adjacentCell(selfCell, Adjacency::Top);
|
||||
if (topCell && selfCell->liquid == topCell->liquid)
|
||||
setPressure(max(selfCell->pressure, topCell->pressure + min(topCell->level, 1.0f)), *selfCell);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::spreadPressure() {
|
||||
for (auto const& selfCell : m_currentActiveCells) {
|
||||
if (!selfCell->liquid)
|
||||
continue;
|
||||
|
||||
auto spreadPressure = [&](Adjacency adjacency, float bias) {
|
||||
auto targetCell = adjacentCell(selfCell, adjacency);
|
||||
if (targetCell && !targetCell->sourceCell)
|
||||
transferPressure((selfCell->pressure + bias - targetCell->pressure) * m_engineParameters.pressureEqualizeFactor, *selfCell, *targetCell, true);
|
||||
};
|
||||
|
||||
if (m_random.randb()) {
|
||||
spreadPressure(Adjacency::Left, 0.0f);
|
||||
spreadPressure(Adjacency::Right, 0.0f);
|
||||
} else {
|
||||
spreadPressure(Adjacency::Right, 0.0f);
|
||||
spreadPressure(Adjacency::Left, 0.0f);
|
||||
}
|
||||
|
||||
spreadPressure(Adjacency::Bottom, 1.0f);
|
||||
spreadPressure(Adjacency::Top, -1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::limitPressure() {
|
||||
for (auto const& selfCell : m_currentActiveCells) {
|
||||
float level = min(selfCell->level, 1.0f);
|
||||
auto topCell = adjacentCell(selfCell, Adjacency::Top);
|
||||
|
||||
// Force the pressure to the cell level if there is empty space above,
|
||||
// otherwise simply make sure the pressure is at least the level
|
||||
if (topCell && !topCell->liquid)
|
||||
setPressure(level, *selfCell);
|
||||
else
|
||||
setPressure(max(selfCell->pressure, level), *selfCell);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::pressureMove() {
|
||||
for (auto const& selfCell : m_currentActiveCells) {
|
||||
if (!selfCell->liquid)
|
||||
continue;
|
||||
|
||||
auto pressureMove = [&](Adjacency adjacency) {
|
||||
auto targetCell = adjacentCell(selfCell, adjacency);
|
||||
if (targetCell && !targetCell->sourceCell && targetCell->level >= selfCell->level) {
|
||||
float amount = (selfCell->pressure - targetCell->pressure) * m_engineParameters.pressureMoveFactor;
|
||||
amount = min(amount, selfCell->level - (1.0f - m_engineParameters.maximumPressureLevelImbalance));
|
||||
amount = min(amount, (1.0f + m_engineParameters.maximumPressureLevelImbalance) - targetCell->level);
|
||||
transferLevel(amount, *selfCell, *targetCell, false);
|
||||
}
|
||||
};
|
||||
|
||||
if (m_random.randb()) {
|
||||
pressureMove(Adjacency::Left);
|
||||
pressureMove(Adjacency::Right);
|
||||
} else {
|
||||
pressureMove(Adjacency::Right);
|
||||
pressureMove(Adjacency::Left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::spreadOverfill() {
|
||||
for (auto const& selfCell : m_currentActiveCells) {
|
||||
if (!selfCell->liquid || selfCell->sourceCell)
|
||||
continue;
|
||||
|
||||
auto spreadOverfill = [&](Adjacency adjacency, float factor) {
|
||||
float overfill = selfCell->level - 1.0f;
|
||||
if (overfill > 0.0f) {
|
||||
auto targetCell = adjacentCell(selfCell, adjacency);
|
||||
if (targetCell)
|
||||
transferLevel(min(overfill, (selfCell->level - targetCell->level)) * factor, *selfCell, *targetCell, false);
|
||||
}
|
||||
};
|
||||
|
||||
spreadOverfill(Adjacency::Top, m_engineParameters.spreadOverfillUpFactor);
|
||||
|
||||
if (m_random.randb()) {
|
||||
spreadOverfill(Adjacency::Left, m_engineParameters.spreadOverfillLateralFactor);
|
||||
spreadOverfill(Adjacency::Right, m_engineParameters.spreadOverfillLateralFactor);
|
||||
} else {
|
||||
spreadOverfill(Adjacency::Right, m_engineParameters.spreadOverfillLateralFactor);
|
||||
spreadOverfill(Adjacency::Left, m_engineParameters.spreadOverfillLateralFactor);
|
||||
}
|
||||
|
||||
spreadOverfill(Adjacency::Bottom, m_engineParameters.spreadOverfillDownFactor);
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::levelMove() {
|
||||
for (auto const& selfCell : m_currentActiveCells) {
|
||||
if (!selfCell->liquid)
|
||||
continue;
|
||||
|
||||
auto belowCell = adjacentCell(selfCell, Adjacency::Bottom);
|
||||
if (belowCell)
|
||||
transferLevel(min(1.0f - belowCell->level, selfCell->level), *selfCell, *belowCell, false);
|
||||
|
||||
setLevel(selfCell->level * (1.0f - m_cellWorld->drainLevel(selfCell->position)), *selfCell);
|
||||
|
||||
auto lateralMove = [&](Adjacency adjacency) {
|
||||
auto targetCell = adjacentCell(selfCell, adjacency);
|
||||
if (targetCell)
|
||||
transferLevel((selfCell->level - targetCell->level) * m_engineParameters.lateralMoveFactor, *selfCell, *targetCell, false);
|
||||
};
|
||||
|
||||
if (m_random.randb()) {
|
||||
lateralMove(Adjacency::Left);
|
||||
lateralMove(Adjacency::Right);
|
||||
} else {
|
||||
lateralMove(Adjacency::Right);
|
||||
lateralMove(Adjacency::Left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::findInteractions() {
|
||||
for (auto const& selfCell : m_currentActiveCells) {
|
||||
if (!selfCell->liquid)
|
||||
continue;
|
||||
|
||||
for (auto adjacency : {Adjacency::Bottom, Adjacency::Top, Adjacency::Left, Adjacency::Right}) {
|
||||
auto targetCell = adjacentCell(selfCell, adjacency);
|
||||
if (!targetCell) {
|
||||
Vec2I adjacentPos = selfCell->position;
|
||||
if (adjacency == Adjacency::Left)
|
||||
adjacentPos += Vec2I(-1, 0);
|
||||
else if (adjacency == Adjacency::Right)
|
||||
adjacentPos += Vec2I(1, 0);
|
||||
else if (adjacency == Adjacency::Bottom)
|
||||
adjacentPos += Vec2I(0, -1);
|
||||
else if (adjacency == Adjacency::Top)
|
||||
adjacentPos += Vec2I(0, 1);
|
||||
m_liquidCollisions.add(make_tuple(selfCell->position, *selfCell->liquid, adjacentPos));
|
||||
|
||||
} else if (targetCell->liquid && *targetCell->liquid != *selfCell->liquid) {
|
||||
if (targetCell->level <= m_engineParameters.interactTransformationLevel
|
||||
|| selfCell->level <= m_engineParameters.interactTransformationLevel) {
|
||||
if (selfCell->level > targetCell->level)
|
||||
targetCell->liquid = selfCell->liquid;
|
||||
else
|
||||
selfCell->liquid = targetCell->liquid;
|
||||
} else {
|
||||
// Make sure to add the point pair in a predictable order so that any
|
||||
// combination of Vec2I points will be unique in m_liquidInteractions
|
||||
if (selfCell->position < targetCell->position)
|
||||
m_liquidInteractions.add(make_tuple(selfCell->position, *selfCell->liquid, targetCell->position, *targetCell->liquid));
|
||||
else
|
||||
m_liquidInteractions.add(make_tuple(targetCell->position, *targetCell->liquid, selfCell->position, *selfCell->liquid));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::finish() {
|
||||
m_currentActiveCells.clear();
|
||||
|
||||
for (auto& workingCellPair : take(m_workingCells)) {
|
||||
if (workingCellPair.second && !workingCellPair.second->sourceCell) {
|
||||
if (workingCellPair.second->liquid) {
|
||||
if (workingCellPair.second->level < m_engineParameters.minimumLiquidLevel)
|
||||
workingCellPair.second->level = 0.0f;
|
||||
} else {
|
||||
workingCellPair.second->level = 0.0f;
|
||||
}
|
||||
|
||||
if (workingCellPair.second->level == 0.0f) {
|
||||
workingCellPair.second->liquid = {};
|
||||
workingCellPair.second->pressure = 0.0f;
|
||||
}
|
||||
|
||||
m_cellWorld->setFlow(workingCellPair.second->position, CellularLiquidFlowCell<LiquidId>{
|
||||
workingCellPair.second->liquid, workingCellPair.second->level, workingCellPair.second->pressure});
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const& interaction : take(m_liquidInteractions))
|
||||
m_cellWorld->liquidInteraction(get<0>(interaction), get<1>(interaction), get<2>(interaction), get<3>(interaction));
|
||||
|
||||
for (auto const& interaction : take(m_liquidCollisions))
|
||||
m_cellWorld->liquidCollision(get<0>(interaction), get<1>(interaction), get<2>(interaction));
|
||||
|
||||
for (auto const& c : take(m_nextActiveCells)) {
|
||||
auto visit = [this](Vec2I p) {
|
||||
p = m_cellWorld->uniqueLocation(p);
|
||||
auto cell = workingCell(p);
|
||||
if (cell && cell->liquid)
|
||||
m_activeCells[*cell->liquid].add(p);
|
||||
};
|
||||
|
||||
visit(c);
|
||||
visit(c + Vec2I(-1, 0));
|
||||
visit(c + Vec2I(1, 0));
|
||||
visit(c + Vec2I(0, -1));
|
||||
visit(c + Vec2I(0, 1));
|
||||
}
|
||||
|
||||
eraseWhere(m_activeCells, [](auto const& p) {
|
||||
return p.second.empty();
|
||||
});
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
typename LiquidCellEngine<LiquidId>::WorkingCell* LiquidCellEngine<LiquidId>::workingCell(Vec2I p) {
|
||||
p = m_cellWorld->uniqueLocation(p);
|
||||
|
||||
auto res = m_workingCells.insert(make_pair(p, Maybe<WorkingCell>()));
|
||||
if (res.second) {
|
||||
auto cellData = m_cellWorld->cell(p);
|
||||
if (auto flowCell = cellData.template ptr<CellularLiquidFlowCell<LiquidId>>())
|
||||
res.first->second = WorkingCell{p, flowCell->liquid, false, flowCell->level, flowCell->pressure, nullptr, nullptr, nullptr, nullptr};
|
||||
else if (auto sourceCell = cellData.template ptr<CellularLiquidSourceCell<LiquidId>>())
|
||||
res.first->second = WorkingCell{p, sourceCell->liquid, true, 1.0f, sourceCell->pressure, nullptr, nullptr, nullptr, nullptr};
|
||||
}
|
||||
return res.first->second.ptr();
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
typename LiquidCellEngine<LiquidId>::WorkingCell* LiquidCellEngine<LiquidId>::adjacentCell(
|
||||
WorkingCell* cell, Adjacency adjacency) {
|
||||
auto getCell = [this](WorkingCell*& cellptr, Vec2I cellPos) {
|
||||
if (cellptr)
|
||||
return cellptr;
|
||||
cellptr = workingCell(cellPos);
|
||||
return cellptr;
|
||||
};
|
||||
|
||||
if (adjacency == Adjacency::Left)
|
||||
return getCell(cell->leftCell, cell->position + Vec2I(-1, 0));
|
||||
else if (adjacency == Adjacency::Right)
|
||||
return getCell(cell->rightCell, cell->position + Vec2I(1, 0));
|
||||
else if (adjacency == Adjacency::Bottom)
|
||||
return getCell(cell->bottomCell, cell->position + Vec2I(0, -1));
|
||||
else if (adjacency == Adjacency::Top)
|
||||
return getCell(cell->topCell, cell->position + Vec2I(0, 1));
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::setPressure(float pressure, WorkingCell& cell) {
|
||||
if (!cell.liquid || cell.sourceCell)
|
||||
return;
|
||||
|
||||
if (fabs(cell.pressure - pressure) > m_engineParameters.minimumLivenPressureChange)
|
||||
m_nextActiveCells.add(cell.position);
|
||||
cell.pressure = pressure;
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::transferPressure(float amount, WorkingCell& source, WorkingCell& dest, bool allowReverse) {
|
||||
if (amount < 0.0f && allowReverse) {
|
||||
return transferPressure(-amount, dest, source, false);
|
||||
} else if (amount > 0.0f) {
|
||||
if (!source.liquid)
|
||||
return;
|
||||
|
||||
if (source.sourceCell && dest.sourceCell)
|
||||
return;
|
||||
|
||||
if (dest.liquid && dest.liquid != source.liquid)
|
||||
return;
|
||||
|
||||
amount = min(amount, source.pressure);
|
||||
|
||||
if (!source.sourceCell)
|
||||
source.pressure -= amount;
|
||||
|
||||
if (dest.liquid && !dest.sourceCell)
|
||||
dest.pressure += amount;
|
||||
|
||||
if (amount > m_engineParameters.minimumLivenPressureChange) {
|
||||
m_nextActiveCells.add(source.position);
|
||||
m_nextActiveCells.add(dest.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::setLevel(float level, WorkingCell& cell) {
|
||||
if (!cell.liquid || cell.sourceCell)
|
||||
return;
|
||||
|
||||
if (fabs(cell.level - level) > m_engineParameters.minimumLivenLevelChange)
|
||||
m_nextActiveCells.add(cell.position);
|
||||
|
||||
cell.level = level;
|
||||
|
||||
if (cell.level <= 0.0f) {
|
||||
cell.liquid = {};
|
||||
cell.level = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename LiquidId>
|
||||
void LiquidCellEngine<LiquidId>::transferLevel(
|
||||
float amount, WorkingCell& source, WorkingCell& dest, bool allowReverse) {
|
||||
if (amount < 0.0f && allowReverse) {
|
||||
transferLevel(-amount, dest, source, false);
|
||||
|
||||
} else if (amount > 0.0f) {
|
||||
if (!source.liquid)
|
||||
return;
|
||||
|
||||
if (source.sourceCell && dest.sourceCell)
|
||||
return;
|
||||
|
||||
if (dest.liquid && dest.liquid != source.liquid)
|
||||
return;
|
||||
|
||||
amount = min(amount, source.level);
|
||||
if (!source.sourceCell)
|
||||
source.level -= amount;
|
||||
|
||||
if (!dest.sourceCell) {
|
||||
dest.level += amount;
|
||||
dest.liquid = source.liquid;
|
||||
}
|
||||
|
||||
if (!source.sourceCell && source.level == 0.0f)
|
||||
source.liquid = {};
|
||||
|
||||
if (amount > m_engineParameters.minimumLivenLevelChange) {
|
||||
m_nextActiveCells.add(source.position);
|
||||
m_nextActiveCells.add(dest.position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
54
source/base/StarConfiguration.cpp
Normal file
54
source/base/StarConfiguration.cpp
Normal file
|
@ -0,0 +1,54 @@
|
|||
#include "StarConfiguration.hpp"
|
||||
#include "StarFile.hpp"
|
||||
#include "StarLogging.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
Configuration::Configuration(Json defaultConfiguration, Json currentConfiguration)
|
||||
: m_defaultConfig(defaultConfiguration), m_currentConfig(currentConfiguration) {}
|
||||
|
||||
Json Configuration::defaultConfiguration() const {
|
||||
return m_defaultConfig;
|
||||
}
|
||||
|
||||
Json Configuration::currentConfiguration() const {
|
||||
return m_currentConfig;
|
||||
}
|
||||
|
||||
Json Configuration::get(String const& key) const {
|
||||
MutexLocker locker(m_mutex);
|
||||
return m_currentConfig.get(key, {});
|
||||
}
|
||||
|
||||
Json Configuration::getPath(String const& path) const {
|
||||
MutexLocker locker(m_mutex);
|
||||
return m_currentConfig.query(path, {});
|
||||
}
|
||||
|
||||
Json Configuration::getDefault(String const& key) const {
|
||||
MutexLocker locker(m_mutex);
|
||||
return m_defaultConfig.get(key, {});
|
||||
}
|
||||
|
||||
Json Configuration::getDefaultPath(String const& path) const {
|
||||
MutexLocker locker(m_mutex);
|
||||
return m_defaultConfig.query(path, {});
|
||||
}
|
||||
|
||||
void Configuration::set(String const& key, Json const& value) {
|
||||
MutexLocker locker(m_mutex);
|
||||
if (key == "configurationVersion")
|
||||
throw ConfigurationException("cannot set configurationVersion");
|
||||
|
||||
m_currentConfig = m_currentConfig.set(key, value);
|
||||
}
|
||||
|
||||
void Configuration::setPath(String const& path, Json const& value) {
|
||||
MutexLocker locker(m_mutex);
|
||||
if (path.splitAny("[].").get(0) == "configurationVersion")
|
||||
throw ConfigurationException("cannot set configurationVersion");
|
||||
|
||||
m_currentConfig = m_currentConfig.setPath(path, value);
|
||||
}
|
||||
|
||||
}
|
39
source/base/StarConfiguration.hpp
Normal file
39
source/base/StarConfiguration.hpp
Normal file
|
@ -0,0 +1,39 @@
|
|||
#ifndef STAR_CONFIGURATION_HPP
|
||||
#define STAR_CONFIGURATION_HPP
|
||||
|
||||
#include "StarJson.hpp"
|
||||
#include "StarThread.hpp"
|
||||
#include "StarVersion.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(Configuration);
|
||||
|
||||
STAR_EXCEPTION(ConfigurationException, StarException);
|
||||
|
||||
class Configuration {
|
||||
public:
|
||||
Configuration(Json defaultConfiguration, Json currentConfiguration);
|
||||
|
||||
Json defaultConfiguration() const;
|
||||
Json currentConfiguration() const;
|
||||
|
||||
Json get(String const& key) const;
|
||||
Json getPath(String const& path) const;
|
||||
|
||||
Json getDefault(String const& key) const;
|
||||
Json getDefaultPath(String const& path) const;
|
||||
|
||||
void set(String const& key, Json const& value);
|
||||
void setPath(String const& path, Json const& value);
|
||||
|
||||
private:
|
||||
mutable Mutex m_mutex;
|
||||
|
||||
Json m_defaultConfig;
|
||||
Json m_currentConfig;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
96
source/base/StarDirectoryAssetSource.cpp
Normal file
96
source/base/StarDirectoryAssetSource.cpp
Normal file
|
@ -0,0 +1,96 @@
|
|||
#include "StarDirectoryAssetSource.hpp"
|
||||
#include "StarFile.hpp"
|
||||
#include "StarJsonExtra.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
DirectoryAssetSource::DirectoryAssetSource(String const& baseDirectory, StringList const& ignorePatterns) {
|
||||
m_baseDirectory = baseDirectory;
|
||||
m_ignorePatterns = ignorePatterns;
|
||||
|
||||
// Load metadata from either /_metadata or /.metadata, in that order.
|
||||
for (auto fileName : {"/_metadata", "/.metadata"}) {
|
||||
String metadataFile = toFilesystem(fileName);
|
||||
if (File::isFile(metadataFile)) {
|
||||
try {
|
||||
m_metadataFile = String(fileName);
|
||||
m_metadata = Json::parseJson(File::readFileString(metadataFile)).toObject();
|
||||
break;
|
||||
} catch (JsonException const& e) {
|
||||
throw AssetSourceException(strf("Could not load metadata file '%s' from assets", metadataFile), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't scan metadata files
|
||||
m_ignorePatterns.append("^/_metadata$");
|
||||
m_ignorePatterns.append("^/\\.metadata$");
|
||||
|
||||
scanAll("/", m_assetPaths);
|
||||
|
||||
m_assetPaths.sort();
|
||||
}
|
||||
|
||||
JsonObject DirectoryAssetSource::metadata() const {
|
||||
return m_metadata;
|
||||
}
|
||||
|
||||
StringList DirectoryAssetSource::assetPaths() const {
|
||||
return m_assetPaths;
|
||||
}
|
||||
|
||||
IODevicePtr DirectoryAssetSource::open(String const& path) {
|
||||
auto file = make_shared<File>(toFilesystem(path));
|
||||
file->open(IOMode::Read);
|
||||
return file;
|
||||
}
|
||||
|
||||
ByteArray DirectoryAssetSource::read(String const& path) {
|
||||
auto device = open(path);
|
||||
return device->readBytes(device->size());
|
||||
}
|
||||
|
||||
String DirectoryAssetSource::toFilesystem(String const& path) const {
|
||||
if (!path.beginsWith("/"))
|
||||
throw AssetSourceException::format("Asset path '%s' must be absolute in DirectoryAssetSource::toFilesystem", path);
|
||||
else
|
||||
return File::relativeTo(m_baseDirectory, File::convertDirSeparators(path.substr(1)));
|
||||
}
|
||||
|
||||
void DirectoryAssetSource::setMetadata(JsonObject metadata) {
|
||||
if (metadata != m_metadata) {
|
||||
if (!m_metadataFile)
|
||||
m_metadataFile = String("/_metadata");
|
||||
|
||||
m_metadata = move(metadata);
|
||||
|
||||
if (m_metadata.empty())
|
||||
File::remove(toFilesystem(*m_metadataFile));
|
||||
else
|
||||
File::writeFile(Json(m_metadata).printJson(2, true), toFilesystem(*m_metadataFile));
|
||||
}
|
||||
}
|
||||
|
||||
void DirectoryAssetSource::scanAll(String const& assetDirectory, StringList& output) const {
|
||||
auto shouldIgnore = [this](String const& assetPath) {
|
||||
for (auto const& pattern : m_ignorePatterns) {
|
||||
if (assetPath.regexMatch(pattern, false, false))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// path must be passed in including the trailing '/'
|
||||
String fsDirectory = toFilesystem(assetDirectory);
|
||||
for (auto entry : File::dirList(fsDirectory)) {
|
||||
String assetPath = assetDirectory + entry.first;
|
||||
if (entry.second) {
|
||||
scanAll(assetPath + "/", output);
|
||||
} else {
|
||||
if (!shouldIgnore(assetPath))
|
||||
output.append(move(assetPath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
41
source/base/StarDirectoryAssetSource.hpp
Normal file
41
source/base/StarDirectoryAssetSource.hpp
Normal file
|
@ -0,0 +1,41 @@
|
|||
#ifndef STAR_DIRECTORY_ASSET_SOURCE_HPP
|
||||
#define STAR_DIRECTORY_ASSET_SOURCE_HPP
|
||||
|
||||
#include "StarAssetSource.hpp"
|
||||
#include "StarString.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(DirectoryAssetSource);
|
||||
|
||||
class DirectoryAssetSource : public AssetSource {
|
||||
public:
|
||||
// Any file that forms an asset path that matches any of the patterns in
|
||||
// 'ignorePatterns' is ignored.
|
||||
DirectoryAssetSource(String const& baseDirectory, StringList const& ignorePatterns = {});
|
||||
|
||||
JsonObject metadata() const override;
|
||||
StringList assetPaths() const override;
|
||||
|
||||
IODevicePtr open(String const& path) override;
|
||||
ByteArray read(String const& path) override;
|
||||
|
||||
// Converts an asset path to the path on the filesystem
|
||||
String toFilesystem(String const& path) const;
|
||||
|
||||
// Update metadata file or add a new one.
|
||||
void setMetadata(JsonObject metadata);
|
||||
|
||||
private:
|
||||
void scanAll(String const& assetDirectory, StringList& output) const;
|
||||
|
||||
String m_baseDirectory;
|
||||
List<String> m_ignorePatterns;
|
||||
Maybe<String> m_metadataFile;
|
||||
JsonObject m_metadata;
|
||||
StringList m_assetPaths;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
486
source/base/StarMixer.cpp
Normal file
486
source/base/StarMixer.cpp
Normal file
|
@ -0,0 +1,486 @@
|
|||
#include "StarMixer.hpp"
|
||||
#include "StarIterator.hpp"
|
||||
#include "StarInterpolation.hpp"
|
||||
#include "StarTime.hpp"
|
||||
#include "StarLogging.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
namespace {
|
||||
float rateOfChangeFromRampTime(float rampTime) {
|
||||
static const float MaxRate = 10000.0f;
|
||||
|
||||
if (rampTime < 1.0f / MaxRate)
|
||||
return MaxRate;
|
||||
else
|
||||
return 1.0f / rampTime;
|
||||
}
|
||||
}
|
||||
|
||||
AudioInstance::AudioInstance(Audio const& audio)
|
||||
: m_audio(audio) {
|
||||
m_mixerGroup = MixerGroup::Effects;
|
||||
|
||||
m_volume = {1.0f, 1.0f, 0};
|
||||
|
||||
m_pitchMultiplier = 1.0f;
|
||||
m_pitchMultiplierTarget = 1.0f;
|
||||
m_pitchMultiplierVelocity = 0;
|
||||
|
||||
m_loops = 0;
|
||||
m_stopping = false;
|
||||
m_finished = false;
|
||||
|
||||
m_rangeMultiplier = 1.0f;
|
||||
|
||||
m_clockStopFadeOut = 0.0f;
|
||||
}
|
||||
|
||||
Maybe<Vec2F> AudioInstance::position() const {
|
||||
MutexLocker locker(m_mutex);
|
||||
return m_position;
|
||||
}
|
||||
|
||||
void AudioInstance::setPosition(Maybe<Vec2F> position) {
|
||||
MutexLocker locker(m_mutex);
|
||||
m_position = position;
|
||||
}
|
||||
|
||||
void AudioInstance::translate(Vec2F const& distance) {
|
||||
MutexLocker locker(m_mutex);
|
||||
if (m_position)
|
||||
*m_position += distance;
|
||||
else
|
||||
m_position = distance;
|
||||
}
|
||||
|
||||
float AudioInstance::rangeMultiplier() const {
|
||||
MutexLocker locker(m_mutex);
|
||||
return m_rangeMultiplier;
|
||||
}
|
||||
|
||||
void AudioInstance::setRangeMultiplier(float rangeMultiplier) {
|
||||
MutexLocker locker(m_mutex);
|
||||
m_rangeMultiplier = rangeMultiplier;
|
||||
}
|
||||
|
||||
void AudioInstance::setVolume(float targetValue, float rampTime) {
|
||||
starAssert(targetValue >= 0);
|
||||
MutexLocker locker(m_mutex);
|
||||
|
||||
if (m_stopping)
|
||||
return;
|
||||
|
||||
if (rampTime <= 0.0f) {
|
||||
m_volume.value = targetValue;
|
||||
m_volume.target = targetValue;
|
||||
m_volume.velocity = 0.0f;
|
||||
} else {
|
||||
m_volume.target = targetValue;
|
||||
m_volume.velocity = rateOfChangeFromRampTime(rampTime);
|
||||
}
|
||||
}
|
||||
|
||||
void AudioInstance::setPitchMultiplier(float targetValue, float rampTime) {
|
||||
starAssert(targetValue >= 0);
|
||||
MutexLocker locker(m_mutex);
|
||||
|
||||
if (m_stopping)
|
||||
return;
|
||||
|
||||
if (rampTime <= 0.0f) {
|
||||
m_pitchMultiplier = targetValue;
|
||||
m_pitchMultiplierTarget = targetValue;
|
||||
m_pitchMultiplierVelocity = 0.0f;
|
||||
} else {
|
||||
m_pitchMultiplierTarget = targetValue;
|
||||
m_pitchMultiplierVelocity = rateOfChangeFromRampTime(rampTime);
|
||||
}
|
||||
}
|
||||
|
||||
int AudioInstance::loops() const {
|
||||
MutexLocker locker(m_mutex);
|
||||
return m_loops;
|
||||
}
|
||||
|
||||
void AudioInstance::setLoops(int loops) {
|
||||
MutexLocker locker(m_mutex);
|
||||
m_loops = loops;
|
||||
}
|
||||
|
||||
double AudioInstance::currentTime() const {
|
||||
return m_audio.currentTime();
|
||||
}
|
||||
|
||||
double AudioInstance::totalTime() const {
|
||||
return m_audio.totalTime();
|
||||
}
|
||||
|
||||
void AudioInstance::seekTime(double time) {
|
||||
m_audio.seekTime(time);
|
||||
}
|
||||
|
||||
MixerGroup AudioInstance::mixerGroup() const {
|
||||
MutexLocker locker(m_mutex);
|
||||
return m_mixerGroup;
|
||||
}
|
||||
|
||||
void AudioInstance::setMixerGroup(MixerGroup mixerGroup) {
|
||||
MutexLocker locker(m_mutex);
|
||||
m_mixerGroup = mixerGroup;
|
||||
}
|
||||
|
||||
void AudioInstance::setClockStart(Maybe<int64_t> clockStartTime) {
|
||||
MutexLocker locker(m_mutex);
|
||||
m_clockStart = clockStartTime;
|
||||
}
|
||||
|
||||
void AudioInstance::setClockStop(Maybe<int64_t> clockStopTime, int64_t fadeOutTime) {
|
||||
MutexLocker locker(m_mutex);
|
||||
m_clockStop = clockStopTime;
|
||||
m_clockStopFadeOut = fadeOutTime;
|
||||
}
|
||||
|
||||
void AudioInstance::stop(float rampTime) {
|
||||
MutexLocker locker(m_mutex);
|
||||
|
||||
if (rampTime <= 0.0f) {
|
||||
m_volume.value = 0.0f;
|
||||
m_volume.target = 0.0f;
|
||||
m_volume.velocity = 0.0f;
|
||||
|
||||
m_pitchMultiplierTarget = 0.0f;
|
||||
m_pitchMultiplierVelocity = 0.0f;
|
||||
} else {
|
||||
m_volume.target = 0.0f;
|
||||
m_volume.velocity = rateOfChangeFromRampTime(rampTime);
|
||||
}
|
||||
|
||||
m_stopping = true;
|
||||
}
|
||||
|
||||
bool AudioInstance::finished() const {
|
||||
return m_finished;
|
||||
}
|
||||
|
||||
Mixer::Mixer(unsigned sampleRate, unsigned channels) {
|
||||
m_sampleRate = sampleRate;
|
||||
m_channels = channels;
|
||||
|
||||
m_volume = {1.0f, 1.0f, 0};
|
||||
|
||||
m_groupVolumes[MixerGroup::Effects] = {1.0f, 1.0f, 0};
|
||||
m_groupVolumes[MixerGroup::Music] = {1.0f, 1.0f, 0};
|
||||
m_groupVolumes[MixerGroup::Cinematic] = {1.0f, 1.0f, 0};
|
||||
}
|
||||
|
||||
unsigned Mixer::sampleRate() const {
|
||||
return m_sampleRate;
|
||||
}
|
||||
|
||||
unsigned Mixer::channels() const {
|
||||
return m_channels;
|
||||
}
|
||||
|
||||
void Mixer::addEffect(String const& effectName, EffectFunction effectFunction, float rampTime) {
|
||||
MutexLocker locker(m_effectsMutex);
|
||||
m_effects[effectName] = make_shared<EffectInfo>(EffectInfo{effectFunction, 0.0f, rateOfChangeFromRampTime(rampTime), false});
|
||||
}
|
||||
|
||||
void Mixer::removeEffect(String const& effectName, float rampTime) {
|
||||
MutexLocker locker(m_effectsMutex);
|
||||
if (m_effects.contains(effectName))
|
||||
m_effects[effectName]->velocity = -rateOfChangeFromRampTime(rampTime);
|
||||
}
|
||||
|
||||
StringList Mixer::currentEffects() {
|
||||
MutexLocker locker(m_effectsMutex);
|
||||
return m_effects.keys();
|
||||
}
|
||||
|
||||
bool Mixer::hasEffect(String const& effectName) {
|
||||
MutexLocker locker(m_effectsMutex);
|
||||
return m_effects.contains(effectName);
|
||||
}
|
||||
|
||||
void Mixer::setVolume(float volume, float rampTime) {
|
||||
MutexLocker locker(m_mutex);
|
||||
m_volume.target = volume;
|
||||
m_volume.velocity = rateOfChangeFromRampTime(rampTime);
|
||||
}
|
||||
|
||||
void Mixer::play(AudioInstancePtr sample) {
|
||||
MutexLocker locker(m_queueMutex);
|
||||
m_audios.add(move(sample), AudioState{List<float>(m_channels, 1.0f)});
|
||||
}
|
||||
|
||||
void Mixer::stopAll(float rampTime) {
|
||||
MutexLocker locker(m_queueMutex);
|
||||
float vel = rateOfChangeFromRampTime(rampTime);
|
||||
for (auto const& p : m_audios)
|
||||
p.first->stop(vel);
|
||||
}
|
||||
|
||||
void Mixer::read(int16_t* outBuffer, size_t frameCount) {
|
||||
// Make this method as least locky as possible by copying all the needed
|
||||
// member data before the expensive audio / effect stuff.
|
||||
unsigned sampleRate;
|
||||
unsigned channels;
|
||||
float volume;
|
||||
float volumeVelocity;
|
||||
float targetVolume;
|
||||
Map<MixerGroup, RampedValue> groupVolumes;
|
||||
|
||||
{
|
||||
MutexLocker locker(m_mutex);
|
||||
sampleRate = m_sampleRate;
|
||||
channels = m_channels;
|
||||
volume = m_volume.value;
|
||||
volumeVelocity = m_volume.velocity;
|
||||
targetVolume = m_volume.target;
|
||||
groupVolumes = m_groupVolumes;
|
||||
}
|
||||
|
||||
size_t bufferSize = frameCount * m_channels;
|
||||
m_mixBuffer.resize(bufferSize, 0);
|
||||
|
||||
float time = (float)frameCount / sampleRate;
|
||||
float beginVolume = volume;
|
||||
float endVolume = approach(targetVolume, volume, volumeVelocity * time);
|
||||
|
||||
Map<MixerGroup, float> groupEndVolumes;
|
||||
for (auto p : groupVolumes)
|
||||
groupEndVolumes[p.first] = approach(p.second.target, p.second.value, p.second.velocity * time);
|
||||
|
||||
auto sampleStartTime = Time::millisecondsSinceEpoch();
|
||||
unsigned millisecondsInBuffer = (bufferSize * 1000) / (channels * sampleRate);
|
||||
auto sampleEndTime = sampleStartTime + millisecondsInBuffer;
|
||||
|
||||
for (size_t i = 0; i < bufferSize; ++i)
|
||||
outBuffer[i] = 0;
|
||||
|
||||
{
|
||||
MutexLocker locker(m_queueMutex);
|
||||
// Mix all active sounds
|
||||
for (auto& p : m_audios) {
|
||||
auto& audioInstance = p.first;
|
||||
auto& audioState = p.second;
|
||||
|
||||
MutexLocker audioLocker(audioInstance->m_mutex);
|
||||
|
||||
if (audioInstance->m_finished)
|
||||
continue;
|
||||
|
||||
if (audioInstance->m_clockStart && *audioInstance->m_clockStart > sampleEndTime)
|
||||
continue;
|
||||
|
||||
float groupVolume = groupVolumes[audioInstance->m_mixerGroup].value;
|
||||
float groupEndVolume = groupEndVolumes[audioInstance->m_mixerGroup];
|
||||
|
||||
bool finished = false;
|
||||
|
||||
float audioStopVolBegin = audioInstance->m_volume.value;
|
||||
float audioStopVolEnd = (audioInstance->m_volume.velocity > 0)
|
||||
? approach(audioInstance->m_volume.target, audioStopVolBegin, audioInstance->m_volume.velocity * time)
|
||||
: audioInstance->m_volume.value;
|
||||
|
||||
float pitchMultiplier = (audioInstance->m_pitchMultiplierVelocity > 0)
|
||||
? approach(audioInstance->m_pitchMultiplierTarget, audioInstance->m_pitchMultiplier, audioInstance->m_pitchMultiplierVelocity * time)
|
||||
: audioInstance->m_pitchMultiplier;
|
||||
|
||||
if (audioStopVolEnd == 0.0f && audioInstance->m_stopping)
|
||||
finished = true;
|
||||
|
||||
size_t ramt = 0;
|
||||
if (audioInstance->m_clockStart && *audioInstance->m_clockStart > sampleStartTime) {
|
||||
int silentSamples = (*audioInstance->m_clockStart - sampleStartTime) * sampleRate / 1000;
|
||||
for (unsigned i = 0; i < silentSamples * channels; ++i)
|
||||
m_mixBuffer[i] = 0;
|
||||
ramt += silentSamples * channels;
|
||||
}
|
||||
ramt += audioInstance->m_audio.resample(channels, sampleRate, m_mixBuffer.ptr() + ramt, bufferSize - ramt, pitchMultiplier);
|
||||
while (ramt != bufferSize && !finished) {
|
||||
// Only seek back to the beginning and read more data if loops is < 0
|
||||
// (loop forever), or we have more loops to go, otherwise, the sample is
|
||||
// finished.
|
||||
if (audioInstance->m_loops != 0) {
|
||||
audioInstance->m_audio.seekSample(0);
|
||||
ramt += audioInstance->m_audio.resample(channels, sampleRate, m_mixBuffer.ptr() + ramt, bufferSize - ramt, pitchMultiplier);
|
||||
if (audioInstance->m_loops > 0)
|
||||
--audioInstance->m_loops;
|
||||
} else {
|
||||
finished = true;
|
||||
}
|
||||
}
|
||||
if (audioInstance->m_clockStop && *audioInstance->m_clockStop < sampleEndTime) {
|
||||
for (size_t s = 0; s < ramt / channels; ++s) {
|
||||
unsigned millisecondsInBuffer = (s * 1000) / sampleRate;
|
||||
auto sampleTime = sampleStartTime + millisecondsInBuffer;
|
||||
if (sampleTime > *audioInstance->m_clockStop) {
|
||||
float volume = 0.0f;
|
||||
if (audioInstance->m_clockStopFadeOut > 0)
|
||||
volume = 1.0f - (float)(sampleTime - *audioInstance->m_clockStop) / (float)audioInstance->m_clockStopFadeOut;
|
||||
|
||||
if (volume <= 0) {
|
||||
for (size_t c = 0; c < channels; ++c)
|
||||
m_mixBuffer[s * channels + c] = 0;
|
||||
} else {
|
||||
for (size_t c = 0; c < channels; ++c)
|
||||
m_mixBuffer[s * channels + c] = m_mixBuffer[s * channels + c] * volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sampleEndTime > *audioInstance->m_clockStop + audioInstance->m_clockStopFadeOut)
|
||||
finished = true;
|
||||
}
|
||||
|
||||
for (size_t s = 0; s < ramt / channels; ++s) {
|
||||
float vol = lerp((float)s / frameCount, beginVolume * groupVolume * audioStopVolBegin, endVolume * groupEndVolume * audioStopVolEnd);
|
||||
for (size_t c = 0; c < channels; ++c) {
|
||||
float sample = m_mixBuffer[s * channels + c] * vol * audioState.positionalChannelVolumes[c] * audioInstance->m_volume.value;
|
||||
outBuffer[s * channels + c] = clamp(sample + outBuffer[s * channels + c], -32767.0f, 32767.0f);
|
||||
}
|
||||
}
|
||||
|
||||
audioInstance->m_volume.value = audioStopVolEnd;
|
||||
audioInstance->m_finished = finished;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
MutexLocker locker(m_effectsMutex);
|
||||
// Apply all active effects
|
||||
for (auto const& pair : m_effects) {
|
||||
auto const& effectInfo = pair.second;
|
||||
if (effectInfo->finished)
|
||||
continue;
|
||||
|
||||
float effectBegin = effectInfo->amount;
|
||||
float effectEnd;
|
||||
if (effectInfo->velocity < 0)
|
||||
effectEnd = approach(0.0f, effectBegin, -effectInfo->velocity * time);
|
||||
else
|
||||
effectEnd = approach(1.0f, effectBegin, effectInfo->velocity * time);
|
||||
|
||||
std::copy(outBuffer, outBuffer + bufferSize, m_mixBuffer.begin());
|
||||
effectInfo->effectFunction(m_mixBuffer.ptr(), frameCount, channels);
|
||||
|
||||
for (size_t s = 0; s < frameCount; ++s) {
|
||||
float amt = lerp((float)s / frameCount, effectBegin, effectEnd);
|
||||
for (size_t c = 0; c < channels; ++c) {
|
||||
int16_t prev = outBuffer[s * channels + c];
|
||||
outBuffer[s * channels + c] = lerp(amt, prev, m_mixBuffer[s * channels + c]);
|
||||
}
|
||||
}
|
||||
|
||||
effectInfo->amount = effectEnd;
|
||||
effectInfo->finished = effectInfo->amount <= 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
MutexLocker locker(m_mutex);
|
||||
|
||||
m_volume.value = endVolume;
|
||||
|
||||
for (auto p : groupEndVolumes)
|
||||
m_groupVolumes[p.first].value = p.second;
|
||||
}
|
||||
}
|
||||
|
||||
Mixer::EffectFunction Mixer::lowpass(size_t avgSize) const {
|
||||
struct LowPass {
|
||||
LowPass(size_t avgSize) : avgSize(avgSize) {}
|
||||
|
||||
size_t avgSize;
|
||||
List<Deque<float>> filter;
|
||||
|
||||
void operator()(int16_t* buffer, size_t frames, unsigned channels) {
|
||||
filter.resize(channels);
|
||||
for (size_t f = 0; f < frames; ++f) {
|
||||
for (size_t c = 0; c < channels; ++c) {
|
||||
auto& filterChannel = filter[c];
|
||||
filterChannel.append(buffer[f * channels + c] / 32767.0f);
|
||||
while (filterChannel.size() > avgSize)
|
||||
filterChannel.takeFirst();
|
||||
buffer[f * channels + c] = sum(filterChannel) / (float)avgSize * 32767.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return LowPass(avgSize);
|
||||
}
|
||||
|
||||
Mixer::EffectFunction Mixer::echo(float time, float dry, float wet) const {
|
||||
struct Echo {
|
||||
unsigned echoLength;
|
||||
float dry;
|
||||
float wet;
|
||||
List<Deque<float>> filter;
|
||||
|
||||
void operator()(int16_t* buffer, size_t frames, unsigned channels) {
|
||||
if (echoLength == 0)
|
||||
return;
|
||||
|
||||
filter.resize(channels);
|
||||
for (size_t c = 0; c < channels; ++c) {
|
||||
auto& filterChannel = filter[c];
|
||||
if (filterChannel.empty())
|
||||
filterChannel.resize(echoLength, 0);
|
||||
}
|
||||
|
||||
for (size_t f = 0; f < frames; ++f) {
|
||||
for (size_t c = 0; c < channels; ++c) {
|
||||
auto& filterChannel = filter[c];
|
||||
buffer[f * channels + c] = buffer[f * channels + c] * dry + filter[c][0] * wet;
|
||||
filterChannel.append(buffer[f * channels + c]);
|
||||
while (filterChannel.size() > echoLength)
|
||||
filterChannel.takeFirst();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return Echo{(unsigned)(time * m_sampleRate), dry, wet, {}};
|
||||
}
|
||||
|
||||
void Mixer::setGroupVolume(MixerGroup group, float targetValue, float rampTime) {
|
||||
MutexLocker locker(m_mutex);
|
||||
if (rampTime <= 0.0f) {
|
||||
m_groupVolumes[group].value = targetValue;
|
||||
m_groupVolumes[group].target = targetValue;
|
||||
m_groupVolumes[group].velocity = 0.0f;
|
||||
} else {
|
||||
m_groupVolumes[group].target = targetValue;
|
||||
m_groupVolumes[group].velocity = rateOfChangeFromRampTime(rampTime);
|
||||
}
|
||||
}
|
||||
|
||||
void Mixer::update(PositionalAttenuationFunction positionalAttenuationFunction) {
|
||||
{
|
||||
MutexLocker locker(m_queueMutex);
|
||||
eraseWhere(m_audios, [&](auto& p) {
|
||||
if (p.first->m_finished)
|
||||
return true;
|
||||
|
||||
if (positionalAttenuationFunction && p.first->m_position) {
|
||||
for (unsigned c = 0; c < m_channels; ++c)
|
||||
p.second.positionalChannelVolumes[c] = 1.0f - positionalAttenuationFunction(c, *p.first->m_position, p.first->m_rangeMultiplier);
|
||||
} else {
|
||||
for (unsigned c = 0; c < m_channels; ++c)
|
||||
p.second.positionalChannelVolumes[c] = 1.0f;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
MutexLocker locker(m_effectsMutex);
|
||||
eraseWhere(m_effects, [](auto const& p) {
|
||||
return p.second->finished;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
167
source/base/StarMixer.hpp
Normal file
167
source/base/StarMixer.hpp
Normal file
|
@ -0,0 +1,167 @@
|
|||
#ifndef STAR_MIXER_HPP
|
||||
#define STAR_MIXER_HPP
|
||||
|
||||
#include "StarAudio.hpp"
|
||||
#include "StarThread.hpp"
|
||||
#include "StarList.hpp"
|
||||
#include "StarMap.hpp"
|
||||
#include "StarSet.hpp"
|
||||
#include "StarVector.hpp"
|
||||
#include "StarMaybe.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(AudioInstance);
|
||||
STAR_CLASS(Mixer);
|
||||
|
||||
struct RampedValue {
|
||||
float value;
|
||||
float target;
|
||||
float velocity;
|
||||
};
|
||||
|
||||
enum class MixerGroup : uint8_t {
|
||||
Effects,
|
||||
Music,
|
||||
Cinematic
|
||||
};
|
||||
|
||||
class AudioInstance {
|
||||
public:
|
||||
AudioInstance(Audio const& audio);
|
||||
|
||||
Maybe<Vec2F> position() const;
|
||||
void setPosition(Maybe<Vec2F> position);
|
||||
// If the audio has no position, sets the position to zero before translating
|
||||
void translate(Vec2F const& distance);
|
||||
|
||||
float rangeMultiplier() const;
|
||||
void setRangeMultiplier(float rangeMultiplier);
|
||||
|
||||
void setVolume(float targetValue, float rampTime = 0.0f);
|
||||
void setPitchMultiplier(float targetValue, float rampTime = 0.0f);
|
||||
|
||||
// Returns the currently remaining loops
|
||||
int loops() const;
|
||||
// Sets the remaining loops, set to 0 to stop looping
|
||||
void setLoops(int loops);
|
||||
|
||||
// Returns the current audio playing time position
|
||||
double currentTime() const;
|
||||
// Total length of time of the audio in seconds
|
||||
double totalTime() const;
|
||||
// Seeks the audio to the current time in seconds
|
||||
void seekTime(double time);
|
||||
|
||||
// The MixerGroup defaults to Effects
|
||||
MixerGroup mixerGroup() const;
|
||||
void setMixerGroup(MixerGroup mixerGroup);
|
||||
|
||||
// If set, uses wall clock time in milliseconds to set precise start and stop
|
||||
// times for the AudioInstance
|
||||
void setClockStart(Maybe<int64_t> clockStartTime);
|
||||
void setClockStop(Maybe<int64_t> clockStopTime, int64_t fadeOutTime = 0);
|
||||
|
||||
void stop(float rampTime = 0.0f);
|
||||
bool finished() const;
|
||||
|
||||
private:
|
||||
friend class Mixer;
|
||||
|
||||
mutable Mutex m_mutex;
|
||||
|
||||
Audio m_audio;
|
||||
|
||||
MixerGroup m_mixerGroup;
|
||||
|
||||
RampedValue m_volume;
|
||||
|
||||
float m_pitchMultiplier;
|
||||
float m_pitchMultiplierTarget;
|
||||
float m_pitchMultiplierVelocity;
|
||||
|
||||
int m_loops;
|
||||
bool m_stopping;
|
||||
bool m_finished;
|
||||
|
||||
Maybe<Vec2F> m_position;
|
||||
float m_rangeMultiplier;
|
||||
|
||||
Maybe<int64_t> m_clockStart;
|
||||
Maybe<int64_t> m_clockStop;
|
||||
int64_t m_clockStopFadeOut;
|
||||
};
|
||||
|
||||
// Thread safe mixer class with basic effects support.
|
||||
class Mixer {
|
||||
public:
|
||||
typedef function<void(int16_t* buffer, size_t frames, unsigned channels)> EffectFunction;
|
||||
typedef function<float(unsigned, Vec2F, float)> PositionalAttenuationFunction;
|
||||
|
||||
Mixer(unsigned sampleRate, unsigned channels);
|
||||
|
||||
unsigned sampleRate() const;
|
||||
unsigned channels() const;
|
||||
|
||||
// Construct a really crappy low-pass filter based on averaging
|
||||
EffectFunction lowpass(size_t avgSize) const;
|
||||
// Construct a very simple echo filter.
|
||||
EffectFunction echo(float time, float dry, float wet) const;
|
||||
|
||||
// Adds / removes effects that affect all playback.
|
||||
void addEffect(String const& effectName, EffectFunction effectFunction, float rampTime);
|
||||
void removeEffect(String const& effectName, float rampTime);
|
||||
StringList currentEffects();
|
||||
bool hasEffect(String const& effectName);
|
||||
|
||||
// Global volume
|
||||
void setVolume(float volume, float rampTime);
|
||||
|
||||
// per mixer group volume
|
||||
void setGroupVolume(MixerGroup group, float targetValue, float rampTime = 0.0f);
|
||||
|
||||
void play(AudioInstancePtr sample);
|
||||
|
||||
void stopAll(float rampTime);
|
||||
|
||||
// Reads pending audio data. This is thread safe with the other Mixer
|
||||
// methods, but only one call to read may be active at a time.
|
||||
void read(int16_t* samples, size_t frameCount);
|
||||
|
||||
// Call within the main loop of the program using Mixer, calculates
|
||||
// positional attenuation of audio and does cleanup.
|
||||
void update(PositionalAttenuationFunction positionalAttenuationFunction = {});
|
||||
|
||||
private:
|
||||
struct EffectInfo {
|
||||
EffectFunction effectFunction;
|
||||
float amount;
|
||||
float velocity;
|
||||
bool finished;
|
||||
};
|
||||
|
||||
struct AudioState {
|
||||
List<float> positionalChannelVolumes;
|
||||
};
|
||||
|
||||
Mutex m_mutex;
|
||||
unsigned m_sampleRate;
|
||||
unsigned m_channels;
|
||||
|
||||
RampedValue m_volume;
|
||||
|
||||
Mutex m_queueMutex;
|
||||
|
||||
HashMap<AudioInstancePtr, AudioState> m_audios;
|
||||
|
||||
Mutex m_effectsMutex;
|
||||
StringMap<shared_ptr<EffectInfo>> m_effects;
|
||||
|
||||
List<int16_t> m_mixBuffer;
|
||||
|
||||
Map<MixerGroup, RampedValue> m_groupVolumes;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
158
source/base/StarPackedAssetSource.cpp
Normal file
158
source/base/StarPackedAssetSource.cpp
Normal file
|
@ -0,0 +1,158 @@
|
|||
#include "StarPackedAssetSource.hpp"
|
||||
#include "StarDirectoryAssetSource.hpp"
|
||||
#include "StarOrderedSet.hpp"
|
||||
#include "StarDataStreamDevices.hpp"
|
||||
#include "StarDataStreamExtra.hpp"
|
||||
#include "StarSha256.hpp"
|
||||
#include "StarFile.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
void PackedAssetSource::build(DirectoryAssetSource& directorySource, String const& targetPackedFile,
|
||||
StringList const& extensionSorting, BuildProgressCallback progressCallback) {
|
||||
FilePtr file = File::open(targetPackedFile, IOMode::ReadWrite | IOMode::Truncate);
|
||||
|
||||
DataStreamIODevice ds(file);
|
||||
|
||||
ds.writeData("SBAsset6", 8);
|
||||
// Skip 8 bytes, this will be a pointer to the index once we are done.
|
||||
ds.seek(8, IOSeek::Relative);
|
||||
|
||||
// Insert every found entry into the packed file, and also simultaneously
|
||||
// compute the full index.
|
||||
StringMap<pair<uint64_t, uint64_t>> index;
|
||||
|
||||
OrderedHashSet<String> extensionOrdering;
|
||||
for (auto const& str : extensionSorting)
|
||||
extensionOrdering.add(str.toLower());
|
||||
|
||||
StringList assetPaths = directorySource.assetPaths();
|
||||
|
||||
// Returns a value for the asset that can be used to predictably sort assets
|
||||
// by name and then by extension, where every extension listed in
|
||||
// "extensionSorting" will come first, and then any extension not listed will
|
||||
// come after.
|
||||
auto getOrderingValue = [&extensionOrdering](String const& asset) -> pair<size_t, String> {
|
||||
String extension;
|
||||
auto lastDot = asset.findLast(".");
|
||||
if (lastDot != NPos)
|
||||
extension = asset.substr(lastDot + 1);
|
||||
|
||||
if (auto i = extensionOrdering.indexOf(extension.toLower())) {
|
||||
return {*i, asset.toLower()};
|
||||
} else {
|
||||
return {extensionOrdering.size(), asset.toLower()};
|
||||
}
|
||||
};
|
||||
|
||||
assetPaths.sort([&getOrderingValue](String const& a, String const& b) {
|
||||
return getOrderingValue(a) < getOrderingValue(b);
|
||||
});
|
||||
|
||||
for (size_t i = 0; i < assetPaths.size(); ++i) {
|
||||
String const& assetPath = assetPaths[i];
|
||||
ByteArray contents = directorySource.read(assetPath);
|
||||
|
||||
if (progressCallback)
|
||||
progressCallback(i, assetPaths.size(), directorySource.toFilesystem(assetPath), assetPath);
|
||||
index.add(assetPath, {ds.pos(), contents.size()});
|
||||
ds.writeBytes(contents);
|
||||
}
|
||||
|
||||
uint64_t indexStart = ds.pos();
|
||||
ds.writeData("INDEX", 5);
|
||||
ds.write(directorySource.metadata());
|
||||
ds.write(index);
|
||||
|
||||
ds.seek(8);
|
||||
ds.write(indexStart);
|
||||
}
|
||||
|
||||
PackedAssetSource::PackedAssetSource(String const& filename) {
|
||||
m_packedFile = File::open(filename, IOMode::Read);
|
||||
|
||||
DataStreamIODevice ds(m_packedFile);
|
||||
if (ds.readBytes(8) != ByteArray("SBAsset6", 8))
|
||||
throw AssetSourceException("Packed assets file format unrecognized!");
|
||||
|
||||
uint64_t indexStart = ds.read<uint64_t>();
|
||||
|
||||
ds.seek(indexStart);
|
||||
ByteArray header = ds.readBytes(5);
|
||||
if (header != ByteArray("INDEX", 5))
|
||||
throw AssetSourceException("No index header found!");
|
||||
ds.read(m_metadata);
|
||||
ds.read(m_index);
|
||||
}
|
||||
|
||||
JsonObject PackedAssetSource::metadata() const {
|
||||
return m_metadata;
|
||||
}
|
||||
|
||||
StringList PackedAssetSource::assetPaths() const {
|
||||
return m_index.keys();
|
||||
}
|
||||
|
||||
IODevicePtr PackedAssetSource::open(String const& path) {
|
||||
struct AssetReader : public IODevice {
|
||||
AssetReader(FilePtr file, StreamOffset offset, StreamOffset size)
|
||||
: file(file), fileOffset(offset), assetSize(size), assetPos(0) {
|
||||
setMode(IOMode::Read);
|
||||
}
|
||||
|
||||
size_t read(char* data, size_t len) override {
|
||||
len = min<StreamOffset>(len, assetSize - assetPos);
|
||||
file->readFullAbsolute(fileOffset + assetPos, data, len);
|
||||
assetPos += len;
|
||||
return len;
|
||||
}
|
||||
|
||||
size_t write(char const*, size_t) override {
|
||||
throw IOException("Assets IODevices are read-only");
|
||||
}
|
||||
|
||||
StreamOffset size() override {
|
||||
return assetSize;
|
||||
}
|
||||
|
||||
StreamOffset pos() override {
|
||||
return assetPos;
|
||||
}
|
||||
|
||||
bool atEnd() override {
|
||||
return assetPos >= assetSize;
|
||||
}
|
||||
|
||||
void seek(StreamOffset p, IOSeek mode) override {
|
||||
if (mode == IOSeek::Absolute)
|
||||
assetPos = p;
|
||||
else if (mode == IOSeek::Relative)
|
||||
assetPos = clamp<StreamOffset>(assetPos + p, 0, assetSize);
|
||||
else
|
||||
assetPos = clamp<StreamOffset>(assetSize - p, 0, assetSize);
|
||||
}
|
||||
|
||||
FilePtr file;
|
||||
StreamOffset fileOffset;
|
||||
StreamOffset assetSize;
|
||||
StreamOffset assetPos;
|
||||
};
|
||||
|
||||
auto p = m_index.ptr(path);
|
||||
if (!p)
|
||||
throw AssetSourceException::format("Requested file '%s' does not exist in the packed assets file", path);
|
||||
|
||||
return make_shared<AssetReader>(m_packedFile, p->first, p->second);
|
||||
}
|
||||
|
||||
ByteArray PackedAssetSource::read(String const& path) {
|
||||
auto p = m_index.ptr(path);
|
||||
if (!p)
|
||||
throw AssetSourceException::format("Requested file '%s' does not exist in the packed assets file", path);
|
||||
|
||||
ByteArray data(p->second, 0);
|
||||
m_packedFile->readFullAbsolute(p->first, data.ptr(), p->second);
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
45
source/base/StarPackedAssetSource.hpp
Normal file
45
source/base/StarPackedAssetSource.hpp
Normal file
|
@ -0,0 +1,45 @@
|
|||
#ifndef STAR_PACKED_ASSET_SOURCE_HPP
|
||||
#define STAR_PACKED_ASSET_SOURCE_HPP
|
||||
|
||||
#include "StarOrderedMap.hpp"
|
||||
#include "StarFile.hpp"
|
||||
#include "StarDirectoryAssetSource.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(PackedAssetSource);
|
||||
|
||||
class PackedAssetSource : public AssetSource {
|
||||
public:
|
||||
typedef function<void(size_t, size_t, String, String)> BuildProgressCallback;
|
||||
|
||||
// Build a packed asset file from the given DirectoryAssetSource.
|
||||
//
|
||||
// 'extensionSorting' sorts the packed file with file extensions that case
|
||||
// insensitive match the given extensions in the order they are given. If a
|
||||
// file has an extension that doesn't match any in this list, it goes after
|
||||
// all other files. All files are sorted secondarily by case insensitive
|
||||
// alphabetical order.
|
||||
//
|
||||
// If given, 'progressCallback' will be called with the total number of
|
||||
// files, the current file number, the file name, and the asset path.
|
||||
static void build(DirectoryAssetSource& directorySource, String const& targetPackedFile,
|
||||
StringList const& extensionSorting = {}, BuildProgressCallback progressCallback = {});
|
||||
|
||||
PackedAssetSource(String const& packedFileName);
|
||||
|
||||
JsonObject metadata() const override;
|
||||
StringList assetPaths() const override;
|
||||
|
||||
IODevicePtr open(String const& path) override;
|
||||
ByteArray read(String const& path) override;
|
||||
|
||||
private:
|
||||
FilePtr m_packedFile;
|
||||
JsonObject m_metadata;
|
||||
OrderedHashMap<String, pair<uint64_t, uint64_t>> m_index;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
9
source/base/StarVersion.cpp.in
Normal file
9
source/base/StarVersion.cpp.in
Normal file
|
@ -0,0 +1,9 @@
|
|||
#include "StarVersion.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
char const* const StarVersionString = "1.4.4";
|
||||
char const* const StarSourceIdentifierString = "${STAR_SOURCE_IDENTIFIER}";
|
||||
char const* const StarArchitectureString = "${STAR_SYSTEM} ${STAR_ARCHITECTURE}";
|
||||
|
||||
}
|
16
source/base/StarVersion.hpp
Normal file
16
source/base/StarVersion.hpp
Normal file
|
@ -0,0 +1,16 @@
|
|||
#ifndef STAR_VERSION_HPP
|
||||
#define STAR_VERSION_HPP
|
||||
|
||||
#include "StarConfig.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
extern char const* const StarVersionString;
|
||||
extern char const* const StarSourceIdentifierString;
|
||||
extern char const* const StarArchitectureString;
|
||||
|
||||
typedef uint32_t VersionNumber;
|
||||
|
||||
}
|
||||
|
||||
#endif
|
46
source/base/StarVersionOptionParser.cpp
Normal file
46
source/base/StarVersionOptionParser.cpp
Normal file
|
@ -0,0 +1,46 @@
|
|||
#include "StarVersionOptionParser.hpp"
|
||||
#include "StarFile.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
void VersionOptionParser::printVersion(std::ostream& os) {
|
||||
format(os, "Starbound Version %s (%s)\n", StarVersionString, StarArchitectureString);
|
||||
format(os, "Source Identifier - %s\n", StarSourceIdentifierString);
|
||||
}
|
||||
|
||||
VersionOptionParser::VersionOptionParser() {
|
||||
addSwitch("help", "Show help text");
|
||||
addSwitch("version", "Print version info");
|
||||
}
|
||||
|
||||
VersionOptionParser::Options VersionOptionParser::parseOrDie(StringList const& cmdLineArguments) const {
|
||||
Options options;
|
||||
StringList errors;
|
||||
tie(options, errors) = OptionParser::parseOptions(cmdLineArguments);
|
||||
|
||||
if (options.switches.contains("version"))
|
||||
printVersion(std::cout);
|
||||
|
||||
if (options.switches.contains("help"))
|
||||
printHelp(std::cout);
|
||||
|
||||
if (options.switches.contains("version") || options.switches.contains("help"))
|
||||
std::exit(0);
|
||||
|
||||
if (!errors.empty()) {
|
||||
for (auto const& err : errors)
|
||||
coutf("Error: %s\n", err);
|
||||
coutf("\n");
|
||||
printHelp(std::cout);
|
||||
std::exit(1);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
VersionOptionParser::Options VersionOptionParser::commandParseOrDie(int argc, char** argv) {
|
||||
setCommandName(File::baseName(argv[0]));
|
||||
return parseOrDie(StringList(argc - 1, argv + 1));
|
||||
}
|
||||
|
||||
}
|
27
source/base/StarVersionOptionParser.hpp
Normal file
27
source/base/StarVersionOptionParser.hpp
Normal file
|
@ -0,0 +1,27 @@
|
|||
#ifndef STAR_VERSION_OPTION_PARSER_HPP
|
||||
#define STAR_VERSION_OPTION_PARSER_HPP
|
||||
|
||||
#include "StarOptionParser.hpp"
|
||||
#include "StarVersion.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
// Option parser that accepts -h to print the help and exit and -v to print the
|
||||
// version and exit.
|
||||
class VersionOptionParser : public OptionParser {
|
||||
public:
|
||||
static void printVersion(std::ostream& os);
|
||||
|
||||
VersionOptionParser();
|
||||
|
||||
// Parse the command line options, or, in the case of an error, -h, or -v,
|
||||
// prints the appropriate text and immediately exits.
|
||||
Options parseOrDie(StringList const& cmdLineArguments) const;
|
||||
|
||||
// First sets the command name based on argv[0], then calls parseOrDie.
|
||||
Options commandParseOrDie(int argc, char** argv);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
358
source/base/StarWorldGeometry.cpp
Normal file
358
source/base/StarWorldGeometry.cpp
Normal file
|
@ -0,0 +1,358 @@
|
|||
#include "StarWorldGeometry.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
function<float(float, float)> WorldGeometry::xDiffFunction() const {
|
||||
if (m_size[0] == 0) {
|
||||
return [](float x1, float x2) -> float { return x1 - x2; };
|
||||
} else {
|
||||
unsigned xsize = m_size[0];
|
||||
return [xsize](float x1, float x2) -> float { return wrapDiffF<float>(x1, x2, xsize); };
|
||||
}
|
||||
}
|
||||
|
||||
function<Vec2F(Vec2F, Vec2F)> WorldGeometry::diffFunction() const {
|
||||
if (m_size[0] == 0) {
|
||||
return [](Vec2F const& a, Vec2F const& b) -> Vec2F { return a - b; };
|
||||
} else {
|
||||
unsigned xsize = m_size[0];
|
||||
return [xsize](Vec2F const& a, Vec2F const& b) -> Vec2F {
|
||||
return Vec2F(wrapDiffF<float>(a[0], b[0], xsize), a[1] - b[1]);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function<float(float, float, float)> WorldGeometry::xLerpFunction(Maybe<float> discontinuityThreshold) const {
|
||||
if (m_size[0] == 0) {
|
||||
return [](float, float min, float) -> float { return min; };
|
||||
} else {
|
||||
unsigned xsize = m_size[0];
|
||||
return [discontinuityThreshold, xsize](float offset, float min, float max) -> float {
|
||||
float distance = wrapDiffF<float>(max, min, xsize);
|
||||
if (discontinuityThreshold && distance > *discontinuityThreshold)
|
||||
return min + distance;
|
||||
return min + offset * distance;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function<Vec2F(float, Vec2F, Vec2F)> WorldGeometry::lerpFunction(Maybe<float> discontinuityThreshold) const {
|
||||
if (m_size[0] == 0) {
|
||||
return [](float, Vec2F const& min, Vec2F const&) -> Vec2F { return min; };
|
||||
} else {
|
||||
unsigned xsize = m_size[0];
|
||||
return [discontinuityThreshold, xsize](float offset, Vec2F const& min, Vec2F const& max) -> Vec2F {
|
||||
Vec2F distance = Vec2F(wrapDiffF<float>(max[0], min[0], xsize), max[1] - min[1]);
|
||||
if (discontinuityThreshold && distance.magnitude() > *discontinuityThreshold)
|
||||
return min + distance;
|
||||
return min + offset * distance;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
StaticList<RectF, 2> WorldGeometry::splitRect(RectF const& bbox) const {
|
||||
if (bbox.isNull() || m_size[0] == 0)
|
||||
return {bbox};
|
||||
|
||||
Vec2F minWrap = xwrap(bbox.min());
|
||||
RectF bboxWrap = RectF(minWrap, minWrap + bbox.size());
|
||||
|
||||
// This does not work for ranges greater than m_size[0] wide!
|
||||
starAssert(bbox.xMax() - bbox.xMin() <= (float)m_size[0]);
|
||||
|
||||
// Since min is wrapped, we're only checking to see if max is on the other
|
||||
// side of the wrap point
|
||||
if (bboxWrap.xMax() > m_size[0]) {
|
||||
return {RectF(bboxWrap.xMin(), bboxWrap.yMin(), m_size[0], bboxWrap.yMax()),
|
||||
RectF(0, bboxWrap.yMin(), bboxWrap.xMax() - m_size[0], bboxWrap.yMax())};
|
||||
} else {
|
||||
return {bboxWrap};
|
||||
}
|
||||
}
|
||||
|
||||
StaticList<RectF, 2> WorldGeometry::splitRect(RectF bbox, Vec2F const& position) const {
|
||||
bbox.translate(position);
|
||||
return splitRect(bbox);
|
||||
}
|
||||
|
||||
StaticList<RectI, 2> WorldGeometry::splitRect(RectI const bbox) const {
|
||||
if (bbox.isNull() || m_size[0] == 0)
|
||||
return {bbox};
|
||||
|
||||
Vec2I minWrap = xwrap(bbox.min());
|
||||
RectI bboxWrap = RectI(minWrap, minWrap + bbox.size());
|
||||
|
||||
// This does not work for ranges greater than m_size[0] wide!
|
||||
starAssert(bbox.xMax() - bbox.xMin() <= (int)m_size[0]);
|
||||
|
||||
// Since min is wrapped, we're only checking to see if max is on the other
|
||||
// side of the wrap point
|
||||
if (bboxWrap.xMax() > (int)m_size[0]) {
|
||||
return {RectI(bboxWrap.xMin(), bboxWrap.yMin(), m_size[0], bboxWrap.yMax()),
|
||||
RectI(0, bboxWrap.yMin(), bboxWrap.xMax() - m_size[0], bboxWrap.yMax())};
|
||||
} else {
|
||||
return {bboxWrap};
|
||||
}
|
||||
}
|
||||
|
||||
StaticList<Line2F, 2> WorldGeometry::splitLine(Line2F line, bool preserveDirection) const {
|
||||
if (m_size[0] == 0)
|
||||
return {line};
|
||||
|
||||
bool swapDirection = line.makePositive() && preserveDirection;
|
||||
Vec2F minWrap = xwrap(line.min());
|
||||
|
||||
// diff is safe because we're looking for the line gnostic diff
|
||||
Line2F lineWrap = Line2F(minWrap, minWrap + line.diff());
|
||||
|
||||
// Since min is wrapped, we're only checking to see if max is on the other
|
||||
// side of the wrap point
|
||||
if (lineWrap.max()[0] > m_size[0]) {
|
||||
Vec2F intersection = lineWrap.intersection(Line2F(Vec2F(m_size[0], 0), Vec2F(m_size)), true).point;
|
||||
if (swapDirection)
|
||||
return {Line2F(lineWrap.max() - Vec2F(m_size[0], 0), Vec2F(0, intersection[1])),
|
||||
Line2F(Vec2F(m_size[0], intersection[1]), lineWrap.min())};
|
||||
else
|
||||
return {Line2F(lineWrap.min(), Vec2F(m_size[0], intersection[1])),
|
||||
Line2F(Vec2F(0, intersection[1]), lineWrap.max() - Vec2F(m_size[0], 0))};
|
||||
} else {
|
||||
if (swapDirection)
|
||||
lineWrap.reverse();
|
||||
return {lineWrap};
|
||||
}
|
||||
}
|
||||
|
||||
StaticList<Line2F, 2> WorldGeometry::splitLine(Line2F line, Vec2F const& position, bool preserveDirection) const {
|
||||
line.translate(position);
|
||||
return splitLine(line, preserveDirection);
|
||||
}
|
||||
|
||||
StaticList<PolyF, 2> WorldGeometry::splitPoly(PolyF const& poly) const {
|
||||
if (poly.isNull() || m_size[0] == 0)
|
||||
return {poly};
|
||||
|
||||
Array<PolyF, 2> res;
|
||||
bool polySelect = false;
|
||||
|
||||
Line2F worldBoundRight = {Vec2F(m_size[0], 0), Vec2F(m_size[0], 1)};
|
||||
Line2F worldBoundLeft = {Vec2F(0, 0), Vec2F(0, 1)};
|
||||
|
||||
for (unsigned i = 0; i < poly.sides(); i++) {
|
||||
Line2F segment = poly.side(i);
|
||||
if ((segment.min()[0] < 0) ^ (segment.max()[0] < 0)) {
|
||||
Vec2F worldCorrect = {(float)m_size[0], 0};
|
||||
Vec2F intersect = segment.intersection(worldBoundLeft, true).point;
|
||||
if (segment.min()[0] < 0) {
|
||||
res[polySelect].add(segment.min() + worldCorrect);
|
||||
res[polySelect].add(Vec2F(m_size[0], intersect[1]));
|
||||
polySelect = !polySelect;
|
||||
res[polySelect].add(Vec2F(0, intersect[1]));
|
||||
} else {
|
||||
res[polySelect].add(segment.min());
|
||||
res[polySelect].add(Vec2F(0, intersect[1]));
|
||||
polySelect = !polySelect;
|
||||
res[polySelect].add(Vec2F(m_size[0], intersect[1]));
|
||||
}
|
||||
} else if ((segment.min()[0] > m_size[0]) ^ (segment.max()[0] > m_size[0])) {
|
||||
Vec2F worldCorrect = {(float)m_size[0], 0};
|
||||
Vec2F intersect = segment.intersection(worldBoundRight, true).point;
|
||||
if (segment.min()[0] > m_size[0]) {
|
||||
res[polySelect].add(segment.min() - worldCorrect);
|
||||
res[polySelect].add(Vec2F(0, intersect[1]));
|
||||
polySelect = !polySelect;
|
||||
res[polySelect].add(Vec2F(m_size[0], intersect[1]));
|
||||
} else {
|
||||
res[polySelect].add(segment.min());
|
||||
res[polySelect].add(Vec2F(m_size[0], intersect[1]));
|
||||
polySelect = !polySelect;
|
||||
res[polySelect].add(Vec2F(0, intersect[1]));
|
||||
}
|
||||
} else {
|
||||
if (segment.min()[0] < 0) {
|
||||
res[polySelect].add(segment.min() + Vec2F((float)m_size[0], 0));
|
||||
} else if (segment.min()[0] > m_size[0]) {
|
||||
res[polySelect].add(segment.min() - Vec2F((float)m_size[0], 0));
|
||||
} else {
|
||||
res[polySelect].add(segment.min());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (res[1].isNull())
|
||||
return {res[0]};
|
||||
if (res[0].isNull())
|
||||
return {res[1]};
|
||||
else
|
||||
return {res[0], res[1]};
|
||||
}
|
||||
|
||||
StaticList<PolyF, 2> WorldGeometry::splitPoly(PolyF poly, Vec2F const& position) const {
|
||||
poly.translate(position);
|
||||
return splitPoly(poly);
|
||||
}
|
||||
|
||||
StaticList<Vec2I, 2> WorldGeometry::splitXRegion(Vec2I const& xRegion) const {
|
||||
if (m_size[0] == 0)
|
||||
return {xRegion};
|
||||
|
||||
starAssert(xRegion[1] >= xRegion[0]);
|
||||
|
||||
// This does not work for ranges greater than m_size[0] wide!
|
||||
starAssert(xRegion[1] - xRegion[0] <= (int)m_size[0]);
|
||||
|
||||
int x1 = xwrap(xRegion[0]);
|
||||
int x2 = x1 + xRegion[1] - xRegion[0];
|
||||
|
||||
if (x2 > (int)m_size[0]) {
|
||||
return {Vec2I(x1, m_size[0]), Vec2I(0.0f, x2 - m_size[0])};
|
||||
} else {
|
||||
return {{x1, x2}};
|
||||
}
|
||||
}
|
||||
|
||||
StaticList<Vec2F, 2> WorldGeometry::splitXRegion(Vec2F const& xRegion) const {
|
||||
if (m_size[0] == 0)
|
||||
return {xRegion};
|
||||
|
||||
starAssert(xRegion[1] >= xRegion[0]);
|
||||
|
||||
// This does not work for ranges greater than m_size[0] wide!
|
||||
starAssert(xRegion[1] - xRegion[0] <= (float)m_size[0]);
|
||||
|
||||
float x1 = xwrap(xRegion[0]);
|
||||
float x2 = x1 + xRegion[1] - xRegion[0];
|
||||
|
||||
if (x2 > m_size[0]) {
|
||||
return {Vec2F(x1, m_size[0]), Vec2F(0.0f, x2 - m_size[0])};
|
||||
} else {
|
||||
return {{x1, x2}};
|
||||
}
|
||||
}
|
||||
|
||||
bool WorldGeometry::rectContains(RectF const& rect, Vec2F const& pos) const {
|
||||
auto wpos = xwrap(pos);
|
||||
for (auto const& r : splitRect(rect)) {
|
||||
if (r.contains(wpos))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WorldGeometry::rectIntersectsRect(RectF const& rect1, RectF const& rect2) const {
|
||||
for (auto const& r1 : splitRect(rect1)) {
|
||||
for (auto const& r2 : splitRect(rect2)) {
|
||||
if (r1.intersects(r2))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RectF WorldGeometry::rectOverlap(RectF const& rect1, RectF const& rect2) const {
|
||||
return rect1.overlap(RectF::withSize(nearestTo(rect1.min(), rect2.min()), rect2.size()));
|
||||
}
|
||||
|
||||
bool WorldGeometry::polyContains(PolyF const& poly, Vec2F const& pos) const {
|
||||
auto wpos = xwrap(pos);
|
||||
for (auto const& p : splitPoly(poly)) {
|
||||
if (p.contains(wpos))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
float WorldGeometry::polyOverlapArea(PolyF const& poly1, PolyF const& poly2) const {
|
||||
float area = 0.0f;
|
||||
for (auto const& p1 : splitPoly(poly1)) {
|
||||
for (auto const& p2 : splitPoly(poly2))
|
||||
area += PolyF::clip(p1, p2).convexArea();
|
||||
}
|
||||
return area;
|
||||
}
|
||||
|
||||
bool WorldGeometry::lineIntersectsRect(Line2F const& line, RectF const& rect) const {
|
||||
for (auto l : splitLine(line)) {
|
||||
for (auto box : splitRect(rect)) {
|
||||
if (box.intersects(l)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WorldGeometry::lineIntersectsPoly(Line2F const& line, PolyF const& poly) const {
|
||||
for (auto a : splitLine(line)) {
|
||||
for (auto b : splitPoly(poly)) {
|
||||
if (b.intersects(a)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WorldGeometry::polyIntersectsPoly(PolyF const& polyA, PolyF const& polyB) const {
|
||||
for (auto a : splitPoly(polyA)) {
|
||||
for (auto b : splitPoly(polyB)) {
|
||||
if (b.intersects(a))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WorldGeometry::rectIntersectsCircle(RectF const& rect, Vec2F const& center, float radius) const {
|
||||
if (rect.contains(center))
|
||||
return true;
|
||||
for (auto const& e : rect.edges()) {
|
||||
if (lineIntersectsCircle(e, center, radius))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WorldGeometry::lineIntersectsCircle(Line2F const& line, Vec2F const& center, float radius) const {
|
||||
for (auto const& sline : splitLine(line)) {
|
||||
if (sline.distanceTo(nearestTo(sline.center(), center)) <= radius)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Maybe<Vec2F> WorldGeometry::lineIntersectsPolyAt(Line2F const& line, PolyF const& poly) const {
|
||||
for (auto a : splitLine(line, true)) {
|
||||
for (auto b : splitPoly(poly)) {
|
||||
if (auto intersection = b.lineIntersection(a))
|
||||
return intersection->point;
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
float WorldGeometry::polyDistance(PolyF const& poly, Vec2F const& point) const {
|
||||
auto spoint = nearestTo(poly.center(), point);
|
||||
return poly.distance(spoint);
|
||||
}
|
||||
|
||||
Vec2F WorldGeometry::nearestCoordInBox(RectF const& box, Vec2F const& pos) const {
|
||||
RectF t(box);
|
||||
auto offset = t.center();
|
||||
auto r = diff(pos, offset);
|
||||
t.setCenter({});
|
||||
return t.nearestCoordTo(r) + offset;
|
||||
}
|
||||
|
||||
Vec2F WorldGeometry::diffToNearestCoordInBox(RectF const& box, Vec2F const& pos) const {
|
||||
RectF t(box);
|
||||
auto offset = t.center();
|
||||
auto r = diff(pos, offset);
|
||||
t.setCenter({});
|
||||
auto coord = t.nearestCoordTo(r) + offset;
|
||||
return diff(pos, coord);
|
||||
}
|
||||
|
||||
}
|
263
source/base/StarWorldGeometry.hpp
Normal file
263
source/base/StarWorldGeometry.hpp
Normal file
|
@ -0,0 +1,263 @@
|
|||
#ifndef STAR_WORLD_GEOMETRY_HPP
|
||||
#define STAR_WORLD_GEOMETRY_HPP
|
||||
|
||||
#include "StarPoly.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
STAR_CLASS(WorldGeometry);
|
||||
|
||||
// Utility class for dealing with the non-euclidean nature of the World.
|
||||
// Handles the surprisingly complex job of deciding intersections and splitting
|
||||
// geometry across the world wrap boundary.
|
||||
class WorldGeometry {
|
||||
public:
|
||||
// A null WorldGeometry will have diff / wrap methods etc be the normal
|
||||
// euclidean variety.
|
||||
WorldGeometry();
|
||||
WorldGeometry(unsigned width, unsigned height);
|
||||
WorldGeometry(Vec2U const& size);
|
||||
|
||||
bool isNull();
|
||||
|
||||
bool operator==(WorldGeometry const& other) const;
|
||||
bool operator!=(WorldGeometry const& other) const;
|
||||
|
||||
unsigned width() const;
|
||||
unsigned height() const;
|
||||
Vec2U size() const;
|
||||
|
||||
// Wrap given point back into world space by wrapping x
|
||||
int xwrap(int x) const;
|
||||
float xwrap(float x) const;
|
||||
// Only wraps x component.
|
||||
Vec2F xwrap(Vec2F const& pos) const;
|
||||
Vec2I xwrap(Vec2I const& pos) const;
|
||||
|
||||
// y value is clamped to be in the range [0, height)
|
||||
float yclamp(float y) const;
|
||||
|
||||
// Wraps and clamps position
|
||||
Vec2F limit(Vec2F const& pos) const;
|
||||
|
||||
bool crossesWrap(float xMin, float xMax) const;
|
||||
|
||||
// Do these two inexes point to the same location
|
||||
bool equal(Vec2I const& p1, Vec2I const& p2) const;
|
||||
|
||||
// Same as wrap, returns unsigned type.
|
||||
unsigned index(int x) const;
|
||||
Vec2U index(Vec2I const& i) const;
|
||||
|
||||
// returns right only distance from x2 to x1 (or x1 - x2). Always positive.
|
||||
int pdiff(int x1, int x2) const;
|
||||
|
||||
// Shortest difference between two given points. Always returns diff on the
|
||||
// "side" that x1 is on.
|
||||
float diff(float x1, float x2) const;
|
||||
int diff(int x1, int x2) const;
|
||||
|
||||
// Same but for 2d vectors
|
||||
Vec2F diff(Vec2F const& p1, Vec2F const& p2) const;
|
||||
Vec2I diff(Vec2I const& p1, Vec2I const& p2) const;
|
||||
|
||||
// Midpoint of the shortest line connecting two points.
|
||||
Vec2F midpoint(Vec2F const& p1, Vec2F const& p2) const;
|
||||
|
||||
function<float(float, float)> xDiffFunction() const;
|
||||
function<Vec2F(Vec2F, Vec2F)> diffFunction() const;
|
||||
function<float(float, float, float)> xLerpFunction(Maybe<float> discontinuityThreshold = {}) const;
|
||||
function<Vec2F(float, Vec2F, Vec2F)> lerpFunction(Maybe<float> discontinuityThreshold = {}) const;
|
||||
|
||||
// Wrapping functions are not guaranteed to work for objects larger than
|
||||
// worldWidth / 2. Bad things can happen.
|
||||
|
||||
// Split the given Rect across world boundaries.
|
||||
StaticList<RectF, 2> splitRect(RectF const& bbox) const;
|
||||
// Split the given Rect after translating it by position.
|
||||
StaticList<RectF, 2> splitRect(RectF bbox, Vec2F const& position) const;
|
||||
|
||||
StaticList<RectI, 2> splitRect(RectI bbox) const;
|
||||
|
||||
// Same but for Line
|
||||
StaticList<Line2F, 2> splitLine(Line2F line, bool preserveDirection = false) const;
|
||||
StaticList<Line2F, 2> splitLine(Line2F line, Vec2F const& position, bool preserveDirection = false) const;
|
||||
|
||||
// Same but for Poly
|
||||
StaticList<PolyF, 2> splitPoly(PolyF const& poly) const;
|
||||
StaticList<PolyF, 2> splitPoly(PolyF poly, Vec2F const& position) const;
|
||||
|
||||
// Split a horizontal region of the world across the world wrap point.
|
||||
StaticList<Vec2I, 2> splitXRegion(Vec2I const& xRegion) const;
|
||||
StaticList<Vec2F, 2> splitXRegion(Vec2F const& xRegion) const;
|
||||
|
||||
bool rectContains(RectF const& rect1, Vec2F const& pos) const;
|
||||
bool rectIntersectsRect(RectF const& rect1, RectF const& rect2) const;
|
||||
RectF rectOverlap(RectF const& rect1, RectF const& rect2) const;
|
||||
bool polyContains(PolyF const& poly, Vec2F const& pos) const;
|
||||
float polyOverlapArea(PolyF const& poly1, PolyF const& poly2) const;
|
||||
|
||||
bool lineIntersectsRect(Line2F const& line, RectF const& rect) const;
|
||||
bool lineIntersectsPoly(Line2F const& line, PolyF const& poly) const;
|
||||
bool polyIntersectsPoly(PolyF const& poly1, PolyF const& poly2) const;
|
||||
|
||||
bool rectIntersectsCircle(RectF const& rect, Vec2F const& center, float radius) const;
|
||||
bool lineIntersectsCircle(Line2F const& line, Vec2F const& center, float radius) const;
|
||||
|
||||
Maybe<Vec2F> lineIntersectsPolyAt(Line2F const& line, PolyF const& poly) const;
|
||||
|
||||
// Returns the distance from a point to any part of the given poly
|
||||
float polyDistance(PolyF const& poly, Vec2F const& point) const;
|
||||
|
||||
// Produces a point that is on the same "side" of the world as the source point.
|
||||
int nearestTo(int source, int target) const;
|
||||
float nearestTo(float source, float target) const;
|
||||
Vec2I nearestTo(Vec2I const& source, Vec2I const& target) const;
|
||||
Vec2F nearestTo(Vec2F const& source, Vec2F const& target) const;
|
||||
|
||||
Vec2F nearestCoordInBox(RectF const& box, Vec2F const& pos) const;
|
||||
Vec2F diffToNearestCoordInBox(RectF const& box, Vec2F const& pos) const;
|
||||
|
||||
private:
|
||||
Vec2U m_size;
|
||||
};
|
||||
|
||||
inline WorldGeometry::WorldGeometry()
|
||||
: m_size(Vec2U()) {}
|
||||
|
||||
inline WorldGeometry::WorldGeometry(unsigned width, unsigned height)
|
||||
: m_size(width, height) {}
|
||||
|
||||
inline WorldGeometry::WorldGeometry(Vec2U const& size)
|
||||
: m_size(size) {}
|
||||
|
||||
inline bool WorldGeometry::isNull() {
|
||||
return m_size == Vec2U();
|
||||
}
|
||||
|
||||
inline bool WorldGeometry::operator==(WorldGeometry const& other) const {
|
||||
return m_size == other.m_size;
|
||||
}
|
||||
|
||||
inline bool WorldGeometry::operator!=(WorldGeometry const& other) const {
|
||||
return m_size != other.m_size;
|
||||
}
|
||||
|
||||
inline unsigned WorldGeometry::width() const {
|
||||
return m_size[0];
|
||||
}
|
||||
|
||||
inline unsigned WorldGeometry::height() const {
|
||||
return m_size[1];
|
||||
}
|
||||
|
||||
inline Vec2U WorldGeometry::size() const {
|
||||
return m_size;
|
||||
}
|
||||
|
||||
inline int WorldGeometry::xwrap(int x) const {
|
||||
if (m_size[0] == 0)
|
||||
return x;
|
||||
else
|
||||
return pmod<int>(x, m_size[0]);
|
||||
}
|
||||
|
||||
inline float WorldGeometry::xwrap(float x) const {
|
||||
if (m_size[0] == 0)
|
||||
return x;
|
||||
else
|
||||
return pfmod<float>(x, m_size[0]);
|
||||
}
|
||||
|
||||
inline Vec2F WorldGeometry::xwrap(Vec2F const& pos) const {
|
||||
return {xwrap(pos[0]), pos[1]};
|
||||
}
|
||||
|
||||
inline Vec2I WorldGeometry::xwrap(Vec2I const& pos) const {
|
||||
return {xwrap(pos[0]), pos[1]};
|
||||
}
|
||||
|
||||
inline float WorldGeometry::yclamp(float y) const {
|
||||
return clamp<float>(y, 0, std::nextafter(m_size[1], 0.0f));
|
||||
}
|
||||
|
||||
inline Vec2F WorldGeometry::limit(Vec2F const& pos) const {
|
||||
return {xwrap(pos[0]), yclamp(pos[1])};
|
||||
}
|
||||
|
||||
inline bool WorldGeometry::crossesWrap(float xMin, float xMax) const {
|
||||
return xwrap(xMax) < xwrap(xMin);
|
||||
}
|
||||
|
||||
inline bool WorldGeometry::equal(Vec2I const& p1, Vec2I const& p2) const {
|
||||
return index(p1) == index(p2);
|
||||
}
|
||||
|
||||
inline unsigned WorldGeometry::index(int x) const {
|
||||
return (unsigned)xwrap(x);
|
||||
}
|
||||
|
||||
inline Vec2U WorldGeometry::index(Vec2I const& i) const {
|
||||
return Vec2U(xwrap(i[0]), i[1]);
|
||||
}
|
||||
|
||||
inline int WorldGeometry::pdiff(int x1, int x2) const {
|
||||
if (m_size[0] == 0)
|
||||
return x1 - x2;
|
||||
else
|
||||
return pmod<int>(x1 - x2, m_size[0]);
|
||||
}
|
||||
|
||||
inline float WorldGeometry::diff(float x1, float x2) const {
|
||||
if (m_size[0] == 0)
|
||||
return x1 - x2;
|
||||
else
|
||||
return wrapDiffF<float>(x1, x2, m_size[0]);
|
||||
}
|
||||
|
||||
inline int WorldGeometry::diff(int x1, int x2) const {
|
||||
if (m_size[0] == 0)
|
||||
return x1 - x2;
|
||||
else
|
||||
return wrapDiff<int>(x1, x2, m_size[0]);
|
||||
}
|
||||
|
||||
inline Vec2F WorldGeometry::diff(Vec2F const& p1, Vec2F const& p2) const {
|
||||
float xdiff = diff(p1[0], p2[0]);
|
||||
return {xdiff, p1[1] - p2[1]};
|
||||
}
|
||||
|
||||
inline Vec2I WorldGeometry::diff(Vec2I const& p1, Vec2I const& p2) const {
|
||||
int xdiff = diff(p1[0], p2[0]);
|
||||
return {xdiff, p1[1] - p2[1]};
|
||||
}
|
||||
|
||||
inline Vec2F WorldGeometry::midpoint(Vec2F const& p1, Vec2F const& p2) const {
|
||||
return xwrap(diff(p1, p2) / 2 + p2);
|
||||
}
|
||||
|
||||
inline int WorldGeometry::nearestTo(int source, int target) const {
|
||||
if (abs(target - source) < (int)(m_size[0] / 2))
|
||||
return target;
|
||||
else
|
||||
return diff(target, source) + source;
|
||||
}
|
||||
|
||||
inline float WorldGeometry::nearestTo(float source, float target) const {
|
||||
if (abs(target - source) < (float)(m_size[0] / 2))
|
||||
return target;
|
||||
else
|
||||
return diff(target, source) + source;
|
||||
}
|
||||
|
||||
inline Vec2I WorldGeometry::nearestTo(Vec2I const& source, Vec2I const& target) const {
|
||||
return Vec2I(nearestTo(source[0], target[0]), target[1]);
|
||||
}
|
||||
|
||||
inline Vec2F WorldGeometry::nearestTo(Vec2F const& source, Vec2F const& target) const {
|
||||
return Vec2F(nearestTo(source[0], target[0]), target[1]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
Loading…
Add table
Add a link
Reference in a new issue