| /* | |
| * Copyright 2023 Google LLC | |
| * | |
| * Use of this source code is governed by a BSD-style license that can be | |
| * found in the LICENSE file. | |
| */ | |
| #include "include/core/SkBlendMode.h" | |
| #include "include/core/SkCanvas.h" | |
| #include "include/core/SkColor.h" | |
| #include "include/core/SkCubicMap.h" | |
| #include "include/core/SkImage.h" | |
| #include "include/core/SkPoint.h" | |
| #include "include/core/SkSamplingOptions.h" | |
| #include "include/core/SkScalar.h" | |
| #include "include/core/SkShader.h" | |
| #include "include/core/SkTileMode.h" | |
| #include "include/core/SkVertices.h" | |
| #include "include/effects/SkGradient.h" | |
| #include "include/private/SkFloatingPoint.h" | |
| #include "tools/DecodeUtils.h" | |
| #include "tools/Resources.h" | |
| #include "tools/timer/TimeUtils.h" | |
| #include "tools/viewer/Slide.h" | |
| #include "imgui.h" | |
| #include <array> | |
| #include <cmath> | |
| #include <cstddef> | |
| #include <cstdint> | |
| #include <iterator> | |
| #include <vector> | |
| namespace { | |
| float lerp(float a, float b, float t) { | |
| return a + (b - a)*t; | |
| } | |
| sk_sp<SkShader> make_image_shader(const char* resource) { | |
| sk_sp<SkImage> img = ToolUtils::GetResourceAsImage(resource); | |
| // Normalize to 1x1 for UV sampling. | |
| const auto lm = SkMatrix::Scale(1.0f/img->width(), 1.0f/img->height()); | |
| return img->makeShader(SkTileMode::kDecal, | |
| SkTileMode::kDecal, | |
| SkSamplingOptions(SkFilterMode::kLinear), | |
| &lm); | |
| } | |
| static constexpr struct ShaderFactory { | |
| const char* fName; | |
| sk_sp<SkShader> (*fBuild)(); | |
| } gShaderFactories[] = { | |
| { | |
| "Img (Mandrill)", | |
| []() ->sk_sp<SkShader> { return make_image_shader("images/mandrill_512.png"); } | |
| }, | |
| { | |
| "Img (Baby Tux)", | |
| []() ->sk_sp<SkShader> { return make_image_shader("images/baby_tux.png"); } | |
| }, | |
| { | |
| "Img (Brickwork)", | |
| []() ->sk_sp<SkShader> { return make_image_shader("images/brickwork-texture.jpg"); } | |
| }, | |
| { | |
| "Radial Gradient", | |
| []() ->sk_sp<SkShader> { | |
| static constexpr SkColor4f gColors[] = { | |
| SkColors::kGreen, SkColors::kRed, SkColors::kBlue, SkColors::kGreen | |
| }; | |
| return SkShaders::RadialGradient({0.5f, 0.5f}, 0.5f, | |
| {{gColors, {}, SkTileMode::kRepeat}, {}}); | |
| } | |
| }, | |
| { | |
| "Colors", | |
| []() ->sk_sp<SkShader> { return nullptr; } | |
| }, | |
| }; | |
| static constexpr struct VertexAnimator { | |
| const char* fName; | |
| void (*fAanimate)(const std::vector<SkPoint>& uv, float t, std::vector<SkPoint>& out); | |
| } gVertexAnimators[] = { | |
| { | |
| "Cylinderator", | |
| // Simulate a cylinder rolling sideways across the 1x1 uv space. | |
| [](const std::vector<SkPoint>& uvs, float t, std::vector<SkPoint>& out) { | |
| static constexpr float kCylRadius = .2f; | |
| const auto cyl_pos = t; | |
| for (size_t i = 0; i < uvs.size(); ++i) { | |
| const auto& uv = uvs[i]; | |
| if (uv.fX <= cyl_pos) { | |
| out[i] = uv; | |
| continue; | |
| } | |
| const auto arc_len = uv.fX - cyl_pos, | |
| arc_ang = arc_len/kCylRadius; | |
| out[i] = SkPoint{ | |
| cyl_pos + std::sin(arc_ang)*kCylRadius, | |
| uv.fY, | |
| }; | |
| } | |
| }, | |
| }, | |
| { | |
| "Squircillator", | |
| // Pull all vertices towards center, proportionally, such that the outer square edge | |
| // is mapped to a circle for t == 1. | |
| [](const std::vector<SkPoint>& uvs, float t, std::vector<SkPoint>& out) { | |
| for (size_t i = 0; i < uvs.size(); ++i) { | |
| // remap to [-.5,.5] | |
| const auto uv = (uvs[i] - SkPoint{0.5,0.5}); | |
| // Distance from center to outer edge for the line pasing through uv. | |
| const auto d = uv.length()*0.5f/std::max(std::abs(uv.fX), std::abs(uv.fY)); | |
| // Scale needed to pull the outer edge to the r=0.5 circle at t == 1. | |
| const auto s = lerp(1, (0.5f / d), t); | |
| out[i] = uv*s + SkPoint{0.5, 0.5}; | |
| } | |
| }, | |
| }, | |
| { | |
| "Twirlinator", | |
| // Rotate vertices proportional to their distance to center. | |
| [](const std::vector<SkPoint>& uvs, float t, std::vector<SkPoint>& out) { | |
| static constexpr float kMaxRotate = SK_FloatPI*4; | |
| for (size_t i = 0; i < uvs.size(); ++i) { | |
| // remap to [-.5,.5] | |
| const auto uv = (uvs[i] - SkPoint{0.5,0.5}); | |
| const auto angle = kMaxRotate * t * uv.length(); | |
| out[i] = SkMatrix::RotateRad(angle).mapPoint(uv) + SkPoint{0.5, 0.5}; | |
| } | |
| }, | |
| }, | |
| { | |
| "Wigglynator", | |
| [](const std::vector<SkPoint>& uvs, float t, std::vector<SkPoint>& out) { | |
| const float radius = t*0.2f/(std::sqrt(uvs.size()) - 1); | |
| for (size_t i = 0; i < uvs.size(); ++i) { | |
| const float phase = i*SK_FloatPI*0.31f, | |
| angle = phase + t*SK_FloatPI*2; | |
| out[i] = uvs[i] + SkVector{ | |
| radius*std::cos(angle), | |
| radius*std::sin(angle), | |
| }; | |
| } | |
| }, | |
| }, | |
| { | |
| "None", | |
| [](const std::vector<SkPoint>& uvs, float, std::vector<SkPoint>& out) { out = uvs; }, | |
| }, | |
| }; | |
| class MeshSlide final : public Slide { | |
| public: | |
| MeshSlide() : fTimeMapper({0.5f, 0}, {0.5f, 1}) { fName = "Mesh"; } | |
| void load(SkScalar w, SkScalar h) override { | |
| fSize = {w, h}; | |
| this->initMesh(256); | |
| this->initShader(gShaderFactories[0]); | |
| } | |
| void resize(SkScalar w, SkScalar h) override { fSize = {w, h}; } | |
| void draw(SkCanvas* canvas) override { | |
| SkAutoCanvasRestore acr(canvas, true); | |
| SkPaint p; | |
| p.setAntiAlias(true); | |
| p.setColor(SK_ColorWHITE); | |
| static constexpr float kMeshFraction = 0.85f; | |
| const float mesh_size = std::min(fSize.fWidth, fSize.fHeight)*kMeshFraction; | |
| canvas->translate((fSize.fWidth - mesh_size) * 0.5f, | |
| (fSize.fHeight - mesh_size) * 0.5f); | |
| canvas->scale(mesh_size, mesh_size); | |
| auto verts = SkVertices::MakeCopy(SkVertices::kTriangles_VertexMode, | |
| fVertices.size(), | |
| fVertices.data(), | |
| fShader ? fUVs.data() : nullptr, | |
| fShader ? nullptr : fColors.data(), | |
| fIndices.size(), | |
| fIndices.data()); | |
| p.setShader(fShader); | |
| canvas->drawVertices(verts, SkBlendMode::kModulate, p); | |
| if (fShowMesh) { | |
| p.setShader(nullptr); | |
| p.setColor(SK_ColorBLUE); | |
| p.setStroke(true); | |
| p.setStrokeWidth(0.5f/mesh_size); | |
| SkASSERT(fIndices.size() % 6 == 0); | |
| for (auto i = fIndices.cbegin(); i < fIndices.cend(); i += 6) { | |
| canvas->drawLine(fVertices[i[0]], fVertices[i[1]], p); | |
| canvas->drawLine(fVertices[i[1]], fVertices[i[2]], p); | |
| canvas->drawLine(fVertices[i[2]], fVertices[i[0]], p); | |
| canvas->drawLine(fVertices[i[3]], fVertices[i[4]], p); | |
| canvas->drawLine(fVertices[i[4]], fVertices[i[5]], p); | |
| canvas->drawLine(fVertices[i[5]], fVertices[i[3]], p); | |
| } | |
| p.setStrokeCap(SkPaint::kRound_Cap); | |
| p.setStrokeWidth(5/mesh_size); | |
| canvas->drawPoints(SkCanvas::kPoints_PointMode, fVertices, p); | |
| } | |
| this->drawControls(); | |
| } | |
| bool animate(double nanos) override { | |
| if (!fTimeBase) { | |
| fTimeBase = nanos; | |
| } | |
| // Oscillating between 0..1 | |
| const float t = | |
| std::abs((std::fmod((nanos - fTimeBase)*0.000000001*fAnimationSpeed, 2) - 1)); | |
| // Add some easing | |
| fCurrentAnimator->fAanimate(fUVs, fTimeMapper.computeYFromX(t), fVertices); | |
| return true; | |
| } | |
| private: | |
| void initMesh(size_t vertex_count) { | |
| // Generate an NxN mesh. For simplicity, we keep the vertices in normalized space | |
| // (1x1 same as UVs), and scale the mesh up when rendering. | |
| const auto n = static_cast<size_t>(std::sqrt(vertex_count)); | |
| SkASSERT(n > 0); | |
| SkASSERT(n == std::sqrt(vertex_count)); | |
| fVertices.resize(vertex_count); | |
| fUVs.resize(vertex_count); | |
| fColors.resize(vertex_count); | |
| for (size_t i = 0; i < vertex_count; ++i) { | |
| fVertices[i] = fUVs[i] = { | |
| static_cast<float>(i % n) / (n - 1), | |
| static_cast<float>(i / n) / (n - 1), | |
| }; | |
| fColors[i] = SkColorSetRGB(!!(i%2)*255, | |
| !!(i%3)*255, | |
| !!((i+1)%3)*255); | |
| } | |
| // Trivial triangle tessellation pattern: | |
| // | |
| // *---*---* | |
| // | /|\ | | |
| // | / | \ | | |
| // |/ | \| | |
| // *---*---* | |
| // |\ | /| | |
| // | \ | / | | |
| // | \|/ | | |
| // *---*---* | |
| const size_t triangle_count = 2*(n - 1)*(n - 1), | |
| index_count = 3*triangle_count; | |
| fIndices.clear(); | |
| fIndices.reserve(index_count); | |
| for (size_t i = 0; i < n - 1; ++i) { | |
| for (size_t j = 0; j < n - 1; ++j) { | |
| const auto row_0_idx = j*n + i, | |
| row_1_idx = row_0_idx + n, | |
| off_0 = (i + j) % 2, | |
| off_1 = 1 - off_0; | |
| fIndices.push_back(row_0_idx + 0); | |
| fIndices.push_back(row_0_idx + 1); | |
| fIndices.push_back(row_1_idx + off_0); | |
| fIndices.push_back(row_0_idx + off_1); | |
| fIndices.push_back(row_1_idx + 1); | |
| fIndices.push_back(row_1_idx + 0); | |
| } | |
| } | |
| SkASSERT(fIndices.size() == index_count); | |
| } | |
| void initShader(const ShaderFactory& fact) { | |
| fShader = fact.fBuild(); | |
| fCurrentShaderFactory = &fact; | |
| } | |
| void drawControls() { | |
| ImGui::Begin("Mesh Options"); | |
| if (ImGui::BeginCombo("Texture", fCurrentShaderFactory->fName)) { | |
| for (const auto& fact : gShaderFactories) { | |
| const auto is_selected = (fCurrentShaderFactory->fName == fact.fName); | |
| if (ImGui::Selectable(fact.fName) && !is_selected) { | |
| this->initShader(fact); | |
| } | |
| if (is_selected) { | |
| ImGui::SetItemDefaultFocus(); | |
| } | |
| } | |
| ImGui::EndCombo(); | |
| } | |
| if (ImGui::BeginCombo("Animator", fCurrentAnimator->fName)) { | |
| for (const auto& anim : gVertexAnimators) { | |
| const auto is_selected = (fCurrentAnimator->fName == anim.fName); | |
| if (ImGui::Selectable(anim.fName) && !is_selected) { | |
| fCurrentAnimator = &anim; | |
| fTimeBase = 0; | |
| } | |
| if (is_selected) { | |
| ImGui::SetItemDefaultFocus(); | |
| } | |
| } | |
| ImGui::EndCombo(); | |
| } | |
| struct GSizeInfo { | |
| const char* fLabel; | |
| size_t fCount; | |
| }; | |
| static constexpr auto gSizeInfo = std::to_array<GSizeInfo>({ | |
| GSizeInfo{ "4x4", 16 }, | |
| GSizeInfo{ "8x8", 64 }, | |
| GSizeInfo{ "16x16", 256 }, | |
| GSizeInfo{ "32x32", 1024 }, | |
| GSizeInfo{ "64x64", 4096 }, | |
| GSizeInfo{ "128x128", 16384 }, | |
| }); | |
| ImGui::SliderInt("Mesh Size", | |
| &fMeshSizeSelector, | |
| 0, std::size(gSizeInfo) - 1, | |
| gSizeInfo[fMeshSizeSelector].fLabel); | |
| if (fVertices.size() != gSizeInfo[fMeshSizeSelector].fCount) { | |
| this->initMesh(gSizeInfo[fMeshSizeSelector].fCount); | |
| } | |
| ImGui::SliderFloat("Speed", &fAnimationSpeed, 0.25, 4, "%.2f"); | |
| ImGui::Checkbox("Show mesh", &fShowMesh); | |
| ImGui::End(); | |
| } | |
| SkSize fSize; | |
| sk_sp<SkShader> fShader; | |
| std::vector<SkPoint> fVertices, | |
| fUVs; | |
| std::vector<SkColor> fColors; | |
| std::vector<uint16_t> fIndices; | |
| double fTimeBase = 0; | |
| const SkCubicMap fTimeMapper; | |
| // UI stuff | |
| const ShaderFactory* fCurrentShaderFactory = &gShaderFactories[0]; | |
| const VertexAnimator* fCurrentAnimator = &gVertexAnimators[0]; | |
| int fMeshSizeSelector = 2; | |
| float fAnimationSpeed = 1.f; | |
| bool fShowMesh = false; | |
| }; | |
| } // anonymous namespace | |
| DEF_SLIDE(return new MeshSlide{};) |