211511237f
The default behavior is to not let the user move the slide to the top or left. Implement for SampleSlide and stub out for NIMASlide (which isn't so easy to implement). Change-Id: I16074e704f4086199932d83e28c28d1ee39fbd9d Reviewed-on: https://skia-review.googlesource.com/151600 Reviewed-by: Brian Osman <brianosman@google.com> Commit-Queue: Ben Wagner <bungeman@google.com>
472 lines
14 KiB
C++
472 lines
14 KiB
C++
/*
|
|
* Copyright 2018 Google Inc.
|
|
*
|
|
* Use of this source code is governed by a BSD-style license that can be
|
|
* found in the LICENSE file.
|
|
*/
|
|
|
|
#include "NIMASlide.h"
|
|
|
|
#include "SkAnimTimer.h"
|
|
#include "SkOSPath.h"
|
|
#include "Resources.h"
|
|
#include "imgui.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
using namespace sk_app;
|
|
using namespace nima;
|
|
|
|
// ImGui expects an array of const char* when displaying a ListBox. This function is for an
|
|
// overload of ImGui::ListBox that takes a getter so that ListBox works with
|
|
// std::vector<std::string>.
|
|
static bool vector_getter(void* v, int index, const char** out) {
|
|
auto vector = reinterpret_cast<std::vector<std::string>*>(v);
|
|
*out = vector->at(index).c_str();
|
|
return true;
|
|
}
|
|
|
|
// A wrapper class that handles rendering of ActorImages (renderable components NIMA Actors).
|
|
class NIMAActorImage {
|
|
public:
|
|
NIMAActorImage(ActorImage* actorImage, SkImage* texture, SkPaint* paint)
|
|
: fActorImage(actorImage)
|
|
, fTexture(texture)
|
|
, fPaint(paint)
|
|
, fSkinned(false)
|
|
, fPositions()
|
|
, fTexs()
|
|
, fBoneIdx()
|
|
, fBoneWgt()
|
|
, fIndices()
|
|
, fBones()
|
|
, fVertices(nullptr)
|
|
, fRenderFlags(0) {
|
|
// Update the vertices and bones.
|
|
this->updateVertices(true);
|
|
this->updateBones();
|
|
}
|
|
|
|
void render(SkCanvas* canvas, uint32_t renderFlags) {
|
|
bool dirty = renderFlags != fRenderFlags;
|
|
fRenderFlags = renderFlags;
|
|
|
|
bool useImmediate = renderFlags & kImmediate_RenderFlag;
|
|
bool useCache = renderFlags & kCache_RenderFlag;
|
|
bool drawBounds = renderFlags & kBounds_RenderFlag;
|
|
|
|
// Don't use the cache if drawing in immediate mode.
|
|
useCache &= !useImmediate;
|
|
|
|
if (fActorImage->doesAnimationVertexDeform() || dirty) {
|
|
// These are vertices that transform beyond just bone transforms, so they must be
|
|
// updated every frame.
|
|
// If the render flags are dirty, reset the vertices object.
|
|
this->updateVertices(!useCache);
|
|
}
|
|
|
|
// Update the bones.
|
|
this->updateBones();
|
|
|
|
// Deform the bones in immediate mode.
|
|
sk_sp<SkVertices> vertices = fVertices;
|
|
if (useImmediate) {
|
|
vertices = fVertices->applyBones(fBones.data(), fBones.size());
|
|
}
|
|
|
|
// Draw the vertices object.
|
|
this->drawVerticesObject(vertices.get(), canvas, !useImmediate);
|
|
|
|
// Draw the bounds.
|
|
if (drawBounds && fActorImage->renderOpacity() > 0.0f) {
|
|
// Get the bounds.
|
|
SkRect bounds = vertices->bounds();
|
|
|
|
// Approximate bounds if not using immediate transforms.
|
|
if (!useImmediate) {
|
|
const SkRect originalBounds = fBones[0].mapRect(vertices->bounds());
|
|
bounds = originalBounds;
|
|
for (size_t i = 1; i < fBones.size(); i++) {
|
|
const SkVertices::Bone& matrix = fBones[i];
|
|
bounds.join(matrix.mapRect(originalBounds));
|
|
}
|
|
}
|
|
|
|
// Draw the bounds.
|
|
SkPaint paint;
|
|
paint.setStyle(SkPaint::kStroke_Style);
|
|
paint.setColor(0xFFFF0000);
|
|
canvas->drawRect(bounds, paint);
|
|
}
|
|
}
|
|
|
|
int drawOrder() const { return fActorImage->drawOrder(); }
|
|
|
|
private:
|
|
void updateVertices(bool isVolatile) {
|
|
// Update whether the image is skinned.
|
|
fSkinned = fActorImage->connectedBoneCount() > 0;
|
|
|
|
// Retrieve data from the image.
|
|
uint32_t vertexCount = fActorImage->vertexCount();
|
|
uint32_t vertexStride = fActorImage->vertexStride();
|
|
float* vertexData = fActorImage->vertices();
|
|
uint32_t indexCount = fActorImage->triangleCount() * 3;
|
|
uint16_t* indexData = fActorImage->triangles();
|
|
|
|
// Don't render if not visible.
|
|
if (!vertexCount || fActorImage->textureIndex() < 0) {
|
|
fPositions.clear();
|
|
fTexs.clear();
|
|
fBoneIdx.clear();
|
|
fBoneWgt.clear();
|
|
fIndices.clear();
|
|
return;
|
|
}
|
|
|
|
// Split the vertex data.
|
|
fPositions.resize(vertexCount);
|
|
fTexs.resize(vertexCount);
|
|
fIndices.resize(indexCount);
|
|
if (fSkinned) {
|
|
fBoneIdx.resize(vertexCount * 4);
|
|
fBoneWgt.resize(vertexCount * 4);
|
|
}
|
|
for (uint32_t i = 0; i < vertexCount; i ++) {
|
|
uint32_t j = i * vertexStride;
|
|
|
|
// Get the attributes.
|
|
float* attrPosition = vertexData + j;
|
|
float* attrTex = vertexData + j + 2;
|
|
float* attrBoneIdx = vertexData + j + 4;
|
|
float* attrBoneWgt = vertexData + j + 8;
|
|
|
|
// Get deformed positions if necessary.
|
|
if (fActorImage->doesAnimationVertexDeform()) {
|
|
attrPosition = fActorImage->animationDeformedVertices() + i * 2;
|
|
}
|
|
|
|
// Set the data.
|
|
fPositions[i].set(attrPosition[0], attrPosition[1]);
|
|
fTexs[i].set(attrTex[0] * fTexture->width(), attrTex[1] * fTexture->height());
|
|
if (fSkinned) {
|
|
for (uint32_t k = 0; k < 4; k ++) {
|
|
fBoneIdx[i][k] = static_cast<uint32_t>(attrBoneIdx[k]);
|
|
fBoneWgt[i][k] = attrBoneWgt[k];
|
|
}
|
|
}
|
|
}
|
|
memcpy(fIndices.data(), indexData, indexCount * sizeof(uint16_t));
|
|
|
|
// Update the vertices object.
|
|
fVertices = SkVertices::MakeCopy(SkVertices::kTriangles_VertexMode,
|
|
vertexCount,
|
|
fPositions.data(),
|
|
fTexs.data(),
|
|
nullptr,
|
|
fBoneIdx.data(),
|
|
fBoneWgt.data(),
|
|
fIndices.size(),
|
|
fIndices.data(),
|
|
isVolatile);
|
|
}
|
|
|
|
void updateBones() {
|
|
// NIMA matrices are a collection of 6 floats.
|
|
constexpr int kNIMAMatrixSize = 6;
|
|
|
|
// Set up the matrices for the first time.
|
|
if (fBones.size() == 0) {
|
|
int numMatrices = 1;
|
|
if (fSkinned) {
|
|
numMatrices = fActorImage->boneInfluenceMatricesLength() / kNIMAMatrixSize;
|
|
}
|
|
|
|
// Initialize all matrices to the identity matrix.
|
|
fBones.assign(numMatrices, {{ 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f }});
|
|
}
|
|
|
|
if (fSkinned) {
|
|
// Update the matrices.
|
|
float* matrixData = fActorImage->boneInfluenceMatrices();
|
|
memcpy(fBones.data(), matrixData, fBones.size() * kNIMAMatrixSize * sizeof(float));
|
|
}
|
|
|
|
// Set the zero matrix to be the world transform.
|
|
memcpy(fBones.data(),
|
|
fActorImage->worldTransform().values(),
|
|
kNIMAMatrixSize * sizeof(float));
|
|
}
|
|
|
|
void drawVerticesObject(SkVertices* vertices, SkCanvas* canvas, bool useBones) const {
|
|
// Determine the blend mode.
|
|
SkBlendMode blendMode;
|
|
switch (fActorImage->blendMode()) {
|
|
case BlendMode::Off: {
|
|
blendMode = SkBlendMode::kSrc;
|
|
break;
|
|
}
|
|
case BlendMode::Normal: {
|
|
blendMode = SkBlendMode::kSrcOver;
|
|
break;
|
|
}
|
|
case BlendMode::Additive: {
|
|
blendMode = SkBlendMode::kPlus;
|
|
break;
|
|
}
|
|
case BlendMode::Multiply: {
|
|
blendMode = SkBlendMode::kMultiply;
|
|
break;
|
|
}
|
|
case BlendMode::Screen: {
|
|
blendMode = SkBlendMode::kScreen;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Set the opacity.
|
|
fPaint->setAlpha(static_cast<U8CPU>(fActorImage->renderOpacity() * 255));
|
|
|
|
// Draw the vertices.
|
|
if (useBones) {
|
|
canvas->drawVertices(vertices, fBones.data(), fBones.size(), blendMode, *fPaint);
|
|
} else {
|
|
canvas->drawVertices(vertices, blendMode, *fPaint);
|
|
}
|
|
|
|
// Reset the opacity.
|
|
fPaint->setAlpha(255);
|
|
}
|
|
|
|
private:
|
|
ActorImage* fActorImage;
|
|
SkImage* fTexture;
|
|
SkPaint* fPaint;
|
|
|
|
bool fSkinned;
|
|
std::vector<SkPoint> fPositions;
|
|
std::vector<SkPoint> fTexs;
|
|
std::vector<SkVertices::BoneIndices> fBoneIdx;
|
|
std::vector<SkVertices::BoneWeights> fBoneWgt;
|
|
std::vector<uint16_t> fIndices;
|
|
|
|
std::vector<SkVertices::Bone> fBones;
|
|
sk_sp<SkVertices> fVertices;
|
|
|
|
uint32_t fRenderFlags;
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Represents an Actor, or an animated character, in NIMA.
|
|
class NIMAActor : public Actor {
|
|
public:
|
|
NIMAActor(const std::string& basePath)
|
|
: fTexture(nullptr)
|
|
, fActorImages()
|
|
, fPaint()
|
|
, fAnimations() {
|
|
// Load the NIMA data.
|
|
std::string nimaPath((basePath + ".nima").c_str());
|
|
INHERITED::load(nimaPath);
|
|
|
|
// Load the image asset.
|
|
sk_sp<SkData> imageData = SkData::MakeFromFileName((basePath + ".png").c_str());
|
|
fTexture = SkImage::MakeFromEncoded(imageData);
|
|
|
|
// Create the paint.
|
|
fPaint.setShader(fTexture->makeShader(nullptr));
|
|
fPaint.setFilterQuality(SkFilterQuality::kLow_SkFilterQuality);
|
|
|
|
// Load the image nodes.
|
|
fActorImages.reserve(m_ImageNodeCount);
|
|
for (uint32_t i = 0; i < m_ImageNodeCount; i ++) {
|
|
fActorImages.emplace_back(m_ImageNodes[i], fTexture.get(), &fPaint);
|
|
}
|
|
|
|
// Sort the image nodes.
|
|
std::sort(fActorImages.begin(), fActorImages.end(), [](auto a, auto b) {
|
|
return a.drawOrder() < b.drawOrder();
|
|
});
|
|
|
|
// Get the list of animations.
|
|
fAnimations.reserve(m_AnimationsCount);
|
|
for (uint32_t i = 0; i < m_AnimationsCount; i ++) {
|
|
fAnimations.push_back(m_Animations[i].name());
|
|
}
|
|
}
|
|
|
|
void render(SkCanvas* canvas, uint32_t renderFlags) {
|
|
// Render the image nodes.
|
|
for (auto& image : fActorImages) {
|
|
image.render(canvas, renderFlags);
|
|
}
|
|
}
|
|
|
|
const std::vector<std::string>& getAnimations() const {
|
|
return fAnimations;
|
|
}
|
|
|
|
private:
|
|
sk_sp<SkImage> fTexture;
|
|
std::vector<NIMAActorImage> fActorImages;
|
|
SkPaint fPaint;
|
|
std::vector<std::string> fAnimations;
|
|
|
|
typedef Actor INHERITED;
|
|
};
|
|
|
|
//////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
NIMASlide::NIMASlide(const SkString& name, const SkString& path)
|
|
: fBasePath()
|
|
, fActor(nullptr)
|
|
, fPlaying(true)
|
|
, fTime(0.0f)
|
|
, fRenderFlags(0)
|
|
, fAnimation(nullptr)
|
|
, fAnimationIndex(0) {
|
|
fName = name;
|
|
|
|
// Get the path components.
|
|
SkString baseName = SkOSPath::Basename(path.c_str());
|
|
baseName.resize(baseName.size() - 5);
|
|
SkString dirName = SkOSPath::Dirname(path.c_str());
|
|
SkString basePath = SkOSPath::Join(dirName.c_str(), baseName.c_str());
|
|
|
|
// Save the base path.
|
|
fBasePath = std::string(basePath.c_str());
|
|
}
|
|
|
|
NIMASlide::~NIMASlide() {}
|
|
|
|
SkISize NIMASlide::getDimensions() const {
|
|
return SkISize::MakeEmpty(); // TODO
|
|
}
|
|
|
|
void NIMASlide::draw(SkCanvas* canvas) {
|
|
canvas->save();
|
|
|
|
for (int i = 0; i < 10; i ++) {
|
|
for (int j = 0; j < 10; j ++) {
|
|
canvas->save();
|
|
|
|
canvas->translate(1250 - 250 * i, 1250 - 250 * j);
|
|
canvas->scale(0.5, -0.5);
|
|
|
|
// Render the actor.
|
|
fActor->render(canvas, fRenderFlags);
|
|
|
|
canvas->restore();
|
|
}
|
|
}
|
|
|
|
canvas->restore();
|
|
|
|
// Render the GUI.
|
|
this->renderGUI();
|
|
}
|
|
|
|
void NIMASlide::load(SkScalar winWidth, SkScalar winHeight) {
|
|
this->resetActor();
|
|
}
|
|
|
|
void NIMASlide::unload() {
|
|
// Discard resources.
|
|
fAnimation = nullptr;
|
|
fActor.reset(nullptr);
|
|
}
|
|
|
|
bool NIMASlide::animate(const SkAnimTimer& timer) {
|
|
// Apply the animation.
|
|
if (fAnimation) {
|
|
if (fPlaying) {
|
|
fTime = std::fmod(timer.secs(), fAnimation->max());
|
|
}
|
|
fAnimation->time(fTime);
|
|
fAnimation->apply(1.0f);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool NIMASlide::onChar(SkUnichar c) {
|
|
return false;
|
|
}
|
|
|
|
bool NIMASlide::onMouse(SkScalar x, SkScalar y, Window::InputState state, uint32_t modifiers) {
|
|
return false;
|
|
}
|
|
|
|
void NIMASlide::resetActor() {
|
|
// Create the actor.
|
|
fActor = std::make_unique<NIMAActor>(fBasePath);
|
|
|
|
// Get the animation.
|
|
fAnimation = fActor->animationInstance(fActor->getAnimations()[fAnimationIndex]);
|
|
}
|
|
|
|
void NIMASlide::renderGUI() {
|
|
ImGui::SetNextWindowSize(ImVec2(300, 0));
|
|
ImGui::Begin("NIMA");
|
|
|
|
// List of animations.
|
|
auto animations = const_cast<std::vector<std::string>&>(fActor->getAnimations());
|
|
ImGui::PushItemWidth(-1);
|
|
if (ImGui::ListBox("Animations",
|
|
&fAnimationIndex,
|
|
vector_getter,
|
|
reinterpret_cast<void*>(&animations),
|
|
animations.size(),
|
|
5)) {
|
|
resetActor();
|
|
}
|
|
|
|
// Playback control.
|
|
ImGui::Spacing();
|
|
if (ImGui::Button("Play")) {
|
|
fPlaying = true;
|
|
}
|
|
ImGui::SameLine();
|
|
if (ImGui::Button("Pause")) {
|
|
fPlaying = false;
|
|
}
|
|
|
|
// Time slider.
|
|
ImGui::PushItemWidth(-1);
|
|
ImGui::SliderFloat("Time", &fTime, 0.0f, fAnimation->max(), "Time: %.3f");
|
|
|
|
// Backend control.
|
|
int useImmediate = SkToBool(fRenderFlags & kImmediate_RenderFlag);
|
|
ImGui::Spacing();
|
|
ImGui::RadioButton("Skia Backend", &useImmediate, 0);
|
|
ImGui::RadioButton("Immediate Backend", &useImmediate, 1);
|
|
if (useImmediate) {
|
|
fRenderFlags |= kImmediate_RenderFlag;
|
|
} else {
|
|
fRenderFlags &= ~kImmediate_RenderFlag;
|
|
}
|
|
|
|
// Cache control.
|
|
bool useCache = SkToBool(fRenderFlags & kCache_RenderFlag);
|
|
ImGui::Spacing();
|
|
ImGui::Checkbox("Cache Vertices", &useCache);
|
|
if (useCache) {
|
|
fRenderFlags |= kCache_RenderFlag;
|
|
} else {
|
|
fRenderFlags &= ~kCache_RenderFlag;
|
|
}
|
|
|
|
// Bounding box toggle.
|
|
bool drawBounds = SkToBool(fRenderFlags & kBounds_RenderFlag);
|
|
ImGui::Spacing();
|
|
ImGui::Checkbox("Draw Bounds", &drawBounds);
|
|
if (drawBounds) {
|
|
fRenderFlags |= kBounds_RenderFlag;
|
|
} else {
|
|
fRenderFlags &= ~kBounds_RenderFlag;
|
|
}
|
|
|
|
ImGui::End();
|
|
}
|