MVP for a Pong game
This commit is contained in:
parent
14343e7fd0
commit
e9d0de8cdd
17 changed files with 517 additions and 109 deletions
|
@ -21,4 +21,4 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
add_subdirectory(src)
|
add_subdirectory(src)
|
||||||
add_subdirectory(examples/simple)
|
add_subdirectory(examples)
|
2
examples/CMakeLists.txt
Normal file
2
examples/CMakeLists.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
add_subdirectory(simple)
|
||||||
|
add_subdirectory(pong)
|
4
examples/pong/CMakeLists.txt
Normal file
4
examples/pong/CMakeLists.txt
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
add_executable(paradiso_pong pong.cpp)
|
||||||
|
|
||||||
|
target_link_libraries(paradiso_pong paradiso_core)
|
259
examples/pong/pong.cpp
Normal file
259
examples/pong/pong.cpp
Normal file
|
@ -0,0 +1,259 @@
|
||||||
|
/**
|
||||||
|
* paradiso - Paradigmen der Softwareentwicklung
|
||||||
|
*
|
||||||
|
* (c) Copyright 2023 Hartmut Seichter
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <paradiso/bitmap.hpp>
|
||||||
|
#include <paradiso/context.hpp>
|
||||||
|
#include <paradiso/geometry.hpp>
|
||||||
|
#include <paradiso/renderer.hpp>
|
||||||
|
#include <paradiso/shader.hpp>
|
||||||
|
#include <paradiso/sprite.hpp>
|
||||||
|
#include <paradiso/vector.hpp>
|
||||||
|
#include <paradiso/window.hpp>
|
||||||
|
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <iomanip>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
struct PongStage {
|
||||||
|
|
||||||
|
using Vec2 = paradiso::Vector2<float>;
|
||||||
|
|
||||||
|
Vec2 tl{Vec2::make(-1.0f, -1.0f)};
|
||||||
|
Vec2 br{Vec2::make(+1.0f, +1.0f)};
|
||||||
|
|
||||||
|
enum class TouchPoint { None, Left, Right, Bottom, Top };
|
||||||
|
|
||||||
|
TouchPoint touch(const Vec2& o, float eps = 0.001f) noexcept {
|
||||||
|
std::unordered_map<TouchPoint, float> deltas;
|
||||||
|
deltas[TouchPoint::Left] = std::abs(o.x() - tl.x());
|
||||||
|
deltas[TouchPoint::Right] = std::abs(o.x() - br.x());
|
||||||
|
deltas[TouchPoint::Bottom] = std::abs(o.y() - br.y());
|
||||||
|
deltas[TouchPoint::Top] = std::abs(o.y() - tl.y());
|
||||||
|
|
||||||
|
auto min_el = *std::min_element(
|
||||||
|
std::begin(deltas), std::end(deltas),
|
||||||
|
[](const auto& l, const auto& r) { return l.second < r.second; });
|
||||||
|
|
||||||
|
auto t = (min_el.second <= eps) ? min_el.first : TouchPoint::None;
|
||||||
|
|
||||||
|
if (t != TouchPoint::None) {
|
||||||
|
sprite.bitmap.pixel(0,0) = paradiso::RGBA::white();
|
||||||
|
} else {
|
||||||
|
sprite.bitmap.pixel(0,0) = paradiso::RGBA::black();
|
||||||
|
}
|
||||||
|
|
||||||
|
return t;
|
||||||
|
};
|
||||||
|
|
||||||
|
// sprite
|
||||||
|
paradiso::Sprite sprite{
|
||||||
|
.bitmap = paradiso::Bitmap::from_data(paradiso::Size{1, 1},
|
||||||
|
paradiso::RGBA::from_rgb(0x80,0xFF,0xFF))
|
||||||
|
};
|
||||||
|
|
||||||
|
void draw(const paradiso::Shader& shader) {
|
||||||
|
renderer.draw(sprite, shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
paradiso::Renderer renderer{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PongPaddle {
|
||||||
|
|
||||||
|
static constexpr float whoopiness = 0.95f;
|
||||||
|
|
||||||
|
// sprite
|
||||||
|
paradiso::Sprite sprite{
|
||||||
|
.bitmap = paradiso::Bitmap::from_data(paradiso::Size{1, 1},
|
||||||
|
paradiso::RGBA::white()),
|
||||||
|
.pivot = paradiso::Vector2<float>::make(0.0f, -0.9f),
|
||||||
|
.scale = paradiso::Vector2<float>::make(0.25f, 0.01f)};
|
||||||
|
|
||||||
|
// keyboard handler
|
||||||
|
void on_keyboard(const paradiso::Window::KeyboardInputStack& input) {
|
||||||
|
|
||||||
|
if (input.size()) {
|
||||||
|
if (input.top().key == 'A' || input.top().key == 263) {
|
||||||
|
velocity_horizontal -= 0.01;
|
||||||
|
} else if (input.top().key == 'D' || input.top().key == 262) {
|
||||||
|
velocity_horizontal += 0.01;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw(const paradiso::Shader& shader) {
|
||||||
|
|
||||||
|
// update internal state
|
||||||
|
velocity_horizontal *= whoopiness;
|
||||||
|
sprite.pivot.x() += velocity_horizontal;
|
||||||
|
|
||||||
|
std::clamp(sprite.pivot.x(), -0.5f, 0.5f);
|
||||||
|
|
||||||
|
// update shader uniforms
|
||||||
|
shader.set_uniform("pivot", sprite.pivot);
|
||||||
|
shader.set_uniform("scale", sprite.scale);
|
||||||
|
shader.set_uniform("rotation", sprite.rotation);
|
||||||
|
|
||||||
|
renderer.draw(sprite, shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
float velocity_horizontal{};
|
||||||
|
|
||||||
|
paradiso::Renderer renderer{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PongBall {
|
||||||
|
// sprite
|
||||||
|
paradiso::Sprite sprite{
|
||||||
|
.bitmap = paradiso::Bitmap::from_data(paradiso::Size{1, 1},
|
||||||
|
paradiso::RGBA::white()),
|
||||||
|
.pivot = paradiso::Vector2<float>::make(0.0f, 0.0f),
|
||||||
|
.scale = paradiso::Vector2<float>::make(0.0125f, 0.0125f),
|
||||||
|
};
|
||||||
|
|
||||||
|
void interact(PongStage& stage) {
|
||||||
|
|
||||||
|
auto touch = stage.touch(sprite.pivot);
|
||||||
|
|
||||||
|
switch (touch) {
|
||||||
|
case PongStage::TouchPoint::Top:
|
||||||
|
case PongStage::TouchPoint::Bottom:
|
||||||
|
velocity.y() *= -1;
|
||||||
|
break;
|
||||||
|
case PongStage::TouchPoint::Left:
|
||||||
|
case PongStage::TouchPoint::Right:
|
||||||
|
velocity.x() *= -1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// interact ball & paddle
|
||||||
|
void interact(const PongPaddle& paddle) {
|
||||||
|
const auto& ps = paddle.sprite.scale;
|
||||||
|
const auto& pp = paddle.sprite.pivot;
|
||||||
|
|
||||||
|
auto left_x = pp.x() - ps.x();
|
||||||
|
auto right_x = pp.x() + ps.x();
|
||||||
|
|
||||||
|
static constexpr float eps{0.01f};
|
||||||
|
|
||||||
|
if (sprite.pivot.x() >= left_x && sprite.pivot.x() <= right_x &&
|
||||||
|
std::abs(pp.y() - sprite.pivot.y()) < eps) {
|
||||||
|
|
||||||
|
velocity.y() *= -1;
|
||||||
|
velocity.x() += paddle.velocity_horizontal * paddle.whoopiness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void update() { sprite.pivot += velocity; }
|
||||||
|
|
||||||
|
void draw(const paradiso::Shader& shader) {
|
||||||
|
|
||||||
|
std::clamp(sprite.pivot.x(), -0.5f, 0.5f);
|
||||||
|
|
||||||
|
// update shader uniforms
|
||||||
|
shader.set_uniform("pivot", sprite.pivot);
|
||||||
|
shader.set_uniform("scale", sprite.scale);
|
||||||
|
shader.set_uniform("rotation", sprite.rotation);
|
||||||
|
|
||||||
|
renderer.draw(sprite, shader);
|
||||||
|
}
|
||||||
|
|
||||||
|
paradiso::Vector2<float> velocity{};
|
||||||
|
|
||||||
|
paradiso::Renderer renderer{};
|
||||||
|
|
||||||
|
constexpr void push(const auto& impulse) noexcept { velocity += impulse; }
|
||||||
|
};
|
||||||
|
|
||||||
|
auto main() -> int {
|
||||||
|
|
||||||
|
// Ausgabefenster ... sieht aus als wäre es auf dem Stack
|
||||||
|
auto window = paradiso::Window();
|
||||||
|
|
||||||
|
// wir bauen ein Fenster ...
|
||||||
|
window
|
||||||
|
.set_size(paradiso::Size{.width = 720, .height = 720}) // ... Grösse
|
||||||
|
.set_position(paradiso::Point{.x = 100, .y = 100}) // ... Position
|
||||||
|
.set_title("PardiSO.Pong") // ... Titel
|
||||||
|
.set_visible(true); // ... und jetzt anzeigen!
|
||||||
|
|
||||||
|
// der Fenster Kontext
|
||||||
|
auto ctx = paradiso::Context{};
|
||||||
|
|
||||||
|
// ein Shader (Schattierungsprogramm)
|
||||||
|
auto shader = paradiso::Shader{};
|
||||||
|
|
||||||
|
// wir nutzen einen vorgefertigten shader
|
||||||
|
shader.load_preset(paradiso::Shader::Preset::Sprite);
|
||||||
|
|
||||||
|
auto paddle = PongPaddle{};
|
||||||
|
auto ball = PongBall{};
|
||||||
|
auto stage = PongStage{};
|
||||||
|
|
||||||
|
ball.push(paradiso::Vector2<float>::make(0.0001f, -0.0005f));
|
||||||
|
|
||||||
|
// ein viewport stellt die Sicht der Kamera dar, d.h. bei quadratischen
|
||||||
|
// Pixeln sollte hier auch eine dementsprechende Grösse eingestellt
|
||||||
|
// werden
|
||||||
|
ctx.set_viewport(paradiso::Rectangle{
|
||||||
|
.position = paradiso::Point{.x = 0, .y = 0},
|
||||||
|
.size = window.client_size()
|
||||||
|
.maximal_extent() // wir wollen das
|
||||||
|
// Seitenverhältnis beibehalten
|
||||||
|
});
|
||||||
|
|
||||||
|
// nothing beats a classic look
|
||||||
|
ctx.set_clearcolor(paradiso::RGBA::from_rgb(0x00, 0x00, 0x00));
|
||||||
|
|
||||||
|
// das update führt den hier mitgegebnen Ausdruck innerhalb der internen
|
||||||
|
// Updates des Fensters auf. Es wird hier auch explizit ein bool gefordert
|
||||||
|
// damit das update auch zu jederzeit unterbrochen werden kann
|
||||||
|
while (window.update([&](paradiso::Window& w) -> bool {
|
||||||
|
auto me = window.client_size().maximal_extent();
|
||||||
|
|
||||||
|
ctx.set_viewport(paradiso::Rectangle{
|
||||||
|
.position = paradiso::Point{.x = 0, .y = 0},
|
||||||
|
.size = me // wir wollen das
|
||||||
|
// Seitenverhältnis beibehalten
|
||||||
|
});
|
||||||
|
|
||||||
|
// hier wird das eigentliche löschen des vorherigen Inhalts ausgelöst
|
||||||
|
ctx.clear();
|
||||||
|
|
||||||
|
paddle.on_keyboard(w.keyboard_input());
|
||||||
|
|
||||||
|
// still here
|
||||||
|
bool want_close =
|
||||||
|
(w.keyboard_input().size() && w.keyboard_input().top().key == 'Q');
|
||||||
|
|
||||||
|
if (!w.keyboard_input().empty()) {
|
||||||
|
if (w.keyboard_input().top().key == 'R') {
|
||||||
|
ball.sprite.pivot.x() = 0.0f;
|
||||||
|
ball.sprite.pivot.y() = 0.9f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ball.interact(stage);
|
||||||
|
ball.interact(paddle);
|
||||||
|
|
||||||
|
ball.update();
|
||||||
|
|
||||||
|
stage.draw(shader);
|
||||||
|
paddle.draw(shader);
|
||||||
|
ball.draw(shader);
|
||||||
|
|
||||||
|
// ... signalisiere ob wir weitermachen wollen ...
|
||||||
|
return !want_close;
|
||||||
|
})) {
|
||||||
|
};
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
|
@ -1,16 +1,9 @@
|
||||||
set(paradiso_src
|
|
||||||
main.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
add_executable(
|
add_executable(
|
||||||
paradiso
|
paradiso_simple
|
||||||
${paradiso_src}
|
simple.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(paradiso
|
target_link_libraries(paradiso_simple
|
||||||
paradiso_core
|
paradiso_core
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(paradiso
|
|
||||||
PRIVATE
|
|
||||||
lib)
|
|
|
@ -16,66 +16,6 @@
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
|
||||||
void setup_shaders(paradiso::Shader& shader) {
|
|
||||||
const auto unlit_v = R"(
|
|
||||||
#version 400 core
|
|
||||||
|
|
||||||
layout (location = 0) in vec3 vertices;
|
|
||||||
layout (location = 1) in vec3 normals;
|
|
||||||
layout (location = 2) in vec2 texture_coords;
|
|
||||||
|
|
||||||
// pivot der sprite
|
|
||||||
uniform vec2 pivot = vec2( 0.0, 0.0 );
|
|
||||||
// scale
|
|
||||||
uniform vec2 scale = vec2( 1.0, 1.0 );
|
|
||||||
// rotation
|
|
||||||
uniform float rotation = 0.2;
|
|
||||||
|
|
||||||
// wir sind natuerlich in homogenenen 3D Koordinaten unterwegs
|
|
||||||
mat4 mm = mat4(
|
|
||||||
vec4( scale.x, 0.0, 0.0, 0.0),
|
|
||||||
vec4( 0.0, scale.y, 0.0, 0.0),
|
|
||||||
vec4( 0.0, 0.0, 1.0, 0.0),
|
|
||||||
vec4( pivot, 0.0, 1.0)
|
|
||||||
);
|
|
||||||
|
|
||||||
float sir = sin(rotation);
|
|
||||||
float cor = cos(rotation);
|
|
||||||
|
|
||||||
mat4 mr = mat4(
|
|
||||||
vec4( cor, sir, 0.0, 0.0),
|
|
||||||
vec4(-sir, cor, 0.0, 0.0),
|
|
||||||
vec4( 0.0, 0.0, 1.0, 0.0),
|
|
||||||
vec4( 0.0, 0.0, 0.0, 1.0)
|
|
||||||
);
|
|
||||||
|
|
||||||
out vec2 tex_c; // das hier reicht die texturkoordinaten durch
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
tex_c = texture_coords; // umstaendlich aber notwendig
|
|
||||||
gl_Position = mm * mr * vec4(vertices, 1.0); // unsere eigentliche shader position
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
const auto unlit_f = R"(
|
|
||||||
#version 400 core
|
|
||||||
|
|
||||||
uniform sampler2D tex_color; // hier ist unsere sprite textur (bitmap)
|
|
||||||
|
|
||||||
in vec2 tex_c; // da sind die texturkoordinaten wieder
|
|
||||||
|
|
||||||
out vec4 frag_color; // das hier wird der output (pixelwert/fragment)
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
frag_color = texture(tex_color,tex_c);
|
|
||||||
})";
|
|
||||||
|
|
||||||
shader.set_source(paradiso::Shader::Type::Vertex, unlit_v);
|
|
||||||
shader.set_source(paradiso::Shader::Type::Fragment, unlit_f);
|
|
||||||
|
|
||||||
shader.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
auto main() -> int {
|
auto main() -> int {
|
||||||
|
|
||||||
// Ausgabefenster ... sieht aus als wäre es auf dem Stack
|
// Ausgabefenster ... sieht aus als wäre es auf dem Stack
|
||||||
|
@ -85,7 +25,7 @@ auto main() -> int {
|
||||||
window
|
window
|
||||||
.set_size(paradiso::Size{.width = 1280, .height = 720}) // ... Grösse
|
.set_size(paradiso::Size{.width = 1280, .height = 720}) // ... Grösse
|
||||||
.set_position(paradiso::Point{.x = 100, .y = 100}) // ... Position
|
.set_position(paradiso::Point{.x = 100, .y = 100}) // ... Position
|
||||||
.set_title("PardiSO") // ... Titel
|
.set_title("PardiSO.Simple") // ... Titel
|
||||||
.set_visible(true); // ... und jetzt anzeigen!
|
.set_visible(true); // ... und jetzt anzeigen!
|
||||||
|
|
||||||
// der Fenster Kontext
|
// der Fenster Kontext
|
||||||
|
@ -107,36 +47,39 @@ auto main() -> int {
|
||||||
// ein Shader (Schattierungsprogramm)
|
// ein Shader (Schattierungsprogramm)
|
||||||
auto shader = paradiso::Shader{};
|
auto shader = paradiso::Shader{};
|
||||||
|
|
||||||
// hier werden die Shader Programme geladen, kompiliert usw.
|
// wir nutzen einen vorgefertigten shader
|
||||||
setup_shaders(shader);
|
shader.load_preset(paradiso::Shader::Preset::Sprite);
|
||||||
|
|
||||||
// kein schönes Design: dies sind globale Variablen ...
|
// kein schönes Design: dies sind globale Variablen ...
|
||||||
uint8_t slider_value = 0xFF;
|
uint8_t slider_value = 0xFF;
|
||||||
bool want_close{false};
|
bool want_close{false};
|
||||||
|
|
||||||
// eine sehr rudimentäre Eingabebehandlung. Bei vorhandenen
|
// hier "vor-deklariert" der Input-handler für unser Demo
|
||||||
// Eingaben landen diese hier. Wer sich am Design beteiligen
|
auto SimpleKeyboardHandler =
|
||||||
// möchte: hier gibt es viel Potential zur Verbesserung ;)
|
[&](const paradiso::Window::KeyboardInputStack& input) {
|
||||||
window.set_keyboardcallback(
|
// ohne Input kein Handling ...
|
||||||
[&](auto& w, int key, int scancode, int action, int mods) {
|
if (input.empty())
|
||||||
if (key == 'Q' || key == 256) // Q oder ESC beenden das Programm
|
return;
|
||||||
|
|
||||||
|
if (input.top().key == 'Q' ||
|
||||||
|
input.top().key == 256) // Q oder ESC beenden das Programm
|
||||||
want_close = true;
|
want_close = true;
|
||||||
else if (key == 'B') { // kleine Spielerei
|
else if (input.top().key == 'B') { // kleine Spielerei
|
||||||
slider_value += 10;
|
slider_value += 10;
|
||||||
} else if (key == 'W') {
|
} else if (input.top().key == 'W') {
|
||||||
sprite.pivot.y() += 0.1f;
|
sprite.pivot.y() += 0.1f;
|
||||||
} else if (key == 'S') {
|
} else if (input.top().key == 'S') {
|
||||||
sprite.pivot.y() -= 0.1f;
|
sprite.pivot.y() -= 0.1f;
|
||||||
} else if (key == 'A') {
|
} else if (input.top().key == 'A') {
|
||||||
sprite.pivot.x() -= 0.1f;
|
sprite.pivot.x() -= 0.1f;
|
||||||
} else if (key == 'D') {
|
} else if (input.top().key == 'D') {
|
||||||
sprite.pivot.x() += 0.1f;
|
sprite.pivot.x() += 0.1f;
|
||||||
} else if (key == 'P') {
|
} else if (input.top().key == 'P') {
|
||||||
sprite.scale *= 0.9;
|
sprite.scale *= 0.9;
|
||||||
} else if (key == 'R') {
|
} else if (input.top().key == 'R') {
|
||||||
sprite.rotation += 0.1;
|
sprite.rotation += 0.1;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
// das update führt den hier mitgegebnen Ausdruck innerhalb der internen
|
// das update führt den hier mitgegebnen Ausdruck innerhalb der internen
|
||||||
// Updates des Fensters auf. Es wird hier auch explizit ein bool gefordert
|
// Updates des Fensters auf. Es wird hier auch explizit ein bool gefordert
|
||||||
|
@ -152,11 +95,15 @@ auto main() -> int {
|
||||||
// Pixeln sollte hier auch eine dementsprechende Grösse eingestellt
|
// Pixeln sollte hier auch eine dementsprechende Grösse eingestellt
|
||||||
// werden
|
// werden
|
||||||
ctx.set_viewport(paradiso::Rectangle{
|
ctx.set_viewport(paradiso::Rectangle{
|
||||||
|
.position = paradiso::Point{.x = 0, .y = 0},
|
||||||
.size =
|
.size =
|
||||||
w.client_size().maximal_extent() // wir wollen das
|
w.client_size().maximal_extent() // wir wollen das
|
||||||
// Seitenverhältnis beibehalten
|
// Seitenverhältnis beibehalten
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// handle keyboard input ...
|
||||||
|
SimpleKeyboardHandler(w.keyboard_input());
|
||||||
|
|
||||||
// hier wird das eigentliche löschen des vorherigen Inhalts ausgelöst
|
// hier wird das eigentliche löschen des vorherigen Inhalts ausgelöst
|
||||||
ctx.clear();
|
ctx.clear();
|
||||||
|
|
|
@ -12,9 +12,11 @@ set(paradiso_srcs
|
||||||
src/window.cpp
|
src/window.cpp
|
||||||
src/renderer.cpp
|
src/renderer.cpp
|
||||||
src/context.cpp
|
src/context.cpp
|
||||||
|
src/shader_sprite.hpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(paradiso_incs
|
set(paradiso_incs
|
||||||
|
include/paradiso/aabb.hpp
|
||||||
include/paradiso/bitmap.hpp
|
include/paradiso/bitmap.hpp
|
||||||
include/paradiso/geometry.hpp
|
include/paradiso/geometry.hpp
|
||||||
include/paradiso/sprite.hpp
|
include/paradiso/sprite.hpp
|
||||||
|
|
61
src/lib/include/paradiso/aabb.hpp
Normal file
61
src/lib/include/paradiso/aabb.hpp
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Hartmut Seichter
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef PARADISO_AABB_HPP
|
||||||
|
#define PARADISO_AABB_HPP
|
||||||
|
|
||||||
|
#include <paradiso/vector.hpp>
|
||||||
|
|
||||||
|
namespace paradiso {
|
||||||
|
struct AABB final {
|
||||||
|
|
||||||
|
Vector2<float> top_left{};
|
||||||
|
Vector2<float> bottom_right{};
|
||||||
|
|
||||||
|
static constexpr AABB from_vertices(auto& vertices) noexcept {
|
||||||
|
|
||||||
|
Matrix<2, 2, float> min_max;
|
||||||
|
min_max.set_slice<2, 1>(0, 0) = vertices.front();
|
||||||
|
min_max.set_slice<2, 1>(1, 0) = vertices.front();
|
||||||
|
|
||||||
|
for (const auto& v : vertices) {
|
||||||
|
min_max(0, 0) = std::min(min_max(0, 0), v.x());
|
||||||
|
min_max(0, 1) = std::min(min_max(0, 1), v.y());
|
||||||
|
min_max(1, 0) = std::min(min_max(1, 0), v.x());
|
||||||
|
min_max(1, 1) = std::min(min_max(1, 1), v.y());
|
||||||
|
}
|
||||||
|
|
||||||
|
return {.top_left = Vector2<float>::make(min_max(0, 0), min_max(0, 1)),
|
||||||
|
.bottom_right =
|
||||||
|
Vector2<float>::make(min_max(1, 0), min_max(1, 1))};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool constexpr is_inside(const Vector2<float>& v) const {
|
||||||
|
return (v.x() >= top_left.x() && v.x() <= bottom_right.x() &&
|
||||||
|
v.y() >= top_left.y() && v.y() <= bottom_right.y());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace paradiso
|
||||||
|
|
||||||
|
#endif
|
|
@ -60,6 +60,7 @@ struct Bitmap final {
|
||||||
* @return reference to itself
|
* @return reference to itself
|
||||||
*/
|
*/
|
||||||
constexpr auto fill(const RGBA& color) noexcept {
|
constexpr auto fill(const RGBA& color) noexcept {
|
||||||
|
change_count++;
|
||||||
std::fill(data.begin(), data.end(), color);
|
std::fill(data.begin(), data.end(), color);
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
@ -71,6 +72,7 @@ struct Bitmap final {
|
||||||
* @return reference to pixel data at position
|
* @return reference to pixel data at position
|
||||||
*/
|
*/
|
||||||
constexpr RGBA& pixel(std::integral auto x, std::integral auto y) {
|
constexpr RGBA& pixel(std::integral auto x, std::integral auto y) {
|
||||||
|
change_count++;
|
||||||
return data[y * size.width + x];
|
return data[y * size.width + x];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,8 +87,14 @@ struct Bitmap final {
|
||||||
return data[y * size.width + x];
|
return data[y * size.width + x];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr void force_change() noexcept
|
||||||
|
{
|
||||||
|
change_count++;
|
||||||
|
}
|
||||||
|
|
||||||
Size size{.width = 0, .height = 0}; //!< extent of bitmap
|
Size size{.width = 0, .height = 0}; //!< extent of bitmap
|
||||||
std::vector<RGBA> data{}; //!< data storage
|
std::vector<RGBA> data{}; //!< data storage
|
||||||
|
std::uint64_t change_count{};
|
||||||
};
|
};
|
||||||
} // namespace paradiso
|
} // namespace paradiso
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
#include <paradiso/globals.hpp>
|
#include <paradiso/globals.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace paradiso {
|
namespace paradiso {
|
||||||
|
|
||||||
|
@ -84,7 +85,7 @@ template <typename Scalar, typename Derived> struct MatrixBase {
|
||||||
|
|
||||||
constexpr void normalize() { *this /= this->norm(); }
|
constexpr void normalize() { *this /= this->norm(); }
|
||||||
|
|
||||||
static constexpr Scalar dot(const Derived& a, const Derived& b) {
|
static constexpr Scalar dot(const auto& a, const auto& b) {
|
||||||
return std::inner_product(std::begin(a), std::end(a), std::begin(b),
|
return std::inner_product(std::begin(a), std::end(a), std::begin(b),
|
||||||
Scalar{0});
|
Scalar{0});
|
||||||
}
|
}
|
||||||
|
@ -111,6 +112,13 @@ template <typename Scalar, typename Derived> struct MatrixBase {
|
||||||
e -= b;
|
e -= b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr void operator+=(const Derived& b) {
|
||||||
|
std::transform((*this).begin(),(*this).end(),b.begin(),(*this).begin(),std::plus<>());
|
||||||
|
}
|
||||||
|
constexpr void operator-=(const Derived& b) {
|
||||||
|
std::transform((*this).begin(),(*this).end(),b.begin(),(*this).begin(),std::minus<>());
|
||||||
|
}
|
||||||
|
|
||||||
constexpr const Derived operator*(const Scalar& b) const {
|
constexpr const Derived operator*(const Scalar& b) const {
|
||||||
Derived r(derived());
|
Derived r(derived());
|
||||||
for (auto& e : r)
|
for (auto& e : r)
|
||||||
|
|
|
@ -79,6 +79,14 @@ struct RGBA final {
|
||||||
pixel = (pixel & 0xFFFFFF00) | v;
|
pixel = (pixel & 0xFFFFFF00) | v;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static constexpr RGBA white() noexcept {
|
||||||
|
return RGBA::from_rgb(0xFF, 0xFF, 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
static constexpr RGBA black() noexcept {
|
||||||
|
return RGBA::from_rgb(0x00, 0x00, 0x00);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} // namespace paradiso
|
} // namespace paradiso
|
||||||
|
|
||||||
|
|
|
@ -42,9 +42,13 @@ struct Shader final {
|
||||||
|
|
||||||
enum class Type { Vertex, Fragment, Geometry, Compute };
|
enum class Type { Vertex, Fragment, Geometry, Compute };
|
||||||
|
|
||||||
|
enum class Preset { Sprite };
|
||||||
|
|
||||||
void set_source(Type t, const std::string& c) { source_[t] = c; }
|
void set_source(Type t, const std::string& c) { source_[t] = c; }
|
||||||
std::string source(Type t) const { return source_.at(t); }
|
std::string source(Type t) const { return source_.at(t); }
|
||||||
|
|
||||||
|
bool load_preset(Preset preset);
|
||||||
|
|
||||||
bool build();
|
bool build();
|
||||||
|
|
||||||
bool ready() const;
|
bool ready() const;
|
||||||
|
@ -54,9 +58,11 @@ struct Shader final {
|
||||||
const Shader&
|
const Shader&
|
||||||
set_uniform_at_location(int location,
|
set_uniform_at_location(int location,
|
||||||
float v) const; //!< sets a float in a shader
|
float v) const; //!< sets a float in a shader
|
||||||
|
|
||||||
const Shader& set_uniform_at_location(
|
const Shader& set_uniform_at_location(
|
||||||
int location,
|
int location,
|
||||||
uint32_t v) const; //!< sets a 32bit unsigned in a shader
|
uint32_t v) const; //!< sets a 32bit unsigned in a shader
|
||||||
|
|
||||||
const Shader& set_uniform_at_location(
|
const Shader& set_uniform_at_location(
|
||||||
int location,
|
int location,
|
||||||
int32_t v) const; //!< sets a 32bit signed in a shader
|
int32_t v) const; //!< sets a 32bit signed in a shader
|
||||||
|
|
|
@ -27,19 +27,24 @@
|
||||||
#include <paradiso/geometry.hpp>
|
#include <paradiso/geometry.hpp>
|
||||||
|
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <stack>
|
||||||
|
|
||||||
namespace paradiso {
|
namespace paradiso {
|
||||||
|
|
||||||
|
struct KeyboardInput final {
|
||||||
|
int key{}, scancode{}, action{}, mods{};
|
||||||
|
};
|
||||||
|
|
||||||
struct Window final {
|
struct Window final {
|
||||||
|
|
||||||
using Size = paradiso::Size;
|
using Size = paradiso::Size;
|
||||||
using Position = paradiso::Point;
|
using Position = paradiso::Point;
|
||||||
|
using KeyboardInputStack = std::stack<KeyboardInput>;
|
||||||
|
|
||||||
using on_updatecallback_t = std::function<bool(
|
using on_updatecallback_t = std::function<bool(
|
||||||
Window&)>; //! update needs to return true to continue
|
Window&)>; //! update needs to return true to continue
|
||||||
using on_resizecallback_t =
|
using on_resizecallback_t =
|
||||||
std::function<void(Window&)>; //! resize is 'informal'
|
std::function<void(Window&)>; //! resize is 'informal'
|
||||||
using on_keyboardcallback_t =
|
|
||||||
std::function<void(Window&, int, int, int, int)>;
|
|
||||||
|
|
||||||
Window();
|
Window();
|
||||||
~Window();
|
~Window();
|
||||||
|
@ -66,17 +71,20 @@ struct Window final {
|
||||||
Window& set_fullscreen(bool use_fullscreen);
|
Window& set_fullscreen(bool use_fullscreen);
|
||||||
|
|
||||||
void set_resizecallback(on_resizecallback_t f) { on_resize_ = f; }
|
void set_resizecallback(on_resizecallback_t f) { on_resize_ = f; }
|
||||||
void set_keyboardcallback(on_keyboardcallback_t f) { on_keyboard_ = f; }
|
|
||||||
|
|
||||||
bool visible() const;
|
bool visible() const;
|
||||||
Window& set_visible(bool is_visible);
|
Window& set_visible(bool is_visible);
|
||||||
|
|
||||||
|
const KeyboardInputStack& keyboard_input() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct impl;
|
struct impl;
|
||||||
std::unique_ptr<impl> impl_;
|
std::unique_ptr<impl> impl_;
|
||||||
|
|
||||||
on_resizecallback_t on_resize_;
|
on_resizecallback_t on_resize_;
|
||||||
on_keyboardcallback_t on_keyboard_;
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace paradiso
|
} // namespace paradiso
|
||||||
|
|
|
@ -38,14 +38,14 @@ namespace paradiso {
|
||||||
struct Renderer::impl {
|
struct Renderer::impl {
|
||||||
|
|
||||||
uint64_t change_count{std::numeric_limits<uint64_t>::max()};
|
uint64_t change_count{std::numeric_limits<uint64_t>::max()};
|
||||||
|
uint64_t change_count_texture{std::numeric_limits<uint64_t>::max()};
|
||||||
|
|
||||||
|
// below corresponds to GL state
|
||||||
uint32_t vertex_array_obj{};
|
uint32_t vertex_array_obj{};
|
||||||
uint32_t element_buffer_obj{};
|
uint32_t element_buffer_obj{};
|
||||||
std::vector<uint32_t> vertex_buffer_ob{};
|
std::vector<uint32_t> vertex_buffer_ob{};
|
||||||
uint32_t texture_id{};
|
uint32_t texture_id{};
|
||||||
|
|
||||||
impl() = default;
|
|
||||||
|
|
||||||
~impl() { release(); }
|
~impl() { release(); }
|
||||||
|
|
||||||
bool ready() const {
|
bool ready() const {
|
||||||
|
@ -153,7 +153,7 @@ struct Renderer::impl {
|
||||||
glGenerateMipmap(GL_TEXTURE_2D);
|
glGenerateMipmap(GL_TEXTURE_2D);
|
||||||
glGenerateTextureMipmap(texture_id);
|
glGenerateTextureMipmap(texture_id);
|
||||||
|
|
||||||
} else {
|
} else if (image.change_count != change_count_texture) {
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
glBindTexture(GL_TEXTURE_2D, texture_id);
|
glBindTexture(GL_TEXTURE_2D, texture_id);
|
||||||
|
@ -169,6 +169,8 @@ struct Renderer::impl {
|
||||||
GL_BGRA, // format
|
GL_BGRA, // format
|
||||||
GL_UNSIGNED_BYTE, // type
|
GL_UNSIGNED_BYTE, // type
|
||||||
image.data.data()); // pointer
|
image.data.data()); // pointer
|
||||||
|
|
||||||
|
change_count_texture = image.change_count;
|
||||||
}
|
}
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
|
|
|
@ -22,6 +22,8 @@
|
||||||
*/
|
*/
|
||||||
#include "paradiso/shader.hpp"
|
#include "paradiso/shader.hpp"
|
||||||
|
|
||||||
|
#include "shader_sprite.hpp"
|
||||||
|
|
||||||
#include "glad/glad.h"
|
#include "glad/glad.h"
|
||||||
|
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
@ -199,6 +201,18 @@ Shader::~Shader() {}
|
||||||
|
|
||||||
bool Shader::ready() const { return impl_->is_valid(); }
|
bool Shader::ready() const { return impl_->is_valid(); }
|
||||||
|
|
||||||
|
bool Shader::load_preset(Preset preset) {
|
||||||
|
switch (preset) {
|
||||||
|
case Preset::Sprite:
|
||||||
|
this->set_source(paradiso::Shader::Type::Vertex, sprite_unlit_vertex);
|
||||||
|
this->set_source(paradiso::Shader::Type::Fragment,
|
||||||
|
sprite_unlit_fragment);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return build();
|
||||||
|
}
|
||||||
|
|
||||||
const Shader& Shader::set_uniform_at_location(int location, float v) const {
|
const Shader& Shader::set_uniform_at_location(int location, float v) const {
|
||||||
impl_->bind(location, v);
|
impl_->bind(location, v);
|
||||||
return *this;
|
return *this;
|
||||||
|
|
83
src/lib/src/shader_sprite.hpp
Normal file
83
src/lib/src/shader_sprite.hpp
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2023 Hartmut Seichter
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
* SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
#ifndef PARADISO_SHADER_SPRITE_HPP
|
||||||
|
#define PARADISO_SHADER_SPRITE_HPP
|
||||||
|
|
||||||
|
namespace paradiso {
|
||||||
|
|
||||||
|
static constexpr auto sprite_unlit_vertex = R"(
|
||||||
|
#version 400 core
|
||||||
|
|
||||||
|
layout (location = 0) in vec3 vertices;
|
||||||
|
layout (location = 1) in vec3 normals;
|
||||||
|
layout (location = 2) in vec2 texture_coords;
|
||||||
|
|
||||||
|
// pivot der sprite
|
||||||
|
uniform vec2 pivot = vec2( 0.0, 0.0 );
|
||||||
|
// scale
|
||||||
|
uniform vec2 scale = vec2( 1.0, 1.0 );
|
||||||
|
// rotation
|
||||||
|
uniform float rotation = 0.0;
|
||||||
|
|
||||||
|
// wir sind natuerlich in homogenenen 3D Koordinaten unterwegs
|
||||||
|
mat4 mm = mat4(
|
||||||
|
vec4( scale.x, 0.0, 0.0, 0.0),
|
||||||
|
vec4( 0.0, scale.y, 0.0, 0.0),
|
||||||
|
vec4( 0.0, 0.0, 1.0, 0.0),
|
||||||
|
vec4( pivot, 0.0, 1.0)
|
||||||
|
);
|
||||||
|
|
||||||
|
float sir = sin(rotation);
|
||||||
|
float cor = cos(rotation);
|
||||||
|
|
||||||
|
mat4 mr = mat4(
|
||||||
|
vec4( cor, sir, 0.0, 0.0),
|
||||||
|
vec4(-sir, cor, 0.0, 0.0),
|
||||||
|
vec4( 0.0, 0.0, 1.0, 0.0),
|
||||||
|
vec4( 0.0, 0.0, 0.0, 1.0)
|
||||||
|
);
|
||||||
|
|
||||||
|
out vec2 tex_c; // das hier reicht die texturkoordinaten durch
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
tex_c = texture_coords; // umstaendlich aber notwendig
|
||||||
|
gl_Position = mm * mr * vec4(vertices, 1.0); // unsere eigentliche shader transformation
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
|
||||||
|
static constexpr auto sprite_unlit_fragment = R"(
|
||||||
|
#version 400 core
|
||||||
|
|
||||||
|
uniform sampler2D tex_color; // hier ist unsere sprite textur (bitmap)
|
||||||
|
|
||||||
|
in vec2 tex_c; // da sind die texturkoordinaten wieder
|
||||||
|
|
||||||
|
out vec4 frag_color; // das hier wird der output (pixelwert/fragment)
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
frag_color = texture(tex_color,tex_c);
|
||||||
|
})";
|
||||||
|
} // namespace paradiso
|
||||||
|
|
||||||
|
#endif
|
|
@ -39,6 +39,8 @@ struct Window::impl {
|
||||||
|
|
||||||
GLFWwindow* window_ = nullptr;
|
GLFWwindow* window_ = nullptr;
|
||||||
|
|
||||||
|
KeyboardInputStack keyboard_input_{};
|
||||||
|
|
||||||
std::tuple<int, int> _old_size;
|
std::tuple<int, int> _old_size;
|
||||||
std::tuple<int, int> _old_pos;
|
std::tuple<int, int> _old_pos;
|
||||||
|
|
||||||
|
@ -79,12 +81,9 @@ struct Window::impl {
|
||||||
int action, int mods) {
|
int action, int mods) {
|
||||||
Window::impl* impl =
|
Window::impl* impl =
|
||||||
static_cast<Window::impl*>(glfwGetWindowUserPointer(window));
|
static_cast<Window::impl*>(glfwGetWindowUserPointer(window));
|
||||||
impl->parent_.get().on_keyboard_(impl->parent_, key, scancode, action,
|
|
||||||
mods);
|
impl->keyboard_input_.emplace(KeyboardInput{
|
||||||
// input::get()._key_code = scancode;
|
.key = key, .scancode = scancode, .action = action, .mods = mods});
|
||||||
// input::get()._key_pressed = action;
|
|
||||||
// action 0,1,2
|
|
||||||
// std::cout << __FUNCTION__ << action << std::endl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void charmods_callback(GLFWwindow* window, unsigned int codepoint,
|
static void charmods_callback(GLFWwindow* window, unsigned int codepoint,
|
||||||
|
@ -179,9 +178,10 @@ struct Window::impl {
|
||||||
|
|
||||||
bool update(Window::on_updatecallback_t cb) {
|
bool update(Window::on_updatecallback_t cb) {
|
||||||
if (window_ && !glfwWindowShouldClose(window_)) {
|
if (window_ && !glfwWindowShouldClose(window_)) {
|
||||||
// TODO lock and unlock the current input system to allow for late
|
|
||||||
// events coming in
|
// delete events
|
||||||
// input::get().reset();
|
paradiso::Window::KeyboardInputStack{}.swap(keyboard_input_);
|
||||||
|
|
||||||
|
|
||||||
// get new events
|
// get new events
|
||||||
glfwPollEvents();
|
glfwPollEvents();
|
||||||
|
@ -263,10 +263,8 @@ struct Window::impl {
|
||||||
//
|
//
|
||||||
//
|
//
|
||||||
Window::Window()
|
Window::Window()
|
||||||
: impl_(std::make_unique<Window::impl>(*this)), on_resize_([](Window&) {}),
|
: impl_(std::make_unique<Window::impl>(*this)), on_resize_([](Window&) {}) {
|
||||||
on_keyboard_([](Window&, int, int, int, int) {})
|
}
|
||||||
|
|
||||||
{}
|
|
||||||
|
|
||||||
Window::~Window() {}
|
Window::~Window() {}
|
||||||
|
|
||||||
|
@ -308,4 +306,9 @@ Window& Window::set_title(std::string_view title) {
|
||||||
impl_->set_title(title);
|
impl_->set_title(title);
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
const Window::KeyboardInputStack& Window::keyboard_input() const {
|
||||||
|
return impl_->keyboard_input_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
} // namespace paradiso
|
} // namespace paradiso
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue