diff --git a/CMakeLists.txt b/CMakeLists.txt index 6e406c3..42c54d2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,7 @@ add_subdirectory(assets/blender/buildings/parts) add_subdirectory(assets/blender/characters) add_subdirectory(resources) add_subdirectory(src/text_editor) +add_subdirectory(src/features) add_executable(Game Game.cpp ${WATER_SRC}) target_include_directories(Game PRIVATE src/gamedata) diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt index afd5f21..1fd4015 100644 --- a/resources/CMakeLists.txt +++ b/resources/CMakeLists.txt @@ -13,6 +13,17 @@ foreach(DIR_NAME ${DIRECTORY_LIST}) DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${DIR_NAME}) list(APPEND TARGET_PATHS ${CMAKE_CURRENT_BINARY_DIR}/${DIR_NAME}) endforeach() +set(COPY_FILES main/flare.png) +set(TARGET_FILES) +foreach(PFILE ${COPY_FILES}) + add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${PFILE} + COMMAND ${CMAKE_COMMAND} -E copy + ${CMAKE_CURRENT_SOURCE_DIR}/${PFILE} + ${CMAKE_CURRENT_BINARY_DIR}/${PFILE} + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${PFILE} + ) + list(APPEND TARGET_FILES ${CMAKE_CURRENT_BINARY_DIR}/${PFILE}) +endforeach() #add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/terrain/world_map.png # COMMAND unzip -o ${CMAKE_CURRENT_SOURCE_DIR}/world_map.kra mergedimage.png -d ${CMAKE_CURRENT_BINARY_DIR}/world_map @@ -31,4 +42,4 @@ add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/terrain/brushes.png DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/brushes.kra) list(APPEND TARGET_PATHS ${CMAKE_CURRENT_BINARY_DIR}/terrain/brushes.png) -add_custom_target(stage_resources ALL DEPENDS ${TARGET_PATHS}) +add_custom_target(stage_resources ALL DEPENDS ${TARGET_PATHS} ${TARGET_FILES}) diff --git a/resources/main/flare.png b/resources/main/flare.png new file mode 100644 index 0000000..9f8a2a8 Binary files /dev/null and b/resources/main/flare.png differ diff --git a/resources/main/particles.material b/resources/main/particles.material new file mode 100644 index 0000000..542aeef --- /dev/null +++ b/resources/main/particles.material @@ -0,0 +1,19 @@ +material Examples/Flare +{ + technique + { + pass + { + lighting off + scene_blend add + depth_write off + diffuse vertexcolour + + texture_unit + { + texture flare.png + } + } + } +} + diff --git a/src/features/CMakeLists.txt b/src/features/CMakeLists.txt new file mode 100644 index 0000000..d4c4717 --- /dev/null +++ b/src/features/CMakeLists.txt @@ -0,0 +1,3 @@ +project(features) +# ... +add_subdirectory(characters) diff --git a/src/features/characters/CMakeLists.txt b/src/features/characters/CMakeLists.txt new file mode 100644 index 0000000..f4c10ca --- /dev/null +++ b/src/features/characters/CMakeLists.txt @@ -0,0 +1,33 @@ +project(characters) +find_package(OGRE REQUIRED COMPONENTS Bites Paging Terrain CONFIG) +find_package(ZLIB) +find_package(SDL2) +find_package(assimp REQUIRED CONFIG) +find_package(OgreProcedural REQUIRED CONFIG) +find_package(pugixml REQUIRED CONFIG) +find_package(flecs REQUIRED CONFIG) +find_package(Tracy REQUIRED CONFIG) +add_executable(demo main.cpp TimeEvents.cpp) +target_link_libraries(demo OgreMain OgreBites) +file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/resources.cfg" + DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") +add_custom_command(TARGET demo POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_directory + "${CMAKE_BINARY_DIR}/resources" + "${CMAKE_CURRENT_BINARY_DIR}/resources" + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_SOURCE_DIR}/resources/main/jaiqua.mesh" + "${CMAKE_CURRENT_BINARY_DIR}/resources/main/jaiqua.mesh" + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_SOURCE_DIR}/resources/main/jaiqua.skeleton" + "${CMAKE_CURRENT_BINARY_DIR}/resources/main/jaiqua.skeleton" + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_SOURCE_DIR}/resources/main/jaiqua.material" + "${CMAKE_CURRENT_BINARY_DIR}/resources/main/jaiqua.material" + COMMAND ${CMAKE_COMMAND} -E copy + "${CMAKE_CURRENT_SOURCE_DIR}/resources/main/blue_jaiqua.jpg" + "${CMAKE_CURRENT_BINARY_DIR}/resources/main/blue_jaiqua.jpg" + DEPENDS "${CMAKE_BINARY_DIR}/resources" + COMMENT "Copying generated resources from root build dir to local build dir" +) + diff --git a/src/features/characters/TimeEvents.cpp b/src/features/characters/TimeEvents.cpp new file mode 100644 index 0000000..f337b49 --- /dev/null +++ b/src/features/characters/TimeEvents.cpp @@ -0,0 +1,207 @@ +// +// TimeEvents.cpp +// OGRE +// +// Created by Chilly Willy on 11/29/25. +// + +#include "TimeEvents.h" + +#include + + +void TimeEventDispatcher::addEventList(const TimeEventList * list) +{ + if (std::find(mEventLists.begin(), mEventLists.end(), list) == mEventLists.end()) + { + mEventLists.push_back(list); + } +} + +void TimeEventDispatcher::removeEventList(const TimeEventList * list) +{ + auto i = std::find(mEventLists.begin(), mEventLists.end(), list); + if (i != mEventLists.end()) + { + mEventLists.erase(i); + } +} + +void TimeEventDispatcher::addListener(TimeEventListener * listener) +{ + if (std::find(mListeners.begin(), mListeners.end(), listener) == mListeners.end()) + { + mListeners.push_back(listener); + } +} + +void TimeEventDispatcher::removeListener(TimeEventListener * listener) +{ + auto i = std::find(mListeners.begin(), mListeners.end(), listener); + if (i != mListeners.end()) + { + mListeners.erase(i); + } +} + +/* + lastTime, thisTime, n=loops + + lastTime is the thisTime from the last frame and thisTime will be the lastTime in the next frame. + Events at those times should only be dispatched once so we should include one and exclude the other. + + If we include thisTime and exclude lastTime, then we need to dispatch events before we start playing, + eg when we call setTimePosition(), and we would need to somehow know whether it should be dispatched + as Forward or Backward. + + If we include lastTime and exclude thisTime, then we don't need to do anything before playing but + we need to include the length/0 when we loop or finish, which requires special handling anyway. + + Looping Forward: + next lastTime + thisTime < length thisTime lastTime <= t < thisTime + thisTime = length 0 lastTime <= t <= length + thisTime > length thisTime - n * length lastTime <= t <= length ... 0 <= t < thisTime + + ... 0 <= t <= length + + Looping Backward: + next lastTime + thisTime > 0 thisTime lastTime >= t > thisTime + thisTime = 0 0 !!! lastTime >= t >= 0 + thisTime < 0 thisTime + n * length lastTime >= t >= 0 ... length >= t > thisTime + + ... length >= t >= 0 + + Not-Looping Forward: + next lastTime + thisTime < length thisTime lastTime <= t < thisTime + thisTime = length length lastTime <= t <= length + thisTime > length length lastTime <= t <= length + + + Not-Looping Backward: + next lastTime + thisTime > 0 thisTime lastTime >= t > thisTime + thisTime = 0 0 lastTime >= t >= 0 + thisTime < 0 0 lastTime >= t >= 0 + + */ + +void TimeEventDispatcher::dispatch(float lastTime, float thisTime, int loops, float length) +{ + if ((loops > 0) || (thisTime > lastTime)) + { + while (loops--) + { + dispatchForwardInclusive(lastTime, length); + lastTime = 0.0f; + } + if (thisTime >= length) + { + dispatchForwardInclusive(lastTime, length); + } + else + { + dispatchForwardExclusive(lastTime, thisTime); + } + } + else if ((loops < 0) || (thisTime < lastTime)) + { + if (lastTime == 0.0f) + { + /* length mod length = 0 mod length + + Either we've already been looping backward and the last frame just happened + to stop on 0, in which case we already triggered its events, or we are just + starting to to loop backward, in which case we should not trigger events at + 0 until we do a full backward run through the animation. If we haven't been + looping then we would not get in here because thisTime == lastTime == 0.0f. + For the same reason we also know loops < 0. + */ + lastTime = length; + ++loops; + } + while (loops++) + { + dispatchBackwardInclusive(lastTime, 0.0f); + lastTime = length; + } + if (thisTime <= 0.0f) + { + dispatchBackwardInclusive(lastTime, 0.0f); + } + else + { + dispatchBackwardExclusive(lastTime, thisTime); + } + } + else + { + // Nothing doing + } +} + +void TimeEventDispatcher::dispatchForwardInclusive(float lastTime, float thisTime) +{ + for (const TimeEventList * events : mEventLists) + { + for (auto ie = events->begin(); ie != events->end(); ++ie) + { + if (ie->first >= lastTime && ie->first <= thisTime) + { + dispatchEvent(ie->second, TED_FORWARD); + } + } + } +} + +void TimeEventDispatcher::dispatchForwardExclusive(float lastTime, float thisTime) +{ + for (const TimeEventList * events : mEventLists) + { + for (auto ie = events->begin(); ie != events->end(); ++ie) + { + if (ie->first >= lastTime && ie->first < thisTime) + { + dispatchEvent(ie->second, TED_FORWARD); + } + } + } +} + +void TimeEventDispatcher::dispatchBackwardInclusive(float lastTime, float thisTime) +{ + for (const TimeEventList * events : mEventLists) + { + for (auto ie = events->rbegin(); ie != events->rend(); ++ie) + { + if (ie->first <= lastTime && ie->first >= thisTime) + { + dispatchEvent(ie->second, TED_BACKWARD); + } + } + } +} + +void TimeEventDispatcher::dispatchBackwardExclusive(float lastTime, float thisTime) +{ + for (const TimeEventList * events : mEventLists) + { + for (auto ie = events->rbegin(); ie != events->rend(); ++ie) + { + if (ie->first <= lastTime && ie->first > thisTime) + { + dispatchEvent(ie->second, TED_BACKWARD); + } + } + } +} + +void TimeEventDispatcher::dispatchEvent(const std::string & name, TimeEventDirection direction) +{ + for (TimeEventListener * listener : mListeners) + { + listener->eventOccurred(name, direction); + } +} diff --git a/src/features/characters/TimeEvents.h b/src/features/characters/TimeEvents.h new file mode 100644 index 0000000..533922e --- /dev/null +++ b/src/features/characters/TimeEvents.h @@ -0,0 +1,58 @@ +// +// TimeEvents.h +// OGRE +// +// Created by Chilly Willy on 11/29/25. +// + +#ifndef INCLUDE_OGRE_TIME_EVENTS_H +#define INCLUDE_OGRE_TIME_EVENTS_H + + +#include +#include +#include + + +typedef std::multimap TimeEventList; + + +enum TimeEventDirection +{ + TED_FORWARD, + TED_BACKWARD, +}; + + +class TimeEventListener +{ +public: + virtual void eventOccurred(const std::string & name, TimeEventDirection direction) {} +}; + + +class TimeEventDispatcher +{ +public: + void addEventList(const TimeEventList * list); + void removeEventList(const TimeEventList * list); + + void addListener(TimeEventListener * listener); + void removeListener(TimeEventListener * listener); + + void dispatch(float lastTime, float thisTime, int loops, float length); + +private: + void dispatchForwardInclusive(float lastTime, float thisTime); + void dispatchForwardExclusive(float lastTime, float thisTime); + void dispatchBackwardInclusive(float lastTime, float thisTime); + void dispatchBackwardExclusive(float lastTime, float thisTime); + + void dispatchEvent(const std::string & name, TimeEventDirection direction); + + std::vector mEventLists; + std::vector mListeners; +}; + + +#endif /* INCLUDE_OGRE_TIME_EVENTS_H */ diff --git a/src/features/characters/main.cpp b/src/features/characters/main.cpp new file mode 100644 index 0000000..b461600 --- /dev/null +++ b/src/features/characters/main.cpp @@ -0,0 +1,1043 @@ +#include +#include +#include +// #ifdef RTSHADER_SYSTEM_BUILD_EXT_SHADERS +#include +#include +#include +#include "TimeEvents.h" + +// #endif + +/* The RootMotionApplier applies the transforms from a specified + NodeAnimationTrack to an Entity's parent SceneNode instead of + to its Skeleton's Bone. + + Won’t work if root bone has an ancestor that is not + origin-positioned, identity-oriented, and unity-scaled. + + If root bone has a non-origin-positioned, non-identity-oriented, + or non-unity-scaled binding pose, playing multiple root-movement + animations will apply the binding pose to the SceneNode multiple + times (ie mess it up) (i think) + + TODO: + + - add setEnabled() or disable() or unapply() that unapplies all, + some, or no components of applied rotation. + + - add option of which components of translation and rotation to + retain/reset when looping (instead of the currently hardcoded + reseting of translation.y and retaining of rotation.y) + + - maybe implement scale for shrinking and growing animations lol +*/ +class RootMotionApplier { +public: + RootMotionApplier(Ogre::AnimationState *animationState, + Ogre::Entity *entity, Ogre::NodeAnimationTrack *track) + : mEntity(entity) + , mTrack(track) + , mAppliedTranslation(Ogre::Vector3::ZERO) + , mAppliedRotation(Ogre::Quaternion::IDENTITY) + { + assert(animationState); + assert(entity); + assert(track); + + // Get root bone binding pose. + + Ogre::Bone *rootBone = + entity->getSkeleton()->getBone(track->getHandle()); + mRootBindingPosition = rootBone->getInitialPosition(); + mRootBindingOrientation = rootBone->getInitialOrientation(); + mRootBindingOrientationInverse = + mRootBindingOrientation.Inverse(); + + // Measure total transformation for root. + + Ogre::TransformKeyFrame *tkfBeg = track->getNodeKeyFrame(0); + Ogre::TransformKeyFrame *tkfEnd = + track->getNodeKeyFrame(track->getNumKeyFrames() - 1); + + Ogre::Quaternion begRotation = mRootBindingOrientation * + tkfBeg->getRotation() * + mRootBindingOrientationInverse; + Ogre::Quaternion endRotation = mRootBindingOrientation * + tkfEnd->getRotation() * + mRootBindingOrientationInverse; + mLoopRotation = endRotation * begRotation.Inverse(); + + // TODO: there's probably a smarter way to limit rotation to y-axis + Ogre::Matrix3 mat; + mLoopRotation.ToRotationMatrix(mat); + Ogre::Radian yAngle, zAngle, xAngle; + mat.ToEulerAnglesYZX(yAngle, zAngle, xAngle); + mat.FromEulerAnglesYZX(yAngle, Ogre::Radian(0.0f), + Ogre::Radian(0.0f)); + mLoopRotation.FromRotationMatrix(mat); + + mLoopRotationInverse = mLoopRotation.Inverse(); + + Ogre::Vector3 begTranslation = + mRootBindingPosition + tkfBeg->getTranslate() - + begRotation * mRootBindingPosition; + Ogre::Vector3 endTranslation = + mRootBindingPosition + tkfEnd->getTranslate() - + endRotation * mRootBindingPosition; + mLoopTranslation = + endTranslation - mLoopRotation * begTranslation; + mLoopTranslation.y = 0.0f; + + // Suppress root bone movement + + if (!animationState->hasBlendMask()) { + animationState->createBlendMask( + entity->getSkeleton()->getNumBones(), 1.0f); + } + animationState->setBlendMaskEntry(track->getHandle(), 0.0f); + } + + void apply(int loops, float thisTime) + { + Ogre::SceneNode *sceneNode = mEntity->getParentSceneNode(); + Ogre::TransformKeyFrame tkf(0, 0); + + // Unapply transform from last frame + + sceneNode->rotate(mAppliedRotation.Inverse()); + sceneNode->translate(-mAppliedTranslation, + Ogre::Node::TS_LOCAL); + + // Apply periodic loop transforms + + while (loops < 0) { + sceneNode->rotate(mLoopRotationInverse); + sceneNode->translate(-mLoopTranslation, + Ogre::Node::TS_LOCAL); + loops++; + } + while (loops > 0) { + sceneNode->translate(mLoopTranslation, + Ogre::Node::TS_LOCAL); + sceneNode->rotate(mLoopRotation); + loops--; + } + + // Apply transform from this frame + + mTrack->getInterpolatedKeyFrame(thisTime, &tkf); + + mAppliedRotation = mRootBindingOrientation * tkf.getRotation() * + mRootBindingOrientationInverse; + mAppliedTranslation = mRootBindingPosition + + tkf.getTranslate() - + mAppliedRotation * mRootBindingPosition; + + sceneNode->translate(mAppliedTranslation, Ogre::Node::TS_LOCAL); + sceneNode->rotate(mAppliedRotation); + } + +private: + Ogre::Entity *mEntity; + Ogre::NodeAnimationTrack *mTrack; + + Ogre::Vector3 mRootBindingPosition; + Ogre::Quaternion mRootBindingOrientation; + Ogre::Quaternion mRootBindingOrientationInverse; + + Ogre::Vector3 mLoopTranslation; + Ogre::Quaternion mLoopRotation; + Ogre::Quaternion mLoopRotationInverse; + + Ogre::Vector3 mAppliedTranslation; + Ogre::Quaternion mAppliedRotation; +}; + +class AnimationUpdater : public Ogre::ControllerValue { +public: + AnimationUpdater(Ogre::AnimationState *animationState) + : mAnimationState(animationState) + { + assert(animationState); + } + + float getValue(void) const override + { + return mAnimationState->getTimePosition() / + mAnimationState->getLength(); + } + + void setValue(float timeDelta) override + { + // Don't assume AnimationState::addTime()'s internal time-updating (ie modulo) implementation. + + float lastTime = mAnimationState->getTimePosition(); + mAnimationState->addTime(timeDelta); + float thisTime = mAnimationState->getTimePosition(); + + float length = mAnimationState->getLength(); + bool loop = mAnimationState->getLoop(); + int loops = loop ? (int)std::round( + (lastTime + timeDelta - thisTime) / + length) : + 0; + + // Apply Movement + + if (mRootMotionApplier) { + mRootMotionApplier->apply(loops, thisTime); + } + + // Dispatch Events + + if (mTimeEventDispatcher) { + mTimeEventDispatcher->dispatch(lastTime, thisTime, + loops, length); + } + } + + void setUseRootMotion(Ogre::Entity *entity, + Ogre::NodeAnimationTrack *track) + { + mRootMotionApplier.reset( + new RootMotionApplier(mAnimationState, entity, track)); + } + + RootMotionApplier *getRootMotionApplier() + { + return mRootMotionApplier.get(); + } + + void setUseTimeEvents(bool use) + { + if (use) { + if (!mTimeEventDispatcher) { + mTimeEventDispatcher.reset( + new TimeEventDispatcher); + } + } else { + mTimeEventDispatcher.reset(); + } + } + + TimeEventDispatcher *getTimeEventDispatcher() + { + return mTimeEventDispatcher.get(); + } + +private: + Ogre::AnimationState *mAnimationState; + std::unique_ptr mRootMotionApplier; + std::unique_ptr mTimeEventDispatcher; +}; + +class SoundwaveUpdater : public Ogre::ControllerValue { +public: + SoundwaveUpdater(Ogre::BillboardSet *bbs) + : mBbs(bbs) + , kSoundwaveColor(1.0f, 0.4f, 0.0f) + , kSoundwaveSizeBeg(2.0f) + , kSoundwaveSizeEnd(10.0f) + , kSoundwaveTime(0.3f) + { + } + + static std::shared_ptr create(Ogre::BillboardSet *bbs) + { + return std::make_shared(bbs); + } + + void addSoundwave(const Ogre::Vector3 &pos) + { + Ogre::Billboard *bb = + mBbs->createBillboard(pos, kSoundwaveColor); + bb->setDimensions(kSoundwaveSizeBeg, kSoundwaveSizeBeg); + mBbs->_updateBounds(); + } + +private: + float getValue(void) const override + { + return 0.0f; + } + + void setValue(float timeDelta) override + { + float grow = (kSoundwaveSizeEnd - kSoundwaveSizeBeg) * + timeDelta / kSoundwaveTime; + + for (int i = 0; i < mBbs->getNumBillboards(); + /* conditional inc in loop */) { + Ogre::Billboard *bb = mBbs->getBillboard(i); + + float size = bb->getOwnWidth() + grow; + + if (size <= kSoundwaveSizeEnd) { + float d = Ogre::Math::inverseLerp( + kSoundwaveSizeBeg, kSoundwaveSizeEnd, + size); + + bb->setDimensions(size, size); + bb->setColour(kSoundwaveColor * (1.0f - d)); + ++i; + } else { + mBbs->removeBillboard(i); + } + } + } + + Ogre::BillboardSet *mBbs; + + const Ogre::ColourValue kSoundwaveColor; + const float kSoundwaveSizeBeg; + const float kSoundwaveSizeEnd; + const float kSoundwaveTime; +}; + +class FootfallListener final : public TimeEventListener { +public: + FootfallListener(Ogre::Entity *entity, + SoundwaveUpdater *soundwaveUpdater) + : mEntity(entity) + , mSoundwaveUpdater(soundwaveUpdater) + { + } + +private: + void eventOccurred(const std::string &name, + TimeEventDirection direction) override + { + Ogre::Bone *toe; + if (name == "footfall-l") { + toe = mEntity->getSkeleton()->getBone("Ltoe"); + } else if (name == "footfall-r") { + toe = mEntity->getSkeleton()->getBone("Rtoe"); + } else { + return; + } + + Ogre::Vector3 toePos = + mEntity->getParentSceneNode() + ->convertLocalToWorldPosition( + toe->_getDerivedPosition()); + toePos.y = 0.0f; + + mSoundwaveUpdater->addSoundwave(toePos); + } + + Ogre::Entity *mEntity; + SoundwaveUpdater *mSoundwaveUpdater; +}; + +/*----------------------------------------------------------------------------- +| The jaiqua mesh has the vertices baked quite a distance from local origin. +| This moves the mesh to the origin and moves the skeleton's Spineroot bone. +-----------------------------------------------------------------------------*/ +static void tweakJaiquaMesh(const Ogre::MeshPtr &mesh, Ogre::Bone *rootBone) +{ + Ogre::SkeletonPtr skeleton = mesh->getSkeleton(); + // Get root bone's binding position + const Ogre::Vector3 bindPos = rootBone->getInitialPosition(); + + // Re-bind it at origin (preserving y) + + rootBone->setPosition(0.0f, bindPos.y, 0.0f); + skeleton->setBindingPose(); + + // Move all the vertices by the same amount + // There's no shared vertices and only 1 submesh + + const Ogre::VertexData *vertexData = + mesh->getSubMeshes()[0]->vertexData; + const Ogre::VertexElement *posElem = + vertexData->vertexDeclaration->findElementBySemantic( + Ogre::VES_POSITION); + Ogre::HardwareVertexBufferSharedPtr vbuf = + vertexData->vertexBufferBinding->getBuffer( + posElem->getSource()); + Ogre::DefaultHardwareBufferManagerBase bfrMgr; + Ogre::HardwareVertexBufferPtr shadowBuffer = bfrMgr.createVertexBuffer( + vbuf->getVertexSize(), vbuf->getNumVertices(), + Ogre::HBU_CPU_ONLY); + shadowBuffer->copyData(*vbuf); + + Ogre::HardwareBufferLockGuard vertexLock( + shadowBuffer, Ogre::HardwareBuffer::HBL_NORMAL); + unsigned char *vertex = static_cast(vertexLock.pData); + float *pFloat; + + for (size_t i = 0; i < vertexData->vertexCount; ++i) { + posElem->baseVertexPointerToElement(vertex, &pFloat); + pFloat[0] -= bindPos.x; + pFloat[2] -= bindPos.z; + vertex += shadowBuffer->getVertexSize(); + } + + vertexLock.unlock(); + + vbuf->copyData(*shadowBuffer); +} + +/*----------------------------------------------------------------------------- +| The jaiqua sneak animation doesn't loop properly. This method tweaks the +| animation to loop properly by altering the Spineroot bone track. +| We also move the Sneak animation to start closer to the origin. +-----------------------------------------------------------------------------*/ +static void tweakSneakAnim(const Ogre::SkeletonPtr &skel) +{ + // Move Sneak animation closer to origin + Ogre::Bone *rootBone = skel->getBone("Spineroot"); + Ogre::Animation *animation = skel->getAnimation("Sneak"); + Ogre::NodeAnimationTrack *rootTrack = + animation->getNodeTrack(rootBone->getHandle()); + + Ogre::Vector3 start = rootTrack->getNodeKeyFrame(0)->getTranslate(); + start.y = 0.0f; + + for (size_t i = 0; i < rootTrack->getNumKeyFrames(); ++i) { + Ogre::TransformKeyFrame *kf = rootTrack->getNodeKeyFrame(i); + kf->setTranslate(kf->getTranslate() - start); + } + + // Tweak Sneak animation + const float ANIM_CHOP = 8.0f; + for (const auto &it : + animation->_getNodeTrackList()) // for every node track... + { + Ogre::NodeAnimationTrack *track = it.second; + + // get the keyframe at the chopping point + Ogre::TransformKeyFrame oldKf(0, 0); + track->getInterpolatedKeyFrame(ANIM_CHOP, &oldKf); + + // drop all keyframes after the chopping point + while (track->getKeyFrame(track->getNumKeyFrames() - 1) + ->getTime() >= ANIM_CHOP - 0.3f) + track->removeKeyFrame(track->getNumKeyFrames() - 1); + + // create a new keyframe at chopping point, and get the first keyframe + Ogre::TransformKeyFrame *newKf = + track->createNodeKeyFrame(ANIM_CHOP); + Ogre::TransformKeyFrame *startKf = track->getNodeKeyFrame(0); + + Ogre::Bone *bone = skel->getBone(track->getHandle()); + + if (bone->getName() == + "Spineroot") // adjust spine root relative to new location + { + newKf->setTranslate(oldKf.getTranslate()); + newKf->setRotation(oldKf.getRotation()); + newKf->setScale(oldKf.getScale()); + } else // make all other bones loop back + { + newKf->setTranslate(startKf->getTranslate()); + newKf->setRotation(startKf->getRotation()); + newKf->setScale(startKf->getScale()); + } + } + + animation->setLength(ANIM_CHOP); +} + +static void insertFootfallEvents(Ogre::Entity *entity, TimeEventList &eventList) +{ + /* This function creates events coinciding with the footfalls in the sneak animation. + Events are just strings associated with a timestamp that will be triggered during + the playback of an animation and dispatched to a user-provided TimeEventListener. + + Normally these would be manually created along with the animation itself. Here we + create them programmatically by stepping through the animation, applying the pose + to a skeleton, and looking for when each toe bone crosses the y=0 plane. + */ + + Ogre::SkeletonInstance *skeleton = entity->getSkeleton(); + Ogre::Bone *lToeBone = skeleton->getBone("Ltoe"); + Ogre::Bone *rToeBone = skeleton->getBone("Rtoe"); + Ogre::Animation *animation = skeleton->getAnimation("Sneak"); + Ogre::AnimationState *as = entity->getAnimationState("Sneak"); + + // We want the root transforms applied. + + if (as->hasBlendMask()) { + as->setBlendMaskEntry( + skeleton->getBone("Spineroot")->getHandle(), 1.0f); + } + + // Get ending position before loop to compare with start position. + + as->setTimePosition(as->getLength()); + entity->_updateSkeleton(); + + float lToePrevY = lToeBone->_getDerivedPosition().y; + float rToePrevY = rToeBone->_getDerivedPosition().y; + + // This particular animation has keyframes for all tracks on a regular + // framerate so we can use keyframe times from any track for sampling. + + Ogre::NodeAnimationTrack *lToeTrack = + animation->getNodeTrack(lToeBone->getHandle()); + + for (size_t i = 0; i < lToeTrack->getNumKeyFrames(); ++i) { + Ogre::TransformKeyFrame *tkf = lToeTrack->getNodeKeyFrame(i); + float time = tkf->getTime(); + + as->setTimePosition(time); + entity->_updateSkeleton(); + + float lToeY = lToeBone->_getDerivedPosition().y; + float rToeY = rToeBone->_getDerivedPosition().y; + + // printf("t: %f l: %f r: %f\n", tkf->getTime(), lToeY, rToeY); + + if ((lToePrevY > 0.0f) && (lToeY < 0.0f)) { + eventList.insert(std::make_pair(time, "footfall-l")); + } + if ((rToePrevY > 0.0f) && (rToeY < 0.0f)) { + eventList.insert(std::make_pair(time, "footfall-r")); + } + + lToePrevY = lToeY; + rToePrevY = rToeY; + } + + /* + // Do it manually + + eventList.clear(); + + mFootfallEvents.insert(std::make_pair(0.666667f, "footfall-l")); + mFootfallEvents.insert(std::make_pair(2.000000f, "footfall-r")); + mFootfallEvents.insert(std::make_pair(3.500000f, "footfall-l")); + mFootfallEvents.insert(std::make_pair(4.166667f, "footfall-r")); + mFootfallEvents.insert(std::make_pair(4.666667f, "footfall-l")); + */ +} + +static void generateBoundingBox(Ogre::Entity *entity) +{ + // This is a hacky way to make a close-fitting bounding box + // for the Sneak animation with the root movement suppressed. + Ogre::Skeleton *skeleton = entity->getSkeleton(); + Ogre::Bone *rootBone = skeleton->getBone("Spineroot"); + Ogre::Animation *animation = skeleton->getAnimation("Sneak"); + Ogre::AnimationState *as = entity->getAnimationState("Sneak"); + Ogre::NodeAnimationTrack *rootTrack = + animation->getNodeTrack(rootBone->getHandle()); + + // Suppress root-bone movement. + + if (!as->hasBlendMask()) { + as->createBlendMask(skeleton->getNumBones(), 1.0f); + } + as->setBlendMaskEntry(rootBone->getHandle(), 0.0f); + as->setEnabled(true); + + // Generate bounding box from skeleton. + + entity->setUpdateBoundingBoxFromSkeleton(true); + + Ogre::AxisAlignedBox sneakBounds = Ogre::AxisAlignedBox::EXTENT_NULL; + + for (size_t i = 0; i < rootTrack->getNumKeyFrames(); ++i) { + Ogre::TransformKeyFrame *kf = rootTrack->getNodeKeyFrame(i); + + as->setTimePosition(kf->getTime()); + entity->_updateSkeleton(); + + Ogre::AxisAlignedBox bbox = entity->getBoundingBox(); + sneakBounds.merge(bbox); + } + + entity->getMesh()->_setBounds(sneakBounds); +} + +static void createSoundwaveMaterial(const Ogre::String &material_name, + const Ogre::String &group_name) +{ + // Make image + + const int w = 128; + const int h = 128; + + Ogre::Image image(Ogre::PF_BYTE_L, w, h); + image.setTo(Ogre::ColourValue::Black); + + // Draw ring + float r = (w - 1) / 2.0f; + float rr = r * r; + + for (int y = 0; y < h; ++y) { + for (int x = 0; x < w; ++x) { + float dx = x - r; + float dy = y - r; + float dd = dx * dx + dy * dy; + + if (dd > rr) + continue; + + *image.getData(x, y) = 255 * dd / rr; + } + } + + // Make texture from image + + Ogre::TexturePtr t = Ogre::TextureManager::getSingleton().loadImage( + material_name, group_name, image); + + // Make material + + Ogre::MaterialPtr material = + Ogre::MaterialManager::getSingleton().create(material_name, + Ogre::RGN_DEFAULT); + + material->setCullingMode(Ogre::CULL_NONE); + material->setSceneBlending(Ogre::SBT_ADD); + material->setLightingEnabled(false); + material->setDepthWriteEnabled(false); + material->setDepthBias(0, 1); + + Ogre::Pass *pass = material->getTechnique(0)->getPass(0); + + pass->setVertexColourTracking(Ogre::TVC_EMISSIVE); + + // Attach the texture to the material texture unit (single layer) and setup properties + Ogre::TextureUnitState *tus = pass->createTextureUnitState(); + tus->setTexture(t); +} + +enum VisualiseBoundingBoxMode { kVisualiseNone, kVisualiseOne, kVisualiseAll }; + +struct KeyHandler : public OgreBites::InputListener { + VisualiseBoundingBoxMode mVisualiseBoundingBoxMode; + KeyHandler() + : OgreBites::InputListener() + { + mVisualiseBoundingBoxMode = kVisualiseNone; + } + bool keyPressed(const OgreBites::KeyboardEvent &evt) override + { +#if 0 + if (true) { + // Handle keypresses. + switch (evt.keysym.sym) { + case 'v': + // Toggle visualise bounding boxes. + switch (mVisualiseBoundingBoxMode) { + case kVisualiseNone: + setVisualiseBoundingBoxMode( + kVisualiseOne); + break; + case kVisualiseOne: + setVisualiseBoundingBoxMode( + kVisualiseAll); + break; + case kVisualiseAll: + setVisualiseBoundingBoxMode( + kVisualiseNone); + break; + } + return true; + break; + + case 'b': { + // Toggle bone based bounding boxes for all models. + enableBoneBoundingBoxMode(!mBoneBoundingBoxes); + return true; + } break; + default: + break; + } + } +#endif + return false; + } +}; +class App : public OgreBites::ApplicationContext { + void locateResources() override + { + Ogre::ResourceGroupManager::getSingleton().createResourceGroup( + "Characters", true); + Ogre::ResourceGroupManager::getSingleton().createResourceGroup( + "Water", true); + Ogre::ResourceGroupManager::getSingleton().createResourceGroup( + "LuaScripts", false); + Ogre::ResourceGroupManager::getSingleton().addResourceLocation( + "./lua-scripts", "FileSystem", "LuaScripts", true, + true); + Ogre::ResourceGroupManager::getSingleton().addResourceLocation( + "./characters/male", "FileSystem", "Characters", false, + true); + Ogre::ResourceGroupManager::getSingleton().addResourceLocation( + "./characters/female", "FileSystem", "Characters", + false, true); + OgreBites::ApplicationContext::locateResources(); + } + void loadResources() override + { + Ogre::TextureManager::getSingleton().load("flare.png", + "General"); + Ogre::TextureManager::getSingleton().load("blue_jaiqua.jpg", + "General"); + Ogre::MaterialManager::getSingleton().load("Examples/Flare", + "General"); + Ogre::MaterialManager::getSingleton().load("Examples/Rockwall", + "General"); + Ogre::MaterialManager::getSingleton().load("jaiqua", "General"); + Ogre::MeshManager::getSingleton().load("jaiqua.mesh", + "General"); + } + KeyHandler kbd; + +public: + App() + : OgreBites::ApplicationContext("FeatureCharactersDemo") + , NUM_MODELS(6) + { + mBoundingBoxModelIndex = 0; + mBoneBoundingBoxes = false; + mBoneBoundingBoxesItemName = "Bone AABBs"; + } + void setVisualiseBoundingBoxMode(VisualiseBoundingBoxMode mode) + { + kbd.mVisualiseBoundingBoxMode = mode; + for (int i = 0; i < NUM_MODELS; i++) { + switch (kbd.mVisualiseBoundingBoxMode) { + case kVisualiseNone: + mModelNodes[i]->setDisplaySceneNode(false); + mModelNodes[i]->showBoundingBox(false); + mEntities[i]->showBoundingSphere(false); + break; + case kVisualiseOne: + mModelNodes[i]->setDisplaySceneNode( + i == mBoundingBoxModelIndex); + mModelNodes[i]->showBoundingBox( + i == mBoundingBoxModelIndex); + mEntities[i]->showBoundingSphere( + i == mBoundingBoxModelIndex); + break; + case kVisualiseAll: + mModelNodes[i]->setDisplaySceneNode(true); + mModelNodes[i]->showBoundingBox(true); + mEntities[i]->showBoundingSphere(true); + break; + } + } + } + void enableBoneBoundingBoxMode(bool enable) + { + // update bone bounding box mode for all models + mBoneBoundingBoxes = enable; + for (int iModel = 0; iModel < NUM_MODELS; iModel++) { + Ogre::SceneNode *node = mModelNodes[iModel]; + for (unsigned int iObj = 0; + iObj < node->numAttachedObjects(); ++iObj) { + if (Ogre::Entity *ent = + dynamic_cast( + node->getAttachedObject( + iObj))) { + ent->setUpdateBoundingBoxFromSkeleton( + mBoneBoundingBoxes); + Ogre::Node::queueNeedUpdate( + node); // when turning off bone bounding boxes, need to force an update + } + } + } + } + +public: + void setupContent(Ogre::Viewport *mViewport, + Ogre::SceneManager *mSceneMgr, + OgreBites::CameraMan *mCameraMan) + { +#ifdef OGRE_BUILD_COMPONENT_RTSHADERSYSTEM + // Make this viewport work with shader generator scheme. + mShaderGenerator->invalidateScheme(Ogre::MSN_SHADERGEN); + mViewport->setMaterialScheme(Ogre::MSN_SHADERGEN); + + //Add the hardware skinning to the shader generator default render state + auto srsHardwareSkinning = + mShaderGenerator->createSubRenderState( + Ogre::RTShader::SRS_HARDWARE_SKINNING); + Ogre::RTShader::RenderState *renderState = + mShaderGenerator->getRenderState(Ogre::MSN_SHADERGEN); + renderState->addTemplateSubRenderState(srsHardwareSkinning); + + Ogre::MaterialPtr pCast1 = + Ogre::MaterialManager::getSingleton().getByName( + "Ogre/RTShader/shadow_caster_dq_skinning_1weight"); + Ogre::MaterialPtr pCast2 = + Ogre::MaterialManager::getSingleton().getByName( + "Ogre/RTShader/shadow_caster_dq_skinning_2weight"); + Ogre::MaterialPtr pCast3 = + Ogre::MaterialManager::getSingleton().getByName( + "Ogre/RTShader/shadow_caster_dq_skinning_3weight"); + Ogre::MaterialPtr pCast4 = + Ogre::MaterialManager::getSingleton().getByName( + "Ogre/RTShader/shadow_caster_dq_skinning_4weight"); + + Ogre::RTShader::HardwareSkinningFactory:: + setCustomShadowCasterMaterials( + Ogre::RTShader::ST_DUAL_QUATERNION, pCast1, + pCast2, pCast3, pCast4); + + // auto srs = mShaderGenerator->createSubRenderState( + // Ogre::RTShader::SRS_SHADOW_MAPPING); + // srs->setParameter("light_count", "2"); + // renderState->addTemplateSubRenderState(srs); + + // set shadow properties + // mSceneMgr->setShadowTechnique(SHADOWTYPE_TEXTURE_MODULATIVE_INTEGRATED); + mSceneMgr->setShadowTechnique(Ogre::SHADOWTYPE_NONE); + mSceneMgr->setShadowTextureCount(2); + mSceneMgr->setShadowTextureSize(512); +#endif + + // add a little ambient lighting + mSceneMgr->setAmbientLight(Ogre::ColourValue(0.3, 0.3, 0.3)); + + Ogre::SceneNode *lightsBbsNode = + mSceneMgr->getRootSceneNode()->createChildSceneNode(); + Ogre::BillboardSet *bbs; + + // Create billboard set for lights . + bbs = mSceneMgr->createBillboardSet(); + bbs->setMaterialName("Examples/Flare"); + lightsBbsNode->attachObject(bbs); + + // add a blue spotlight + Ogre::Light *l = + mSceneMgr->createLight(Ogre::Light::LT_SPOTLIGHT); + Ogre::Vector3 pos(-40, 180, -10); + Ogre::SceneNode *ln = + mSceneMgr->getRootSceneNode()->createChildSceneNode( + pos); + ln->attachObject(l); + ln->setDirection(-pos); + l->setDiffuseColour(0.0, 0.0, 0.5); + bbs->createBillboard(pos)->setColour(l->getDiffuseColour()); + + // add a green spotlight. + l = mSceneMgr->createLight(Ogre::Light::LT_SPOTLIGHT); + pos = Ogre::Vector3(0, 150, -100); + ln = mSceneMgr->getRootSceneNode()->createChildSceneNode(pos); + ln->attachObject(l); + ln->setDirection(-pos); + l->setDiffuseColour(0.0, 0.5, 0.0); + bbs->createBillboard(pos)->setColour(l->getDiffuseColour()); + + // create a floor mesh resource + Ogre::MeshManager::getSingleton().createPlane( + "floor", Ogre::RGN_DEFAULT, + Ogre::Plane(Ogre::Vector3::UNIT_Y, -1), 250, 250, 25, + 25, true, 1, 15, 15, Ogre::Vector3::UNIT_Z); + + // add a floor to our scene using the floor mesh we created + Ogre::Entity *floor = mSceneMgr->createEntity("Floor", "floor"); + floor->setMaterialName("Examples/Rockwall"); + floor->setCastShadows(false); + mSceneMgr->getRootSceneNode()->attachObject(floor); + + // set camera initial transform and speed + mCameraMan->setStyle(OgreBites::CS_ORBIT); + setWindowGrab(false); + mCameraMan->setYawPitchDist(Ogre::Degree(0), Ogre::Degree(25), + 100); + mCameraMan->setTopSpeed(50); + + setupModels(mSceneMgr, mViewport); + } + + void setupModels(Ogre::SceneManager *mSceneMgr, + Ogre::Viewport *mViewport) + { + // Load the mesh with shadow buffers so we can access vertex data + Ogre::MeshPtr mesh = Ogre::MeshManager::getSingleton().load( + "jaiqua.mesh", Ogre::RGN_DEFAULT, Ogre::HBU_CPU_TO_GPU, + Ogre::HBU_CPU_TO_GPU, true, true); + Ogre::SkeletonPtr skeleton = mesh->getSkeleton(); + Ogre::Bone *rootBone = skeleton->getBone("Spineroot"); + Ogre::Animation *animation = skeleton->getAnimation("Sneak"); + Ogre::NodeAnimationTrack *sneakRootTrack = + animation->getNodeTrack(rootBone->getHandle()); + + tweakJaiquaMesh(mesh, rootBone); + tweakSneakAnim(skeleton); + + Ogre::SceneNode *sn = NULL; + Ogre::Entity *ent = NULL; + Ogre::AnimationState *as = NULL; + + auto &controllerMgr = Ogre::ControllerManager::getSingleton(); + + // Create soundwave material, billboard set, and updater. + + createSoundwaveMaterial("soundwave", Ogre::RGN_DEFAULT); + + Ogre::BillboardSet *soundwaveBbs = + mSceneMgr->createBillboardSet(); + soundwaveBbs->setMaterialName("soundwave"); + soundwaveBbs->setBillboardType(Ogre::BBT_PERPENDICULAR_COMMON); + soundwaveBbs->setCommonDirection(Ogre::Vector3::UNIT_Y); + soundwaveBbs->setCommonUpVector(Ogre::Vector3::NEGATIVE_UNIT_Z); + mSceneMgr->getRootSceneNode() + ->createChildSceneNode() + ->attachObject(soundwaveBbs); + + std::shared_ptr soundwaveUpdater = + SoundwaveUpdater::create(soundwaveBbs); + controllerMgr.createFrameTimePassthroughController( + soundwaveUpdater); + + // Create models, animation updaters, and footfall listeners. + + for (int i = 0; i < NUM_MODELS; i++) { + // create scene nodes for the models at regular angular intervals + sn = mSceneMgr->getRootSceneNode() + ->createChildSceneNode(); + sn->yaw(Ogre::Radian(Ogre::Math::TWO_PI * (float)i / + (float)NUM_MODELS)); + sn->translate(0, 0, -20, Ogre::Node::TS_LOCAL); + mModelNodes.push_back(sn); + + // create and attach a jaiqua entity + ent = mSceneMgr->createEntity( + "Jaiqua" + + Ogre::StringConverter::toString(i + 1), + "jaiqua.mesh"); + ent->setMaterialName("jaiqua"); + mEntities.push_back(ent); + sn->attachObject(ent); + + mFootfallListeners.push_back( + std::make_unique( + ent, soundwaveUpdater.get())); + + // enable the entity's sneaking animation at a random speed and loop it manually since translation is involved + as = ent->getAnimationState("Sneak"); + as->setEnabled(true); + as->setLoop(true); + + auto updater = std::make_shared(as); + updater->setUseRootMotion(ent, sneakRootTrack); + updater->setUseTimeEvents(true); + TimeEventDispatcher *ted = + updater->getTimeEventDispatcher(); + ted->addEventList(&mSneakEvents); + ted->addListener(mFootfallListeners[i].get()); + + controllerMgr.createController( + controllerMgr.getFrameTimeSource(), updater, + Ogre::ScaleControllerFunction::create( + Ogre::Math::RangeRandom(0.5, 1.5))); + + mAnimUpdaters.push_back(updater.get()); + } + + insertFootfallEvents(mEntities[0], mSneakEvents); + generateBoundingBox(mEntities[0]); + + // create name and value for skinning mode + Ogre::StringVector names; + names.push_back("Help"); + names.push_back("Skinning"); + names.push_back(mBoneBoundingBoxesItemName); + + // create a params panel to display the help and skinning mode + Ogre::String value = "Software"; + enableBoneBoundingBoxMode(false); // update status panel entry + + // make sure we query the correct scheme + Ogre::MaterialManager::getSingleton().setActiveScheme( + mViewport->getMaterialScheme()); + + // change the value if hardware skinning is enabled + Ogre::MaterialPtr entityMaterial = + ent->getSubEntity(0)->getMaterial(); + if (entityMaterial) { + Ogre::Technique *bestTechnique = + entityMaterial->getBestTechnique(); + if (bestTechnique) { + Ogre::Pass *pass = bestTechnique->getPass(0); + if (pass && pass->hasVertexProgram() && + pass->getVertexProgram() + ->isSkeletalAnimationIncluded()) { + value = "Hardware"; + } + } + } + } + +public: + void cleanupContent() + { + mModelNodes.clear(); + mEntities.clear(); + mAnimUpdaters.clear(); + mFootfallListeners.clear(); + Ogre::MeshManager::getSingleton().remove("floor", + Ogre::RGN_DEFAULT); + Ogre::MaterialManager::getSingleton().remove("soundwave", + Ogre::RGN_DEFAULT); + Ogre::TextureManager::getSingleton().remove("soundwave", + Ogre::RGN_DEFAULT); + } + + const int NUM_MODELS; + int mBoundingBoxModelIndex; // which model to show the bounding box for + bool mBoneBoundingBoxes; + Ogre::String mBoneBoundingBoxesItemName; + + std::vector mModelNodes; + std::vector mEntities; + std::vector mAnimUpdaters; + std::vector > mFootfallListeners; + + TimeEventList mSneakEvents; +}; +int main() +{ + App ctx; + ctx.initApp(); + Ogre::Root *root = ctx.getRoot(); + Ogre::SceneManager *scnMgr = root->createSceneManager(); + Ogre::RTShader::ShaderGenerator *shadergen = + Ogre::RTShader::ShaderGenerator::getSingletonPtr(); + shadergen->addSceneManager(scnMgr); + + Ogre::Light *light = scnMgr->createLight("MainLight"); + Ogre::SceneNode *lightNode = + scnMgr->getRootSceneNode()->createChildSceneNode(); + lightNode->setPosition(0, 10, 15); + lightNode->attachObject(light); + + Ogre::SceneNode *camNode = + scnMgr->getRootSceneNode()->createChildSceneNode(); + camNode->setPosition(0, 0, 15); + camNode->lookAt(Ogre::Vector3(0, 0, -1), Ogre::Node::TS_PARENT); + + Ogre::Camera *cam = scnMgr->createCamera("myCam"); + cam->setNearClipDistance(5); + cam->setAutoAspectRatio(true); + camNode->attachObject(cam); + + ctx.getRenderWindow()->addViewport(cam); + +#if 0 + Ogre::Entity *ent = scnMgr->createEntity("Sinbad.mesh"); + Ogre::SceneNode *node = + scnMgr->getRootSceneNode()->createChildSceneNode(); + node->attachObject(ent); +#endif + KeyHandler keyHandler; + ctx.addInputListener(&keyHandler); + OgreBites::CameraMan *mCameraMan = new OgreBites::CameraMan(camNode); + + ctx.setupContent(cam->getViewport(), scnMgr, mCameraMan); + + ctx.getRoot()->startRendering(); + ctx.setWindowGrab(false); + ctx.cleanupContent(); + ctx.closeApp(); + return 0; +} diff --git a/src/features/characters/resources.cfg b/src/features/characters/resources.cfg new file mode 100644 index 0000000..e2b30cc --- /dev/null +++ b/src/features/characters/resources.cfg @@ -0,0 +1,82 @@ +# Ogre Core Resources +[OgreInternal] +#FileSystem=./Media/Main +FileSystem=resources/main +FileSystem=resources/shaderlib +#FileSystem=./Media/RTShaderLib +FileSystem=resources/terrain + +# Resources required by OgreBites::Trays +[Essential] +#Zip=./Media/packs/SdkTrays.zip +#Zip=./Media/packs/profiler.zip + +## this line will end up in the [Essential] group +#FileSystem=./Media/thumbnails + +# Common sample resources needed by many of the samples. +# Rarely used resources should be separately loaded by the +# samples which require them. +[General] +FileSystem=skybox +FileSystem=resources/buildings +FileSystem=resources/buildings/parts/pier +FileSystem=resources/buildings/parts/furniture +FileSystem=resources/vehicles +FileSystem=resources/debug +FileSystem=resources/fonts +# PBR media must come before the scripts that reference it +#FileSystem=./Media/PBR +#FileSystem=./Media/PBR/filament + +#FileSystem=./Media/materials/programs/GLSL +#FileSystem=./Media/materials/programs/GLSL120 +#FileSystem=./Media/materials/programs/GLSL150 +#FileSystem=./Media/materials/programs/GLSL400 +#FileSystem=./Media/materials/programs/GLSLES +#FileSystem=./Media/materials/programs/SPIRV +#FileSystem=./Media/materials/programs/Cg +#FileSystem=./Media/materials/programs/HLSL +#FileSystem=./Media/materials/programs/HLSL_Cg +#FileSystem=./Media/materials/scripts +#FileSystem=./Media/materials/textures +#FileSystem=./Media/materials/textures/terrain +#FileSystem=./Media/models +#FileSystem=./Media/particle +#FileSystem=./Media/DeferredShadingMedia +#FileSystem=./Media/DeferredShadingMedia/DeferredShading/post +#FileSystem=./Media/PCZAppMedia +#FileSystem=./Media/materials/scripts/SSAO +#FileSystem=./Media/materials/textures/SSAO +#FileSystem=./Media/volumeTerrain +#FileSystem=./Media/CSMShadows + +#Zip=./Media/packs/cubemap.zip +#Zip=./Media/packs/cubemapsJS.zip +#Zip=./Media/packs/dragon.zip +#Zip=./Media/packs/fresneldemo.zip +#Zip=./Media/packs/ogredance.zip +#Zip=./Media/packs/Sinbad.zip +#Zip=./Media/packs/skybox.zip +#Zip=./Media/volumeTerrain/volumeTerrainBig.zip + +#Zip=./Media/packs/DamagedHelmet.zip +#Zip=./Media/packs/filament_shaders.zip + +#[BSPWorld] +#Zip=./Media/packs/oa_rpg3dm2.pk3 +#Zip=./Media/packs/ogretestmap.zip + +# Materials for visual tests +#[Tests] +#FileSystem=/media/slapin/library/ogre/ogre-sdk/Tests/Media +[LuaScripts] +FileSystem=lua-scripts + +#[Characters] +#FileSystem=./characters +[Audio] +FileSystem=./audio/gui + +[Water] +FileSystem=water diff --git a/src/features/characters/resources/main/blue_jaiqua.jpg b/src/features/characters/resources/main/blue_jaiqua.jpg new file mode 100644 index 0000000..6016e14 Binary files /dev/null and b/src/features/characters/resources/main/blue_jaiqua.jpg differ diff --git a/src/features/characters/resources/main/jaiqua.material b/src/features/characters/resources/main/jaiqua.material new file mode 100644 index 0000000..20ec3cd --- /dev/null +++ b/src/features/characters/resources/main/jaiqua.material @@ -0,0 +1,24 @@ +material jaiqua +{ + technique + { + pass + { + texture_unit + { + texture blue_jaiqua.jpg + tex_address_mode clamp + } + + rtshader_system + { + // In case the system uses the RTSS, the following line will ensure + // that hardware animation is used. + // Alternatively, you can derive this information programatically via + // HardwareSkinningFactory::prepareEntityForSkinning + hardware_skinning 38 2 dual_quaternion true false + } + } + } + +} diff --git a/src/features/characters/resources/main/jaiqua.mesh b/src/features/characters/resources/main/jaiqua.mesh new file mode 100644 index 0000000..e1ccd34 Binary files /dev/null and b/src/features/characters/resources/main/jaiqua.mesh differ diff --git a/src/features/characters/resources/main/jaiqua.skeleton b/src/features/characters/resources/main/jaiqua.skeleton new file mode 100644 index 0000000..3d7622c Binary files /dev/null and b/src/features/characters/resources/main/jaiqua.skeleton differ