Open13
Siv3D Advent Calendar 2024 ネタ帳
ピン留めされたアイテム
物理演算でボールを転がして、ゴールに到達したかを判定する
-
P2Body
から元になった基本図形を取得し、通常の図形どうしのあたり判定をするのがポイントです。
# include <Siv3D.hpp> // Siv3D v0.6.15
void Main()
{
// ウィンドウを 1280x720 のサイズに変更する
Window::Resize(1280, 720);
// 背景色を設定する
Scene::SetBackground(ColorF{ 0.2, 0.5, 0.8 });
// フォント
const Font font = Font{ FontMethod::MSDF, 48, Typeface::Heavy }.setBufferThickness(4);
// 2D 物理演算のシミュレーションステップ(秒)
constexpr double StepTime = (1.0 / 200.0);
// 2D 物理演算のシミュレーション蓄積時間(秒)
double accumulatedTime = 0.0;
// 2D 物理演算のワールド
P2World world;
// 床
Array<P2Body> grounds;
grounds << world.createRect(P2Static, Vec2{ 640, 600 }, SizeF{ 1000, 10 });
grounds << world.createLine(P2Static, Vec2{ 300, 500 }, Line{ -200, -20, 200, 20 });
// ボール
const P2Body ball = world.createCircle(P2Dynamic, Vec2{ 200, 400 }, 20);
// ゴール判定領域
const RectF goalArea{ 980, 500, 40, 100 };
// ゴールしたか
bool goal = false;
while (System::Update())
{
// ゴール後は物理演算を停止する
if (not goal)
{
for (accumulatedTime += Scene::DeltaTime(); StepTime <= accumulatedTime; accumulatedTime -= StepTime)
{
// 2D 物理演算のワールドを StepTime 秒進める
world.update(StepTime);
}
}
{
// createCircle() で作成したボールから P2Circle を取得できる。
const P2Circle& circleShape = static_cast<const P2Circle&>(ball.shape(0));
// P2Circle から Circle を取得できるので、RectF とのあたり判定が可能
if (circleShape.getCircle().intersects(goalArea))
{
// ゴール領域とボールが重なったらゴール
goal = true;
}
}
// 地面を描画する
for (const auto& ground : grounds)
{
ground.draw();
}
// ボールを描画する
ball.draw();
// ゴール判定領域を描画する
goalArea.draw(ColorF{ 0.2, 1.0, 0.6, 0.5 });
// ゴールしたら SUCCESS! と表示する
if (goal)
{
font(U"SUCCESS!")
.drawAt(TextStyle::OutlineShadow(0.25, ColorF{ 0.11 }, Vec2{ 2, 2 }, ColorF{ 0.11 }),
90, Scene::Center(), ColorF{ 1.0 });
}
}
}
フォント描画で手書き感を出す
# include <Siv3D.hpp> // Siv3D v0.6.15
struct PointsGlyph : OutlineGlyph
{
Array<Array<Vec2>> points;
PointsGlyph() = default;
PointsGlyph(const OutlineGlyph& outlineGlyph, double distance)
: OutlineGlyph{ outlineGlyph }
{
for (const auto& ring : outlineGlyph.rings)
{
const double length = ring.calculateLength();
const int32 count = static_cast<int32>((length / distance) + 1);
distance = (length / count);
Array<Vec2> densifiedPoints;
for (size_t i = 0; i < count; ++i)
{
const double t = (i * distance);
const Vec2 point = ring.calculatePointFromOrigin(t);
densifiedPoints << point;
}
points << densifiedPoints;
}
}
};
struct SketchGlyph : PointsGlyph
{
struct Style
{
double pointDistance = 2.5;
double strokeRandomness = 0.12;
double angleThreshold = 166_deg;
double stretchFactor = 0.45;
double shiftFactor = 0.22;
};
Array<Line> lines;
SketchGlyph() = default;
SketchGlyph(const OutlineGlyph& outlineGlyph, const Style& style)
: PointsGlyph{ outlineGlyph, style.pointDistance }
{
for (const auto& densifiedPoints : points)
{
for (size_t i = 0; i < densifiedPoints.size(); ++i)
{
std::array<Vec2, 10> localPoints{};
for (size_t k = 0; k < localPoints.size(); ++k)
{
localPoints[k] = densifiedPoints[((i + k) % densifiedPoints.size())]
.movedBy(RandomVec2(style.pointDistance * style.strokeRandomness));
}
size_t targetIndex = 1;
for (; targetIndex < (localPoints.size() - 1); ++targetIndex)
{
const Vec2 v0 = (localPoints[0] - localPoints[targetIndex]);
const Vec2 v1 = (localPoints[targetIndex + 1] - localPoints[targetIndex]);
if (v0.getAngle(v1) < style.angleThreshold)
{
break;
}
}
const Vec2 p0 = localPoints[0];
const Vec2 p1 = localPoints[targetIndex];
const Line line = Line{ p0, p1 }.stretched(Random(style.pointDistance * targetIndex * style.stretchFactor));
lines << line.movedBy(line.normal().withLength(Random(style.pointDistance * style.shiftFactor) + Random(style.pointDistance * style.shiftFactor)));
if (4 < targetIndex)
{
i += 2;
}
}
}
}
};
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.99, 0.97, 0.95 });
const Font fontOutline{ 144, Typeface::Bold };
const Font fontSDF{ FontMethod::SDF, 12, Typeface::Bold };
const Font fontBitmap{ FontMethod::Bitmap, 22, Typeface::Bold };
TextEditState textEditState{ U"Siv3Dあいう漢字" };
Array<SketchGlyph> glyphs;
SketchGlyph::Style style;
double baseThickness = 1.7;
double thicknessAttenuationFactor = 0.02;
while (System::Update())
{
Vec2 penPos{ 60, 120 };
fontSDF(textEditState.text).draw(fontOutline.fontSize(), penPos, ColorF{ 1.0, 0.7, 0.8 });
fontBitmap(textEditState.text).draw(fontOutline.fontSize(), penPos, ColorF{ 1.0, 0.7, 0.8, 0.8 });
for (const auto& glyph : glyphs)
{
const Transformer2D tr{ Mat3x2::Translate(penPos + glyph.getOffset()) };
for (const auto& line : glyph.lines)
{
line.draw((baseThickness - line.length() * thicknessAttenuationFactor), ColorF{ 0.3, 0.2, 0.1, 0.3 });
}
penPos.x += glyph.xAdvance;
}
bool hasChanged = false;
if (SimpleGUI::TextBox(textEditState, Vec2{ 60, 340 }, 200)
|| (textEditState.text && (not glyphs)))
{
hasChanged = true;
}
if (SimpleGUI::Slider(U"pointDistance: {:.1f}"_fmt(style.pointDistance), style.pointDistance, 0.2, 8.0, Vec2{ 60, 380 }, 240, 200))
{
hasChanged = true;
}
if (SimpleGUI::Slider(U"strokeRandomness: {:.2f}"_fmt(style.strokeRandomness), style.strokeRandomness, 0.0, 0.5, Vec2{ 60, 420 }, 240, 200))
{
hasChanged = true;
}
if (SimpleGUI::Slider(U"angleThreshold: {:.0f}"_fmt(Math::ToDegrees(style.angleThreshold)), style.angleThreshold, 90_deg, 180_deg, Vec2{ 60, 460 }, 240, 200))
{
hasChanged = true;
}
if (SimpleGUI::Slider(U"stretchFactor: {:.2f}"_fmt(style.stretchFactor), style.stretchFactor, 0.0, 1.0, Vec2{ 60, 500 }, 240, 200))
{
hasChanged = true;
}
if (SimpleGUI::Slider(U"shiftFactor: {:.2f}"_fmt(style.shiftFactor), style.shiftFactor, 0.0, 1.0, Vec2{ 60, 540 }, 240, 200))
{
hasChanged = true;
}
if (SimpleGUI::Slider(U"baseThickness: {:.1f}"_fmt(baseThickness), baseThickness, 0.5, 5.0, Vec2{ 60, 580 }, 240, 200))
{
hasChanged = true;
}
if (SimpleGUI::Slider(U"thicknessAttenuationFactor: {:.2f}"_fmt(thicknessAttenuationFactor), thicknessAttenuationFactor, 0.0, 0.5, Vec2{ 60, 620 }, 360, 200))
{
hasChanged = true;
}
if (hasChanged)
{
glyphs = fontOutline.renderOutlines(textEditState.text, CloseRing::Yes).map([&](const OutlineGlyph& outlineGlyph) { return SketchGlyph{ outlineGlyph, style }; });
}
}
}
獲得したアイテムを輝かしく見せる
- 上下左右方向にシフトした描画で輪郭を足し、アイテムを強調します。
-
Circle::draw()
とCircle::drawPie()
で輝いている背景を作ります。
# include <Siv3D.hpp> // Siv3D v0.6.15
namespace EffectLib
{
void DrawWithOutline(const Texture& texture, const Vec2& pos, double offset, const ColorF& outlineColor)
{
{
const ScopedColorAdd2D colorAdd{ ColorF{ outlineColor, 0.0 } };
texture.drawAt(pos.movedBy(0, -offset), ColorF{ 0.0 });
texture.drawAt(pos.movedBy(-offset, 0), ColorF{ 0.0 });
texture.drawAt(pos.movedBy(0, offset), ColorF{ 0.0 });
texture.drawAt(pos.movedBy(offset, 0), ColorF{ 0.0 });
}
texture.drawAt(pos);
}
void DrawGlowEffect1(const Vec2& center, double radius, const ColorF& color)
{
Circle{ center, (radius * 0.4) }.draw(color, ColorF{ color, 0.0 });
Circle{ center, (radius * 0.6) }.draw(color, ColorF{ color, 0.0 });
Circle{ center, (radius * 0.8) }.draw(color, ColorF{ color, 0.0 });
Circle{ center, radius }.draw(color, ColorF{ color, 0.0 });
}
void DrawGlowEffect2(const Vec2& center, double radius, const ColorF& color)
{
const Circle baseCircle{ center, radius };
for (int32 i = 0; i < 9; ++i)
{
baseCircle.drawPie((-10_deg + i * 40_deg), 20_deg, color, ColorF{ color, 0.0 });
}
}
}
void DrawBackground()
{
for (int32 y = 0; y < (Scene::Height() / 40); ++y)
{
for (int32 x = 0; x < (Scene::Width() / 40); ++x)
{
if (IsEven(x + y))
{
RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.35 });
}
}
}
}
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.32 });
const Texture texture1{ U"🥗"_emoji };
const Texture texture2{ U"💰"_emoji };
while (System::Update())
{
DrawBackground();
{
const Vec2 pos{ 200, 200 };
texture1.drawAt(pos);
}
{
const Vec2 pos{ 500, 200 };
EffectLib::DrawWithOutline(texture1, pos, 3, ColorF{ 0.15 });
}
{
const Vec2 pos{ 800, 200 };
EffectLib::DrawGlowEffect1(pos, 120, ColorF{ 1.0, 0.9, 0.4 });
EffectLib::DrawWithOutline(texture1, pos, 3, ColorF{ 0.15 });
}
{
const Vec2 pos{ 1100, 200 };
EffectLib::DrawGlowEffect1(pos, 120, ColorF{ 1.0, 0.9, 0.4 });
EffectLib::DrawGlowEffect2(pos, 160, ColorF{ 1.0, 0.9, 0.4 });
EffectLib::DrawWithOutline(texture1, pos, 3, ColorF{ 0.15 });
}
{
const Vec2 pos{ 200, 500 };
texture2.drawAt(pos);
}
{
const Vec2 pos{ 500, 500 };
EffectLib::DrawWithOutline(texture2, pos, 6, ColorF{ 0.96 });
}
{
const Vec2 pos{ 800, 500 };
EffectLib::DrawGlowEffect1(pos, 120, ColorF{ 0.2, 0.8, 1.0 });
EffectLib::DrawWithOutline(texture2, pos, 6, ColorF{ 0.96 });
}
{
const Vec2 pos{ 1100, 500 };
EffectLib::DrawGlowEffect1(pos, 120, ColorF{ 0.2, 0.8, 1.0 });
EffectLib::DrawGlowEffect2(pos, 160, ColorF{ 0.2, 0.8, 1.0 });
EffectLib::DrawWithOutline(texture2, pos, 6, ColorF{ 0.96 });
}
}
}
テキストを小気味よく登場させる
# include <Siv3D.hpp> // Siv3D v0.6.15
/// @brief テキストを小気味よく表示します。
/// @param center テキストの中心座標
/// @param font フォント
/// @param text 表示するテキスト
/// @param fontSize フォントサイズ
/// @param textStyle テキストスタイル
/// @param color テキストの色
/// @param t アニメーション進捗(0.0 ~ 1.0)
/// @param step 次の文字が表示されるまでのステップ
/// @param overshootFactor オーバーシュートの強さ(1.0 がデフォルト)
void DrawGlyphsAt(const Vec2& center, const Font& font, const StringView text,
const double fontSize, const TextStyle& textStyle, const ColorF& color,
const double t, const double step, const double overshootFactor)
{
const double scale = (fontSize / font.fontSize());
const SizeF centerOffset = (font(text).region().size * scale * 0.5);
const Array<Glyph> glyphs = font.getGlyphs(text);
const double tt = (t * glyphs.size() * step);
{
const ScopedCustomShader2D shader{ Font::GetPixelShader(font.method(), textStyle.type) };
Graphics2D::SetSDFParameters(textStyle);
Vec2 penPos{ center - centerOffset };
for (auto&& [i, glyph] : Indexed(glyphs))
{
const double e = Math::Pow(EaseOutBack(Math::Saturate(tt - i * step)), overshootFactor);
if (e <= 0.0)
{
break;
}
const Vec2 pos = (penPos + glyph.getOffset(scale)) + (glyph.texture.size * scale * 0.5 * (1.0 - e));
glyph.texture.scaled(scale * e).draw(pos, color);
penPos.x += (glyph.xAdvance * scale);
}
}
}
void DrawBackground()
{
for (int32 y = 0; y < (Scene::Height() / 40); ++y)
{
for (int32 x = 0; x < (Scene::Width() / 40); ++x)
{
if (IsEven(x + y))
{
RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.35 });
}
}
}
}
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.32 });
// フォント
const Font font = Font{ FontMethod::MSDF, 48, Typeface::Heavy }.setBufferThickness(4);
// ストップウォッチ
Stopwatch stopwatch{ StartImmediately::Yes };
// 表示するテキスト
TextEditState textEditState{ U"SUCCESS!" };
// 再生時間
double duration = 0.6;
// 次の文字が表示されるまでのステップ
double step = 0.2;
// オーバーシュートの強さ
double overshootFactor = 3.0;
// 文字の色
HSV color{ 70.0, 0.9, 1.0 };
while (System::Update())
{
DrawBackground();
const double t = (stopwatch.sF() / duration);
DrawGlyphsAt(Scene::Center(), font, textEditState.text, 120,
TextStyle::OutlineShadow(0.25, ColorF{ 0.11 }, Vec2{ 1.5, 1.5 }, ColorF{ 0.11 }), color, t, step, overshootFactor);
SimpleGUI::TextBox(textEditState, Vec2{ 60, 520 }, 240);
if (SimpleGUI::Button(U"\U000F040A", Vec2{ 340, 520 }, 80))
{
stopwatch.restart();
}
SimpleGUI::Slider(U"Duration: {:.2f}"_fmt(duration), duration, 0.1, 2.0, Vec2{ 60, 560 }, 240);
SimpleGUI::Slider(U"Step: {:.2f}"_fmt(step), step, 0.01, 1.0, Vec2{ 60, 600 }, 240);
SimpleGUI::Slider(U"OvershootFactor: {:.1f}"_fmt(overshootFactor), overshootFactor, 1.0, 6.0, Vec2{ 60, 640 }, 240);
SimpleGUI::ColorPicker(color, Vec2{ 440, 520 });
}
}
紙吹雪を降らせる
# include <Siv3D.hpp> // Siv3D v0.6.15
void DrawBackground()
{
for (int32 y = 0; y < (Scene::Height() / 40); ++y)
{
for (int32 x = 0; x < (Scene::Width() / 40); ++x)
{
if (IsEven(x + y))
{
RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.86 });
}
}
}
}
/// @brief 紙吹雪クラス
class Confetti
{
public:
/// @brief 紙吹雪の一片
struct Piece
{
Vec2 startPos{ 0, 0 };
Vec2 startVelocity{ 0, 0 };
double startAngle = 0.0;
double angularVelocity;
double size = 24.0;
double hue = 0.0;
double time = 0.0;
[[nodiscard]]
Vec2 getPos(const Vec2& gravity) const noexcept
{
return (startPos + startVelocity * time + 0.5 * gravity * time * time);
}
[[nodiscard]]
double getAngle() const noexcept
{
return (startAngle + angularVelocity * time);
}
[[nodiscard]]
ColorF getColor() const noexcept
{
return HSV{ hue, 0.85, 0.95 };
}
void draw(const Vec2& gravity) const
{
RectF{ Arg::center(0, 0), size, (size * 0.8) }
.scaled(Periodic::Sine1_1(3s, time), 1.0)
.rotated(getAngle()).movedBy(getPos(gravity)).draw(getColor());
}
[[nodiscard]]
static Piece Make(const Vec2& pos, const Vec2& startVelocity, const double size, const double hue) noexcept
{
return
{
.startPos = pos,
.startVelocity = startVelocity,
.startAngle = Random(2_pi),
.angularVelocity = (Random(-180_deg, 180_deg) + Random(-180_deg, 180_deg)),
.size = size,
.hue = hue,
};
}
};
/// @brief デフォルトコンストラクタ
[[nodiscard]]
Confetti() = default;
/// @brief 紙吹雪を初期化します
/// @param lifeTime 紙吹雪の寿命
/// @param gravity 重力
[[nodiscard]]
explicit Confetti(const Duration& lifeTime, const Vec2& gravity = Vec2{ 0, 100 })
: m_lifeTimeSec{ lifeTime.count() }
, m_gravity{ gravity } {}
/// @brief 紙吹雪の一片を追加します
/// @param pos 初期位置
/// @param startVelocity 初期速度
/// @param size 紙の大きさ
/// @param hue 色相
void add(const Vec2& pos, const Vec2& startVelocity, const double size, const double hue)
{
m_pieces << Piece::Make(pos, startVelocity, size, hue);
}
/// @brief 紙吹雪をクリアします
void clear()
{
m_pieces.clear();
}
/// @brief 紙吹雪を更新します
/// @param deltaTime 前フレームからの経過時間(秒)
void update(double deltaTime = Scene::DeltaTime())
{
for (auto& piece : m_pieces)
{
piece.time += deltaTime;
}
m_pieces.remove_if([lifeTime = m_lifeTimeSec](const Piece& piece) { return (lifeTime < piece.time); });
}
/// @brief 紙吹雪を描画します
void draw() const
{
for (const auto& piece : m_pieces)
{
piece.draw(m_gravity);
}
}
/// @brief 紙吹雪の数を返します
/// @return 紙吹雪の数
[[nodiscard]]
size_t num_pieces() const
{
return m_pieces.size();
}
private:
Array<Piece> m_pieces;
double m_lifeTimeSec = 4.0;
Vec2 m_gravity{ 0, 0 };
};
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.82 });
const Texture texture{ U"🏆"_emoji };
Confetti confetti{ 3.5s };
const double SpawnInterval = 0.03;
double accumulatedTime = 0.0;
while (System::Update())
{
// 更新
{
accumulatedTime += Scene::DeltaTime();
while (SpawnInterval <= accumulatedTime)
{
const Vec2 pos = RandomVec2(RectF{ -40, -40, (Scene::Width() + 80), 20 });
const Vec2 startVelocity = (Vec2{ 0, 240 } + RandomVec2(120.0));
const double size = Random(20.0, 28.0);
const double hue = Random(360.0);
confetti.add(pos, startVelocity, size, hue);
accumulatedTime -= SpawnInterval;
}
confetti.update();
}
// 描画
{
DrawBackground();
Circle{ Scene::Center(), 100 }.draw(ColorF{ 0.4 });
texture.drawAt(Scene::Center());
confetti.draw();
}
}
}
図形だけで作る画面遷移アニメーション
# include <Siv3D.hpp> // Siv3D v0.6.15
void DrawBackground(const Font& font, StringView text)
{
for (int32 y = 0; y < (Scene::Height() / 40); ++y)
{
for (int32 x = 0; x < (Scene::Width() / 40); ++x)
{
if (IsEven(x + y))
{
RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.86 });
}
}
}
font(text).drawAt(TextStyle::OutlineShadow(0.25, ColorF{ 0.11 }, Vec2{ 1.5, 1.5 }, ColorF{ 0.11 }), 180, Scene::Center(), ColorF{ 1.0 });
}
namespace TransitionLib
{
// 円形の画面が真っ暗に閉じていく
void IrisOut(double t)
{
t = Math::Saturate(t * 1.5);
const double radius = Scene::Size().length() * 0.5;
Circle{ Scene::Center(), radius }.drawFrame((t * radius), 0, ColorF{ 0.11 });
}
// 真っ暗な画面から円形に開いていく
void IrisIn(double t)
{
t = Math::Saturate(-0.5 + t * 1.5);
const double radius = Scene::Size().length() * 0.5;
Circle{ Scene::Center(), radius }.drawFrame(((1.0 - t) * radius), 0, ColorF{ 0.11 });
}
// 筆で塗りつぶすように画面が消えていく
void PaintOut(double t)
{
const double f = (t * 6.0);
const int32 fi = static_cast<int32>(Math::Ceil(f));
const double rectHeight = (Scene::Height() / 6.0);
for (int32 y = 0; y < fi; ++y)
{
const double s = Math::Saturate(1 - ((y + 1) - f));
if (IsEven(y))
{
RectF{ -100, (rectHeight * y), ((Scene::Width() + 200) * s), rectHeight }.draw(HSV{ 40, s })
.draw(Arg::left = HSV{ 40 }, Arg::right = HSV{ 40, s });
}
else
{
RectF{ Arg::topRight((Scene::Width() + 100), (rectHeight * y)), ((Scene::Width() + 200) * s), rectHeight }
.draw(Arg::left = HSV{ 40, s }, Arg::right = HSV{ 40 });
}
}
}
// 筆で塗りつぶすように画面が現れる
void PaintIn(double t)
{
const double f = ((1.0 - t) * 6.0);
const int32 fi = static_cast<int32>(Math::Ceil(f));
const double rectHeight = (Scene::Height() / 6.0);
for (int32 y = 0; y < fi; ++y)
{
const double s = Math::Saturate(1 - ((y + 1) - f));
if (IsEven(y))
{
RectF{ -100, (rectHeight * (5 - y)), ((Scene::Width() + 200) * s), rectHeight }
.draw(Arg::left = HSV{ 40 }, Arg::right = HSV{ 40, s });
}
else
{
RectF{ Arg::topRight((Scene::Width() + 100), (rectHeight * (5 - y))), ((Scene::Width() + 200) * s), rectHeight }
.draw(Arg::left = HSV{ 40, s }, Arg::right = HSV{ 40 });
}
}
}
// 回転する四角形で画面が消えていく
void QuadOut(double t)
{
t = EaseInOutExpo(t);
Polygon polygon = Scene::Rect().scaledAt(Scene::Center(), 4).asPolygon();
const Quad hole = Scene::Rect().scaledAt(Scene::Center(), (1.0 - t)).rotatedAt(Scene::Center(), (t * 360_deg));
if (polygon.addHole(hole))
{
polygon.draw(HSV{ 200, 0.5, 1.0 });
}
else
{
Scene::Rect().draw(HSV{ 200, 0.5, 1.0 });
}
}
// 回転する四角形で画面が現れる
void QuadIn(double t)
{
t = EaseInOutExpo(t);
Polygon polygon = Scene::Rect().scaledAt(Scene::Center(), 4).asPolygon();
const Quad hole = Scene::Rect().scaledAt(Scene::Center(), t).rotatedAt(Scene::Center(), (t * 360_deg));
if (polygon.addHole(hole))
{
polygon.draw(HSV{ 200, 0.5, 1.0 });
}
else
{
Scene::Rect().draw(HSV{ 200, 0.5, 1.0 });
}
}
}
struct GameData
{
static constexpr Duration TransitionTime = 1.5s;
Font font = Font{ FontMethod::MSDF, 48, Typeface::Heavy }.setBufferThickness(4);
};
using App = SceneManager<int32, GameData>;
class Scene0 : public App::Scene
{
public:
Scene0(const InitData& init)
: IScene{ init } {}
void update() override
{
if (MouseL.down())
{
changeScene(1, GameData::TransitionTime);
}
}
void draw() const override
{
DrawBackground(getData().font, U"Scene: {}"_fmt(getState()));
}
void drawFadeIn(double t) const override
{
draw();
TransitionLib::IrisIn(t);
}
void drawFadeOut(double t) const override
{
draw();
TransitionLib::PaintOut(t);
}
};
class Scene1 : public App::Scene
{
public:
Scene1(const InitData& init)
: IScene{ init } {}
void update() override
{
if (MouseL.down())
{
changeScene(2, GameData::TransitionTime);
}
}
void draw() const override
{
DrawBackground(getData().font, U"Scene: {}"_fmt(getState()));
}
void drawFadeIn(double t) const override
{
draw();
TransitionLib::PaintIn(t);
}
void drawFadeOut(double t) const override
{
draw();
TransitionLib::QuadOut(t);
}
};
class Scene2 : public App::Scene
{
public:
Scene2(const InitData& init)
: IScene{ init } {}
void update() override
{
if (MouseL.down())
{
changeScene(0, GameData::TransitionTime);
}
}
void draw() const override
{
DrawBackground(getData().font, U"Scene: {}"_fmt(getState()));
}
void drawFadeIn(double t) const override
{
draw();
TransitionLib::QuadIn(t);
}
void drawFadeOut(double t) const override
{
draw();
TransitionLib::IrisOut(t);
}
};
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.82 });
App manager;
manager.add<Scene0>(0);
manager.add<Scene1>(1);
manager.add<Scene2>(2);
manager.init(0, GameData::TransitionTime);
while (System::Update())
{
if (not manager.update())
{
break;
}
}
}
ノードグラフ
# include <Siv3D.hpp> // Siv3D v0.6.15
void DrawBackground()
{
for (int32 y = 0; y < (Scene::Height() / 40); ++y)
{
for (int32 x = 0; x < (Scene::Width() / 40); ++x)
{
if (IsEven(x + y))
{
RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.24 });
}
}
}
}
struct Node
{
using IDType = uint32;
IDType nodeID = 0;
Rect rect{ 0 };
uint64 lastUpdateTime = 0;
void draw() const
{
rect.rounded(8)
.drawShadow(Vec2{ 0, 3 }, 12, 3, ColorF{ 0.0, 0.5 }, false)
.draw(ColorF{ 0.04 });
const RectF titleRect{ rect.pos, rect.w, 36 };
titleRect.rounded(8, 8, 0, 0).draw(ColorF{ 0.1, 0.5, 0.2 });
SimpleGUI::GetFont()(U"Node #{}"_fmt(nodeID))
.drawAt(TextStyle::Shadow(Vec2{ 1, 1 }, ColorF{ 0.11 }), 22, titleRect.center().movedBy(0, -1), ColorF{ 0.96 });
}
[[nodiscard]]
Vec2 getInputPos() const
{
return rect.leftCenter().movedBy(20, 0);
}
[[nodiscard]]
Vec2 getOutputPos() const
{
return rect.rightCenter().movedBy(-20, 0);
}
};
struct Wire
{
Node::IDType from;
Node::IDType to;
};
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.22 });
HashTable<Node::IDType, Node> nodes;
nodes.emplace(1, Node{ 1, Rect{ 100, 300, 200, 300 } });
nodes.emplace(2, Node{ 2, Rect{ 500, 100, 200, 300 } });
nodes.emplace(3, Node{ 3, Rect{ 900, 400, 200, 300 } });
Array<Wire> wires;
wires << Wire{ 1, 2 };
wires << Wire{ 2, 3 };
// ドラッグ中のノードの ID
Optional<Node::IDType> draggingNodeID;
while (System::Update())
{
// 更新
{
if (draggingNodeID && MouseL.up())
{
draggingNodeID.reset();
}
for (auto&& [nodeID, node] : nodes)
{
if (node.rect.leftClicked())
{
draggingNodeID = nodeID;
node.lastUpdateTime = Time::GetMillisecSinceEpoch();
}
else if (draggingNodeID == nodeID)
{
node.rect.moveBy(Cursor::Delta());
}
}
}
// 描画
{
DrawBackground();
// 更新時間の昇順に描画する
{
Array<std::pair<uint64, Node::IDType>> sortedNodes;
for (auto&& [nodeID, node] : nodes)
{
sortedNodes.emplace_back(node.lastUpdateTime, nodeID);
}
sortedNodes.sort();
for (const auto& [_unused, nodeID] : sortedNodes)
{
nodes[nodeID].draw();
}
}
for (const auto& wire : wires)
{
const Vec2 p0 = nodes[wire.from].getOutputPos();
const Vec2 p3 = nodes[wire.to].getInputPos();
const Vec2 pCenter = p0.getMidpoint(p3);
const Vec2 p1{ pCenter.x, p0.y };
const Vec2 p2{ pCenter.x, p3.y };
const Bezier3 bezier{ p0, p1, p2, p3 };
bezier.draw(5, ColorF{ 0.7 });
p0.asCircle(8).draw(ColorF{ 0.7 });
p3.asCircle(8).draw(ColorF{ 0.7 });
}
}
}
}
計算機能付きテキストボックス
# include <Siv3D.hpp> // Siv3D v0.6.15
void DrawBackground()
{
for (int32 y = 0; y < (Scene::Height() / 40); ++y)
{
for (int32 x = 0; x < (Scene::Width() / 40); ++x)
{
if (IsEven(x + y))
{
RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.25 });
}
}
}
}
class CalcTextBox
{
public:
[[nodiscard]]
CalcTextBox() = default;
[[nodiscard]]
explicit CalcTextBox(double defaultValue)
: m_textEditState{ Format(defaultValue) }
, m_value{ defaultValue } {}
void update(const Vec2& pos, double width)
{
if (SimpleGUI::TextBox(m_textEditState, pos, width))
{
m_active = true;
}
if (m_active && (not m_textEditState.active))
{
m_active = false;
if (const auto value = EvalOpt(m_textEditState.text))
{
m_value = *value;
}
m_textEditState = TextEditState{ Format(m_value) };
}
}
[[nodiscard]]
double value() const
{
return m_value;
}
private:
TextEditState m_textEditState;
double m_value = 0.0;
bool m_active = false;
};
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.2 });
CalcTextBox c0{ 400.0 };
CalcTextBox c1{ 200.0 };
while (System::Update())
{
DrawBackground();
Rect{ 80, 80, 240, 160 }.rounded(20).draw(ColorF{ 0.9 });
SimpleGUI::GetFont()(U"W").draw(21, Vec2{ 100, 120 }, ColorF{ 0.11 });
c0.update(Vec2{ 130, 120 }, 160);
SimpleGUI::GetFont()(U"H").draw(21, Vec2{ 100, 160 }, ColorF{ 0.11 });
c1.update(Vec2{ 130, 160 }, 160);
RectF{ 400, 80, c0.value(), c1.value() }
.drawShadow(Vec2{ 2, 2 }, 12, 1)
.draw(ColorF{ 0.2, 0.5, 0.8 });
}
}
テクスチャ付きテキストを描く
- 文字部分だけ非透過にしたレンダーテクスチャに、自由に描画します。
# include <Siv3D.hpp> // Siv3D v0.6.15
void DrawBackground()
{
for (int32 y = 0; y < (Scene::Height() / 40); ++y)
{
for (int32 x = 0; x < (Scene::Width() / 40); ++x)
{
if (IsEven(x + y))
{
RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.28 });
}
}
}
}
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.25 });
const MSRenderTexture renderTexture{ Size{ 1280, 720 } };
const Font font{ FontMethod::MSDF, 64, Typeface::Heavy };
// 泡
Array<Circle> bubbles;
for (int32 i = 0; i < 80; ++i)
{
bubbles.push_back(Circle{ RandomVec2(RectF{ renderTexture.size() }), Random(6.0, 12.0) });
}
while (System::Update())
{
const double deltaTime = Scene::DeltaTime();
// 泡を移動させる
for (auto& bubble : bubbles)
{
bubble.moveBy(0, bubble.r * (-24 * deltaTime));
if ((bubble.y + bubble.r) < 0)
{
bubble.r = Random(6.0, 12.0);
bubble.x = Random(renderTexture.width());
bubble.y += (renderTexture.height() + bubble.r);
}
}
{
{
const ScopedRenderTarget2D target{ renderTexture.clear(ColorF{ 1.0, 1.0, 1.0, 0.0 }) };
// テキストをアルファだけ描く
{
BlendState bs = BlendState::Default2D;
bs.srcAlpha = Blend::One;
bs.dstAlpha = Blend::One;
bs.opAlpha = BlendOp::Max;
const ScopedRenderStates2D state{ bs };
font(U"Siv3D").drawAt(240, (renderTexture.size() / 2).movedBy(0, -140));
font(U"文字の描画").drawAt(240, (renderTexture.size() / 2).movedBy(0, 140));
}
// 背景のグラデーションを描く
Rect{ renderTexture.size() }.draw(Arg::top(0.6, 1.0, 1.0), Arg::bottom(0.0, 0.3, 0.7));
// 泡を描く
for (const auto& bubble : bubbles)
{
bubble.draw(ColorF{ 1.0, 0.3 });
}
}
Graphics2D::Flush();
renderTexture.resolve();
}
DrawBackground();
// テクスチャ付きテキストを描く
renderTexture.draw();
// テキストの輪郭を描く
font(U"Siv3D").drawAt(TextStyle::Outline(0.1, ColorF{ 0.11 }), 240, (renderTexture.size() / 2).movedBy(0, -140), ColorF{ 0.0, 0.0 });
font(U"文字の描画").drawAt(TextStyle::Outline(0.1, ColorF{ 0.11 }), 240, (renderTexture.size() / 2).movedBy(0, 140), ColorF{ 0.0, 0.0 });
}
}
画像を Polygon で切り抜く
# include <Siv3D.hpp> // Siv3D v0.6.15
void DrawBackground()
{
for (int32 y = 0; y < (Scene::Height() / 40); ++y)
{
for (int32 x = 0; x < (Scene::Width() / 40); ++x)
{
if (IsEven(x + y))
{
RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.88 });
}
}
}
}
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.84 });
const Texture texture{ U"example/siv3d-kun.png", TextureDesc::Mipped };
LineString points;
Polygon polygon, polygonBuffered;
Buffer2D buffer2D;
while (System::Update())
{
if (MouseL.pressed())
{
const Point pos = Cursor::Pos();
if (points.isEmpty() || points.back() != pos)
{
points << pos;
}
}
if (MouseL.up())
{
if (points.size() <= 3)
{
polygon = Polygon{};
}
else
{
polygon = Polygon::CorrectOne(points).simplified(1);
polygonBuffered = polygon.calculateBuffer(2);
buffer2D = polygon.toBuffer2D(Vec2{ 0, 0 }, texture.size());
}
points.clear();
}
DrawBackground();
texture.region().draw();
texture.draw();
points.draw(4.0, ColorF{ 1.0, 0.0, 0.0, 0.5 });
if (polygon)
{
polygon.draw(ColorF{ 1.0, 0.0, 0.0, 0.5 });
{
const Vec2 center{ 800, 360 };
const Vec2 offset = (center - polygon.boundingRect().pos - (polygon.boundingRect().size / 2));
const Transformer2D tr{ Mat3x2::Translate(offset).scaled(2, center) };
for (int32 y = 0; y < 3; ++y)
{
const Transformer2D tr2{ Mat3x2::Translate(2, y) };
polygonBuffered.draw(ColorF{ 0.7 });
}
polygon.draw(ColorF{ 0.6, 0.9, 1.0 });
polygon.drawFrame(3.0, ColorF{ 0.99 });
buffer2D.draw(texture);
}
}
}
}
炎で侵食するシェーダ
- 炎による侵食の効果を伴ってテクスチャを描くシェーダです。
fire_dissolve.hlsl
//
// Textures
//
Texture2D g_texture0 : register(t0);
Texture2D g_texture1 : register(t1);
SamplerState g_sampler0 : register(s0);
SamplerState g_sampler1 : register(s1);
namespace s3d
{
//
// VS Output / PS Input
//
struct PSInput
{
float4 position : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
}
//
// Constant Buffer
//
cbuffer PSConstants2D : register(b0)
{
float4 g_colorAdd;
float4 g_sdfParam;
float4 g_sdfOutlineColor;
float4 g_sdfShadowColor;
float4 g_internal;
}
cbuffer FireDissolve : register(b1)
{
float g_dissolveThreshold;
float g_flameThreshold;
// float2 _padding;
float3 g_flameColor;
}
float4 PS(s3d::PSInput input) : SV_TARGET
{
float4 texColor = g_texture0.Sample(g_sampler0, input.uv);
float value = g_texture1.Sample(g_sampler1, input.uv).r;
float diff = (value - g_dissolveThreshold);
float burn = smoothstep(g_flameThreshold, 0.0, diff);
texColor.rgb += (burn * g_flameColor);
float dissolve = smoothstep(0.0, 0.02, diff);
texColor.a *= dissolve;
return (texColor * input.color) + g_colorAdd;
}
fire_dissolve.frag
# version 410
//
// Textures
//
uniform sampler2D Texture0;
uniform sampler2D Texture1;
//
// PSInput
//
layout(location = 0) in vec4 Color;
layout(location = 1) in vec2 UV;
//
// PSOutput
//
layout(location = 0) out vec4 FragColor;
//
// Constant Buffer
//
layout(std140) uniform PSConstants2D
{
vec4 g_colorAdd;
vec4 g_sdfParam;
vec4 g_sdfOutlineColor;
vec4 g_sdfShadowColor;
vec4 g_internal;
};
layout(std140) uniform FireDissolve
{
float g_dissolveThreshold;
float g_flameThreshold;
// vec2 _padding;
vec3 g_flameColor;
};
//
// Functions
//
void main()
{
vec4 texColor = texture(Texture0, UV);
float value = texture(Texture1, UV).r;
float diff = (value - g_dissolveThreshold);
float burn = smoothstep(g_flameThreshold, 0.0, diff);
texColor.rgb += (burn * g_flameColor);
float dissolve = smoothstep(0.0, 0.02, diff);
texColor.a *= dissolve;
FragColor = (texColor * Color) + g_colorAdd;
}
# include <Siv3D.hpp> // Siv3D v0.6.15
void DrawBackground()
{
for (int32 y = 0; y < (Scene::Height() / 40); ++y)
{
for (int32 x = 0; x < (Scene::Width() / 40); ++x)
{
if (IsEven(x + y))
{
RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.25 });
}
}
}
}
/// @brief 指定されたサイズのノイズ画像を生成します。
/// @param size 画像のサイズ
/// @return 生成したノイズ画像
Image GenerateNoiseImage(const Size& size)
{
Image image{ size };
PerlinNoise noise{ RandomUint32() };
for (int32 y = 0; y < size.y; ++y)
{
for (int32 x = 0; x < size.x; ++x)
{
const double t = noise.octave2D0_1((x / 64.0), (y / 64.0), 5);
image[y][x] = ColorF{ t };
}
}
return image;
}
/// @brief 炎による侵食関連の定数バッファ
struct FireDissolve
{
float dissolveThreshold;
float flameThreshold;
Float2 _padding;
Float3 flameColor;
};
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.21 });
const Texture texture{ U"example/siv3d-kun.png", TextureDesc::Mipped };
Texture noiseTexture{ GenerateNoiseImage(texture.size()), TextureDesc::Mipped };
const PixelShader ps = HLSL{ U"fire_dissolve.hlsl", U"PS" } | GLSL{ U"fire_dissolve.frag", {{ U"PSConstants2D", 0 }, { U"FireDissolve", 1 }} };
if (not ps)
{
throw Error{ U"Failed to load a shader file" };
}
ConstantBuffer<FireDissolve> cb;
// 侵食の閾値
double dissolveThreshold = 0.0;
// 炎の閾値
double flameThreshold = 0.1;
// 炎の色
HSV flameColor{ 30, 1.0, 1.0 };
while (System::Update())
{
cb->dissolveThreshold = static_cast<float>(dissolveThreshold);
cb->flameThreshold = static_cast<float>(flameThreshold);
cb->flameColor = flameColor.toColorF().rgb();
// 背景と比較用のテクスチャ描画
{
DrawBackground();
texture.drawAt(Scene::Rect().getRelativePoint(0.2, 0.4));
noiseTexture.drawAt(Scene::Rect().getRelativePoint(0.8, 0.4));
}
// 炎による侵食を伴うテクスチャ描画
{
Graphics2D::SetPSConstantBuffer(1, cb);
Graphics2D::SetPSTexture(1, noiseTexture);
const ScopedCustomShader2D shader{ ps };
texture.drawAt(Scene::Rect().getRelativePoint(0.5, 0.4));
}
// GUI
{
SimpleGUI::Slider(U"dissolveThreshold: {:.2f}"_fmt(dissolveThreshold), dissolveThreshold, -0.2, 1.0, Vec2{ 120, 560 }, 240, 240);
SimpleGUI::Slider(U"flameThreshold: {:.2f}"_fmt(flameThreshold), flameThreshold, 0.0, 0.2, Vec2{ 120, 600 }, 240, 240);
SimpleGUI::ColorPicker(flameColor, Vec2{ 640, 560 });
if (SimpleGUI::Button(U"Regenerate", Vec2{ 840, 560 }))
{
noiseTexture = Texture{ GenerateNoiseImage(texture.size()), TextureDesc::Mipped };
}
}
}
}
頂点シェーダで波を描く
wave.hlsl
namespace s3d
{
//
// VS Output
//
struct PSInput
{
float4 position : SV_POSITION;
float4 color : COLOR0;
float2 uv : TEXCOORD0;
};
//
// Siv3D Functions
//
float4 Transform2D(float2 pos, float2x4 t)
{
return float4((t._13_14 + (pos.x * t._11_12) + (pos.y * t._21_22)), t._23_24);
}
}
//
// Constant Buffer
//
cbuffer VSConstants2D : register(b0)
{
row_major float2x4 g_transform;
float4 g_colorMul;
}
cbuffer SoftShape : register(b1)
{
float g_t;
}
//
// Functions
//
float CalcWave(float x, float t)
{
return sin(x * 0.011 + t) * 6
+ sin(x * 0.013 + -t * 1.1) * 4
+ sin(x * 0.017 + t * 1.3) * 3;
}
s3d::PSInput VS(uint id: SV_VERTEXID)
{
s3d::PSInput result;
float2 pos;
float4 color;
uint xi = (id / 6);
uint i = (id % 6);
if ((i == 0) || (i == 2) || (i == 3))
{
pos.x = (xi * 8);
}
else
{
pos.x = ((xi + 1) * 8);
}
if ((i == 2) || (i == 3) || (i == 5))
{
pos.y = 720;
color = float4(0.0, 0.3, 0.6, 1.0);
}
else
{
pos.y = 480 + CalcWave(pos.x, g_t);
color = float4(0.1, 0.6, 0.9, 1.0);
}
result.position = s3d::Transform2D(pos, g_transform);
result.color = color;
result.uv = float2(0, 0);
return result;
}
wave.vert
# version 410
//
// VS Input (nothing)
//
//
// VS Output
//
layout(location = 0) out vec4 Color;
layout(location = 1) out vec2 UV;
out gl_PerVertex
{
vec4 gl_Position;
};
//
// Siv3D Functions
//
vec4 s3d_Transform2D(const vec2 pos, const vec4 t[2])
{
return vec4(t[0].zw + (pos.x * t[0].xy) + (pos.y * t[1].xy), t[1].zw);
}
//
// Constant Buffer
//
layout(std140) uniform VSConstants2D
{
vec4 g_transform[2];
vec4 g_colorMul;
};
layout(std140) uniform Wave
{
float g_t;
};
//
// Functions
//
float CalcWave(float x, float t)
{
return sin(x * 0.011 + t) * 6
+ sin(x * 0.013 + -t * 1.1) * 4
+ sin(x * 0.017 + t * 1.3) * 3;
}
void main()
{
vec2 pos;
vec4 color;
uint xi = (gl_VertexID / 6);
uint i = (gl_VertexID % 6);
if ((i == 0) || (i == 2) || (i == 3))
{
pos.x = (xi * 8);
}
else
{
pos.x = ((xi + 1) * 8);
}
if ((i == 2) || (i == 3) || (i == 5))
{
pos.y = 720;
color = vec4(0.0, 0.3, 0.6, 1.0);
}
else
{
pos.y = 480 + CalcWave(pos.x, g_t);
color = vec4(0.1, 0.6, 0.9, 1.0);
}
gl_Position = s3d_Transform2D(pos, g_transform);
Color = color;
UV = vec2(0, 0);
}
# include <Siv3D.hpp>
void DrawBackground()
{
for (int32 y = 0; y < (Scene::Height() / 40); ++y)
{
for (int32 x = 0; x < (Scene::Width() / 40); ++x)
{
if (IsEven(x + y))
{
RectF{ (x * 40), (y * 40), 40 }.draw(ColorF{ 0.85 });
}
}
}
}
void Main()
{
Window::Resize(1280, 720);
Scene::SetBackground(ColorF{ 0.80 });
const VertexShader vs
= HLSL{ U"wave.hlsl" }
| GLSL{ U"wave.vert", { { U"VSConstants2D", 0 }, { U"Wave", 1 } } };
if (not vs)
{
throw Error{ U"Failed to load a shader file" };
}
ConstantBuffer<float> cb;
bool showWireframe = false;
while (System::Update())
{
cb = static_cast<float>(Scene::Time());
DrawBackground();
{
Graphics2D::SetVSConstantBuffer(1, cb);
const ScopedRenderStates2D states{ showWireframe ? RasterizerState::WireframeCullNone : RasterizerState::Default2D };
const ScopedCustomShader2D shader{ vs };
// 頂点情報の無い三角形を 2 * 160 個描画する
// (頂点情報は頂点シェーダで設定する)
Graphics2D::DrawTriangles(2 * 160);
}
SimpleGUI::CheckBox(showWireframe, U"Wireframe", Vec2{ 60, 60 });
}
}