summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorStanislaw Halik <sthalik@misaki.pl>2023-05-17 17:06:16 +0200
committerStanislaw Halik <sthalik@misaki.pl>2023-05-17 17:08:52 +0200
commit8138cc9a269844b6c0a84c193a7a43aec7010592 (patch)
tree1d7c1f1b37c7f714fd0976bfa839e0b18ca2385d
parent08b89c6575947e8fdcba9b6c329c25aa8472e52b (diff)
wip vobj
-rw-r--r--CMakeLists.txt2
-rw-r--r--doc/shadow-slope.pngbin0 -> 93543 bytes
-rw-r--r--doc/shadow-slope.txt71
-rw-r--r--editor/vobj-editor.cpp69
-rw-r--r--editor/vobj-editor.hpp65
-rw-r--r--loader/impl.cpp1
-rw-r--r--loader/impl.hpp7
-rw-r--r--loader/loader.cpp3
-rw-r--r--loader/loader.hpp4
-rw-r--r--loader/vobj-info.hpp15
-rw-r--r--loader/vobj.cpp102
-rw-r--r--shaders/lightmap.cpp20
-rw-r--r--shaders/lightmap.frag18
-rw-r--r--shaders/lightmap.hpp49
-rw-r--r--shaders/lightmap.vert0
-rw-r--r--src/chunk-scenery.cpp5
-rw-r--r--src/light-falloff.hpp9
-rw-r--r--src/light.cpp42
-rw-r--r--src/light.hpp44
-rw-r--r--vobj/light.pdnbin0 -> 5949 bytes
-rw-r--r--vobj/light.pngbin0 -> 1344 bytes
-rw-r--r--vobj/vobj.json7
22 files changed, 528 insertions, 5 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 813da7de..08a4fb7c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -207,7 +207,7 @@ add_subdirectory(editor)
add_subdirectory(test)
add_subdirectory(anim-crop-tool)
-install(DIRECTORY images anim scenery DESTINATION "share/floormat")
+install(DIRECTORY images anim scenery vobj DESTINATION "share/floormat")
fm_run_hook(fm-userconfig-post)
diff --git a/doc/shadow-slope.png b/doc/shadow-slope.png
new file mode 100644
index 00000000..ee273726
--- /dev/null
+++ b/doc/shadow-slope.png
Binary files differ
diff --git a/doc/shadow-slope.txt b/doc/shadow-slope.txt
new file mode 100644
index 00000000..8bd54df8
--- /dev/null
+++ b/doc/shadow-slope.txt
@@ -0,0 +1,71 @@
+Formulas:
+
+center = 304, 290
+
+def slope(x, y):
+ return (center[1] - y)/(x - center[0])
+
+>>> s1 = slope(231, 516)
+>>> s2 = slope(193, 463)
+>>> atan2((s1 - s2), (1 + s1 * s2)) * 180/pi
+
+21:57 [WhiteShark] I'll just write down the fixed quadrant instructions anyway for the old method
+21:57 [WhiteShark] Q1, make it positive and add 270; Q2, add 180; Q3, make it positive and add 90; Q4, no
+ change
+
+----
+
+Written by WhiteShark:
+
+----
+
+FINDING ANGLES AROUND AN IMAGINARY CIRCLE
+
+STARTING INFORMATION
+The coordinates of the center of our circle and the coordinates of various outside points around it.
+
+STEP 1
+We must choose a direction to be our base of 0°. For simplicity we will have this be directly to the right. This creates an imaginary ray starting from the center of our circle with a slope of 0. All angles will be found relative to this imaginary ray.
+
+STEP 2
+We must find the slope of our second ray which runs from the center of the circle through one of the outside points.
+
+SLOPE FORMULA
+m is the slope of a line
+x1,y1 are the coordinates of our center point
+x2,y2 are the coordinates of the outside point
+
+m = (y2 - y1) / (x2 - x1)
+
+STEP 3
+We now have the slopes of two rays, so we may calculate the acute (smallest) angle between them.
+
+ACUTE ANGLE BETWEEN TWO LINES FORMULA
+θ is the acute angle between two lines
+(Tan^-1 is the inverse tangent)
+θ = Tan^-1 ((slope1 - slope2) / (1 + slope1 * slope2))
+
+Since we know slope1 to be 0, we can greatly simply the formula.
+
+θ = Tan^-1 (-slope2)
+
+STEP 4
+Now we have the acute angle but that could be in either direction relative to our base angle. We must now find out the absolute angle. Since our base ray is running directly to the right, any angle on the bottom half of the circle can be left alone, but any angle on the top half of the circle must be adjusted. To find out if the angle is on the top half of the circle, we first find the delta y between the center point and the outside point.
+
+delta y = y2 - y1
+
+If delta y is negative, we can leave the angle alone; the outside point is below the center point. If it's positive, that means the outside point is above the center point, so we need to adjust the acute angle.
+
+absolute angle = θ + 180
+
+STEP 5
+Now we can find the absolute angle of any outside point compared to the center point and can also easily find the difference between them, but what if we need to find the difference between an angle of 359° and an angle of 1°? It would be easy to accidentally get a difference of 358° when really it's only a difference of 2°. To find out which is applicable, we can test which is smaller.
+
+test1 = |angle2 - angle1|
+test2 = ||angle2 - angle1| - 360|
+if test1 <= test2
+return test1
+else
+return test2
+
+Our above test will find that test1 = 358° and test2 = 2° and therefore return test2. \ No newline at end of file
diff --git a/editor/vobj-editor.cpp b/editor/vobj-editor.cpp
new file mode 100644
index 00000000..533f797a
--- /dev/null
+++ b/editor/vobj-editor.cpp
@@ -0,0 +1,69 @@
+#include "vobj-editor.hpp"
+#include "loader/vobj-info.hpp"
+#include "loader/loader.hpp"
+#include "src/world.hpp"
+#include "src/light.hpp"
+#include <array>
+#include <Corrade/Containers/ArrayView.h>
+#include <Corrade/Containers/StringView.h>
+
+#if defined __clang__ || defined __CLION_IDE__
+#pragma clang diagnostic ignored "-Wweak-vtables"
+#endif
+
+namespace floormat {
+
+[[nodiscard]]
+std::shared_ptr<entity> make_vobj(world& w, entity_type type, object_id id, global_coords pos);
+
+StringView vobj_factory::name() const { return info().name; }
+StringView vobj_factory::descr() const { return info().descr; }
+std::shared_ptr<anim_atlas> vobj_factory::atlas() const { return info().atlas; }
+
+struct light_factory final : vobj_factory
+{
+ entity_type type() const override { return entity_type::light; }
+
+ const vobj_info& info() const override
+ {
+ constexpr auto NAME = "light"_s;
+ static const vobj_info& ret = loader.vobj(NAME);
+ fm_debug_assert(ret.name == NAME);
+ return ret;
+ }
+
+ std::shared_ptr<entity> make(world& w, object_id id, global_coords pos) const override
+ {
+ auto ret = w.make_entity<light>(id, pos, light_proto{});
+ return ret;
+ }
+};
+
+template<typename T> struct factory_ { static constexpr const T value = {}; };
+
+static consteval auto make_factory_array()
+{
+ const auto size = (1uz << entity_type_BITS)-1;
+ std::array<const vobj_factory*, size> array = {};
+ array[(unsigned)entity_type::light] = &factory_<light_factory>::value;
+ return array;
+}
+
+static constexpr auto factory_array = make_factory_array();
+const ArrayView<const vobj_factory* const> vobj_editor::_types = { factory_array.data(), factory_array.size() };
+
+const vobj_factory* vobj_editor::get_factory(entity_type type)
+{
+ const auto idx = size_t(type);
+ fm_debug_assert(idx < std::size(factory_array));
+ const auto* ptr = factory_array[idx];
+ if (!ptr)
+ {
+ fm_warn_once("invalid vobj type '%zu'", idx);
+ return {};
+ }
+ else
+ return ptr;
+}
+
+} // namespace floormat
diff --git a/editor/vobj-editor.hpp b/editor/vobj-editor.hpp
new file mode 100644
index 00000000..3ebf7bd8
--- /dev/null
+++ b/editor/vobj-editor.hpp
@@ -0,0 +1,65 @@
+#pragma once
+#include "src/entity-type.hpp"
+#include <memory>
+//#include <Corrade/Containers/ArrayView.h>
+
+namespace Corrade::Containers {
+template<typename T> class BasicStringView;
+using StringView = BasicStringView<const char>;
+template<typename T> class ArrayView;
+} // namespace Corrade::Containers
+
+namespace floormat {
+
+struct world;
+struct global_coords;
+struct entity;
+struct anim_atlas;
+struct vobj_info;
+
+#ifdef __clang__
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wweak-vtables"
+#endif
+
+struct vobj_factory
+{
+ constexpr vobj_factory() = default;
+ virtual constexpr ~vobj_factory() noexcept = default;
+ virtual const vobj_info& info() const = 0;
+ virtual entity_type type() const = 0;
+ virtual std::shared_ptr<entity> make(world& w, object_id id, global_coords pos) const = 0;
+
+ StringView name() const;
+ StringView descr() const;
+ std::shared_ptr<anim_atlas> atlas() const;
+};
+
+#ifdef __clang__
+#pragma clang diagnostic pop
+#endif
+
+struct vobj_editor final
+{
+ vobj_editor();
+
+ void select_type(entity_type type);
+ void clear_selection();
+
+ [[nodiscard]] static const vobj_factory* get_factory(entity_type type);
+ bool is_type_selected(entity_type type) const;
+ bool is_anything_selected() const;
+
+ static void place_tile(world& w, global_coords pos, const vobj_factory& factory);
+
+ static auto cbegin() noexcept { return _types.cbegin(); }
+ static auto cend() noexcept { return _types.cend(); }
+ static auto begin() noexcept { return _types.cbegin(); }
+ static auto end() noexcept { return _types.cend(); }
+
+private:
+ static const ArrayView<const vobj_factory* const> _types;
+ const vobj_factory* _selected = nullptr;
+};
+
+} // namespace floormat
diff --git a/loader/impl.cpp b/loader/impl.cpp
index 7e29616b..34da792a 100644
--- a/loader/impl.cpp
+++ b/loader/impl.cpp
@@ -1,6 +1,7 @@
#include "impl.hpp"
#include "compat/assert.hpp"
#include "loader/scenery.hpp"
+#include "loader/vobj-info.hpp"
#include <cstring>
#include <Corrade/Containers/Pair.h>
#include <Magnum/Trade/ImageData.h>
diff --git a/loader/impl.hpp b/loader/impl.hpp
index da3326b4..d0159a3d 100644
--- a/loader/impl.hpp
+++ b/loader/impl.hpp
@@ -23,7 +23,9 @@ struct loader_impl final : loader_
tsl::robin_map<StringView, std::shared_ptr<struct tile_atlas>> tile_atlas_map;
tsl::robin_map<StringView, std::shared_ptr<struct anim_atlas>> anim_atlas_map;
+ tsl::robin_map<StringView, const struct vobj_info*> vobj_atlas_map;
std::vector<String> anim_atlases;
+ std::vector<struct vobj_info> vobjs;
std::vector<serialized_scenery> sceneries_array;
tsl::robin_map<StringView, const serialized_scenery*> sceneries_map;
@@ -43,6 +45,11 @@ struct loader_impl final : loader_
void get_scenery_list();
static anim_def deserialize_anim(StringView filename);
+ void get_vobj_list();
+ std::shared_ptr<struct anim_atlas> make_vobj_anim_atlas(StringView name, StringView image_filename);
+ const struct vobj_info& vobj(StringView name) override;
+ ArrayView<struct vobj_info> vobj_list() override;
+
void set_application_working_directory();
StringView startup_directory() noexcept override;
static void system_init();
diff --git a/loader/loader.cpp b/loader/loader.cpp
index eccfb350..d6076d09 100644
--- a/loader/loader.cpp
+++ b/loader/loader.cpp
@@ -30,6 +30,8 @@ StringView loader_::strip_prefix(StringView name)
return name.exceptPrefix(ANIM_PATH.size());
if (name.hasPrefix(SCENERY_PATH))
return name.exceptPrefix(SCENERY_PATH.size());
+ if (name.hasPrefix(VOBJ_PATH))
+ return name.exceptPrefix(VOBJ_PATH.size());
return name;
}
@@ -37,5 +39,6 @@ const StringView loader_::IMAGE_PATH = "images/"_s;
const StringView loader_::ANIM_PATH = "anim/"_s;
const StringView loader_::SCENERY_PATH = "scenery/"_s;
const StringView loader_::TEMP_PATH = "../../../"_s;
+const StringView loader_::VOBJ_PATH = "vobj/"_s;
} // namespace floormat
diff --git a/loader/loader.hpp b/loader/loader.hpp
index f2617f04..3ed519e9 100644
--- a/loader/loader.hpp
+++ b/loader/loader.hpp
@@ -16,6 +16,7 @@ namespace floormat {
struct tile_atlas;
struct anim_atlas;
struct scenery_proto;
+struct vobj_info;
struct loader_
{
@@ -32,6 +33,8 @@ struct loader_
virtual const scenery_proto& scenery(StringView name) noexcept(false) = 0;
virtual StringView startup_directory() noexcept = 0;
static StringView strip_prefix(StringView name);
+ virtual const vobj_info& vobj(StringView name) = 0;
+ virtual ArrayView<struct vobj_info> vobj_list() = 0;
loader_(const loader_&) = delete;
loader_& operator=(const loader_&) = delete;
@@ -42,6 +45,7 @@ struct loader_
static const StringView ANIM_PATH;
static const StringView SCENERY_PATH;
static const StringView TEMP_PATH;
+ static const StringView VOBJ_PATH;
protected:
loader_();
diff --git a/loader/vobj-info.hpp b/loader/vobj-info.hpp
new file mode 100644
index 00000000..8bc7d189
--- /dev/null
+++ b/loader/vobj-info.hpp
@@ -0,0 +1,15 @@
+#pragma once
+#include <memory>
+#include <Corrade/Containers/String.h>
+
+namespace floormat {
+
+struct anim_atlas;
+
+struct vobj_info final
+{
+ String name, descr;
+ std::shared_ptr<anim_atlas> atlas;
+};
+
+} // namespace floormat
diff --git a/loader/vobj.cpp b/loader/vobj.cpp
new file mode 100644
index 00000000..9d167368
--- /dev/null
+++ b/loader/vobj.cpp
@@ -0,0 +1,102 @@
+#include "impl.hpp"
+#include "loader/vobj-info.hpp"
+#include "serialize/json-helper.hpp"
+#include "serialize/corrade-string.hpp"
+#include "src/anim-atlas.hpp"
+#include "src/anim.hpp"
+#include "compat/exception.hpp"
+#include <Corrade/Containers/ArrayViewStl.h>
+
+namespace floormat::loader_detail {
+struct vobj {
+ String name, descr, image_filename;
+};
+} // namespace floormat::loader_detail
+
+using floormat::loader_detail::vobj;
+
+template<>
+struct nlohmann::adl_serializer<vobj> {
+ static void to_json(json& j, const vobj& val);
+ static void from_json(const json& j, vobj& val);
+};
+
+void nlohmann::adl_serializer<vobj>::to_json(json& j, const vobj& val)
+{
+ j["name"] = val.name;
+ if (val.descr)
+ j["description"] = val.descr;
+ j["image"] = val.image_filename;
+}
+
+void nlohmann::adl_serializer<vobj>::from_json(const json& j, vobj& val)
+{
+ val.name = j["name"];
+ if (j.contains("description"))
+ val.descr = j["description"];
+ else
+ val.descr = val.name;
+ val.image_filename = j["image"];
+}
+
+namespace floormat::loader_detail {
+
+std::shared_ptr<struct anim_atlas> loader_impl::make_vobj_anim_atlas(StringView name, StringView image_filename)
+{
+ auto tex = texture(VOBJ_PATH, image_filename);
+ anim_def def;
+ def.object_name = name;
+ const auto size = tex.pixels().size();
+ const auto width = size[1], height = size[0];
+ def.pixel_size = { (unsigned)width, (unsigned)height };
+ def.nframes = 1;
+ def.fps = 0;
+ anim_group g;
+ g.name = "n"_s;
+ anim_frame f;
+ f.size = def.pixel_size;
+ g.frames = { f };
+ def.groups = { std::move(g) };
+
+ auto atlas = std::make_shared<struct anim_atlas>(name, tex, std::move(def));
+ return atlas;
+}
+
+void loader_impl::get_vobj_list()
+{
+ vobjs.clear();
+ vobj_atlas_map.clear();
+
+ auto vec = json_helper::from_json<std::vector<struct vobj>>(Path::join(VOBJ_PATH, "vobj.json"));
+
+ vobjs.reserve(vec.size());
+ vobj_atlas_map.reserve(2*vec.size());
+
+ for (const auto& [name, descr, img_name] : vec)
+ {
+ auto atlas = make_vobj_anim_atlas(name, img_name);
+ auto info = vobj_info{name, descr, atlas};
+ vobjs.push_back(std::move(info));
+ const auto& x = vobjs.back();
+ vobj_atlas_map[x.atlas->name()] = &x;
+ }
+}
+
+ArrayView<vobj_info> loader_impl::vobj_list()
+{
+ if (vobjs.empty())
+ get_vobj_list();
+ return vobjs;
+}
+
+const struct vobj_info& loader_impl::vobj(StringView name)
+{
+ if (vobjs.empty())
+ get_vobj_list();
+ auto it = vobj_atlas_map.find(name);
+ if (it == vobj_atlas_map.end())
+ fm_throw("no such vobj '{}'"_cf, name);
+ return *it->second;
+}
+
+} // namespace floormat::loader_detail
diff --git a/shaders/lightmap.cpp b/shaders/lightmap.cpp
new file mode 100644
index 00000000..3575e424
--- /dev/null
+++ b/shaders/lightmap.cpp
@@ -0,0 +1,20 @@
+#include "lightmap.hpp"
+#include "src/local-coords.hpp"
+
+#ifdef __clang__
+#pragma clang diagnostic ignored "-Wfloat-equal"
+#endif
+
+namespace floormat {
+
+lightmap_shader::~lightmap_shader() = default;
+bool lightmap_shader::light_s::operator==(const light_s&) const noexcept = default;
+
+static constexpr Vector2 output_size = TILE_MAX_DIM * TILE_SIZE2 * 3;
+
+lightmap_shader::lightmap_shader()
+{
+
+}
+
+} // namespace floormat
diff --git a/shaders/lightmap.frag b/shaders/lightmap.frag
new file mode 100644
index 00000000..2179160c
--- /dev/null
+++ b/shaders/lightmap.frag
@@ -0,0 +1,18 @@
+precision mediump float;
+
+struct light_u
+{
+ vec4 color_and_intensity;
+ vec2 center;
+ uint mode;
+};
+
+#define TILE_MAX_DIM 16
+#define TILE_SIZE_X 64
+#define TILE_SIZE_Y 64
+
+#define CHUNK_SIZE_X (TILE_SIZE_X * TILE_MAX_DIM)
+#define CHUNK_SIZE_Y (TILE_SIZE_Y * TILE_MAX_DIM)
+
+layout (location = 0) uniform vec4 color_intensity;
+layout (location = 1) uniform vec2 px;
diff --git a/shaders/lightmap.hpp b/shaders/lightmap.hpp
new file mode 100644
index 00000000..a2d566e6
--- /dev/null
+++ b/shaders/lightmap.hpp
@@ -0,0 +1,49 @@
+#pragma once
+
+#include "light-falloff.hpp"
+#include <Magnum/GL/AbstractShaderProgram.h>
+#include <Magnum/Math/Vector2.h>
+#include <Magnum/Math/Vector3.h>
+#include <Magnum/Math/Vector4.h>
+#include <Magnum/Math/Color.h>
+
+namespace floormat {
+
+struct local_coords;
+
+struct lightmap_shader final : GL::AbstractShaderProgram
+{
+ using Position = GL::Attribute<0, Vector2>;
+
+ explicit lightmap_shader();
+ ~lightmap_shader() override;
+
+ void set_light(Vector2i neighbor_offset, local_coords pos, Vector2b offset);
+ struct light light() const; // is a reader accessor needed?
+
+private:
+ static Vector2i get_px_pos(Vector2i neighbor_offset, local_coords pos, Vector2b offset);
+
+ struct light_u final
+ {
+ Vector4 color_and_intensity;
+ Vector2 center;
+ uint32_t mode;
+ };
+
+ struct light_s final
+ {
+ float intensity = 1;
+ Color3ub color {255, 255, 255};
+ Vector2i center;
+ light_falloff falloff;
+
+ bool operator==(const light_s&) const noexcept;
+ };
+
+ enum { ColorUniform = 0, CenterUniform = 1, FalloffUniform = 2, DepthUniform = 3, };
+
+ light_s _light;
+};
+
+} // namespace floormat
diff --git a/shaders/lightmap.vert b/shaders/lightmap.vert
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/shaders/lightmap.vert
diff --git a/src/chunk-scenery.cpp b/src/chunk-scenery.cpp
index edfbfeb1..a2a61907 100644
--- a/src/chunk-scenery.cpp
+++ b/src/chunk-scenery.cpp
@@ -150,9 +150,8 @@ auto chunk::ensure_scenery_mesh(scenery_scratch_buffers buffers) noexcept -> sce
{
if (e->is_dynamic())
continue;
- if (e->is_virtual())
- continue;
const auto& atlas = e->atlas;
+ fm_debug_assert(atlas != nullptr);
const auto& fr = *e;
const auto pos = e->coord.local();
const auto coord = Vector3(pos) * TILE_SIZE + Vector3(Vector2(fr.offset), 0);
@@ -182,8 +181,6 @@ auto chunk::ensure_scenery_mesh(scenery_scratch_buffers buffers) noexcept -> sce
uint32_t j = 0, i = 0;
for (const auto& e : _entities)
{
- if (e->is_virtual())
- continue;
auto index = e->is_dynamic() ? (uint32_t)-1 : j++;
array[i++] = { e.get(), (uint32_t)-1, e->ordinal(), make_topo_sort_data(*e, index) };
}
diff --git a/src/light-falloff.hpp b/src/light-falloff.hpp
new file mode 100644
index 00000000..e90d11aa
--- /dev/null
+++ b/src/light-falloff.hpp
@@ -0,0 +1,9 @@
+#pragma once
+
+namespace floormat {
+
+enum class light_falloff : uint8_t {
+ linear, quadratic, constant,
+};
+
+} // namespace floormat
diff --git a/src/light.cpp b/src/light.cpp
new file mode 100644
index 00000000..64da857b
--- /dev/null
+++ b/src/light.cpp
@@ -0,0 +1,42 @@
+#include "light.hpp"
+#include "shaders/shader.hpp"
+#include <cmath>
+
+namespace floormat {
+
+light_proto::light_proto() = default;
+light_proto::light_proto(const light_proto&) = default;
+light_proto& light_proto::operator=(const light_proto&) = default;
+light_proto::~light_proto() noexcept = default;
+bool light_proto::operator==(const light_proto&) const = default;
+
+light::light(object_id id, struct chunk& c, const light_proto& proto) :
+ entity{id, c, proto},
+ half_dist{proto.half_dist},
+ color{proto.color},
+ falloff{proto.falloff}
+{
+}
+
+float light::depth_offset() const
+{
+ constexpr auto ret = 4.f / tile_shader::depth_tile_size;
+ return ret;
+}
+
+Vector2 light::ordinal_offset(Vector2b) const { return {}; }
+entity_type light::type() const noexcept { return entity_type::light; }
+bool light::update(size_t, float) { return false; }
+bool light::is_virtual() const { return true; }
+
+float light::calc_intensity(float half_dist, light_falloff falloff)
+{
+ switch (falloff)
+ {
+ case light_falloff::linear: return 2 * half_dist;
+ case light_falloff::quadratic: return std::sqrt(2 * half_dist);
+ default: case light_falloff::constant: return 1;
+ }
+}
+
+} // namespace floormat
diff --git a/src/light.hpp b/src/light.hpp
new file mode 100644
index 00000000..73d28272
--- /dev/null
+++ b/src/light.hpp
@@ -0,0 +1,44 @@
+#pragma once
+#include "src/light-falloff.hpp"
+#include "src/entity.hpp"
+#include <Magnum/Magnum.h>
+#include <Magnum/Math/Vector2.h>
+#include <Magnum/Math/Color.h>
+
+namespace floormat {
+
+struct light_proto : entity_proto
+{
+ light_proto();
+ light_proto(const light_proto&);
+ light_proto& operator=(const light_proto&);
+ ~light_proto() noexcept override;
+ bool operator==(const light_proto&) const;
+
+ Vector2 half_dist{3};
+ Color3ub color{255, 255, 255};
+ light_falloff falloff : 3 = light_falloff::linear;
+ uint8_t symmetric : 1 = true;
+};
+
+struct light final : entity
+{
+ Vector2 half_dist;
+ Color3ub color;
+ light_falloff falloff : 3;
+ uint8_t symmetric : 1 = true;
+
+ light(object_id id, struct chunk& c, const light_proto& proto);
+
+ Vector2 ordinal_offset(Vector2b offset) const override;
+ float depth_offset() const override;
+ entity_type type() const noexcept override;
+ bool update(size_t i, float dt) override;
+ bool is_virtual() const override;
+
+ static float calc_intensity(float half_dist, light_falloff falloff);
+
+ friend struct world;
+};
+
+} // namespace floormat
diff --git a/vobj/light.pdn b/vobj/light.pdn
new file mode 100644
index 00000000..7033a4f6
--- /dev/null
+++ b/vobj/light.pdn
Binary files differ
diff --git a/vobj/light.png b/vobj/light.png
new file mode 100644
index 00000000..56b2b6e1
--- /dev/null
+++ b/vobj/light.png
Binary files differ
diff --git a/vobj/vobj.json b/vobj/vobj.json
new file mode 100644
index 00000000..b3ccc5f8
--- /dev/null
+++ b/vobj/vobj.json
@@ -0,0 +1,7 @@
+[
+ {
+ "name": "light",
+ "description": "Light",
+ "image": "light.png"
+ }
+]