This commit is contained in:
Aria 2025-03-21 22:23:30 +11:00
commit 9c94d113d3
Signed by untrusted user who does not match committer: aria
GPG key ID: 19AB7AA462B8AB3B
10260 changed files with 1237388 additions and 0 deletions

View 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)

View 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;
}
}
}

View 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

View 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

File diff suppressed because it is too large Load diff

380
source/base/StarAssets.hpp Normal file
View 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

View 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

View 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

View 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));
}
}

View 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

View 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

View 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);
}
}

View 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

View 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));
}
}
}
}

View 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
View 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
View 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

View 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;
}
}

View 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

View 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}";
}

View 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

View 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));
}
}

View 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

View 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);
}
}

View 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