diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77a6a3d..26c04a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -48,3 +48,85 @@ jobs: - name: Build all applications and run regression tests run: ./scripts/build-all.sh "${RUNNER_TEMP}/of_v0.12.1_osx_release" + + + linux-render: + name: Linux Mesa rendering + runs-on: ubuntu-24.04 + timeout-minutes: 60 + # A clean Jammy package set avoids dependency conflicts with SDKs preinstalled on hosted runners. + container: + image: ubuntu:22.04@sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc + defaults: + run: + shell: bash + + env: + OF_URL: https://github.com/openframeworks/openFrameworks/releases/download/0.12.1/of_v0.12.1_linux64_gcc6_release.tar.gz + OF_SHA256: d6c1dcab777665b2aa63e5e3d9122cc116f096b3421db3493f795a621b399c63 + LIBGL_ALWAYS_SOFTWARE: "1" + GALLIUM_DRIVER: llvmpipe + DEBIAN_FRONTEND: noninteractive + TZ: Etc/UTC + + steps: + - name: Bootstrap clean Ubuntu container + run: | + apt-get update + apt-get install --yes --no-install-recommends \ + ca-certificates curl git sudo zstd lsb-release pkg-config + mkdir -p "${RUNNER_TEMP}/example-captures" + apt-cache policy libunwind-dev libunwind8 libgstreamer1.0-dev \ + | tee "${RUNNER_TEMP}/example-captures/dependency-policy.log" + # Install the GStreamer prerequisite explicitly; fail here if package resolution breaks. + apt-get install --yes --no-install-recommends libunwind-dev + + - name: Check out repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Cache openFrameworks 0.12.1 + id: cache-openframeworks + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ runner.temp }}/of_v0.12.1_linux64_gcc6_release + key: openframeworks-0.12.1-jammy-container-2edbbc5dc405-${{ runner.arch }}-d6c1dcab7776-v1 + + - name: Download verified openFrameworks release + if: steps.cache-openframeworks.outputs.cache-hit != 'true' + shell: bash + run: | + OF_ARCHIVE="${RUNNER_TEMP}/of_v0.12.1_linux64_gcc6_release.tar.gz" + curl --fail --location --retry 3 --output "$OF_ARCHIVE" "$OF_URL" + echo "$OF_SHA256 $OF_ARCHIVE" | sha256sum --check + tar --extract --gzip --file "$OF_ARCHIVE" --directory "${RUNNER_TEMP}" + + - name: Install official Ubuntu dependencies and software OpenGL + shell: bash + run: | + # This installer comes from the checksum-pinned release and supports Ubuntu 22.04. + sudo env DEBIAN_FRONTEND=noninteractive bash \ + "${RUNNER_TEMP}/of_v0.12.1_linux64_gcc6_release/scripts/linux/ubuntu/install_dependencies.sh" -y + sudo apt-get install --yes --no-install-recommends xvfb xauth mesa-utils libgl1-mesa-dri + + - name: Build all applications and run regression tests + run: ./scripts/build-all.sh "${RUNNER_TEMP}/of_v0.12.1_linux64_gcc6_release" + + - name: Render all examples with Mesa OpenGL + shell: bash + run: | + mkdir -p "${RUNNER_TEMP}/example-captures" + xvfb-run --auto-servernum --server-args="-screen 0 1600x1000x24 +extension GLX -nolisten tcp" \ + bash -euo pipefail -c ' + glxinfo -B | tee "${RUNNER_TEMP}/example-captures/opengl-context.log" + grep -qi llvmpipe "${RUNNER_TEMP}/example-captures/opengl-context.log" + ./scripts/smoke-all.sh "${RUNNER_TEMP}/example-captures" + ' + + - name: Keep rendering evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: openframeworks-rendering + path: ${{ runner.temp }}/example-captures + if-no-files-found: error + retention-days: 7 diff --git a/LICENSES.md b/LICENSES.md index 30cf57a..df4e6c1 100644 --- a/LICENSES.md +++ b/LICENSES.md @@ -14,6 +14,9 @@ single combined work. | `motion-visualization` | [MIT](motion-visualization/LICENSE) | Copyright (c) 2012 Atsushi Tadokoro | | `marching-cubes` | [MIT](marching-cubes/LICENSE) | Copyright (c) 2012 Atsushi Tadokoro | | `ofxBvh` | [MIT](ofxBvh/LICENSE) | Copyright (c) 2012 Daito Manabe; originally published under the Perfume Dev Team name | +| `example-motion-ribbons` | [MIT](example-motion-ribbons/LICENSE) | Copyright (c) 2026 Daito Manabe | +| `example-motion-field` | [MIT](example-motion-field/LICENSE) | Copyright (c) 2026 Daito Manabe | +| `shared` | [MIT](shared/LICENSE) | Copyright (c) 2026 Daito Manabe | | `scripts` | [MIT](scripts/LICENSE) | Copyright (c) 2026 Daito Manabe | | `tests/ofxBvh-boundary` | [MIT](tests/ofxBvh-boundary/LICENSE) | Copyright (c) 2026 Daito Manabe | | `third_party/ofxMarchingCubes` | [LGPL-2.1-or-later](third_party/ofxMarchingCubes/LICENSE.txt) | Copyright (c) 2009 Rui Madeira; adapted by Greg Borenstein | diff --git a/README.md b/README.md index 7f93fa0..2bca991 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,31 @@ [![Build openFrameworks 0.12.1](https://github.com/perfume-dev/example-openFrameworks/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/perfume-dev/example-openFrameworks/actions/workflows/build.yml) -This repository contains openFrameworks examples created around the 2012 -Perfume “Global Site Project.” It is maintained for -[openFrameworks 0.12.1](https://openframeworks.cc/download/), the current -stable release. The original visual character and data formats are preserved -while the application lifecycle, APIs, project files, dependency handling, and -BVH playback safety have been updated. +Motion-capture studies for [openFrameworks 0.12.1](https://openframeworks.cc/download/). +The six examples from the 2012 Perfume “Global Site Project” now use the +programmable OpenGL 3.2 renderer, `ofApp`, member-owned state, and mesh-based +drawing. Two new shader studies turn the bundled motion into luminous ribbons +and animated contour fields. + +## Start with the shader studies + +| Example | What you see | What you learn | +| --- | --- | --- | +| [Motion Ribbons](example-motion-ribbons) | Fine luminous trails following the dancers | Vertex, geometry, and fragment shaders; expanding a line into a ribbon | +| [Motion Field](example-motion-field) | Contours and halos around moving joints | Passing motion to a full-screen fragment shader | + +![Motion Ribbons rendered with its vertex, geometry, and fragment shaders](docs/images/motion-ribbons.png) + +![Motion Field rendered with its full-screen fragment shader](docs/images/motion-field.png) + +These are actual application captures, not concept images. The studies recenter +horizontal root travel to keep the three dancers legible; they show the recorded +poses rather than reconstructing the original stage formation. + +Both use the existing bundled BVH samples and need no external addons or audio +downloads. The [shader guide](docs/shaders.md) explains the rendering stages and +where to start editing. Companion studies are available in the +[Processing repository](https://github.com/perfume-dev/example-processing). ## Stewardship @@ -44,6 +63,8 @@ notices. | `particle-motion-example` | Particle forces driven by tracked joints | | `motion-visualization` | Long-exposure-style joint trajectories | | `marching-cubes` | A metaball surface driven by skeletal endpoints | +| `example-motion-ribbons` | Geometry-shader ribbons following the bundled motion | +| `example-motion-field` | A joint-driven fragment-shader contour field | | `ofxBvh` | The bundled BVH loader and player used by every example | ## Build with openFrameworks 0.12.1 @@ -60,7 +81,7 @@ cd "$OF_ROOT/apps/myApps/example-openFrameworks" ./scripts/build-all.sh "$OF_ROOT" ``` -The script builds all six applications in Release mode and runs the `ofxBvh` +The script builds all eight applications in Release mode and runs the `ofxBvh` boundary/malformed-input and marching-cubes lifecycle regression tests. An individual application can be built with, for example: @@ -69,6 +90,23 @@ make -C example-bvh Release OF_ROOT="$OF_ROOT" open example-bvh/bin/example-bvh.app ``` +To render every example in a hidden test window and save PNG evidence: + +```sh +./scripts/smoke-all.sh /path/to/captures +``` + +The GitHub workflow builds and runs regression tests on macOS, then independently +builds and renders all eight examples in a clean, digest-pinned Ubuntu 22.04 +container with Mesa llvmpipe and Xvfb. The container isolates OF dependencies +from the hosted runner's preinstalled compiler SDKs. +This supplies a real software OpenGL context where hosted macOS runners cannot +provide the required pixel format. The `openframeworks-rendering` artifact contains +screenshots, application logs and the OpenGL context report. Render failures fail +CI; the shader studies are captured at two motion timestamps to check that their +output changes. Local capture also requires a desktop OpenGL context, even though +the test windows stay hidden. The build/capture scripts support macOS and Linux. + The checked-in Xcode projects were regenerated from the openFrameworks 0.12.1 macOS template. They use relative paths and expect the repository layout shown above. If you put the repository elsewhere, use the Makefiles with an explicit @@ -77,7 +115,8 @@ above. If you put the repository elsewhere, use the Makefiles with an explicit ## Motion and audio data `example-bvh` is ready to run with its bundled `A_test.bvh`, `B_test.bvh`, and -`C_test.bvh` files. The other five applications first look for the historical +`C_test.bvh` files. The two new shader studies read those same files. The other +five historical applications first look for the original Perfume Global Site assets in each application's `bin/data` directory: ```text @@ -106,3 +145,8 @@ the two examples by Atsushi Tadokoro retain their original copyright and MIT license notices. See [LICENSES.md](LICENSES.md) for the component-by-component license index; the repository does not apply one blanket license to every directory. + +The maintained example code uses GLM vectors and mesh drawing. `ofxBvh` retains +its older public vector/matrix types so existing addon consumers continue to +compile; its example rendering supports the programmable renderer. The vendored +marching-cubes dependency retains its upstream public interface and attribution. diff --git a/docs/images/motion-field.png b/docs/images/motion-field.png new file mode 100644 index 0000000..e633278 Binary files /dev/null and b/docs/images/motion-field.png differ diff --git a/docs/images/motion-ribbons.png b/docs/images/motion-ribbons.png new file mode 100644 index 0000000..aba01f6 Binary files /dev/null and b/docs/images/motion-ribbons.png differ diff --git a/docs/shaders.md b/docs/shaders.md new file mode 100644 index 0000000..f678f81 --- /dev/null +++ b/docs/shaders.md @@ -0,0 +1,52 @@ +# Motion as a shader input + +The new studies share a small idea: read motion on the CPU, send a compact set +of positions to the GPU, and let the shader decide how that motion looks. +The BVH data remains the source of the animation. + +## Motion Ribbons + +Open [`example-motion-ribbons`](../example-motion-ribbons) first if you want to +learn the geometry stage. + +1. The app samples joint positions over time and builds line segments. +2. The vertex shader transforms each position. +3. The geometry shader expands a segment into a narrow, camera-facing strip. +4. The fragment shader shapes its soft edge and luminous colour. + +This keeps the C++ topology simple. Changing ribbon width or edge falloff does +not require a new BVH parser or a dense CPU-generated tube. Read the example's +README for its controls and shader filenames. + +## Motion Field + +[`example-motion-field`](../example-motion-field) starts with a full-screen +rectangle. The app projects selected joints and sends their positions to the +fragment shader. Each pixel evaluates its relationship to those moving sources, +producing contour lines and overlapping halos. + +Start with the contour spacing, line softness, and colour constants. Keep +expensive work bounded: adding more source joints increases the work for every +pixel, while increasing the window size increases the number of pixels. + +## openFrameworks and Processing + +| Stage | openFrameworks studies | Processing studies | +| --- | --- | --- | +| Motion sampling | C++ and the bundled `ofxBvh` | Java/PDE and the bundled BVH parser | +| Ribbon topology | Geometry shader expands line segments | The sketch creates a triangle strip | +| Vertex shading | GLSL vertex shader | `PShader` vertex shader | +| Colour and soft edges | GLSL fragment shader | `PShader` fragment shader | +| Contour field | Full-screen fragment shader | Full-screen `PShader` | + +Processing's standard `PShader` API exposes vertex and fragment shaders. Its +ribbon study therefore generates the strip in the sketch; it does not require +a custom OpenGL wrapper for a geometry stage. The openFrameworks study uses a +desktop OpenGL 3.2 context and GLSL 150, including a real geometry shader. + +These examples target the stable releases checked for this update: +[openFrameworks 0.12.1](https://openframeworks.cc/download/) and +[Processing 4.5.6](https://github.com/processing/processing4/releases/tag/processing-1434-4.5.6). + +References: [ofShader](https://openframeworks.cc/documentation/gl/ofShader/) and +[Processing PShader](https://processing.org/reference/PShader). diff --git a/example-bvh/example-bvh.xcodeproj/project.pbxproj b/example-bvh/example-bvh.xcodeproj/project.pbxproj index 359d1bf..c71a407 100644 --- a/example-bvh/example-bvh.xcodeproj/project.pbxproj +++ b/example-bvh/example-bvh.xcodeproj/project.pbxproj @@ -40,14 +40,14 @@ "fileEncoding": "4", "isa": "PBXFileReference", "lastKnownFileType": "sourcecode.cpp.h", - "name": "testApp.h", + "name": "ofApp.h", "sourceTree": "" }, "4A8C997B-B3CE-4C44-BC3A-FA84012AB644": { "fileEncoding": "4", "isa": "PBXFileReference", "lastKnownFileType": "sourcecode.cpp.cpp", - "name": "testApp.cpp", + "name": "ofApp.cpp", "sourceTree": "" }, "524986E5-48D9-40CF-A1DE-1AD77A70D460": { diff --git a/example-bvh/src/main.cpp b/example-bvh/src/main.cpp index a79738a..f7fd7c7 100644 --- a/example-bvh/src/main.cpp +++ b/example-bvh/src/main.cpp @@ -1,14 +1,5 @@ -#include "ofMain.h" -#include "testApp.h" +#include "ofApp.h" -//======================================================================== -int main() { - ofGLWindowSettings settings; - settings.setSize(1024, 768); - settings.setGLVersion(2, 1); - settings.windowMode = OF_WINDOW; - - auto window = ofCreateWindow(settings); - ofRunApp(window, std::make_shared()); - ofRunMainLoop(); +int main(int argc, char** argv) { + return example::launch(argc, argv, "example-bvh"); } diff --git a/example-bvh/src/ofApp.cpp b/example-bvh/src/ofApp.cpp new file mode 100644 index 0000000..8262b7e --- /dev/null +++ b/example-bvh/src/ofApp.cpp @@ -0,0 +1,27 @@ +#include "ofApp.h" + +void ofApp::setup() { + runtime.setup(); + ofBackground(0); + playback.load("example-bvh", true, runtime.isSmokeTest()); + camera.setTarget(glm::vec3(0, 80, 0)); + camera.setDistance(450); +} + +void ofApp::update() { + playback.update(runtime.deltaSeconds()); +} + +void ofApp::draw() { + ofEnableDepthTest(); + ofEnableBlendMode(OF_BLENDMODE_ALPHA); + camera.begin(); + for (auto& motion : playback.motions) motion.draw(); + camera.end(); + ofDisableDepthTest(); + if (!playback.ready) { + ofSetColor(255); + ofDrawBitmapString("Unable to load the bundled BVH files.", 20, 30); + } + runtime.finishFrame(playback.ready, "example-bvh", playback.motions.front().getNumJoints() * 48); +} diff --git a/example-bvh/src/ofApp.h b/example-bvh/src/ofApp.h new file mode 100644 index 0000000..1b1ce16 --- /dev/null +++ b/example-bvh/src/ofApp.h @@ -0,0 +1,16 @@ +#pragma once + +#include "../../shared/ExampleRuntime.h" + +class ofApp : public ofBaseApp { +public: + explicit ofApp(const example::RunOptions& options) : runtime(options) {} + void setup() override; + void update() override; + void draw() override; + +private: + example::Runtime runtime; + example::MotionPlayback playback; + ofEasyCam camera; +}; diff --git a/example-bvh/src/testApp.cpp b/example-bvh/src/testApp.cpp deleted file mode 100644 index ab619a6..0000000 --- a/example-bvh/src/testApp.cpp +++ /dev/null @@ -1,93 +0,0 @@ -#include "testApp.h" - -//-------------------------------------------------------------- -void testApp::setup(){ - ofSetFrameRate(60); - ofSetVerticalSync(true); - - ofBackground(0); - - // setup bvh - assetsReady = bvh[0].load("A_test.bvh"); - assetsReady = bvh[1].load("B_test.bvh") && assetsReady; - assetsReady = bvh[2].load("C_test.bvh") && assetsReady; - - for (auto& motion : bvh) - { - motion.play(); - motion.setLoop(true); - } -} - -//-------------------------------------------------------------- -void testApp::update() -{ - for (auto& motion : bvh) - { - motion.update(); - } -} - -//-------------------------------------------------------------- -void testApp::draw(){ - ofEnableDepthTest(); - - ofEnableBlendMode(OF_BLENDMODE_ALPHA); - - cam.begin(); - - for (auto& motion : bvh) - { - motion.draw(); - } - - cam.end(); - - ofDisableDepthTest(); - if (!assetsReady) - { - ofSetColor(255); - ofDrawBitmapString("Unable to load the bundled BVH files.", 20, 30); - } - -} - -//-------------------------------------------------------------- -void testApp::keyPressed(int key){ -} - -//-------------------------------------------------------------- -void testApp::keyReleased(int key){ -} - -//-------------------------------------------------------------- -void testApp::mouseMoved(int x, int y ){ - -} - -//-------------------------------------------------------------- -void testApp::mouseDragged(int x, int y, int button){ - -} - -//-------------------------------------------------------------- -void testApp::mousePressed(int x, int y, int button){ - -} - -//-------------------------------------------------------------- -void testApp::mouseReleased(int x, int y, int button){ -} - -//-------------------------------------------------------------- -void testApp::windowResized(int w, int h){ -} - -//-------------------------------------------------------------- -void testApp::gotMessage(ofMessage msg){ - -} - -//-------------------------------------------------------------- -void testApp::dragEvent(ofDragInfo dragInfo){ -} diff --git a/example-bvh/src/testApp.h b/example-bvh/src/testApp.h deleted file mode 100644 index 90066df..0000000 --- a/example-bvh/src/testApp.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include "ofMain.h" -#include "ofxBvh.h" - -#include - -class testApp : public ofBaseApp { - - public: - void setup() override; - void update() override; - void draw() override; - - void keyPressed(int key) override; - void keyReleased(int key) override; - void mouseMoved(int x, int y) override; - void mouseDragged(int x, int y, int button) override; - void mousePressed(int x, int y, int button) override; - void mouseReleased(int x, int y, int button) override; - void windowResized(int w, int h) override; - void dragEvent(ofDragInfo dragInfo) override; - void gotMessage(ofMessage msg) override; - - std::array bvh; - ofEasyCam cam; - bool assetsReady = false; -}; diff --git a/example-motion-field/.gitignore b/example-motion-field/.gitignore new file mode 100644 index 0000000..51c5894 --- /dev/null +++ b/example-motion-field/.gitignore @@ -0,0 +1,6 @@ +*.app +build/ +DerivedData/ +xcuserdata/ +obj/ +.DS_Store diff --git a/example-motion-field/LICENSE b/example-motion-field/LICENSE new file mode 100644 index 0000000..3d05ecf --- /dev/null +++ b/example-motion-field/LICENSE @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2026 Daito Manabe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +This license covers the new code and shaders in this directory. Referenced +2012 BVH assets retain their original terms; this file does not relicense them. diff --git a/example-motion-field/Makefile b/example-motion-field/Makefile new file mode 100644 index 0000000..ab71e66 --- /dev/null +++ b/example-motion-field/Makefile @@ -0,0 +1,13 @@ +# Attempt to load a config.make file. +# If none is found, project defaults in config.project.make will be used. +ifneq ($(wildcard config.make),) + include config.make +endif + +# make sure the the OF_ROOT location is defined +ifndef OF_ROOT + OF_ROOT=../../../.. +endif + +# call the project makefile! +include $(OF_ROOT)/libs/openFrameworksCompiled/project/makefileCommon/compile.project.mk diff --git a/example-motion-field/Project.xcconfig b/example-motion-field/Project.xcconfig new file mode 100644 index 0000000..7c338e1 --- /dev/null +++ b/example-motion-field/Project.xcconfig @@ -0,0 +1,88 @@ +//THE PATH TO THE ROOT OF OUR OF PATH RELATIVE TO THIS PROJECT. +//THIS NEEDS TO BE DEFINED BEFORE CoreOF.xcconfig IS INCLUDED +OF_PATH = ../../../.. + +//THIS HAS ALL THE HEADER AND LIBS FOR OF CORE +#include "../../../../libs/openFrameworksCompiled/project/osx/CoreOF.xcconfig" + +//UNCOMMENT BELOW TO ENABLE C++ 17 and std::filesystem +CLANG_CXX_LANGUAGE_STANDARD = c++23 +CLANG_C_LANGUAGE_STANDARD = c17 +MACOSX_DEPLOYMENT_TARGET = 11.5 + +// App Settings +PRODUCT_NAME = $(TARGET_NAME) +PRODUCT_NAME[config=Debug] = $(TARGET_NAME)Debug +PRODUCT_BUNDLE_IDENTIFIER = cc.openFrameworks.${TARGET_NAME:rfc1034identifier} +//PRODUCT_BUNDLE_IDENTIFIER[config=Debug] = cc.openFrameworks.$(TARGET_NAME)Debug +DEVELOPMENT_LANGUAGE = English +CODE_SIGN_IDENTITY = - +INFOPLIST_FILE = openFrameworks-Info.plist +GENERATE_INFOPLIST_FILE = YES + +// set application category to games, this is required to enable Game mode +// note: this sets the initial value in the Xcode UI +INFOPLIST_KEY_LSApplicationCategoryType = public.app-category.games + +// VERSIONING - overridden if changed in Xcode UI + +// this is "Version" in the Xcode target Identity UI +// suggested to use semantic versioning format ala #.#.# +MARKETING_VERSION = 0.1.0 + +// If users upgrade project to recommended settings it enables script sandboxing which breaks our post build script +ENABLE_USER_SCRIPT_SANDBOXING=NO + +// this is "Build" in the Xcode target Identity UI, an incremental build number +// important for the App Store as new build submissions need a diff number even +// if MARKETING_VERSION is the same +CURRENT_PROJECT_VERSION = 1 + +// ICONS + +// default oF app icon +ICON_NAME = of.icns +ICON_NAME[config=Debug] = of_debug.icns +ICON_FILE = $(OF_PATH)/libs/openFrameworksCompiled/project/osx/$(ICON_NAME) + +// custom app icon, placed in main project folder +//ICON_NAME = MyApp.icns +//ICON_FILE = $(ICON_NAME) + +// custom app icon with separate Release and Debug versions placed in bin/data +//ICON_NAME = icon.icns +//ICON_NAME[config=Debug] = icon-debug.icns +//ICON_FILE = bin/data/$(ICON_NAME) + +// note: oF 0.7.2 - 0.11 used ICON_FILE_PATH which is no longer used in oF 0.12+ +// ex. change ICON_FILE_PATH = bin/data/ -> ICON_FILE = bin/data/$(ICON_NAME) & +// in a custom openFrameworks-Info.plist, set CFBundleIconFile to ICON_NAME + +//APPSTORE, uncomment next lines to bundle data folder and code sign +//OF_CODESIGN = 1 +//OF_BUNDLE_DATA_FOLDER = 1 +//OF_BUNDLE_DYLIBS = 1 + +HIGH_RESOLUTION_CAPABLE = NO + +// Optional include to keep any permanent settings as CODE_SIGN_IDENTITY. +#include? "App.xcconfig" + +//FOR AV ENGINE SOUND PLAYER UNCOMMENT THREE LINES BELOW +OF_NO_FMOD=1 +USER_PREPROCESSOR_DEFINITIONS="OF_NO_FMOD=1" +LIB_FMOD="" +GCC_PREPROCESSOR_DEFINITIONS=$(inherited) $(USER_PREPROCESSOR_DEFINITIONS) + +OTHER_CFLAGS = $(OF_CORE_CFLAGS) +OTHER_LDFLAGS = $(OF_CORE_LIBS) $(OF_CORE_FRAMEWORKS) +HEADER_SEARCH_PATHS = $(OF_CORE_HEADERS) + +//THIS MAKES SURE THE APP BUILDS INSIDE THE BIN FOLDER +//If you comment this line out the app will be run from DerrivedData and your data/ files won't be accessible unless you uncomment OF_BUNDLE_DATA_FOLDER = 1 above +CONFIGURATION_BUILD_DIR = ${SRCROOT}/bin + +//OF_CORE_BUILD_COMMAND = echo \"💾 Compiling openFrameworks\"\nxcodebuild -project \"$OF_PATH/libs/openFrameworksCompiled/project/osx/openFrameworksLib.xcodeproj\" -target openFrameworks -configuration \"${CONFIGURATION}\" CLANG_CXX_LANGUAGE_STANDARD=$CLANG_CXX_LANGUAGE_STANDARD MACOSX_DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET GCC_PREPROCESSOR_DEFINITIONS='$USER_PREPROCESSOR_DEFINITIONS' + + +OF_CORE_BUILD_COMMAND = xcodebuild -project $OF_PATH/libs/openFrameworksCompiled/project/osx/openFrameworksLib.xcodeproj -target openFrameworks -configuration ${CONFIGURATION} CLANG_CXX_LANGUAGE_STANDARD=$CLANG_CXX_LANGUAGE_STANDARD MACOSX_DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET GCC_PREPROCESSOR_DEFINITIONS=$USER_PREPROCESSOR_DEFINITIONS diff --git a/example-motion-field/README.md b/example-motion-field/README.md new file mode 100644 index 0000000..ea8bd3f --- /dev/null +++ b/example-motion-field/README.md @@ -0,0 +1,58 @@ +# Motion Field + +Three dancing fields, drawn from sparse BVH landmarks with a single full-screen +rectangle. Cyan, coral and ivory isocontours merge and separate as hands, feet +and torso move. A subdued coordinate lattice and generous negative space keep +the result simple. + +Requires **openFrameworks 0.12.1**, desktop **OpenGL 3.2** and this entire +repository. The original 2012 A/B/C motion samples are shared rather than copied. + +## Run + +Place the repository at `openFrameworks/apps/myApps/example-openFrameworks`, +open `example-motion-field.xcodeproj` and select the Release scheme, or: + +```sh +make Release OF_ROOT=/path/to/openFrameworks +make RunRelease OF_ROOT=/path/to/openFrameworks +``` + +Keep `../example-bvh/bin/data/{A,B,C}_test.bvh` in place. + +## How the shader works + +The CPU uploads 13 projected joint positions per dancer as `uJoints[39]`. +Each dancer is horizontally recentered. Positions use canvas-height units, so +radial kernels do not stretch with aspect +ratio. `field.vert` draws one rectangle; `field.frag` sums smooth radial kernels, +extracts equal-potential contours and overlays tiny landmark dots. + +- `uResolution`: framebuffer width and height. +- `uJoints`: sparse positions, ordered dancer A then B then C. +- `uContourDensity`: count of contours per unit of field potential. +- `uTime`: the same playback clock as the BVH pose; pause freezes all animation. + +`fwidth` gives the contour edges pixel-aware antialiasing. The field has a +fixed-size uniform array and no texture, postprocess chain or additional addon. + +This is a pose study, not a reconstruction of the original stage formation. +Playback starts at 12 seconds, where the sample movement is already active. + +## Controls and capture + +- Space: pause/resume. +- R: rewind to the start of the study. +- Up/down arrows: increase/decrease contour density (3–16). +- H: toggle the bottom control legend. + +Use `--smoke-test` for 180 deterministic frames in a hidden window. +`--capture=/absolute/path/frame.png` saves the actual rendered FBO and exits; +`--capture-frame=420` selects a later pose. Both options can be combined. +The program prints `SMOKE_TEST_OK` only after shader/GL, visible-pixel and +joint-motion checks succeed. Automated runs never show or activate a window. +Interactive drawing and capture use the same 1440×900 FBO; other window shapes +are letterboxed to preserve the composition. + +The new source and shaders use [MIT](LICENSE). Shared motion assets and the +ofxBvh dependency retain their existing terms; see the repository license index. diff --git a/example-motion-field/addons.make b/example-motion-field/addons.make new file mode 100644 index 0000000..bec539d --- /dev/null +++ b/example-motion-field/addons.make @@ -0,0 +1 @@ +../ofxBvh diff --git a/example-motion-field/bin/data/.gitkeep b/example-motion-field/bin/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/example-motion-field/bin/data/shaders/field.frag b/example-motion-field/bin/data/shaders/field.frag new file mode 100644 index 0000000..c98e47c --- /dev/null +++ b/example-motion-field/bin/data/shaders/field.frag @@ -0,0 +1,43 @@ +#version 150 +// All coordinates are measured in canvas heights, so circles remain round. +uniform vec2 uResolution; +uniform vec2 uJoints[39]; // 13 sparse BVH landmarks for each of A / B / C. +uniform float uTime; +uniform float uContourDensity; +out vec4 fragColor; + +float contour(float value, float density) { + float phase = value * density; + float distanceToLine = abs(fract(phase - 0.5) - 0.5); + float antialias = max(fwidth(phase), 0.004); + return 1.0 - smoothstep(0.012, 0.012 + antialias, distanceToLine); +} + +void main() { + vec2 p = (gl_FragCoord.xy - 0.5 * uResolution) / uResolution.y; + vec3 color = vec3(0.024, 0.034, 0.055); + vec3 palette[3] = vec3[3](vec3(0.30, 0.92, 0.94), vec3(1.0, 0.37, 0.30), vec3(0.93, 0.91, 0.79)); + // The field is a sum of smooth radial kernels. Contours expose equal-potential curves. + for (int dancer = 0; dancer < 3; ++dancer) { + float field = 0.0; + float dots = 0.0; + for (int joint = 0; joint < 13; ++joint) { + vec2 delta = p - uJoints[dancer * 13 + joint]; + float radius = length(delta); + field += 0.23 * exp(-dot(delta, delta) / 0.0045); + dots += 1.0 - smoothstep(0.0012, 0.0028, radius); + } + float mask = smoothstep(0.035, 0.10, field); + float rings = contour(field, uContourDensity) * mask; + // A slow moving phase only modulates light; pause freezes this and the pose together. + float light = 0.86 + 0.14 * sin(uTime * 0.6 + float(dancer)); + color += palette[dancer] * (rings * 0.75 + min(field, 1.0) * 0.065) * light; + color += vec3(0.75, 0.82, 0.86) * min(dots, 1.0) * 0.55; + } + // Restrained coordinate lattice, with a broad vignette leaving generous negative space. + vec2 cell = abs(fract(p * 18.0 + 0.5) - 0.5); + float grid = 1.0 - smoothstep(0.008, 0.025, min(cell.x, cell.y)); + color += vec3(0.022, 0.028, 0.04) * grid; + color *= 1.0 - 0.28 * smoothstep(0.40, 0.95, length(p)); + fragColor = vec4(color, 1.0); +} diff --git a/example-motion-field/bin/data/shaders/field.vert b/example-motion-field/bin/data/shaders/field.vert new file mode 100644 index 0000000..0592aa8 --- /dev/null +++ b/example-motion-field/bin/data/shaders/field.vert @@ -0,0 +1,6 @@ +#version 150 +uniform mat4 modelViewProjectionMatrix; +in vec4 position; +void main() { + gl_Position = modelViewProjectionMatrix * position; +} diff --git a/example-motion-field/config.make b/example-motion-field/config.make new file mode 100644 index 0000000..3ef32f8 --- /dev/null +++ b/example-motion-field/config.make @@ -0,0 +1,2 @@ +# Place the repository at apps/myApps/example-openFrameworks, or override OF_ROOT. +OF_ROOT ?= ../../../.. diff --git a/example-motion-field/example-motion-field.xcodeproj/project.pbxproj b/example-motion-field/example-motion-field.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f1f98fc --- /dev/null +++ b/example-motion-field/example-motion-field.xcodeproj/project.pbxproj @@ -0,0 +1,360 @@ +{ + "_OFProjectGeneratorVersion": "0.103.0", + "archiveVersion": "1", + "classes": {}, + "objectVersion": "54", + "objects": { + "0672BD73-386E-4AA2-8522-1AE1EBCE268D": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "sourcecode.cpp.cpp", + "name": "ofxBvh.cpp", + "path": "../ofxBvh/src/ofxBvh.cpp", + "sourceTree": "SOURCE_ROOT" + }, + "08231D78-261E-4827-910A-0EF646EF3F4B": { + "children": [ + "B2FD32D8-4A28-48C5-91DE-310A908F1570" + ], + "isa": "PBXGroup", + "name": "ofxBvh", + "path": "../ofxBvh", + "sourceTree": "SOURCE_ROOT" + }, + "0E00E657-BCEF-4C35-A827-29575E2542A0": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "folder", + "name": "data", + "path": "bin/data", + "sourceTree": "SOURCE_ROOT" + }, + "191CD6FA2847E21E0085CBB6": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "text.plist.entitlements", + "path": "of.entitlements", + "sourceTree": "" + }, + "191EF70929D778A400F35F26": { + "isa": "PBXFileReference", + "lastKnownFileType": "folder", + "name": "openFrameworks", + "path": "../../../../libs/openFrameworks", + "sourceTree": "" + }, + "19B789C429E5AB4A0082E9B8": { + "alwaysOutOfDate": "1", + "buildActionMask": "2147483647", + "files": [], + "inputFileListPaths": [], + "inputPaths": [], + "isa": "PBXShellScriptBuildPhase", + "outputFileListPaths": [], + "outputPaths": [], + "runOnlyForDeploymentPostprocessing": "0", + "shellPath": "/bin/sh", + "shellScript": "\"$OF_PATH/scripts/osx/xcode_project.sh\"\n", + "showEnvVarsInLog": "0" + }, + "5D41C62D-77C5-426B-B174-981F8628A2EF": { + "children": [ + "08231D78-261E-4827-910A-0EF646EF3F4B" + ], + "isa": "PBXGroup", + "name": "local_addons", + "path": "", + "sourceTree": "SOURCE_ROOT" + }, + "A002B59D-9D01-4505-9A8C-520FEEFC4152": { + "fileRef": "0672BD73-386E-4AA2-8522-1AE1EBCE268D", + "isa": "PBXBuildFile" + }, + "B2FD32D8-4A28-48C5-91DE-310A908F1570": { + "children": [ + "0672BD73-386E-4AA2-8522-1AE1EBCE268D", + "FB36F179-2574-4CF3-A778-FF0E45F0FC51" + ], + "isa": "PBXGroup", + "name": "src", + "path": "src", + "sourceTree": "" + }, + "BB4B014C10F69532006C3DED": { + "children": [], + "isa": "PBXGroup", + "name": "addons", + "path": "../../../../addons", + "sourceTree": "" + }, + "E42962A92163ECCD00A6A9E2": { + "alwaysOutOfDate": "1", + "buildActionMask": "2147483647", + "files": [], + "inputPaths": [], + "isa": "PBXShellScriptBuildPhase", + "name": "Run Script — Compile OF", + "outputPaths": [], + "runOnlyForDeploymentPostprocessing": "0", + "shellPath": "/bin/sh", + "shellScript": "$OF_CORE_BUILD_COMMAND\n", + "showEnvVarsInLog": "0" + }, + "E4A5B60F29BAAAE400C2D356": { + "buildActionMask": "2147483647", + "dstPath": "", + "dstSubfolderSpec": "6", + "files": [], + "isa": "PBXCopyFilesBuildPhase", + "runOnlyForDeploymentPostprocessing": "0" + }, + "E4B69B4A0A3A1720003C02F2": { + "children": [ + "191CD6FA2847E21E0085CBB6", + "E4B6FCAD0C3E899E008CF71C", + "E4EB6923138AFD0F00A09F29", + "E4B69E1C0A3A1BDC003C02F2", + "191EF70929D778A400F35F26", + "BB4B014C10F69532006C3DED", + "E4B69B5B0A3A1756003C02F2", + "5D41C62D-77C5-426B-B174-981F8628A2EF", + "0E00E657-BCEF-4C35-A827-29575E2542A0" + ], + "isa": "PBXGroup", + "sourceTree": "" + }, + "E4B69B4C0A3A1720003C02F2": { + "attributes": { + "BuildIndependentTargetsInParallel": "YES", + "LastUpgradeCheck": "1540" + }, + "buildConfigurationList": "E4B69B4D0A3A1720003C02F2", + "compatibilityVersion": "Xcode 3.2", + "developmentRegion": "en", + "hasScannedForEncodings": "0", + "isa": "PBXProject", + "knownRegions": [ + "en", + "Base" + ], + "mainGroup": "E4B69B4A0A3A1720003C02F2", + "productRefGroup": "E4B69B4A0A3A1720003C02F2", + "projectDirPath": "", + "projectRoot": "", + "targets": [ + "E4B69B5A0A3A1756003C02F2" + ] + }, + "E4B69B4D0A3A1720003C02F2": { + "buildConfigurations": [ + "E4B69B4E0A3A1720003C02F2", + "E4B69B4F0A3A1720003C02F2" + ], + "defaultConfigurationIsVisible": "0", + "defaultConfigurationName": "Release", + "isa": "XCConfigurationList" + }, + "E4B69B4E0A3A1720003C02F2": { + "baseConfigurationReference": "E4EB6923138AFD0F00A09F29", + "buildSettings": { + "CODE_SIGN_ENTITLEMENTS": "of.entitlements", + "COPY_PHASE_STRIP": "NO", + "ENABLE_TESTABILITY": "YES", + "GCC_OPTIMIZATION_LEVEL": "0", + "GCC_WARN_UNUSED_VARIABLE": "NO", + "HEADER_SEARCH_PATHS": [ + "$(OF_CORE_HEADERS)", + "src" + ], + "OTHER_CPLUSPLUSFLAGS": "-D__MACOSX_CORE__" + }, + "isa": "XCBuildConfiguration", + "name": "Debug" + }, + "E4B69B4F0A3A1720003C02F2": { + "baseConfigurationReference": "E4EB6923138AFD0F00A09F29", + "buildSettings": { + "CODE_SIGN_ENTITLEMENTS": "of.entitlements", + "COPY_PHASE_STRIP": "YES", + "GCC_OPTIMIZATION_LEVEL": "3", + "GCC_UNROLL_LOOPS": "YES", + "HEADER_SEARCH_PATHS": [ + "$(OF_CORE_HEADERS)", + "src" + ], + "OTHER_CPLUSPLUSFLAGS": "-D__MACOSX_CORE__" + }, + "isa": "XCBuildConfiguration", + "name": "Release" + }, + "E4B69B580A3A1756003C02F2": { + "buildActionMask": "2147483647", + "files": [ + "E4B69E200A3A1BDC003C02F2", + "E4B69E210A3A1BDC003C02F2", + "A002B59D-9D01-4505-9A8C-520FEEFC4152" + ], + "isa": "PBXSourcesBuildPhase", + "runOnlyForDeploymentPostprocessing": "0" + }, + "E4B69B590A3A1756003C02F2": { + "buildActionMask": "2147483647", + "files": [], + "isa": "PBXFrameworksBuildPhase", + "runOnlyForDeploymentPostprocessing": "0" + }, + "E4B69B5A0A3A1756003C02F2": { + "buildConfigurationList": "E4B69B5F0A3A1757003C02F2", + "buildPhases": [ + "E42962A92163ECCD00A6A9E2", + "E4B69B580A3A1756003C02F2", + "E4B69B590A3A1756003C02F2", + "E4C2427710CC5ABF004149E2", + "E4A5B60F29BAAAE400C2D356", + "19B789C429E5AB4A0082E9B8" + ], + "buildRules": [], + "dependencies": [], + "isa": "PBXNativeTarget", + "name": "example-motion-field", + "productName": "myOFApp", + "productReference": "E4B69B5B0A3A1756003C02F2", + "productType": "com.apple.product-type.application" + }, + "E4B69B5B0A3A1756003C02F2": { + "explicitFileType": "wrapper.application", + "includeInIndex": "0", + "isa": "PBXFileReference", + "path": "example-motion-fieldDebug.app", + "sourceTree": "BUILT_PRODUCTS_DIR" + }, + "E4B69B5F0A3A1757003C02F2": { + "buildConfigurations": [ + "E4B69B600A3A1757003C02F2", + "E4B69B610A3A1757003C02F2" + ], + "defaultConfigurationIsVisible": "0", + "defaultConfigurationName": "Release", + "isa": "XCConfigurationList" + }, + "E4B69B600A3A1757003C02F2": { + "baseConfigurationReference": "E4EB6923138AFD0F00A09F29", + "buildSettings": { + "ARCHS": "$(ARCHS_STANDARD)", + "COPY_PHASE_STRIP": "NO", + "FRAMEWORK_SEARCH_PATHS": "$(inherited)", + "GCC_DYNAMIC_NO_PIC": "NO", + "HEADER_SEARCH_PATHS": [ + "$(OF_CORE_HEADERS)", + "src", + "src", + "../ofxBvh/src" + ], + "LIBRARY_SEARCH_PATHS": "$(inherited)", + "OTHER_LDFLAGS": [ + "$(OF_CORE_LIBS)", + "$(OF_CORE_FRAMEWORKS)", + "$(LIB_OF_DEBUG)" + ] + }, + "isa": "XCBuildConfiguration", + "name": "Debug" + }, + "E4B69B610A3A1757003C02F2": { + "baseConfigurationReference": "E4EB6923138AFD0F00A09F29", + "buildSettings": { + "ARCHS": "$(ARCHS_STANDARD)", + "COPY_PHASE_STRIP": "YES", + "FRAMEWORK_SEARCH_PATHS": "$(inherited)", + "HEADER_SEARCH_PATHS": [ + "$(OF_CORE_HEADERS)", + "src", + "src", + "../ofxBvh/src" + ], + "LIBRARY_SEARCH_PATHS": "$(inherited)", + "OTHER_LDFLAGS": [ + "$(OF_CORE_LIBS)", + "$(OF_CORE_FRAMEWORKS)", + "$(LIB_OF_RELEASE)" + ], + "baseConfigurationReference": "E4EB6923138AFD0F00A09F29" + }, + "isa": "XCBuildConfiguration", + "name": "Release" + }, + "E4B69E1C0A3A1BDC003C02F2": { + "children": [ + "E4B69E1D0A3A1BDC003C02F2", + "E4B69E1E0A3A1BDC003C02F2", + "E4B69E1F0A3A1BDC003C02F2" + ], + "isa": "PBXGroup", + "path": "src", + "sourceTree": "SOURCE_ROOT" + }, + "E4B69E1D0A3A1BDC003C02F2": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "sourcecode.cpp.cpp", + "name": "main.cpp", + "path": "src/main.cpp", + "sourceTree": "SOURCE_ROOT" + }, + "E4B69E1E0A3A1BDC003C02F2": { + "explicitFileType": "sourcecode.cpp.cpp", + "fileEncoding": "4", + "isa": "PBXFileReference", + "name": "ofApp.cpp", + "path": "src/ofApp.cpp", + "sourceTree": "SOURCE_ROOT" + }, + "E4B69E1F0A3A1BDC003C02F2": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "sourcecode.c.h", + "name": "ofApp.h", + "path": "src/ofApp.h", + "sourceTree": "SOURCE_ROOT" + }, + "E4B69E200A3A1BDC003C02F2": { + "fileRef": "E4B69E1D0A3A1BDC003C02F2", + "isa": "PBXBuildFile" + }, + "E4B69E210A3A1BDC003C02F2": { + "fileRef": "E4B69E1E0A3A1BDC003C02F2", + "isa": "PBXBuildFile" + }, + "E4B6FCAD0C3E899E008CF71C": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "text.plist.xml", + "path": "openFrameworks-Info.plist", + "sourceTree": "" + }, + "E4C2427710CC5ABF004149E2": { + "buildActionMask": "2147483647", + "dstPath": "", + "dstSubfolderSpec": "10", + "files": [], + "isa": "PBXCopyFilesBuildPhase", + "runOnlyForDeploymentPostprocessing": "0" + }, + "E4EB6923138AFD0F00A09F29": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "text.xcconfig", + "path": "Project.xcconfig", + "sourceTree": "" + }, + "FB36F179-2574-4CF3-A778-FF0E45F0FC51": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "sourcecode.cpp.h", + "name": "ofxBvh.h", + "path": "../ofxBvh/src/ofxBvh.h", + "sourceTree": "SOURCE_ROOT" + } + }, + "rootObject": "E4B69B4C0A3A1720003C02F2" +} \ No newline at end of file diff --git a/example-motion-field/example-motion-field.xcodeproj/xcshareddata/xcschemes/example-motion-field Debug.xcscheme b/example-motion-field/example-motion-field.xcodeproj/xcshareddata/xcschemes/example-motion-field Debug.xcscheme new file mode 100644 index 0000000..41585f2 --- /dev/null +++ b/example-motion-field/example-motion-field.xcodeproj/xcshareddata/xcschemes/example-motion-field Debug.xcscheme @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example-motion-field/example-motion-field.xcodeproj/xcshareddata/xcschemes/example-motion-field Release.xcscheme b/example-motion-field/example-motion-field.xcodeproj/xcshareddata/xcschemes/example-motion-field Release.xcscheme new file mode 100644 index 0000000..64abd5a --- /dev/null +++ b/example-motion-field/example-motion-field.xcodeproj/xcshareddata/xcschemes/example-motion-field Release.xcscheme @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example-motion-field/of.entitlements b/example-motion-field/of.entitlements new file mode 100644 index 0000000..f4fc128 --- /dev/null +++ b/example-motion-field/of.entitlements @@ -0,0 +1,24 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.device.audio-input + + com.apple.security.device.bluetooth + + com.apple.security.device.camera + + com.apple.security.device.usb + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.print + + + diff --git a/example-motion-field/openFrameworks-Info.plist b/example-motion-field/openFrameworks-Info.plist new file mode 100644 index 0000000..c362e38 --- /dev/null +++ b/example-motion-field/openFrameworks-Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + ${DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + ${ICON_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${TARGET_NAME} + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleShortVersionString + ${MARKETING_VERSION} + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSCameraUsageDescription + This app needs to access the camera + NSMicrophoneUsageDescription + This app needs to access the microphone + NSHighResolutionCapable + ${HIGH_RESOLUTION_CAPABLE} + NSCameraUseContinuityCameraDeviceType + + + diff --git a/example-motion-field/src/main.cpp b/example-motion-field/src/main.cpp new file mode 100644 index 0000000..ea192d4 --- /dev/null +++ b/example-motion-field/src/main.cpp @@ -0,0 +1,44 @@ +#include "ofMain.h" +#include "ofApp.h" + +#include +#include +#include + +int main(int argc, char* argv[]) { + AppOptions options; + try { + for (int i = 1; i < argc; ++i) { + const std::string argument = argv[i]; + if (argument == "--smoke-test") options.smokeTest = true; + else if (argument.starts_with("--capture=")) { + options.capturePath = argument.substr(10); + if (options.capturePath.empty()) throw std::invalid_argument("Capture path is empty."); + } else if (argument.starts_with("--capture-frame=")) { + const auto value = argument.substr(16); + std::size_t consumed = 0; + options.captureFrame = std::stoi(value, &consumed); + if (consumed != value.size()) throw std::invalid_argument("Capture frame must be an integer."); + } + else { + std::cerr << "Unknown option: " << argument << '\n'; + return 1; + } + } + if (options.captureFrame < 2 || options.captureFrame > 3600 || + (!options.capturePath.empty() && !of::filesystem::path(options.capturePath).is_absolute())) { + throw std::invalid_argument("Use an absolute capture path and a frame between 2 and 3600."); + } + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + ofGLFWWindowSettings settings; + settings.setSize(1440, 900); + settings.setGLVersion(3, 2); + settings.numSamples = 0; // The FBO owns the render target in interactive and automated runs. + settings.visible = !options.automated(); + auto window = ofCreateWindow(settings); + ofRunApp(window, std::make_shared(options)); + return ofRunMainLoop(); +} diff --git a/example-motion-field/src/ofApp.cpp b/example-motion-field/src/ofApp.cpp new file mode 100644 index 0000000..47193df --- /dev/null +++ b/example-motion-field/src/ofApp.cpp @@ -0,0 +1,170 @@ +#include "ofApp.h" +#include +#include +#include + +namespace { +constexpr std::array trackedJoints{ + "Hips", "Chest3", "Head", "LeftShoulder", "LeftElbow", "LeftWrist", + "RightShoulder", "RightElbow", "RightWrist", "LeftKnee", "LeftAnkle", + "RightKnee", "RightAnkle"}; +} + +void ofApp::setup() { + ofSetWindowTitle("Motion Field | perfume-dev"); + ofSetFrameRate(options.automated() ? 0 : 60); + ofSetVerticalSync(!options.automated()); + ofBackground(7, 10, 16); + ofFbo::Settings target; + target.width = 1440; + target.height = 900; + target.internalformat = GL_RGBA; + target.textureTarget = GL_TEXTURE_2D; + canvas.allocate(target); + bool loaded = true; + for (int dancer = 0; dancer < 3; ++dancer) { + const auto filename = std::string(1, static_cast('A' + dancer)) + "_test.bvh"; + loaded = motions[dancer].load("../../../example-bvh/bin/data/" + filename) && loaded; + motions[dancer].setLoop(true); + motions[dancer].play(); + if (motions[dancer].isLoaded()) { + motions[dancer].setPosition(0.0f); + motions[dancer].update(0.0f); + origins[dancer] = glm::vec3(motions[dancer].getJoint(0)->getPosition()); + for (const auto* name : trackedJoints) loaded = motions[dancer].getJoint(name) && loaded; + } + } + loaded = fieldShader.setupShaderFromFile(GL_VERTEX_SHADER, "shaders/field.vert") && loaded; + loaded = fieldShader.setupShaderFromFile(GL_FRAGMENT_SHADER, "shaders/field.frag") && loaded; + loaded = fieldShader.bindDefaults() && loaded; + loaded = fieldShader.linkProgram() && loaded; + // OF 0.12.1's isLoaded/linkProgram result alone does not guarantee successful linkage. + GLint linked = GL_FALSE; + glGetProgramiv(fieldShader.getProgram(), GL_LINK_STATUS, &linked); + ready = loaded && linked == GL_TRUE && canvas.isAllocated(); + if (!ready) { + ofLogError("Motion Field") << "Required BVH, shader, or framebuffer could not be loaded."; + ofExit(1); + return; + } + updatePose(); + previousJoint = jointPositions[5]; +} + +void ofApp::updatePose() { + // CPU: project a sparse pose into a stable 2D coordinate system, measured in canvas heights. + for (int dancer = 0; dancer < 3; ++dancer) { + auto& motion = motions[dancer]; + motion.setPosition(std::fmod(timeline, motion.getDuration()) / motion.getDuration()); + motion.update(0.0f); + const glm::vec3 root(motion.getJoint(0)->getPosition()); + const glm::vec3 origin(root.x, origins[dancer].y, root.z); + for (int joint = 0; joint < jointsPerDancer; ++joint) { + const auto p = glm::vec3(motion.getJoint(trackedJoints[joint])->getPosition()) - origin; + jointPositions[dancer * jointsPerDancer + joint] = + glm::vec2(p.x * 0.0020f + (dancer - 1) * 0.46f, p.y * 0.0020f); + } + } +} + +void ofApp::update() { + if (!ready || paused) return; + timeline += options.automated() ? 1.0f / 60.0f : static_cast(std::min(ofGetLastFrameTime(), 0.05)); + updatePose(); + traveled += glm::distance(previousJoint, jointPositions[5]); + previousJoint = jointPositions[5]; +} + +void ofApp::draw() { + if (!ready) return; + canvas.begin(); + ofClear(7, 10, 16, 255); + ofDisableDepthTest(); + ofSetColor(255); + fieldShader.begin(); + fieldShader.setUniform2f("uResolution", canvas.getWidth(), canvas.getHeight()); + fieldShader.setUniform1f("uTime", timeline); + fieldShader.setUniform1f("uContourDensity", contourDensity); + fieldShader.setUniform2fv("uJoints", &jointPositions.front().x, jointPositions.size()); + // A single rectangle is the entire GPU geometry for this example. + ofDrawRectangle(0, 0, canvas.getWidth(), canvas.getHeight()); + fieldShader.end(); + + ofSetColor(49, 61, 75); + ofDrawLine(68, 104, 1372, 104); + ofDrawLine(68, 808, 1372, 808); + ofSetColor(225, 229, 230); + ofDrawBitmapString("M O T I O N F I E L D", 68, 68); + ofSetColor(126, 140, 156); + ofDrawBitmapString("PERFUME / MOTION STUDY 02", 1110, 68); + if (showHelp) { + ofDrawBitmapString("SPACE pause / R reset / UP-DOWN contours / H help", 68, 849); + ofDrawBitmapString("FRAGMENT FIELD / " + ofToString(contourDensity, 0) + " CONTOURS / " + + ofToString(timeline, 2) + " s" + (paused ? " PAUSED" : ""), 974, 849); + } + canvas.end(); + ofSetColor(255); + // Preserve composition in tall/wide windows with centered letterboxing. + const float scale = std::min(ofGetWidth() / canvas.getWidth(), ofGetHeight() / canvas.getHeight()); + const ofRectangle viewport((ofGetWidth() - canvas.getWidth() * scale) * 0.5f, + (ofGetHeight() - canvas.getHeight() * scale) * 0.5f, + canvas.getWidth() * scale, canvas.getHeight() * scale); + canvas.draw(viewport); + finishAutomatedFrame(); +} + +void ofApp::finishAutomatedFrame() { + if (!options.automated() || ++renderedFrames < options.captureFrame) return; + ofPixels pixels; + canvas.readToPixels(pixels); + if (!pixels.isAllocated() || pixels.getWidth() != 1440 || pixels.getHeight() != 900 || + pixels.getNumChannels() != 4) { + ofLogError("Motion Field") << "Framebuffer readback failed."; + ofExit(1); + return; + } + std::size_t brightPixels = 0; + // Exclude every text/line overlay: only the central artwork can satisfy the check. + for (std::size_t y = 125; y < 750; ++y) { + for (std::size_t x = 68; x < 1372; ++x) { + const auto i = (y * pixels.getWidth() + x) * pixels.getNumChannels(); + if (pixels[i] > 70 || pixels[i + 1] > 70 || pixels[i + 2] > 70) ++brightPixels; + } + } + if (glGetError() != GL_NO_ERROR || brightPixels < 1000 || traveled < 0.001f) { + ofLogError("Motion Field") << "Render validation failed: pixels=" << brightPixels << ", motion=" << traveled; + ofExit(1); + return; + } + const float validatedMotion = traveled; + if (options.smokeTest) { + const float frozenTime = timeline; + keyPressed(' '); + update(); + if (!paused || timeline != frozenTime) { ofExit(1); return; } + keyPressed('r'); + if (timeline != 12.0f) { ofExit(1); return; } + keyPressed(' '); + } + if (!options.capturePath.empty() && !ofSaveImage(pixels, options.capturePath)) { + ofLogError("Motion Field") << "Could not save capture."; + ofExit(1); + return; + } + std::cout << "SMOKE_TEST_OK frames=" << renderedFrames << " brightPixels=" << brightPixels + << " motionDistance=" << validatedMotion << " shaderStages=2" + << " controlsChecked=" << options.smokeTest << std::endl; + ofExit(0); +} + +void ofApp::keyPressed(int key) { + if (key == ' ') paused = !paused; + if (key == 'r' || key == 'R') { + timeline = 12.0f; + updatePose(); + previousJoint = jointPositions[5]; + } + if (key == 'h' || key == 'H') showHelp = !showHelp; + if (key == OF_KEY_UP) contourDensity = std::min(contourDensity + 1.0f, 16.0f); + if (key == OF_KEY_DOWN) contourDensity = std::max(contourDensity - 1.0f, 3.0f); +} diff --git a/example-motion-field/src/ofApp.h b/example-motion-field/src/ofApp.h new file mode 100644 index 0000000..148234a --- /dev/null +++ b/example-motion-field/src/ofApp.h @@ -0,0 +1,35 @@ +#pragma once +#include "ofMain.h" +#include "ofxBvh.h" +#include + +struct AppOptions { + bool smokeTest = false; + std::string capturePath; + int captureFrame = 180; + bool automated() const { return smokeTest || !capturePath.empty(); } +}; + +class ofApp : public ofBaseApp { +public: + explicit ofApp(AppOptions options) : options(std::move(options)) {} + void setup() override; + void update() override; + void draw() override; + void keyPressed(int key) override; + +private: + static constexpr int jointsPerDancer = 13; + AppOptions options; + std::array motions; + std::array origins{}; + std::array jointPositions{}; + ofShader fieldShader; + ofFbo canvas; + bool ready = false, paused = false, showHelp = true; + float timeline = 12.0f, contourDensity = 8.0f, traveled = 0.0f; + int renderedFrames = 0; + glm::vec2 previousJoint{}; + void updatePose(); + void finishAutomatedFrame(); +}; diff --git a/example-motion-ribbons/.gitignore b/example-motion-ribbons/.gitignore new file mode 100644 index 0000000..51c5894 --- /dev/null +++ b/example-motion-ribbons/.gitignore @@ -0,0 +1,6 @@ +*.app +build/ +DerivedData/ +xcuserdata/ +obj/ +.DS_Store diff --git a/example-motion-ribbons/LICENSE b/example-motion-ribbons/LICENSE new file mode 100644 index 0000000..3d05ecf --- /dev/null +++ b/example-motion-ribbons/LICENSE @@ -0,0 +1,24 @@ +MIT License + +Copyright (c) 2026 Daito Manabe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +This license covers the new code and shaders in this directory. Referenced +2012 BVH assets retain their original terms; this file does not relicense them. diff --git a/example-motion-ribbons/Makefile b/example-motion-ribbons/Makefile new file mode 100644 index 0000000..ab71e66 --- /dev/null +++ b/example-motion-ribbons/Makefile @@ -0,0 +1,13 @@ +# Attempt to load a config.make file. +# If none is found, project defaults in config.project.make will be used. +ifneq ($(wildcard config.make),) + include config.make +endif + +# make sure the the OF_ROOT location is defined +ifndef OF_ROOT + OF_ROOT=../../../.. +endif + +# call the project makefile! +include $(OF_ROOT)/libs/openFrameworksCompiled/project/makefileCommon/compile.project.mk diff --git a/example-motion-ribbons/Project.xcconfig b/example-motion-ribbons/Project.xcconfig new file mode 100644 index 0000000..7c338e1 --- /dev/null +++ b/example-motion-ribbons/Project.xcconfig @@ -0,0 +1,88 @@ +//THE PATH TO THE ROOT OF OUR OF PATH RELATIVE TO THIS PROJECT. +//THIS NEEDS TO BE DEFINED BEFORE CoreOF.xcconfig IS INCLUDED +OF_PATH = ../../../.. + +//THIS HAS ALL THE HEADER AND LIBS FOR OF CORE +#include "../../../../libs/openFrameworksCompiled/project/osx/CoreOF.xcconfig" + +//UNCOMMENT BELOW TO ENABLE C++ 17 and std::filesystem +CLANG_CXX_LANGUAGE_STANDARD = c++23 +CLANG_C_LANGUAGE_STANDARD = c17 +MACOSX_DEPLOYMENT_TARGET = 11.5 + +// App Settings +PRODUCT_NAME = $(TARGET_NAME) +PRODUCT_NAME[config=Debug] = $(TARGET_NAME)Debug +PRODUCT_BUNDLE_IDENTIFIER = cc.openFrameworks.${TARGET_NAME:rfc1034identifier} +//PRODUCT_BUNDLE_IDENTIFIER[config=Debug] = cc.openFrameworks.$(TARGET_NAME)Debug +DEVELOPMENT_LANGUAGE = English +CODE_SIGN_IDENTITY = - +INFOPLIST_FILE = openFrameworks-Info.plist +GENERATE_INFOPLIST_FILE = YES + +// set application category to games, this is required to enable Game mode +// note: this sets the initial value in the Xcode UI +INFOPLIST_KEY_LSApplicationCategoryType = public.app-category.games + +// VERSIONING - overridden if changed in Xcode UI + +// this is "Version" in the Xcode target Identity UI +// suggested to use semantic versioning format ala #.#.# +MARKETING_VERSION = 0.1.0 + +// If users upgrade project to recommended settings it enables script sandboxing which breaks our post build script +ENABLE_USER_SCRIPT_SANDBOXING=NO + +// this is "Build" in the Xcode target Identity UI, an incremental build number +// important for the App Store as new build submissions need a diff number even +// if MARKETING_VERSION is the same +CURRENT_PROJECT_VERSION = 1 + +// ICONS + +// default oF app icon +ICON_NAME = of.icns +ICON_NAME[config=Debug] = of_debug.icns +ICON_FILE = $(OF_PATH)/libs/openFrameworksCompiled/project/osx/$(ICON_NAME) + +// custom app icon, placed in main project folder +//ICON_NAME = MyApp.icns +//ICON_FILE = $(ICON_NAME) + +// custom app icon with separate Release and Debug versions placed in bin/data +//ICON_NAME = icon.icns +//ICON_NAME[config=Debug] = icon-debug.icns +//ICON_FILE = bin/data/$(ICON_NAME) + +// note: oF 0.7.2 - 0.11 used ICON_FILE_PATH which is no longer used in oF 0.12+ +// ex. change ICON_FILE_PATH = bin/data/ -> ICON_FILE = bin/data/$(ICON_NAME) & +// in a custom openFrameworks-Info.plist, set CFBundleIconFile to ICON_NAME + +//APPSTORE, uncomment next lines to bundle data folder and code sign +//OF_CODESIGN = 1 +//OF_BUNDLE_DATA_FOLDER = 1 +//OF_BUNDLE_DYLIBS = 1 + +HIGH_RESOLUTION_CAPABLE = NO + +// Optional include to keep any permanent settings as CODE_SIGN_IDENTITY. +#include? "App.xcconfig" + +//FOR AV ENGINE SOUND PLAYER UNCOMMENT THREE LINES BELOW +OF_NO_FMOD=1 +USER_PREPROCESSOR_DEFINITIONS="OF_NO_FMOD=1" +LIB_FMOD="" +GCC_PREPROCESSOR_DEFINITIONS=$(inherited) $(USER_PREPROCESSOR_DEFINITIONS) + +OTHER_CFLAGS = $(OF_CORE_CFLAGS) +OTHER_LDFLAGS = $(OF_CORE_LIBS) $(OF_CORE_FRAMEWORKS) +HEADER_SEARCH_PATHS = $(OF_CORE_HEADERS) + +//THIS MAKES SURE THE APP BUILDS INSIDE THE BIN FOLDER +//If you comment this line out the app will be run from DerrivedData and your data/ files won't be accessible unless you uncomment OF_BUNDLE_DATA_FOLDER = 1 above +CONFIGURATION_BUILD_DIR = ${SRCROOT}/bin + +//OF_CORE_BUILD_COMMAND = echo \"💾 Compiling openFrameworks\"\nxcodebuild -project \"$OF_PATH/libs/openFrameworksCompiled/project/osx/openFrameworksLib.xcodeproj\" -target openFrameworks -configuration \"${CONFIGURATION}\" CLANG_CXX_LANGUAGE_STANDARD=$CLANG_CXX_LANGUAGE_STANDARD MACOSX_DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET GCC_PREPROCESSOR_DEFINITIONS='$USER_PREPROCESSOR_DEFINITIONS' + + +OF_CORE_BUILD_COMMAND = xcodebuild -project $OF_PATH/libs/openFrameworksCompiled/project/osx/openFrameworksLib.xcodeproj -target openFrameworks -configuration ${CONFIGURATION} CLANG_CXX_LANGUAGE_STANDARD=$CLANG_CXX_LANGUAGE_STANDARD MACOSX_DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET GCC_PREPROCESSOR_DEFINITIONS=$USER_PREPROCESSOR_DEFINITIONS diff --git a/example-motion-ribbons/README.md b/example-motion-ribbons/README.md new file mode 100644 index 0000000..6a9248a --- /dev/null +++ b/example-motion-ribbons/README.md @@ -0,0 +1,55 @@ +# Motion Ribbons + +A minimal three-dancer motion study: cyan, coral and ivory traces describe the +hands, feet and head, while a fine live skeleton keeps the choreography legible. +The data is the original A/B/C sample from the 2012 Perfume Global Site Project. + +Requires **openFrameworks 0.12.1**, a desktop **OpenGL 3.2** context and this entire +repository. The example uses modern `ofApp` lifecycle overrides, GLM positions, +RAII-owned meshes/shaders/framebuffer and a programmable rendering pipeline. + +## Run + +Place the repository at `openFrameworks/apps/myApps/example-openFrameworks`, +open `example-motion-ribbons.xcodeproj` and select the Release scheme, or: + +```sh +make Release OF_ROOT=/path/to/openFrameworks +make RunRelease OF_ROOT=/path/to/openFrameworks +``` + +Keep `../example-bvh/bin/data/{A,B,C}_test.bvh` in place. Data is shared using a +relative path from this example's `bin/data`, and is never duplicated. + +## Three shader stages + +1. CPU: sample BVH landmarks into bounded trails and upload colored line segments. + Each dancer is horizontally recentered to keep the composition stable. +2. `ribbon.vert`: transform line endpoints with OF's camera matrix. +3. `ribbon.geom`: expand each line into a screen-facing four-vertex strip. + `uWidth` is its pixel width and `uViewport` is the framebuffer size. +4. `ribbon.frag`: soften the transverse edge and fade old samples through vertex alpha. + +This is an actual geometry shader, so it is a **desktop GL** example, not an +OpenGL ES/mobile example. It uses no immediate-mode OpenGL or point sprites. + +This is a pose/trail study, not a reconstruction of the original stage formation. +Playback starts at 12 seconds, where the sample movement is already active. + +## Controls and capture + +- Space: pause/resume both pose and trails. +- R: rewind to the start of the study and clear trails. +- Drag / scroll: orbit / zoom the camera. +- H: toggle the bottom control legend. + +Launch the executable with `--smoke-test` to render 180 deterministic frames in +a hidden window. `--capture=/absolute/path/frame.png` saves the FBO image and +exits; `--capture-frame=420` chooses another pose. The two modes can be combined. +Validation checks shader linkage, GL errors, visible pixels and actual joint +movement before printing `SMOKE_TEST_OK`. Automated runs never show or activate +a window. Interactive drawing and capture use the same 1440×900 FBO; other +window shapes are letterboxed, and the orbit control area follows the image. + +The new source and shaders use [MIT](LICENSE). Shared motion assets and the +ofxBvh dependency retain their existing terms; see the repository license index. diff --git a/example-motion-ribbons/addons.make b/example-motion-ribbons/addons.make new file mode 100644 index 0000000..bec539d --- /dev/null +++ b/example-motion-ribbons/addons.make @@ -0,0 +1 @@ +../ofxBvh diff --git a/example-motion-ribbons/bin/data/.gitkeep b/example-motion-ribbons/bin/data/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/example-motion-ribbons/bin/data/shaders/ribbon.frag b/example-motion-ribbons/bin/data/shaders/ribbon.frag new file mode 100644 index 0000000..dcbefee --- /dev/null +++ b/example-motion-ribbons/bin/data/shaders/ribbon.frag @@ -0,0 +1,10 @@ +#version 150 +in vec4 ribbonColor; +in float acrossRibbon; +out vec4 fragColor; + +void main() { + // An antialiased soft edge without a second blur pass or large overdraw. + float edge = 1.0 - smoothstep(0.35, 1.0, abs(acrossRibbon)); + fragColor = vec4(ribbonColor.rgb, ribbonColor.a * edge); +} diff --git a/example-motion-ribbons/bin/data/shaders/ribbon.geom b/example-motion-ribbons/bin/data/shaders/ribbon.geom new file mode 100644 index 0000000..2763dda --- /dev/null +++ b/example-motion-ribbons/bin/data/shaders/ribbon.geom @@ -0,0 +1,35 @@ +#version 150 +// A world-space line becomes a constant-pixel-width screen-facing ribbon. +layout(lines) in; +layout(triangle_strip, max_vertices = 4) out; +uniform vec2 uViewport; +uniform float uWidth; +in vec4 vertexColor[]; +out vec4 ribbonColor; +out float acrossRibbon; + +void emitCorner(int endpoint, float side, vec2 offset) { + vec4 clip = gl_in[endpoint].gl_Position; + clip.xy += offset * side * clip.w; + gl_Position = clip; + ribbonColor = vertexColor[endpoint]; + acrossRibbon = side; + EmitVertex(); +} + +void main() { + vec4 a = gl_in[0].gl_Position; + vec4 b = gl_in[1].gl_Position; + // Do not divide by a point behind/on the camera plane. + if (a.w <= 0.001 || b.w <= 0.001) return; + vec2 direction = (b.xy / b.w - a.xy / a.w) * uViewport; + float magnitude = length(direction); + if (magnitude < 0.001) return; + vec2 normal = vec2(-direction.y, direction.x) / magnitude; + vec2 offset = normal * uWidth / uViewport; + emitCorner(0, -1.0, offset); + emitCorner(0, 1.0, offset); + emitCorner(1, -1.0, offset); + emitCorner(1, 1.0, offset); + EndPrimitive(); +} diff --git a/example-motion-ribbons/bin/data/shaders/ribbon.vert b/example-motion-ribbons/bin/data/shaders/ribbon.vert new file mode 100644 index 0000000..dbafb4f --- /dev/null +++ b/example-motion-ribbons/bin/data/shaders/ribbon.vert @@ -0,0 +1,11 @@ +#version 150 +// OF supplies these default mesh attributes and the camera transform. +uniform mat4 modelViewProjectionMatrix; +in vec4 position; +in vec4 color; +out vec4 vertexColor; + +void main() { + gl_Position = modelViewProjectionMatrix * position; + vertexColor = color; +} diff --git a/example-motion-ribbons/config.make b/example-motion-ribbons/config.make new file mode 100644 index 0000000..3ef32f8 --- /dev/null +++ b/example-motion-ribbons/config.make @@ -0,0 +1,2 @@ +# Place the repository at apps/myApps/example-openFrameworks, or override OF_ROOT. +OF_ROOT ?= ../../../.. diff --git a/example-motion-ribbons/example-motion-ribbons.xcodeproj/project.pbxproj b/example-motion-ribbons/example-motion-ribbons.xcodeproj/project.pbxproj new file mode 100644 index 0000000..e84690b --- /dev/null +++ b/example-motion-ribbons/example-motion-ribbons.xcodeproj/project.pbxproj @@ -0,0 +1,360 @@ +{ + "_OFProjectGeneratorVersion": "0.103.0", + "archiveVersion": "1", + "classes": {}, + "objectVersion": "54", + "objects": { + "191CD6FA2847E21E0085CBB6": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "text.plist.entitlements", + "path": "of.entitlements", + "sourceTree": "" + }, + "191EF70929D778A400F35F26": { + "isa": "PBXFileReference", + "lastKnownFileType": "folder", + "name": "openFrameworks", + "path": "../../../../libs/openFrameworks", + "sourceTree": "" + }, + "19B789C429E5AB4A0082E9B8": { + "alwaysOutOfDate": "1", + "buildActionMask": "2147483647", + "files": [], + "inputFileListPaths": [], + "inputPaths": [], + "isa": "PBXShellScriptBuildPhase", + "outputFileListPaths": [], + "outputPaths": [], + "runOnlyForDeploymentPostprocessing": "0", + "shellPath": "/bin/sh", + "shellScript": "\"$OF_PATH/scripts/osx/xcode_project.sh\"\n", + "showEnvVarsInLog": "0" + }, + "4EF3C60D-9E16-4D00-8117-EE96E15C3813": { + "children": [ + "5D92FCD1-4A7D-409F-86ED-0AF488E11728" + ], + "isa": "PBXGroup", + "name": "local_addons", + "path": "", + "sourceTree": "SOURCE_ROOT" + }, + "5D92FCD1-4A7D-409F-86ED-0AF488E11728": { + "children": [ + "B7520A1D-B519-4D22-8E68-94171B73C52A" + ], + "isa": "PBXGroup", + "name": "ofxBvh", + "path": "../ofxBvh", + "sourceTree": "SOURCE_ROOT" + }, + "5EF610DD-C456-408D-AEF5-8A2900E51C01": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "sourcecode.cpp.cpp", + "name": "ofxBvh.cpp", + "path": "../ofxBvh/src/ofxBvh.cpp", + "sourceTree": "SOURCE_ROOT" + }, + "9369AD55-D1AF-48BA-855A-D14901B4453F": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "sourcecode.cpp.h", + "name": "ofxBvh.h", + "path": "../ofxBvh/src/ofxBvh.h", + "sourceTree": "SOURCE_ROOT" + }, + "B7520A1D-B519-4D22-8E68-94171B73C52A": { + "children": [ + "5EF610DD-C456-408D-AEF5-8A2900E51C01", + "9369AD55-D1AF-48BA-855A-D14901B4453F" + ], + "isa": "PBXGroup", + "name": "src", + "path": "src", + "sourceTree": "" + }, + "BB4B014C10F69532006C3DED": { + "children": [], + "isa": "PBXGroup", + "name": "addons", + "path": "../../../../addons", + "sourceTree": "" + }, + "E42962A92163ECCD00A6A9E2": { + "alwaysOutOfDate": "1", + "buildActionMask": "2147483647", + "files": [], + "inputPaths": [], + "isa": "PBXShellScriptBuildPhase", + "name": "Run Script — Compile OF", + "outputPaths": [], + "runOnlyForDeploymentPostprocessing": "0", + "shellPath": "/bin/sh", + "shellScript": "$OF_CORE_BUILD_COMMAND\n", + "showEnvVarsInLog": "0" + }, + "E4A5B60F29BAAAE400C2D356": { + "buildActionMask": "2147483647", + "dstPath": "", + "dstSubfolderSpec": "6", + "files": [], + "isa": "PBXCopyFilesBuildPhase", + "runOnlyForDeploymentPostprocessing": "0" + }, + "E4B69B4A0A3A1720003C02F2": { + "children": [ + "191CD6FA2847E21E0085CBB6", + "E4B6FCAD0C3E899E008CF71C", + "E4EB6923138AFD0F00A09F29", + "E4B69E1C0A3A1BDC003C02F2", + "191EF70929D778A400F35F26", + "BB4B014C10F69532006C3DED", + "E4B69B5B0A3A1756003C02F2", + "4EF3C60D-9E16-4D00-8117-EE96E15C3813", + "F377ED00-1313-423B-8416-AA0096AC9DB7" + ], + "isa": "PBXGroup", + "sourceTree": "" + }, + "E4B69B4C0A3A1720003C02F2": { + "attributes": { + "BuildIndependentTargetsInParallel": "YES", + "LastUpgradeCheck": "1540" + }, + "buildConfigurationList": "E4B69B4D0A3A1720003C02F2", + "compatibilityVersion": "Xcode 3.2", + "developmentRegion": "en", + "hasScannedForEncodings": "0", + "isa": "PBXProject", + "knownRegions": [ + "en", + "Base" + ], + "mainGroup": "E4B69B4A0A3A1720003C02F2", + "productRefGroup": "E4B69B4A0A3A1720003C02F2", + "projectDirPath": "", + "projectRoot": "", + "targets": [ + "E4B69B5A0A3A1756003C02F2" + ] + }, + "E4B69B4D0A3A1720003C02F2": { + "buildConfigurations": [ + "E4B69B4E0A3A1720003C02F2", + "E4B69B4F0A3A1720003C02F2" + ], + "defaultConfigurationIsVisible": "0", + "defaultConfigurationName": "Release", + "isa": "XCConfigurationList" + }, + "E4B69B4E0A3A1720003C02F2": { + "baseConfigurationReference": "E4EB6923138AFD0F00A09F29", + "buildSettings": { + "CODE_SIGN_ENTITLEMENTS": "of.entitlements", + "COPY_PHASE_STRIP": "NO", + "ENABLE_TESTABILITY": "YES", + "GCC_OPTIMIZATION_LEVEL": "0", + "GCC_WARN_UNUSED_VARIABLE": "NO", + "HEADER_SEARCH_PATHS": [ + "$(OF_CORE_HEADERS)", + "src" + ], + "OTHER_CPLUSPLUSFLAGS": "-D__MACOSX_CORE__" + }, + "isa": "XCBuildConfiguration", + "name": "Debug" + }, + "E4B69B4F0A3A1720003C02F2": { + "baseConfigurationReference": "E4EB6923138AFD0F00A09F29", + "buildSettings": { + "CODE_SIGN_ENTITLEMENTS": "of.entitlements", + "COPY_PHASE_STRIP": "YES", + "GCC_OPTIMIZATION_LEVEL": "3", + "GCC_UNROLL_LOOPS": "YES", + "HEADER_SEARCH_PATHS": [ + "$(OF_CORE_HEADERS)", + "src" + ], + "OTHER_CPLUSPLUSFLAGS": "-D__MACOSX_CORE__" + }, + "isa": "XCBuildConfiguration", + "name": "Release" + }, + "E4B69B580A3A1756003C02F2": { + "buildActionMask": "2147483647", + "files": [ + "E4B69E200A3A1BDC003C02F2", + "E4B69E210A3A1BDC003C02F2", + "EFE9A79B-8E39-480A-B50A-1E0CEBC48B9F" + ], + "isa": "PBXSourcesBuildPhase", + "runOnlyForDeploymentPostprocessing": "0" + }, + "E4B69B590A3A1756003C02F2": { + "buildActionMask": "2147483647", + "files": [], + "isa": "PBXFrameworksBuildPhase", + "runOnlyForDeploymentPostprocessing": "0" + }, + "E4B69B5A0A3A1756003C02F2": { + "buildConfigurationList": "E4B69B5F0A3A1757003C02F2", + "buildPhases": [ + "E42962A92163ECCD00A6A9E2", + "E4B69B580A3A1756003C02F2", + "E4B69B590A3A1756003C02F2", + "E4C2427710CC5ABF004149E2", + "E4A5B60F29BAAAE400C2D356", + "19B789C429E5AB4A0082E9B8" + ], + "buildRules": [], + "dependencies": [], + "isa": "PBXNativeTarget", + "name": "example-motion-ribbons", + "productName": "myOFApp", + "productReference": "E4B69B5B0A3A1756003C02F2", + "productType": "com.apple.product-type.application" + }, + "E4B69B5B0A3A1756003C02F2": { + "explicitFileType": "wrapper.application", + "includeInIndex": "0", + "isa": "PBXFileReference", + "path": "example-motion-ribbonsDebug.app", + "sourceTree": "BUILT_PRODUCTS_DIR" + }, + "E4B69B5F0A3A1757003C02F2": { + "buildConfigurations": [ + "E4B69B600A3A1757003C02F2", + "E4B69B610A3A1757003C02F2" + ], + "defaultConfigurationIsVisible": "0", + "defaultConfigurationName": "Release", + "isa": "XCConfigurationList" + }, + "E4B69B600A3A1757003C02F2": { + "baseConfigurationReference": "E4EB6923138AFD0F00A09F29", + "buildSettings": { + "ARCHS": "$(ARCHS_STANDARD)", + "COPY_PHASE_STRIP": "NO", + "FRAMEWORK_SEARCH_PATHS": "$(inherited)", + "GCC_DYNAMIC_NO_PIC": "NO", + "HEADER_SEARCH_PATHS": [ + "$(OF_CORE_HEADERS)", + "src", + "src", + "../ofxBvh/src" + ], + "LIBRARY_SEARCH_PATHS": "$(inherited)", + "OTHER_LDFLAGS": [ + "$(OF_CORE_LIBS)", + "$(OF_CORE_FRAMEWORKS)", + "$(LIB_OF_DEBUG)" + ] + }, + "isa": "XCBuildConfiguration", + "name": "Debug" + }, + "E4B69B610A3A1757003C02F2": { + "baseConfigurationReference": "E4EB6923138AFD0F00A09F29", + "buildSettings": { + "ARCHS": "$(ARCHS_STANDARD)", + "COPY_PHASE_STRIP": "YES", + "FRAMEWORK_SEARCH_PATHS": "$(inherited)", + "HEADER_SEARCH_PATHS": [ + "$(OF_CORE_HEADERS)", + "src", + "src", + "../ofxBvh/src" + ], + "LIBRARY_SEARCH_PATHS": "$(inherited)", + "OTHER_LDFLAGS": [ + "$(OF_CORE_LIBS)", + "$(OF_CORE_FRAMEWORKS)", + "$(LIB_OF_RELEASE)" + ], + "baseConfigurationReference": "E4EB6923138AFD0F00A09F29" + }, + "isa": "XCBuildConfiguration", + "name": "Release" + }, + "E4B69E1C0A3A1BDC003C02F2": { + "children": [ + "E4B69E1D0A3A1BDC003C02F2", + "E4B69E1E0A3A1BDC003C02F2", + "E4B69E1F0A3A1BDC003C02F2" + ], + "isa": "PBXGroup", + "path": "src", + "sourceTree": "SOURCE_ROOT" + }, + "E4B69E1D0A3A1BDC003C02F2": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "sourcecode.cpp.cpp", + "name": "main.cpp", + "path": "src/main.cpp", + "sourceTree": "SOURCE_ROOT" + }, + "E4B69E1E0A3A1BDC003C02F2": { + "explicitFileType": "sourcecode.cpp.cpp", + "fileEncoding": "4", + "isa": "PBXFileReference", + "name": "ofApp.cpp", + "path": "src/ofApp.cpp", + "sourceTree": "SOURCE_ROOT" + }, + "E4B69E1F0A3A1BDC003C02F2": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "sourcecode.c.h", + "name": "ofApp.h", + "path": "src/ofApp.h", + "sourceTree": "SOURCE_ROOT" + }, + "E4B69E200A3A1BDC003C02F2": { + "fileRef": "E4B69E1D0A3A1BDC003C02F2", + "isa": "PBXBuildFile" + }, + "E4B69E210A3A1BDC003C02F2": { + "fileRef": "E4B69E1E0A3A1BDC003C02F2", + "isa": "PBXBuildFile" + }, + "E4B6FCAD0C3E899E008CF71C": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "text.plist.xml", + "path": "openFrameworks-Info.plist", + "sourceTree": "" + }, + "E4C2427710CC5ABF004149E2": { + "buildActionMask": "2147483647", + "dstPath": "", + "dstSubfolderSpec": "10", + "files": [], + "isa": "PBXCopyFilesBuildPhase", + "runOnlyForDeploymentPostprocessing": "0" + }, + "E4EB6923138AFD0F00A09F29": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "text.xcconfig", + "path": "Project.xcconfig", + "sourceTree": "" + }, + "EFE9A79B-8E39-480A-B50A-1E0CEBC48B9F": { + "fileRef": "5EF610DD-C456-408D-AEF5-8A2900E51C01", + "isa": "PBXBuildFile" + }, + "F377ED00-1313-423B-8416-AA0096AC9DB7": { + "fileEncoding": "4", + "isa": "PBXFileReference", + "lastKnownFileType": "folder", + "name": "data", + "path": "bin/data", + "sourceTree": "SOURCE_ROOT" + } + }, + "rootObject": "E4B69B4C0A3A1720003C02F2" +} \ No newline at end of file diff --git a/example-motion-ribbons/example-motion-ribbons.xcodeproj/xcshareddata/xcschemes/example-motion-ribbons Debug.xcscheme b/example-motion-ribbons/example-motion-ribbons.xcodeproj/xcshareddata/xcschemes/example-motion-ribbons Debug.xcscheme new file mode 100644 index 0000000..be78bfc --- /dev/null +++ b/example-motion-ribbons/example-motion-ribbons.xcodeproj/xcshareddata/xcschemes/example-motion-ribbons Debug.xcscheme @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example-motion-ribbons/example-motion-ribbons.xcodeproj/xcshareddata/xcschemes/example-motion-ribbons Release.xcscheme b/example-motion-ribbons/example-motion-ribbons.xcodeproj/xcshareddata/xcschemes/example-motion-ribbons Release.xcscheme new file mode 100644 index 0000000..b77c762 --- /dev/null +++ b/example-motion-ribbons/example-motion-ribbons.xcodeproj/xcshareddata/xcschemes/example-motion-ribbons Release.xcscheme @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example-motion-ribbons/of.entitlements b/example-motion-ribbons/of.entitlements new file mode 100644 index 0000000..f4fc128 --- /dev/null +++ b/example-motion-ribbons/of.entitlements @@ -0,0 +1,24 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.device.audio-input + + com.apple.security.device.bluetooth + + com.apple.security.device.camera + + com.apple.security.device.usb + + com.apple.security.files.user-selected.read-write + + com.apple.security.network.client + + com.apple.security.network.server + + com.apple.security.print + + + diff --git a/example-motion-ribbons/openFrameworks-Info.plist b/example-motion-ribbons/openFrameworks-Info.plist new file mode 100644 index 0000000..c362e38 --- /dev/null +++ b/example-motion-ribbons/openFrameworks-Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + ${DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIconFile + ${ICON_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${TARGET_NAME} + CFBundlePackageType + APPL + CFBundleSignature + ???? + CFBundleShortVersionString + ${MARKETING_VERSION} + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSCameraUsageDescription + This app needs to access the camera + NSMicrophoneUsageDescription + This app needs to access the microphone + NSHighResolutionCapable + ${HIGH_RESOLUTION_CAPABLE} + NSCameraUseContinuityCameraDeviceType + + + diff --git a/example-motion-ribbons/src/main.cpp b/example-motion-ribbons/src/main.cpp new file mode 100644 index 0000000..ea192d4 --- /dev/null +++ b/example-motion-ribbons/src/main.cpp @@ -0,0 +1,44 @@ +#include "ofMain.h" +#include "ofApp.h" + +#include +#include +#include + +int main(int argc, char* argv[]) { + AppOptions options; + try { + for (int i = 1; i < argc; ++i) { + const std::string argument = argv[i]; + if (argument == "--smoke-test") options.smokeTest = true; + else if (argument.starts_with("--capture=")) { + options.capturePath = argument.substr(10); + if (options.capturePath.empty()) throw std::invalid_argument("Capture path is empty."); + } else if (argument.starts_with("--capture-frame=")) { + const auto value = argument.substr(16); + std::size_t consumed = 0; + options.captureFrame = std::stoi(value, &consumed); + if (consumed != value.size()) throw std::invalid_argument("Capture frame must be an integer."); + } + else { + std::cerr << "Unknown option: " << argument << '\n'; + return 1; + } + } + if (options.captureFrame < 2 || options.captureFrame > 3600 || + (!options.capturePath.empty() && !of::filesystem::path(options.capturePath).is_absolute())) { + throw std::invalid_argument("Use an absolute capture path and a frame between 2 and 3600."); + } + } catch (const std::exception& error) { + std::cerr << error.what() << '\n'; + return 1; + } + ofGLFWWindowSettings settings; + settings.setSize(1440, 900); + settings.setGLVersion(3, 2); + settings.numSamples = 0; // The FBO owns the render target in interactive and automated runs. + settings.visible = !options.automated(); + auto window = ofCreateWindow(settings); + ofRunApp(window, std::make_shared(options)); + return ofRunMainLoop(); +} diff --git a/example-motion-ribbons/src/ofApp.cpp b/example-motion-ribbons/src/ofApp.cpp new file mode 100644 index 0000000..c939298 --- /dev/null +++ b/example-motion-ribbons/src/ofApp.cpp @@ -0,0 +1,228 @@ +#include "ofApp.h" +#include +#include +#include + +namespace { +constexpr std::size_t trailLength = 180; +constexpr std::array trackedJoints{ + "LeftWrist", "RightWrist", "LeftAnkle", "RightAnkle", "Head"}; +const std::array palette{ + ofFloatColor(0.30f, 0.92f, 0.94f), ofFloatColor(1.0f, 0.37f, 0.30f), + ofFloatColor(0.93f, 0.91f, 0.79f)}; +} + +void ofApp::setup() { + ofSetWindowTitle("Motion Ribbons | perfume-dev"); + ofSetFrameRate(options.automated() ? 0 : 60); + ofSetVerticalSync(!options.automated()); + ofBackground(7, 10, 16); + ofFbo::Settings target; + target.width = 1440; + target.height = 900; + target.internalformat = GL_RGBA; + target.useDepth = true; + target.textureTarget = GL_TEXTURE_2D; + canvas.allocate(target); + + bool loaded = true; + for (int dancer = 0; dancer < 3; ++dancer) { + const auto filename = std::string(1, static_cast('A' + dancer)) + "_test.bvh"; + loaded = motions[dancer].load("../../../example-bvh/bin/data/" + filename) && loaded; + motions[dancer].setLoop(true); + motions[dancer].play(); + if (motions[dancer].isLoaded()) { + motions[dancer].setPosition(0.0f); + motions[dancer].update(0.0f); + rootOffsets[dancer] = glm::vec3(motions[dancer].getJoint(0)->getPosition()); + for (const auto* name : trackedJoints) loaded = motions[dancer].getJoint(name) && loaded; + } + } + // GLSL 150 is the portable desktop GL 3.2 geometry-shader baseline. + loaded = ribbonShader.setupShaderFromFile(GL_VERTEX_SHADER, "shaders/ribbon.vert") && loaded; + loaded = ribbonShader.setupShaderFromFile(GL_GEOMETRY_SHADER, "shaders/ribbon.geom") && loaded; + loaded = ribbonShader.setupShaderFromFile(GL_FRAGMENT_SHADER, "shaders/ribbon.frag") && loaded; + loaded = ribbonShader.bindDefaults() && loaded; + loaded = ribbonShader.linkProgram() && loaded; + // OF 0.12.1's isLoaded/linkProgram result alone does not guarantee successful linkage. + GLint linked = GL_FALSE; + glGetProgramiv(ribbonShader.getProgram(), GL_LINK_STATUS, &linked); + ready = loaded && linked == GL_TRUE && canvas.isAllocated(); + if (!ready) { + ofLogError("Motion Ribbons") << "Required BVH, shader, or framebuffer could not be loaded."; + ofExit(1); + return; + } + ribbons.setMode(OF_PRIMITIVE_LINES); + skeleton.setMode(OF_PRIMITIVE_LINES); + camera.setNearClip(1.0f); + camera.setFarClip(4000.0f); + camera.setFov(36.0f); + camera.setPosition(0.0f, 190.0f, 850.0f); + camera.lookAt(glm::vec3(0.0f, 0.0f, -55.0f)); + if (options.automated()) camera.disableMouseInput(); + reset(); +} + +glm::vec3 ofApp::position(int dancer, const ofxBvhJoint* joint) const { + // Remove horizontal stage travel so the three studies stay in their own columns. + const glm::vec3 root(motions[dancer].getJoint(0)->getPosition()); + const glm::vec3 origin(root.x, rootOffsets[dancer].y, root.z); + return glm::vec3(joint->getPosition()) - origin + + glm::vec3((dancer - 1) * 230.0f, 0.0f, 0.0f); +} + +void ofApp::reset() { + timeline = 12.0f; + for (auto& dancer : trails) for (auto& trail : dancer) trail.clear(); + for (auto& motion : motions) { + motion.setPosition(timeline / motion.getDuration()); + motion.update(0.0f); + } + previousJoint = position(0, motions[0].getJoint("LeftWrist")); + traveled = 0.0f; + rebuildMeshes(); // Reset also takes effect immediately while playback is paused. +} + +void ofApp::update() { + if (!ready || paused) return; + const float delta = options.automated() ? 1.0f / 60.0f : static_cast(std::min(ofGetLastFrameTime(), 0.05)); + timeline += delta; + for (int dancer = 0; dancer < 3; ++dancer) { + auto& motion = motions[dancer]; + const float seconds = std::fmod(timeline, motion.getDuration()); + // A wrap is a discontinuity; never join the last pose to the first with a ribbon. + if (seconds < delta) for (auto& trail : trails[dancer]) trail.clear(); + motion.setPosition(seconds / motion.getDuration()); + motion.update(0.0f); + for (std::size_t joint = 0; joint < trackedJoints.size(); ++joint) { + auto& trail = trails[dancer][joint]; + trail.push_back(position(dancer, motion.getJoint(trackedJoints[joint]))); + if (trail.size() > trailLength) trail.pop_front(); + } + } + const auto current = position(0, motions[0].getJoint("LeftWrist")); + traveled += glm::distance(previousJoint, current); + previousJoint = current; + rebuildMeshes(); +} + +void ofApp::rebuildMeshes() { + ribbons.clear(); + skeleton.clear(); + for (int dancer = 0; dancer < 3; ++dancer) { + for (const auto& trail : trails[dancer]) { + for (std::size_t i = 1; i < trail.size(); ++i) { + for (const auto sample : {i - 1, i}) { + auto color = palette[dancer]; + color.a = std::pow(static_cast(sample + 1) / trail.size(), 0.8f) * 0.90f; + ribbons.addVertex(trail[sample]); + ribbons.addColor(color); + } + } + } + const auto& motion = motions[dancer]; + for (int j = 0; j < motion.getNumJoints(); ++j) { + const auto* joint = motion.getJoint(j); + if (!joint->getParent()) continue; + skeleton.addVertex(position(dancer, joint)); + skeleton.addVertex(position(dancer, joint->getParent())); + const auto tint = palette[dancer].getLerped(ofFloatColor(1.0f), 0.6f); + skeleton.addColor(ofFloatColor(tint.r, tint.g, tint.b, 0.35f)); + skeleton.addColor(ofFloatColor(tint.r, tint.g, tint.b, 0.35f)); + } + } +} + +void ofApp::draw() { + if (!ready) return; + canvas.begin(); + ofClear(7, 10, 16, 255); + ofEnableBlendMode(OF_BLENDMODE_ALPHA); + ofSetColor(26, 33, 43); + ofDrawLine(68, 104, 1372, 104); + ofDrawLine(68, 808, 1372, 808); + camera.begin(ofRectangle(0, 0, canvas.getWidth(), canvas.getHeight())); + // Pixel widths remain stable when orbiting or resizing. Soft alpha avoids a heavy neon bloom. + ribbonShader.begin(); + ribbonShader.setUniform2f("uViewport", canvas.getWidth(), canvas.getHeight()); + ribbonShader.setUniform1f("uWidth", 10.5f); + ribbons.draw(); + ribbonShader.setUniform1f("uWidth", 1.6f); + skeleton.draw(); + ribbonShader.end(); + camera.end(); + + ofSetColor(225, 229, 230); + ofDrawBitmapString("M O T I O N R I B B O N S", 68, 68); + ofSetColor(126, 140, 156); + ofDrawBitmapString("PERFUME / MOTION STUDY 01", 1110, 68); + ofDrawBitmapString("A / CYAN", 245, 775); + ofDrawBitmapString("B / CORAL", 672, 775); + ofDrawBitmapString("C / IVORY", 1101, 775); + if (showHelp) { + ofDrawBitmapString("SPACE pause / R reset / drag orbit / scroll zoom / H help", 68, 849); + ofDrawBitmapString("VERTEX > GEOMETRY > FRAGMENT / " + ofToString(timeline, 2) + " s" + + (paused ? " PAUSED" : ""), 941, 849); + } + canvas.end(); + ofSetColor(255); + // Preserve composition in tall/wide windows with centered letterboxing. + const float scale = std::min(ofGetWidth() / canvas.getWidth(), ofGetHeight() / canvas.getHeight()); + const ofRectangle viewport((ofGetWidth() - canvas.getWidth() * scale) * 0.5f, + (ofGetHeight() - canvas.getHeight() * scale) * 0.5f, + canvas.getWidth() * scale, canvas.getHeight() * scale); + canvas.draw(viewport); + camera.setControlArea(viewport); + finishAutomatedFrame(); +} + +void ofApp::finishAutomatedFrame() { + if (!options.automated() || ++renderedFrames < options.captureFrame) return; + ofPixels pixels; + canvas.readToPixels(pixels); + if (!pixels.isAllocated() || pixels.getWidth() != 1440 || pixels.getHeight() != 900 || + pixels.getNumChannels() != 4) { + ofLogError("Motion Ribbons") << "Framebuffer readback failed."; + ofExit(1); + return; + } + std::size_t brightPixels = 0; + // Exclude every text/line overlay: only the central artwork can satisfy the check. + for (std::size_t y = 125; y < 750; ++y) { + for (std::size_t x = 68; x < 1372; ++x) { + const auto i = (y * pixels.getWidth() + x) * pixels.getNumChannels(); + if (pixels[i] > 70 || pixels[i + 1] > 70 || pixels[i + 2] > 70) ++brightPixels; + } + } + if (glGetError() != GL_NO_ERROR || brightPixels < 1000 || traveled < 1.0f) { + ofLogError("Motion Ribbons") << "Render validation failed: pixels=" << brightPixels << ", motion=" << traveled; + ofExit(1); + return; + } + const float validatedMotion = traveled; + if (options.smokeTest) { + const float frozenTime = timeline; + keyPressed(' '); + update(); + if (!paused || timeline != frozenTime) { ofExit(1); return; } + keyPressed('r'); + if (timeline != 12.0f) { ofExit(1); return; } + keyPressed(' '); + } + if (!options.capturePath.empty() && !ofSaveImage(pixels, options.capturePath)) { + ofLogError("Motion Ribbons") << "Could not save capture."; + ofExit(1); + return; + } + std::cout << "SMOKE_TEST_OK frames=" << renderedFrames << " brightPixels=" << brightPixels + << " motionDistance=" << validatedMotion << " shaderStages=3" + << " controlsChecked=" << options.smokeTest << std::endl; + ofExit(0); +} + +void ofApp::keyPressed(int key) { + if (key == ' ') paused = !paused; + if (key == 'r' || key == 'R') reset(); + if (key == 'h' || key == 'H') showHelp = !showHelp; +} diff --git a/example-motion-ribbons/src/ofApp.h b/example-motion-ribbons/src/ofApp.h new file mode 100644 index 0000000..1836ac0 --- /dev/null +++ b/example-motion-ribbons/src/ofApp.h @@ -0,0 +1,43 @@ +#pragma once +#include "ofMain.h" +#include "ofxBvh.h" +#include +#include + +struct AppOptions { + bool smokeTest = false; + std::string capturePath; + int captureFrame = 180; + bool automated() const { return smokeTest || !capturePath.empty(); } +}; + +class ofApp : public ofBaseApp { +public: + explicit ofApp(AppOptions options) : options(std::move(options)) {} + void setup() override; + void update() override; + void draw() override; + void keyPressed(int key) override; + +private: + using Pose = std::array; + AppOptions options; + std::array motions; + std::array, 5>, 3> trails; + std::array rootOffsets{}; + ofEasyCam camera; + ofShader ribbonShader; + ofVboMesh ribbons, skeleton; + ofFbo canvas; + bool paused = false; + bool showHelp = true; + bool ready = false; + float timeline = 12.0f; + float traveled = 0.0f; + int renderedFrames = 0; + glm::vec3 previousJoint{}; + void reset(); + void rebuildMeshes(); + glm::vec3 position(int dancer, const ofxBvhJoint* joint) const; + void finishAutomatedFrame(); +}; diff --git a/example-pen-graphics/example-pen-graphics.xcodeproj/project.pbxproj b/example-pen-graphics/example-pen-graphics.xcodeproj/project.pbxproj index efeb1ee..0185c05 100644 --- a/example-pen-graphics/example-pen-graphics.xcodeproj/project.pbxproj +++ b/example-pen-graphics/example-pen-graphics.xcodeproj/project.pbxproj @@ -57,14 +57,14 @@ "fileEncoding": "4", "isa": "PBXFileReference", "lastKnownFileType": "sourcecode.cpp.cpp", - "name": "testApp.cpp", + "name": "ofApp.cpp", "sourceTree": "" }, "68E49E5E-0536-4955-B144-FA5AC4F1BE03": { "fileEncoding": "4", "isa": "PBXFileReference", "lastKnownFileType": "sourcecode.cpp.h", - "name": "testApp.h", + "name": "ofApp.h", "sourceTree": "" }, "AD7E4701-E012-467E-B335-FEF0977E4F7B": { diff --git a/example-pen-graphics/src/main.cpp b/example-pen-graphics/src/main.cpp index 07619f2..dc879fc 100644 --- a/example-pen-graphics/src/main.cpp +++ b/example-pen-graphics/src/main.cpp @@ -1,15 +1,5 @@ -#include "ofMain.h" -#include "testApp.h" +#include "ofApp.h" -//======================================================================== -int main() { - ofGLFWWindowSettings settings; - settings.setSize(1280, 720); - settings.setGLVersion(2, 1); - settings.windowMode = OF_WINDOW; - settings.numSamples = 4; - - auto window = ofCreateWindow(settings); - ofRunApp(window, std::make_shared()); - ofRunMainLoop(); +int main(int argc, char** argv) { + return example::launch(argc, argv, "example-pen-graphics"); } diff --git a/example-pen-graphics/src/ofApp.cpp b/example-pen-graphics/src/ofApp.cpp new file mode 100644 index 0000000..4acf9c8 --- /dev/null +++ b/example-pen-graphics/src/ofApp.cpp @@ -0,0 +1,122 @@ +#include "ofApp.h" + +void ofApp::RibbonTrail::update(const glm::vec3& noiseOffset) { + if (!motion->isFrameNew()) return; + for (size_t age = 0; age < history.size(); ++age) { + const float t = float(age) / float(history.size()); + for (auto& position : history[age]) { + glm::vec3 force{0}; + force.y = -2.5f * (1.0f - std::sin(t * t * PI)) + ofNoise(position.y * 0.0001f + noiseOffset.y) * 1.4f; + force.x = ofSignedNoise(position.x * 0.0001f + noiseOffset.x) * 3.0f; + force.z = ofSignedNoise(position.z * 0.0001f + noiseOffset.z) * 3.0f; + if (position.y < 0) force *= glm::vec3(5.0f, 0.02f, 5.0f); + position += force; + } + } + Frame frame; + for (int i = 0; i < motion->getNumJoints(); ++i) { + const auto* joint = motion->getJoint(i); + for (const auto* child : joint->getChildren()) { + frame.emplace_back(joint->getPosition()); + frame.emplace_back(child->getPosition()); + } + } + history.push_front(std::move(frame)); + if (history.size() > 200) history.pop_back(); + surfaces.clear(); + outlines.clear(); + for (size_t bone = 0; bone + 1 < history.front().size(); bone += 2) { + ofVboMesh strip; + strip.setMode(OF_PRIMITIVE_TRIANGLE_STRIP); + ofPolyline left; + ofPolyline right; + glm::vec3 normal{0, 1, 0}; + for (size_t age = 0; age + 1 < history.size(); ++age) { + const float t = ofMap(float(age), 0, float(history.size()), 0.1f, 1.0f); + const auto& a = history[age][bone]; + const auto& b = history[age][bone + 1]; + const glm::vec3 direction = a - b; + const auto side = example::safeNormalize(glm::cross(direction, glm::vec3(0, 1, 0))); + normal = example::safeNormalize(glm::mix(normal, example::safeNormalize(glm::cross(side, direction)), 0.3f)); + const auto tip = glm::mix(b, a, t); + strip.addVertex(a); + strip.addNormal(normal); + strip.addVertex(tip); + strip.addNormal(normal); + left.addVertex(a); + right.addVertex(tip); + } + surfaces.push_back(std::move(strip)); + outlines.push_back(std::move(left)); + outlines.push_back(std::move(right)); + } + skeleton.clear(); + skeleton.setMode(OF_PRIMITIVE_LINES); + skeleton.addVertices(history.front()); +} + +void ofApp::RibbonTrail::draw() { + // Polygon offset remains part of core OpenGL; geometry lives in vertex buffers. + glEnable(GL_POLYGON_OFFSET_FILL); + glPolygonOffset(1, 1); + ofSetColor(255); + for (auto& surface : surfaces) surface.draw(); + glDisable(GL_POLYGON_OFFSET_FILL); + ofSetColor(0); + for (const auto& outline : outlines) outline.draw(); + skeleton.draw(); +} + +void ofApp::setup() { + runtime.setup(); + ofBackground(255); + playback.load("example-pen-graphics", false, runtime.isSmokeTest()); + for (size_t i = 0; i < trails.size(); ++i) trails[i].motion = &playback.motions[i]; + noiseOffset = {ofRandom(1), ofRandom(1), ofRandom(1)}; + noiseVelocity = {ofRandom(0.001f), ofRandom(0.005f), ofRandom(0.001f)}; + ground.setMode(OF_PRIMITIVE_LINES); + for (int x = -10; x < 10; ++x) { + for (int z = -10; z < 10; ++z) { + const glm::vec3 position(x * 500, 0, z * 500); + ground.addVertex(position + glm::vec3(-10, 0, 0)); + ground.addVertex(position + glm::vec3(10, 0, 0)); + ground.addVertex(position + glm::vec3(0, 0, -10)); + ground.addVertex(position + glm::vec3(0, 0, 10)); + } + } +} + +void ofApp::update() { + playback.update(runtime.deltaSeconds()); + if (!playback.ready) return; + glm::vec3 targetCenter{0}; + for (const auto& motion : playback.motions) targetCenter += glm::vec3(motion.getJoint(0)->getPosition()); + center = glm::mix(center, targetCenter / 3.0f, 1.0f - std::exp(-0.6f * runtime.deltaSeconds())); + for (auto& trail : trails) trail.update(noiseOffset); + noiseOffset += noiseVelocity * runtime.deltaSeconds() * 60.0f; + cameraPosition = glm::mix(cameraPosition, cameraTarget, 1.0f - std::exp(-0.6f * runtime.deltaSeconds())); + camera.setPosition(cameraPosition); + camera.lookAt(glm::vec3(0)); +} + +void ofApp::draw() { + ofEnableDepthTest(); + ofEnableBlendMode(OF_BLENDMODE_ALPHA); + camera.begin(); + ofPushMatrix(); + ofRotateYDeg(runtime.elapsedSeconds() * 20.0f); + ofTranslate(-center.x, -100, -center.z); + ofSetColor(200); + ground.draw(); + for (auto& trail : trails) trail.draw(); + ofPopMatrix(); + camera.end(); + ofDisableDepthTest(); + ofSetColor(20); + playback.drawStatus(); + runtime.finishFrame(playback.ready, "example-pen-graphics", trails.front().skeleton.getNumVertices()); +} + +void ofApp::keyPressed(int) { + cameraTarget = {ofRandom(-600, 600), ofRandom(-100, 200), ofRandom(-600, 600)}; +} diff --git a/example-pen-graphics/src/ofApp.h b/example-pen-graphics/src/ofApp.h new file mode 100644 index 0000000..81d13fc --- /dev/null +++ b/example-pen-graphics/src/ofApp.h @@ -0,0 +1,35 @@ +#pragma once + +#include "../../shared/ExampleRuntime.h" +#include + +class ofApp : public ofBaseApp { +public: + explicit ofApp(const example::RunOptions& options) : runtime(options) {} + void setup() override; + void update() override; + void draw() override; + void keyPressed(int key) override; + +private: + struct RibbonTrail { + using Frame = std::vector; + ofxBvh* motion = nullptr; + std::deque history; + std::vector surfaces; + std::vector outlines; + ofVboMesh skeleton; + void update(const glm::vec3& noiseOffset); + void draw(); + }; + example::Runtime runtime; + example::MotionPlayback playback; + std::array trails; + ofCamera camera; + ofVboMesh ground; + glm::vec3 center{0}; + glm::vec3 cameraPosition{0, 60, -420}; + glm::vec3 cameraTarget{0, 60, -420}; + glm::vec3 noiseOffset{0}; + glm::vec3 noiseVelocity{0}; +}; diff --git a/example-pen-graphics/src/testApp.cpp b/example-pen-graphics/src/testApp.cpp deleted file mode 100644 index b4620a1..0000000 --- a/example-pen-graphics/src/testApp.cpp +++ /dev/null @@ -1,415 +0,0 @@ -#include "testApp.h" - -#include - -namespace { - -const std::array kPerfumeMotionFiles = { - "bvhfiles/aachan.bvh", - "bvhfiles/kashiyuka.bvh", - "bvhfiles/nocchi.bvh" -}; - -const std::array kBundledMotionFiles = { - "../../../example-bvh/bin/data/A_test.bvh", - "../../../example-bvh/bin/data/B_test.bvh", - "../../../example-bvh/bin/data/C_test.bvh" -}; - -bool loadMotionFiles(std::array& motions) { - bool loaded = true; - for (size_t i = 0; i < motions.size(); ++i) { - if (!motions[i].load(kPerfumeMotionFiles[i])) { - ofLogWarning("example-pen-graphics") - << "Using bundled motion fallback for " << kPerfumeMotionFiles[i]; - loaded = motions[i].load(kBundledMotionFiles[i]) && loaded; - } - } - return loaded; -} - -} // namespace - -ofVec3f center, center_t; -ofVec3f campos, campos_t; -ofVec3f offset, offset_v; - -class Tracker -{ -public: - - ofxBvh *bvh = nullptr; - - typedef vector Frame; - deque track; - - struct Buffer - { - ofVec3f v1, v2; - ofVec3f norm; - }; - - typedef vector BufferArray; - vector buffer; - - void setup(ofxBvh *o) - { - bvh = o; - } - - void update() - { - if (bvh->isFrameNew()) - { - // update vertexes flow - - for (size_t i = 0; i < track.size(); ++i) - { - float delta = ofMap(static_cast(i), 0.0f, - static_cast(track.size()), 0.0f, 1.0f); - Frame &f = track[i]; - - for (auto& v : f) - { - ofVec3f force{}; - - // gravity - force.y -= 2.5 * (1 - sin(pow(delta, 2) * PI)); - force.y += ofNoise(v.y * 0.0001 + offset.y) * 1.4; - - force.x += ofSignedNoise(v.x * 0.0001 + offset.x) * 3; - force.z += ofSignedNoise(v.z * 0.0001 + offset.z) * 3; - - if (v.y < 0) - { - force.y *= 0.02; - force.x *= 5; - force.z *= 5; - } - - v += force; - } - } - - Frame f; - for (int i = 0; i < bvh->getNumJoints(); ++i) - { - const ofxBvhJoint *j = bvh->getJoint(i); - for (size_t n = 0; n < j->getChildren().size(); ++n) - { - f.push_back(j->getPosition()); - f.push_back(j->getChildren().at(n)->getPosition()); - } - } - - track.push_front(f); - - if (track.size() > 200) - track.pop_back(); - - - // cache vertexes - - buffer.clear(); - for (size_t n = 0; n + 1 < track.front().size(); n += 2) - { - ofVec3f norm; - - BufferArray arr; - - for (size_t i = 0; i + 1 < track.size(); ++i) - { - float delta = ofMap(static_cast(i), 0.0f, - static_cast(track.size()), 0.1f, 1.0f); - Frame &f1 = track[i]; - - const ofVec3f &v1 = f1[n]; - const ofVec3f &v2 = f1[n + 1]; - const ofVec3f d = v1 - v2; - - const ofVec3f c1 = d.getCrossed(ofVec3f(0, 1, 0)).getNormalized(); - const ofVec3f c = c1.getCrossed(d).getNormalized(); - // if (c.y < 0) c *= -1; - - if (i == 0) norm.set(c); - - ofVec3f m = v1 * delta + v2 * (1 - delta); - norm += (c - norm) * 0.3; - - Buffer buf; - - buf.norm = norm; - buf.v1 = v1; - buf.v2 = m; - - arr.push_back(buf); - } - - buffer.push_back(arr); - } - - } - } - - void draw() - { - glEnable(GL_POLYGON_OFFSET_FILL); - glPolygonOffset(1, 1); - - // draw polygons - - ofSetColor(255); - - for (size_t i = 0; i < buffer.size(); ++i) - { - const BufferArray &arr = buffer[i]; - - glBegin(GL_TRIANGLE_STRIP); - for (const auto& b : arr) - { - glNormal3fv(b.norm.getPtr()); - glVertex3fv(b.v1.getPtr()); - glVertex3fv(b.v2.getPtr()); - } - glEnd(); - } - - // draw outline - - ofSetColor(0); - - for (size_t i = 0; i < buffer.size(); ++i) - { - const BufferArray &arr = buffer[i]; - - glBegin(GL_LINE_STRIP); - for (const auto& b : arr) - { - glNormal3fv(b.norm.getPtr()); - glVertex3fv(b.v1.getPtr()); - } - glEnd(); - } - - for (size_t i = 0; i < buffer.size(); ++i) - { - const BufferArray &arr = buffer[i]; - - glBegin(GL_LINE_STRIP); - for (const auto& b : arr) - { - glNormal3fv(b.norm.getPtr()); - glVertex3fv(b.v2.getPtr()); - } - glEnd(); - } - - if (!track.empty()) - { - Frame &f = track[0]; - - glBegin(GL_LINES); - for (size_t n = 0; n + 1 < f.size(); n += 2) - { - ofVec3f &v1 = f[n]; - ofVec3f &v2 = f[n + 1]; - - glVertex3fv(v1.getPtr()); - glVertex3fv(v2.getPtr()); - } - glEnd(); - } - - glDisable(GL_POLYGON_OFFSET_FILL); - glPolygonOffset(0, 0); - } - -}; - -vector> trackers; - -//-------------------------------------------------------------- -void testApp::setup() -{ - ofSetFrameRate(60); - ofSetVerticalSync(true); - - ofSetSmoothLighting(true); - ofSetGlobalAmbientColor(ofColor(220)); - - ofBackground(255); - - assetsReady = loadMotionFiles(bvh); - - for (auto& motion : bvh) - { - motion.setFrame(4); - } - - audioReady = ofFile::doesFileExist("Perfume_globalsite_sound.wav") - && track.load("Perfume_globalsite_sound.wav"); - if (audioReady) { - track.setLoop(true); - track.play(); - } else { - ofLogNotice("example-pen-graphics") - << "Audio file not found; using a silent internal clock."; - } - - // setup tracker - if (assetsReady) - { - for (auto& motion : bvh) - { - auto tracker = std::make_unique(); - tracker->setup(&motion); - trackers.push_back(std::move(tracker)); - } - } - - offset.x = ofRandom(1); - offset.y = ofRandom(1); - offset.z = ofRandom(1); - offset_v.x = ofRandom(0.001); - offset_v.y = ofRandom(0.005); - offset_v.z = ofRandom(0.001); - - campos_t.set(0, 0, -300); -} - -//-------------------------------------------------------------- -void testApp::update() -{ - if (!assetsReady) return; - const float motionDuration = bvh[0].getDuration(); - const float motionSeconds = audioReady - ? track.getPosition() * track.getDuration() - : std::fmod(ofGetElapsedTimef(), motionDuration); - const float t = ofClamp(motionSeconds / motionDuration, 0.0f, 1.0f); - - center_t.set(0, 0, 0); - - for (auto& motion : bvh) - { - motion.setPosition(t); - motion.update(); - - center_t += motion.getJoint(0)->getPosition(); - } - - center_t /= 3; - center += (center_t - center) * 0.01; - - for (auto& tracker : trackers) - { - tracker->update(); - } - - offset += offset_v; - - cam.setPosition(campos.x, campos.y, campos.z); - cam.lookAt(ofVec3f(0, 0, 0)); - campos += (campos_t - campos) * 0.01; -} - -//-------------------------------------------------------------- -void testApp::draw(){ - glEnable(GL_DEPTH_TEST); - glShadeModel(GL_SMOOTH); - - ofEnableSmoothing(); - - glEnable(GL_LINE_SMOOTH); - glHint(GL_LINE_SMOOTH_HINT, GL_NICEST); - glLineWidth(1); - - ofEnableBlendMode(OF_BLENDMODE_ALPHA); - - light.enable(); - light.setPosition(0, -500, 0); - - cam.begin(); - - ofPushMatrix(); - { - glRotatef(ofGetElapsedTimef() * 20, 0, 1, 0); - glTranslatef(-center.x, -100, -center.z); - - ofSetColor(200); - - for (int x = -10; x < 10; x++) - { - for (int y = -10; y < 10; y++) - { - ofPushMatrix(); - glTranslatef(x * 500, 0, y * 500); - ofDrawLine(10, 0, 0, -10, 0, 0); - ofDrawLine(0, 0, 10, 0, 0, -10); - ofPopMatrix(); - } - } - - ofSetColor(ofColor::white, 80); - for (auto& tracker : trackers) - { - tracker->draw(); - } - } - ofPopMatrix(); - - cam.end(); - - light.disable(); - ofDisableDepthTest(); - ofSetColor(20); - - if (!assetsReady) - ofDrawBitmapString("Motion data is missing. See README.md.", 10, 20); - else if (!audioReady) - ofDrawBitmapString("Audio data is missing; running with a silent clock.", 10, 20); - -} - -//-------------------------------------------------------------- -void testApp::keyPressed(int key){ - campos_t.x = ofRandom(-600, 600); - campos_t.z = ofRandom(-600, 600); - campos_t.y = ofRandom(-100, 200); -} - -//-------------------------------------------------------------- -void testApp::keyReleased(int key){ - -} - -//-------------------------------------------------------------- -void testApp::mouseMoved(int x, int y ){ - -} - -//-------------------------------------------------------------- -void testApp::mouseDragged(int x, int y, int button){ - -} - -//-------------------------------------------------------------- -void testApp::mousePressed(int x, int y, int button){ - -} - -//-------------------------------------------------------------- -void testApp::mouseReleased(int x, int y, int button){ -} - -//-------------------------------------------------------------- -void testApp::windowResized(int w, int h){ -} - -//-------------------------------------------------------------- -void testApp::gotMessage(ofMessage msg){ - -} - -//-------------------------------------------------------------- -void testApp::dragEvent(ofDragInfo dragInfo){ -} diff --git a/example-pen-graphics/src/testApp.h b/example-pen-graphics/src/testApp.h deleted file mode 100644 index 6d384af..0000000 --- a/example-pen-graphics/src/testApp.h +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include "ofMain.h" -#include "ofxBvh.h" - -#include - -class testApp : public ofBaseApp { - - public: - void setup() override; - void update() override; - void draw() override; - - void keyPressed(int key) override; - void keyReleased(int key) override; - void mouseMoved(int x, int y) override; - void mouseDragged(int x, int y, int button) override; - void mousePressed(int x, int y, int button) override; - void mouseReleased(int x, int y, int button) override; - void windowResized(int w, int h) override; - void dragEvent(ofDragInfo dragInfo) override; - void gotMessage(ofMessage msg) override; - - ofSoundPlayer track; - std::array bvh; - - ofCamera cam; - ofLight light; - bool assetsReady = false; - bool audioReady = false; -}; diff --git a/example-sync-sound/example-sync-sound.xcodeproj/project.pbxproj b/example-sync-sound/example-sync-sound.xcodeproj/project.pbxproj index 7d6e51c..74cd2f0 100644 --- a/example-sync-sound/example-sync-sound.xcodeproj/project.pbxproj +++ b/example-sync-sound/example-sync-sound.xcodeproj/project.pbxproj @@ -74,7 +74,7 @@ "fileEncoding": "4", "isa": "PBXFileReference", "lastKnownFileType": "sourcecode.cpp.h", - "name": "testApp.h", + "name": "ofApp.h", "sourceTree": "" }, "BB4B014C10F69532006C3DED": { @@ -88,7 +88,7 @@ "fileEncoding": "4", "isa": "PBXFileReference", "lastKnownFileType": "sourcecode.cpp.cpp", - "name": "testApp.cpp", + "name": "ofApp.cpp", "sourceTree": "" }, "C46DECE4-C688-4892-B7C0-BBA05139EB84": { diff --git a/example-sync-sound/src/main.cpp b/example-sync-sound/src/main.cpp index a79738a..6130ef3 100644 --- a/example-sync-sound/src/main.cpp +++ b/example-sync-sound/src/main.cpp @@ -1,14 +1,5 @@ -#include "ofMain.h" -#include "testApp.h" +#include "ofApp.h" -//======================================================================== -int main() { - ofGLWindowSettings settings; - settings.setSize(1024, 768); - settings.setGLVersion(2, 1); - settings.windowMode = OF_WINDOW; - - auto window = ofCreateWindow(settings); - ofRunApp(window, std::make_shared()); - ofRunMainLoop(); +int main(int argc, char** argv) { + return example::launch(argc, argv, "example-sync-sound"); } diff --git a/example-sync-sound/src/ofApp.cpp b/example-sync-sound/src/ofApp.cpp new file mode 100644 index 0000000..ab7d78d --- /dev/null +++ b/example-sync-sound/src/ofApp.cpp @@ -0,0 +1,63 @@ +#include "ofApp.h" + +void ofApp::Trail::update() { + if (!joint->getBvh()->isFrameNew()) return; + points.push_front(glm::vec3(joint->getPosition())); + if (points.size() > 15) points.pop_back(); + mesh.clear(); + mesh.setMode(OF_PRIMITIVE_LINE_STRIP); + for (size_t i = 0; i + 1 < points.size(); ++i) { + const float age = float(i) / float(points.size() - 1); + const float alpha = (1.0f - age) * ofMap(glm::distance(points[i], points[i + 1]), 3, 5, 0, 1, true); + mesh.addVertex(points[i]); + mesh.addColor(ofFloatColor(1, 1, 1, alpha)); + } +} + +void ofApp::setup() { + runtime.setup(); + ofBackground(0); + playback.load("example-sync-sound", false, runtime.isSmokeTest()); + if (!playback.ready) return; + for (auto& motion : playback.motions) { + for (int i = 0; i < motion.getNumJoints(); ++i) { + Trail trail; + trail.joint = motion.getJoint(i); + trails.push_back(std::move(trail)); + } + } +} + +void ofApp::update() { + const float dt = runtime.deltaSeconds(); + rotation += 6.0f * dt; + playbackSpeed = glm::mix(playbackSpeed, targetSpeed, 1.0f - std::exp(-21.4f * dt)); + playback.update(dt, playbackSpeed); + for (auto& trail : trails) trail.update(); +} + +void ofApp::draw() { + ofEnableDepthTest(); + ofEnableBlendMode(OF_BLENDMODE_ALPHA); + ofPushMatrix(); + ofTranslate(ofGetWidth() * 0.5f, ofGetHeight() * 0.5f + 150.0f); + ofRotateXDeg(-15); + ofRotateYDeg(rotation); + ofScale(1, -1, 1); + ofSetColor(255); + ofDrawLine(glm::vec3(-100, 0, 0), glm::vec3(100, 0, 0)); + ofDrawLine(glm::vec3(0, 0, -100), glm::vec3(0, 0, 100)); + for (auto& motion : playback.motions) motion.draw(); + ofDisableDepthTest(); + ofEnableBlendMode(OF_BLENDMODE_ADD); + for (auto& trail : trails) trail.mesh.draw(); + ofPopMatrix(); + ofEnableBlendMode(OF_BLENDMODE_ALPHA); + ofSetColor(255); + ofDrawBitmapString("Hold a key to scratch\nPlayback speed: " + ofToString(playbackSpeed, 1), 10, 20); + playback.drawStatus(10, 65); + runtime.finishFrame(playback.ready, "example-sync-sound", playback.motions.front().getNumJoints() * 48); +} + +void ofApp::keyPressed(int) { targetSpeed = -1.0f; } +void ofApp::keyReleased(int) { targetSpeed = 1.0f; } diff --git a/example-sync-sound/src/ofApp.h b/example-sync-sound/src/ofApp.h new file mode 100644 index 0000000..2adc8db --- /dev/null +++ b/example-sync-sound/src/ofApp.h @@ -0,0 +1,28 @@ +#pragma once + +#include "../../shared/ExampleRuntime.h" +#include + +class ofApp : public ofBaseApp { +public: + explicit ofApp(const example::RunOptions& options) : runtime(options) {} + void setup() override; + void update() override; + void draw() override; + void keyPressed(int key) override; + void keyReleased(int key) override; + +private: + struct Trail { + const ofxBvhJoint* joint = nullptr; + std::deque points; + ofVboMesh mesh; + void update(); + }; + example::Runtime runtime; + example::MotionPlayback playback; + std::vector trails; + float rotation = 0.0f; + float playbackSpeed = 1.0f; + float targetSpeed = 1.0f; +}; diff --git a/example-sync-sound/src/testApp.cpp b/example-sync-sound/src/testApp.cpp deleted file mode 100644 index 2ebc6fa..0000000 --- a/example-sync-sound/src/testApp.cpp +++ /dev/null @@ -1,250 +0,0 @@ -#include "testApp.h" - -#include - -namespace { - -const std::array kPerfumeMotionFiles = { - "bvhfiles/aachan.bvh", - "bvhfiles/kashiyuka.bvh", - "bvhfiles/nocchi.bvh" -}; - -const std::array kBundledMotionFiles = { - "../../../example-bvh/bin/data/A_test.bvh", - "../../../example-bvh/bin/data/B_test.bvh", - "../../../example-bvh/bin/data/C_test.bvh" -}; - -bool loadMotionFiles(std::array& motions) { - bool loaded = true; - for (size_t i = 0; i < motions.size(); ++i) { - if (!motions[i].load(kPerfumeMotionFiles[i])) { - ofLogWarning("example-sync-sound") - << "Using bundled motion fallback for " << kPerfumeMotionFiles[i]; - loaded = motions[i].load(kBundledMotionFiles[i]) && loaded; - } - } - return loaded; -} - -} // namespace - -class Tracker -{ -public: - - const ofxBvhJoint *joint = nullptr; - deque points; - - void setup(const ofxBvhJoint *o) - { - joint = o; - } - - void update() - { - if (joint->getBvh()->isFrameNew()) - { - points.push_front(joint->getPosition()); - - if (points.size() > 15) - points.pop_back(); - } - } - - void draw() - { - if (points.empty()) return; - - glBegin(GL_LINE_STRIP); - for (size_t i = 0; i + 1 < points.size(); ++i) - { - float a = ofMap(static_cast(i), 0.0f, - static_cast(points.size() - 1), 1.0f, 0.0f, true); - - ofVec3f &p0 = points[i]; - ofVec3f &p1 = points[i + 1]; - - float d = p0.distance(p1); - a *= ofMap(d, 3, 5, 0, 1, true); - - glColor4f(1, 1, 1, a); - glVertex3fv(points[i].getPtr()); - } - glEnd(); - } -}; - -vector> trackers; - -//-------------------------------------------------------------- -void testApp::setup() -{ - ofSetFrameRate(60); - ofSetVerticalSync(true); - - ofBackground(0); - - play_rate = play_rate_t = 1.0f; - rotate = 0.0f; - - assetsReady = loadMotionFiles(bvh); - - for (auto& motion : bvh) - { - motion.setFrame(1); - } - - audioReady = ofFile::doesFileExist("Perfume_globalsite_sound.wav") - && track.load("Perfume_globalsite_sound.wav"); - if (audioReady) { - track.setLoop(true); - track.play(); - } else { - ofLogNotice("example-sync-sound") - << "Audio file not found; using a silent internal clock."; - } - - // setup tracker - if (assetsReady) - { - for (auto& motion : bvh) - { - for (int n = 0; n < motion.getNumJoints(); ++n) - { - auto tracker = std::make_unique(); - tracker->setup(motion.getJoint(n)); - trackers.push_back(std::move(tracker)); - } - } - } -} - -//-------------------------------------------------------------- -void testApp::update() -{ - rotate += 0.1; - - play_rate += (play_rate_t - play_rate) * 0.3; - if (audioReady) track.setSpeed(play_rate); - if (!assetsReady) return; - - const float motionDuration = bvh[0].getDuration(); - float motionSeconds = 0.0f; - if (audioReady) { - motionSeconds = track.getPosition() * track.getDuration(); - } else { - fallbackPlayhead += ofGetLastFrameTime() * play_rate; - fallbackPlayhead = std::fmod(fallbackPlayhead, motionDuration); - if (fallbackPlayhead < 0.0f) fallbackPlayhead += motionDuration; - motionSeconds = fallbackPlayhead; - } - const float t = ofClamp(motionSeconds / motionDuration, 0.0f, 1.0f); - - for (auto& motion : bvh) - { - motion.setPosition(t); - motion.update(); - } - - for (auto& tracker : trackers) - { - tracker->update(); - } -} - -//-------------------------------------------------------------- -void testApp::draw(){ - ofEnableDepthTest(); - - ofEnableBlendMode(OF_BLENDMODE_ALPHA); - - ofPushMatrix(); - { - ofTranslate(ofGetWidth()/2, ofGetHeight()/2); - ofTranslate(0, 150); - - ofRotateDeg(-15, 1, 0, 0); - ofRotateDeg(rotate, 0, 1, 0); - - ofScale(1, -1, 1); - - ofSetColor(ofColor::white); - - ofFill(); - - // draw ground - ofPushMatrix(); - ofRotateDeg(90, 1, 0, 0); - ofDrawLine(100, 0, -100, 0); - ofDrawLine(0, 100, 0, -100); - ofPopMatrix(); - - // draw actor - for (auto& motion : bvh) - { - motion.draw(); - } - - // draw tracker - ofDisableDepthTest(); - ofEnableBlendMode(OF_BLENDMODE_ADD); - - ofSetColor(ofColor::white, 80); - for (auto& tracker : trackers) - { - tracker->draw(); - } - } - ofPopMatrix(); - - ofSetColor(255); - ofDrawBitmapString("press any key to scratch\nplay_rate: " + ofToString(play_rate, 1), 10, 20); - if (!assetsReady) - ofDrawBitmapString("Motion data is missing. See README.md.", 10, 65); - else if (!audioReady) - ofDrawBitmapString("Audio data is missing; running with a silent clock.", 10, 65); -} - -//-------------------------------------------------------------- -void testApp::keyPressed(int key){ - play_rate_t = -1; -} - -//-------------------------------------------------------------- -void testApp::keyReleased(int key){ - play_rate_t = 1; -} - -//-------------------------------------------------------------- -void testApp::mouseMoved(int x, int y ){ - -} - -//-------------------------------------------------------------- -void testApp::mouseDragged(int x, int y, int button){ - -} - -//-------------------------------------------------------------- -void testApp::mousePressed(int x, int y, int button){ - -} - -//-------------------------------------------------------------- -void testApp::mouseReleased(int x, int y, int button){ -} - -//-------------------------------------------------------------- -void testApp::windowResized(int w, int h){ -} - -//-------------------------------------------------------------- -void testApp::gotMessage(ofMessage msg){ - -} - -//-------------------------------------------------------------- -void testApp::dragEvent(ofDragInfo dragInfo){ -} diff --git a/example-sync-sound/src/testApp.h b/example-sync-sound/src/testApp.h deleted file mode 100644 index b39cbd2..0000000 --- a/example-sync-sound/src/testApp.h +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "ofMain.h" -#include "ofxBvh.h" - -#include - -class testApp : public ofBaseApp { - - public: - void setup() override; - void update() override; - void draw() override; - - void keyPressed(int key) override; - void keyReleased(int key) override; - void mouseMoved(int x, int y) override; - void mouseDragged(int x, int y, int button) override; - void mousePressed(int x, int y, int button) override; - void mouseReleased(int x, int y, int button) override; - void windowResized(int w, int h) override; - void dragEvent(ofDragInfo dragInfo) override; - void gotMessage(ofMessage msg) override; - - ofSoundPlayer track; - std::array bvh; - - float rotate = 0.0f; - - float play_rate = 1.0f; - float play_rate_t = 1.0f; - float fallbackPlayhead = 0.0f; - bool assetsReady = false; - bool audioReady = false; -}; diff --git a/marching-cubes/marching-cubes.xcodeproj/project.pbxproj b/marching-cubes/marching-cubes.xcodeproj/project.pbxproj index 848bd4b..2ced9f6 100644 --- a/marching-cubes/marching-cubes.xcodeproj/project.pbxproj +++ b/marching-cubes/marching-cubes.xcodeproj/project.pbxproj @@ -98,7 +98,7 @@ "fileEncoding": "4", "isa": "PBXFileReference", "lastKnownFileType": "sourcecode.cpp.cpp", - "name": "testApp.cpp", + "name": "ofApp.cpp", "sourceTree": "" }, "622082E4-6557-44E8-9FB4-AF3BC4FB0BD3": { @@ -415,7 +415,7 @@ "fileEncoding": "4", "isa": "PBXFileReference", "lastKnownFileType": "sourcecode.cpp.h", - "name": "testApp.h", + "name": "ofApp.h", "sourceTree": "" }, "EBF0D554-7142-497D-9EDD-EBDC8A29A6D9": { diff --git a/marching-cubes/src/MetaBall.h b/marching-cubes/src/MetaBall.h index 3f0eaf1..4a2d696 100644 --- a/marching-cubes/src/MetaBall.h +++ b/marching-cubes/src/MetaBall.h @@ -1,32 +1,16 @@ #pragma once #include "ofMain.h" -#include "ofxMarchingCubes.h" -class MetaBall: public ofPoint{ -public: - ofPoint accel, vel; - float size; - MetaBall(){ - size = ofRandom(5, 10); - } - - void init(const ofPoint& _pos){ - x = _pos.x; - y = _pos.y; - } - - void goTo(const ofPoint& target, float k = 0.1f, float damp = 0.9f){ - accel = (target - *this)*k; - vel += accel; - vel *= damp; - *this += vel; - } - - void update(const ofPoint& _force, float damp = 0.9f){ - vel += _force; - vel *= damp; - *this += vel; +struct MetaBall { + glm::vec3 position{0}; + glm::vec3 velocity{0}; + float size = 1.4f; + + void follow(const glm::vec3& target, float deltaSeconds) { + const float steps = std::min(deltaSeconds * 60.0f, 4.0f); + velocity += (target - position) * 0.3f * steps; + velocity *= std::pow(0.94f, steps); + position += velocity * steps; } }; - diff --git a/marching-cubes/src/main.cpp b/marching-cubes/src/main.cpp index 3e1e85a..d07fe7c 100644 --- a/marching-cubes/src/main.cpp +++ b/marching-cubes/src/main.cpp @@ -1,14 +1,5 @@ -#include "ofMain.h" -#include "testApp.h" +#include "ofApp.h" -//======================================================================== -int main() { - ofGLWindowSettings settings; - settings.setSize(1280, 720); - settings.setGLVersion(2, 1); - settings.windowMode = OF_WINDOW; - - auto window = ofCreateWindow(settings); - ofRunApp(window, std::make_shared()); - ofRunMainLoop(); +int main(int argc, char** argv) { + return example::launch(argc, argv, "marching-cubes"); } diff --git a/marching-cubes/src/ofApp.cpp b/marching-cubes/src/ofApp.cpp new file mode 100644 index 0000000..1231859 --- /dev/null +++ b/marching-cubes/src/ofApp.cpp @@ -0,0 +1,70 @@ +#include "ofApp.h" + +void ofApp::setup() { + runtime.setup(); + playback.load("marching-cubes", false, runtime.isSmokeTest()); + camera.setFov(30); + camera.setDistance(700); + // The vendored addon retains ofPoint in its public interface. + marchingCubes.init(ofPoint(0, 0, 0), ofPoint(550, 550, 550), 60, 60, 60); + if (playback.ready) { + for (auto& motion : playback.motions) { + for (int i = 0; i < motion.getNumJoints(); ++i) { + const auto* joint = motion.getJoint(i); + if (joint->isSite()) balls.push_back({joint, {glm::vec3(joint->getPosition())}}); + } + } + } + surface.setMode(OF_PRIMITIVE_TRIANGLES); + light.setPosition(200, 350, 500); + light.setAmbientColor(ofFloatColor(0.1f, 0.3f, 0.8f)); + light.setDiffuseColor(ofFloatColor(0.7f)); + light.setSpecularColor(ofFloatColor(1.0f, 0.5f, 0.0f)); + material.setDiffuseColor(ofFloatColor(0.8f)); + material.setSpecularColor(ofFloatColor(1.0f)); + material.setShininess(60); +} + +void ofApp::update() { + playback.update(runtime.deltaSeconds()); + if (!playback.ready) return; + marchingCubes.resetIsoValues(); + for (auto& tracked : balls) { + tracked.ball.follow(glm::vec3(tracked.joint->getPosition()), runtime.deltaSeconds()); + const auto& position = tracked.ball.position; + marchingCubes.addMetaBall(ofPoint(position.x, position.y, position.z), tracked.ball.size); + } + marchingCubes.update(0.17f, true); + surface.clear(); + const auto& vertices = marchingCubes.getVertices(); + const auto& normals = marchingCubes.getNormals(); + for (size_t i = 0; i < vertices.size(); ++i) { + surface.addVertex(glm::vec3(vertices[i])); + surface.addNormal(glm::vec3(normals[i])); + } +} + +void ofApp::draw() { + ofBackground(34); + ofEnableDepthTest(); + camera.begin(); + light.enable(); + material.begin(); + ofPushMatrix(); + ofTranslate(0, -80); + ofRotateXDeg(5); + ofSetColor(255); + surface.draw(); + ofPopMatrix(); + material.end(); + light.disable(); + camera.end(); + ofDisableDepthTest(); + ofSetColor(255); + playback.drawStatus(); + runtime.finishFrame(playback.ready, "marching-cubes", surface.getNumVertices()); +} + +void ofApp::keyPressed(int key) { + if (key == 'f') ofToggleFullscreen(); +} diff --git a/marching-cubes/src/ofApp.h b/marching-cubes/src/ofApp.h new file mode 100644 index 0000000..de75029 --- /dev/null +++ b/marching-cubes/src/ofApp.h @@ -0,0 +1,28 @@ +#pragma once + +#include "../../shared/ExampleRuntime.h" +#include "ofxMarchingCubes.h" +#include "MetaBall.h" + +class ofApp : public ofBaseApp { +public: + explicit ofApp(const example::RunOptions& options) : runtime(options) {} + void setup() override; + void update() override; + void draw() override; + void keyPressed(int key) override; + +private: + struct TrackedBall { + const ofxBvhJoint* joint = nullptr; + MetaBall ball; + }; + example::Runtime runtime; + example::MotionPlayback playback; + ofEasyCam camera; + ofxMarchingCubes marchingCubes; + std::vector balls; + ofVboMesh surface; + ofLight light; + ofMaterial material; +}; diff --git a/marching-cubes/src/testApp.cpp b/marching-cubes/src/testApp.cpp deleted file mode 100644 index 6ad4129..0000000 --- a/marching-cubes/src/testApp.cpp +++ /dev/null @@ -1,207 +0,0 @@ -#include "testApp.h" - -namespace { - -const std::array kPerfumeMotionFiles = { - "bvhfiles/aachan.bvh", - "bvhfiles/kashiyuka.bvh", - "bvhfiles/nocchi.bvh" -}; - -const std::array kBundledMotionFiles = { - "../../../example-bvh/bin/data/A_test.bvh", - "../../../example-bvh/bin/data/B_test.bvh", - "../../../example-bvh/bin/data/C_test.bvh" -}; - -bool loadMotionFiles(std::array& motions) { - bool loaded = true; - for (size_t i = 0; i < motions.size(); ++i) { - if (!motions[i].load(kPerfumeMotionFiles[i])) { - ofLogWarning("marching-cubes") - << "Using bundled motion fallback for " << kPerfumeMotionFiles[i]; - loaded = motions[i].load(kBundledMotionFiles[i]) && loaded; - } - } - return loaded; -} - -} // namespace - -float startTime = 0.02; - -//-------------------------------------------------------------- -void testApp::setup() { - ofSetFrameRate(60); - ofSetVerticalSync(true); - - rotate = 0; - - assetsReady = loadMotionFiles(bvh); - - audioReady = ofFile::doesFileExist("Perfume_globalsite_sound.wav") - && track.load("Perfume_globalsite_sound.wav"); - if (audioReady) { - track.setLoop(true); - track.play(); - } else { - ofLogNotice("marching-cubes") - << "Audio file not found; using a silent internal clock."; - } - - camera.setFov(30); - camera.setDistance(700); - - // MarchingCube init - ofPoint iniPos(0,0,0); - ofPoint gridSize(550, 550, 550); - int gridResX = 60; - int gridResY = 60; - int gridResZ = 60; - marchingCubes.init(iniPos, gridSize, gridResX, gridResY, gridResZ); - - // Metaball init - int metaballNum = bvh[0].getNumJoints() + bvh[1].getNumJoints() + bvh[2].getNumJoints(); - metaBalls.resize(metaballNum); - - int n = 0; - for (size_t i = 0; i < bvh.size(); ++i){ - for (int j = 0; j < bvh[i].getNumJoints(); j++) { - const ofxBvhJoint *o = bvh[i].getJoint(j); - if (o->isSite()) { - metaBalls[n].init(o->getPosition()); - metaBalls[n].size = 1.4; - } - n++; - } - } - - light.enable(); - light.setAmbientColor(ofFloatColor(0.1, 0.3, 0.8, 1.0)); - light.setDiffuseColor(ofFloatColor(0.7, 0.7, 0.7)); - light.setSpecularColor(ofFloatColor(1.0, 0.5, 0.0)); -} - -//-------------------------------------------------------------- -void testApp::update(){ - if (!assetsReady) return; - const float motionDuration = bvh[0].getDuration(); - const float motionSeconds = audioReady - ? track.getPosition() * track.getDuration() - : std::fmod(ofGetElapsedTimef(), motionDuration); - const float t = ofClamp(motionSeconds / motionDuration, 0.0f, 1.0f); - - for (auto& motion : bvh) { - motion.setPosition(t); - motion.update(); - } - - marchingCubes.resetIsoValues(); - - int n = 0; - for (size_t i = 0; i < bvh.size(); ++i){ - for (int j = 0; j < bvh[i].getNumJoints(); j++) { - const ofxBvhJoint *o = bvh[i].getJoint(j); - if (o->isSite()) { - if (t > startTime) { - metaBalls[n].goTo(o->getPosition(), 0.3, 0.94); - marchingCubes.addMetaBall(metaBalls[n], metaBalls[n].size); - } else { - metaBalls[n].goTo(o->getPosition(), 1.0, 0.1); - } - } - n++; - } - } - - marchingCubes.update(threshold, true); -} - -//-------------------------------------------------------------- -void testApp::draw(){ - ofBackgroundHex(0x222222); - - camera.begin(); - ofPushMatrix(); - { - ofTranslate(0, -80); - ofRotateDeg(5, 1, 0, 0); - ofScale(1, 1, 1); - - // draw MarchingCubes - vector& vertices = marchingCubes.getVertices(); - vector& normals = marchingCubes.getNormals(); - int numVertices = vertices.size(); - - glEnable(GL_DEPTH_TEST); - glColor3f(1.0f, 1.0f, 1.0f); - glBegin(GL_TRIANGLES); - - for(int i=0; i - -class testApp : public ofBaseApp { - - public: - void setup() override; - void update() override; - void draw() override; - void exit() override; - - void keyPressed(int key) override; - void keyReleased(int key) override; - void mouseMoved(int x, int y) override; - void mouseDragged(int x, int y, int button) override; - void mousePressed(int x, int y, int button) override; - void mouseReleased(int x, int y, int button) override; - void windowResized(int w, int h) override; - void dragEvent(ofDragInfo dragInfo) override; - void gotMessage(ofMessage msg) override; - - ofSoundPlayer track; - std::array bvh; - - float rotate = 0.0f; - float play_rate = 1.0f; - float play_rate_t = 1.0f; - - ofEasyCam camera; - ofImage background; - - ofxMarchingCubes marchingCubes; - vector metaBalls; - - float threshold = 0.17f; - ofLight light; - bool assetsReady = false; - bool audioReady = false; -}; diff --git a/motion-visualization/motion-visualization.xcodeproj/project.pbxproj b/motion-visualization/motion-visualization.xcodeproj/project.pbxproj index fbbd96f..02d79e3 100644 --- a/motion-visualization/motion-visualization.xcodeproj/project.pbxproj +++ b/motion-visualization/motion-visualization.xcodeproj/project.pbxproj @@ -71,7 +71,7 @@ "fileEncoding": "4", "isa": "PBXFileReference", "lastKnownFileType": "sourcecode.cpp.h", - "name": "testApp.h", + "name": "ofApp.h", "sourceTree": "" }, "A275555F-3A37-4876-8C1F-33BE548EB6DC": { @@ -346,7 +346,7 @@ "fileEncoding": "4", "isa": "PBXFileReference", "lastKnownFileType": "sourcecode.cpp.cpp", - "name": "testApp.cpp", + "name": "ofApp.cpp", "sourceTree": "" }, "F77F8C76-5ABD-4EC6-9124-D6447EDF405A": { diff --git a/motion-visualization/src/main.cpp b/motion-visualization/src/main.cpp index 3e1e85a..ccad0c3 100644 --- a/motion-visualization/src/main.cpp +++ b/motion-visualization/src/main.cpp @@ -1,14 +1,5 @@ -#include "ofMain.h" -#include "testApp.h" +#include "ofApp.h" -//======================================================================== -int main() { - ofGLWindowSettings settings; - settings.setSize(1280, 720); - settings.setGLVersion(2, 1); - settings.windowMode = OF_WINDOW; - - auto window = ofCreateWindow(settings); - ofRunApp(window, std::make_shared()); - ofRunMainLoop(); +int main(int argc, char** argv) { + return example::launch(argc, argv, "motion-visualization"); } diff --git a/motion-visualization/src/ofApp.cpp b/motion-visualization/src/ofApp.cpp new file mode 100644 index 0000000..97e600b --- /dev/null +++ b/motion-visualization/src/ofApp.cpp @@ -0,0 +1,72 @@ +#include "ofApp.h" + +void ofApp::setup() { + runtime.setup(); + playback.load("motion-visualization", false, runtime.isSmokeTest()); + if (playback.ready) { + for (auto& motion : playback.motions) { + for (int i = 0; i < motion.getNumJoints(); ++i) trails.push_back({motion.getJoint(i), {}}); + } + } + camera.setFov(45); + camera.setDistance(520); + camera.disableMouseInput(); + if (ofFile::doesFileExist("background.png")) background.load("background.png"); + ribbons.setMode(OF_PRIMITIVE_TRIANGLES); +} + +void ofApp::update() { + rotation += runtime.deltaSeconds() * 2.4f; + playback.update(runtime.deltaSeconds()); + if (!playback.ready) return; + for (auto& trail : trails) { + const glm::vec3 position(trail.joint->getPosition()); + if (trail.points.empty() || glm::distance(position, trail.points.front()) > 1.0f) { + trail.points.push_front(position); + if (trail.points.size() > 200) trail.points.pop_back(); + } + } +} + +void ofApp::draw() { + ofBackground(0); + ofSetColor(255); + if (background.isAllocated()) background.draw(0, 0, ofGetWidth(), ofGetHeight()); + ofEnableBlendMode(OF_BLENDMODE_ADD); + camera.begin(); + ofPushMatrix(); + ofTranslate(0, -80); + ofRotateYDeg(rotation); + + // Camera-facing triangle ribbons replace unsupported wide core-profile lines. + const glm::mat4 inverseView = glm::inverse(ofGetCurrentMatrix(OF_MATRIX_MODELVIEW)); + const glm::vec3 viewDirection = example::safeNormalize(glm::vec3(inverseView[2])); + ribbons.clear(); + for (const auto& trail : trails) { + for (size_t i = 0; i + 1 < trail.points.size(); ++i) { + const auto& a = trail.points[i]; + const auto& b = trail.points[i + 1]; + const float distance = glm::distance(a, b); + if (distance <= 1e-6f || distance >= 40.0f) continue; + const auto side = example::safeNormalize(glm::cross(b - a, viewDirection)) + * ofMap(distance, 0, 30, 0.2f, 2.4f, true); + const ofFloatColor color(ofColor(ofClamp(distance * 20, 0, 255), + ofClamp(127 - distance * 10, 0, 255), ofClamp(255 - distance * 20, 0, 255))); + for (const auto& vertex : {a - side, a + side, b + side, a - side, b + side, b - side}) { + ribbons.addVertex(vertex); + ribbons.addColor(color); + } + } + } + ribbons.draw(); + ofPopMatrix(); + camera.end(); + ofEnableBlendMode(OF_BLENDMODE_ALPHA); + ofSetColor(255); + playback.drawStatus(); + runtime.finishFrame(playback.ready, "motion-visualization", ribbons.getNumVertices()); +} + +void ofApp::keyPressed(int key) { + if (key == 'f') ofToggleFullscreen(); +} diff --git a/motion-visualization/src/ofApp.h b/motion-visualization/src/ofApp.h new file mode 100644 index 0000000..e70eb3d --- /dev/null +++ b/motion-visualization/src/ofApp.h @@ -0,0 +1,26 @@ +#pragma once + +#include "../../shared/ExampleRuntime.h" +#include + +class ofApp : public ofBaseApp { +public: + explicit ofApp(const example::RunOptions& options) : runtime(options) {} + void setup() override; + void update() override; + void draw() override; + void keyPressed(int key) override; + +private: + struct Trail { + const ofxBvhJoint* joint = nullptr; + std::deque points; + }; + example::Runtime runtime; + example::MotionPlayback playback; + std::vector trails; + ofEasyCam camera; + ofImage background; + ofVboMesh ribbons; + float rotation = 0.0f; +}; diff --git a/motion-visualization/src/testApp.cpp b/motion-visualization/src/testApp.cpp deleted file mode 100644 index bb79afe..0000000 --- a/motion-visualization/src/testApp.cpp +++ /dev/null @@ -1,235 +0,0 @@ -#include "testApp.h" - -#include - -namespace { - -const std::array kPerfumeMotionFiles = { - "bvhfiles/aachan.bvh", - "bvhfiles/kashiyuka.bvh", - "bvhfiles/nocchi.bvh" -}; - -const std::array kBundledMotionFiles = { - "../../../example-bvh/bin/data/A_test.bvh", - "../../../example-bvh/bin/data/B_test.bvh", - "../../../example-bvh/bin/data/C_test.bvh" -}; - -bool loadMotionFiles(std::array& motions) { - bool loaded = true; - for (size_t i = 0; i < motions.size(); ++i) { - if (!motions[i].load(kPerfumeMotionFiles[i])) { - ofLogWarning("motion-visualization") - << "Using bundled motion fallback for " << kPerfumeMotionFiles[i]; - loaded = motions[i].load(kBundledMotionFiles[i]) && loaded; - } - } - return loaded; -} - -} // namespace - -int trackerLength = 200; -float startTime = 0.035; - -class Tracker -{ -public: - - const ofxBvhJoint *joint = nullptr; - deque points; - float trackerLength = 0.0f; - - void setup(const ofxBvhJoint *o){ - joint = o; - } - - void update() { - const ofVec3f &p = joint->getPosition(); - - if (points.empty() || p.distance(points.front()) > 1) - points.push_front(joint->getPosition()); - - if (points.size() > trackerLength){ - points.pop_back(); - } - } - - void draw() { - if (points.empty()) return; - - for (size_t i = 0; i + 1 < points.size(); ++i){ - ofVec3f &p0 = points[i]; - ofVec3f &p1 = points[i + 1]; - - float dist = p0.distance(p1); - - if (dist < 40) { - ofSetLineWidth(ofMap(dist, 0, 30, 0, 14)); - ofSetColor(dist*20, 127-dist*10, 255-dist*20); - ofDrawLine(p0.x, p0.y, p0.z, p1.x, p1.y, p1.z); - } - } - } - - void clear() { - points.clear(); - } - - void setTrackerLength(float _trackerLength) { - trackerLength = _trackerLength; - } -}; - -vector> trackers; - -//-------------------------------------------------------------- -void testApp::setup() { - ofSetFrameRate(60); - ofSetVerticalSync(true); - - rotate = 0; - - assetsReady = loadMotionFiles(bvh); - - audioReady = ofFile::doesFileExist("Perfume_globalsite_sound.wav") - && track.load("Perfume_globalsite_sound.wav"); - if (audioReady) { - track.setLoop(true); - track.play(); - } else { - ofLogNotice("motion-visualization") - << "Audio file not found; using a silent internal clock."; - } - - // setup tracker - if (assetsReady) - { - for (auto& motion : bvh) { - for (int n = 0; n < motion.getNumJoints(); ++n) { - auto tracker = std::make_unique(); - tracker->setup(motion.getJoint(n)); - tracker->setTrackerLength(trackerLength); - trackers.push_back(std::move(tracker)); - } - } - } - - camera.setFov(45); - camera.setDistance(360); - camera.disableMouseInput(); - - background.load("background.png"); - -} - -//-------------------------------------------------------------- -void testApp::update() -{ - rotate += 0.04; - - if (!assetsReady) return; - const float motionDuration = bvh[0].getDuration(); - const float motionSeconds = audioReady - ? track.getPosition() * track.getDuration() - : std::fmod(ofGetElapsedTimef(), motionDuration); - const float t = ofClamp(motionSeconds / motionDuration, 0.0f, 1.0f); - - for (auto& motion : bvh) { - motion.setPosition(t); - motion.update(); - } - - for (auto& tracker : trackers) { - if (t > startTime) { - tracker->setTrackerLength(trackerLength); - tracker->update(); - } - } -} - -//-------------------------------------------------------------- -void testApp::draw(){ - ofBackgroundHex(0x000000); - ofSetHexColor(0xffffff); - background.draw(0,0,ofGetWidth(),ofGetHeight()); - - ofEnableDepthTest(); - - camera.begin(); - ofPushMatrix(); - { - ofTranslate(0, -80); - ofRotateDeg(rotate, 0, 1, 0); - ofScale(1, 1, 1); - - // draw tracker - ofDisableDepthTest(); - ofEnableBlendMode(OF_BLENDMODE_ADD); - - //ofSetColor(ofColor::white, 80); - for (auto& tracker : trackers){ - tracker->draw(); - } - - } - ofPopMatrix(); - camera.end(); - ofDisableDepthTest(); - ofSetColor(255); - - if (!assetsReady) - ofDrawBitmapString("Motion data is missing. See README.md.", 10, 20); - else if (!audioReady) - ofDrawBitmapString("Audio data is missing; running with a silent clock.", 10, 20); - -} - -void testApp::exit(){ - -} - -//-------------------------------------------------------------- -void testApp::keyPressed(int key){ - if (key == 'f') { - ofToggleFullscreen(); - } -} - -//-------------------------------------------------------------- -void testApp::keyReleased(int key){ - -} - -//-------------------------------------------------------------- -void testApp::mouseMoved(int x, int y ){ - -} - -//-------------------------------------------------------------- -void testApp::mouseDragged(int x, int y, int button){ - -} - -//-------------------------------------------------------------- -void testApp::mousePressed(int x, int y, int button){ - -} - -//-------------------------------------------------------------- -void testApp::mouseReleased(int x, int y, int button){ -} - -//-------------------------------------------------------------- -void testApp::windowResized(int w, int h){ -} - -//-------------------------------------------------------------- -void testApp::gotMessage(ofMessage msg){ - -} - -//-------------------------------------------------------------- -void testApp::dragEvent(ofDragInfo dragInfo){ -} diff --git a/motion-visualization/src/testApp.h b/motion-visualization/src/testApp.h deleted file mode 100644 index 7108383..0000000 --- a/motion-visualization/src/testApp.h +++ /dev/null @@ -1,37 +0,0 @@ -#pragma once - -#include "ofMain.h" -#include "ofxBvh.h" - -#include - -class testApp : public ofBaseApp { - - public: - void setup() override; - void update() override; - void draw() override; - void exit() override; - - void keyPressed(int key) override; - void keyReleased(int key) override; - void mouseMoved(int x, int y) override; - void mouseDragged(int x, int y, int button) override; - void mousePressed(int x, int y, int button) override; - void mouseReleased(int x, int y, int button) override; - void windowResized(int w, int h) override; - void dragEvent(ofDragInfo dragInfo) override; - void gotMessage(ofMessage msg) override; - - ofSoundPlayer track; - std::array bvh; - - float rotate = 0.0f; - float play_rate = 1.0f; - float play_rate_t = 1.0f; - - ofEasyCam camera; - ofImage background; - bool assetsReady = false; - bool audioReady = false; -}; diff --git a/ofxBvh/src/ofxBvh.cpp b/ofxBvh/src/ofxBvh.cpp index 385b96d..a7f0a2e 100644 --- a/ofxBvh/src/ofxBvh.cpp +++ b/ofxBvh/src/ofxBvh.cpp @@ -5,8 +5,6 @@ #include #include -static inline void billboard(); - namespace { bool parseFloatToken(const string& token, float& value) @@ -283,42 +281,32 @@ void ofxBvh::update(float deltaSeconds) void ofxBvh::draw() { if (!isLoaded()) return; - - ofPushStyle(); - ofFill(); - - for (int i = 0; i < joints.size(); i++) - { - ofxBvhJoint *o = joints[i]; - glPushMatrix(); - glMultMatrixf(o->getGlobalMatrix().getPtr()); - - if (o->isSite()) - { - ofSetColor(ofColor::yellow); - billboard(); - ofDrawCircle(0, 0, 6); - } - else if (o->getChildren().size() == 1) - { - ofSetColor(ofColor::white); - billboard(); - ofDrawCircle(0, 0, 2); - } - else if (o->getChildren().size() > 1) - { - if (o->isRoot()) - ofSetColor(ofColor::cyan); - else - ofSetColor(ofColor::green); - - billboard(); - ofDrawCircle(0, 0, 4); + // Build camera-facing discs using the renderer's GLM matrix stack. + // No fixed-function OpenGL calls: this works in a 3.2 core context. + const glm::mat4 inverseView = glm::inverse(ofGetCurrentMatrix(OF_MATRIX_MODELVIEW)); + const glm::vec3 right(inverseView[0]); + const glm::vec3 up(inverseView[1]); + ofMesh discs; + discs.setMode(OF_PRIMITIVE_TRIANGLES); + constexpr int segments = 16; + for (const auto* joint : joints) { + const glm::vec3 position(joint->getPosition()); + const float radius = joint->isSite() ? 6.0f : (joint->getChildren().size() == 1 ? 2.0f : 4.0f); + const ofFloatColor color(joint->isSite() ? ofColor::yellow + : joint->isRoot() ? ofColor::cyan + : joint->getChildren().size() > 1 ? ofColor::green : ofColor::white); + for (int segment = 0; segment < segments; ++segment) { + const float a = TWO_PI * float(segment) / segments; + const float b = TWO_PI * float(segment + 1) / segments; + discs.addVertex(position); + discs.addVertex(position + radius * (right * std::cos(a) + up * std::sin(a))); + discs.addVertex(position + radius * (right * std::cos(b) + up * std::sin(b))); + for (int vertex = 0; vertex < 3; ++vertex) discs.addColor(color); } - - glPopMatrix(); } - + ofPushStyle(); + ofSetColor(255); + discs.draw(); ofPopStyle(); } @@ -604,53 +592,3 @@ const ofxBvhJoint* ofxBvh::getJoint(const string& name) const const auto it = jointMap.find(name); return it == jointMap.end() ? nullptr : it->second; } - -static inline void billboard() -{ - GLfloat m[16]; - glGetFloatv(GL_MODELVIEW_MATRIX, m); - - constexpr float epsilon = 1e-12f; - - m[8] = -m[12]; - m[9] = -m[13]; - m[10] = -m[14]; - float lengthSquared = m[8] * m[8] + m[9] * m[9] + m[10] * m[10]; - if (lengthSquared > epsilon) - { - const float inverseLength = 1.0f / std::sqrt(lengthSquared); - m[8] *= inverseLength; - m[9] *= inverseLength; - m[10] *= inverseLength; - } - else - { - m[8] = 0.0f; - m[9] = 0.0f; - m[10] = 1.0f; - } - - m[0] = -m[14]; - m[1] = 0.0; - m[2] = m[12]; - lengthSquared = m[0] * m[0] + m[1] * m[1] + m[2] * m[2]; - if (lengthSquared > epsilon) - { - const float inverseLength = 1.0f / std::sqrt(lengthSquared); - m[0] *= inverseLength; - m[1] *= inverseLength; - m[2] *= inverseLength; - } - else - { - m[0] = 1.0f; - m[1] = 0.0f; - m[2] = 0.0f; - } - - m[4] = m[9] * m[2] - m[10] * m[1]; - m[5] = m[10] * m[0] - m[8] * m[2]; - m[6] = m[8] * m[1] - m[9] * m[0]; - - glLoadMatrixf(m); -} diff --git a/particle-motion-example/particle-motion-example.xcodeproj/project.pbxproj b/particle-motion-example/particle-motion-example.xcodeproj/project.pbxproj index 5e2f048..cc2d64e 100644 --- a/particle-motion-example/particle-motion-example.xcodeproj/project.pbxproj +++ b/particle-motion-example/particle-motion-example.xcodeproj/project.pbxproj @@ -62,7 +62,7 @@ "fileEncoding": "4", "isa": "PBXFileReference", "lastKnownFileType": "sourcecode.cpp.cpp", - "name": "testApp.cpp", + "name": "ofApp.cpp", "sourceTree": "" }, "9BE696A4-1E70-463C-B9BE-75BAC6A5A775": { @@ -350,7 +350,7 @@ "fileEncoding": "4", "isa": "PBXFileReference", "lastKnownFileType": "sourcecode.cpp.h", - "name": "testApp.h", + "name": "ofApp.h", "sourceTree": "" } }, diff --git a/particle-motion-example/src/main.cpp b/particle-motion-example/src/main.cpp index 1349dcd..842ef2c 100644 --- a/particle-motion-example/src/main.cpp +++ b/particle-motion-example/src/main.cpp @@ -1,14 +1,5 @@ -#include "ofMain.h" -#include "testApp.h" +#include "ofApp.h" -//======================================================================== -int main() { - ofGLWindowSettings settings; - settings.setSize(1280, 768); - settings.setGLVersion(2, 1); - settings.windowMode = OF_WINDOW; - - auto window = ofCreateWindow(settings); - ofRunApp(window, std::make_shared()); - ofRunMainLoop(); +int main(int argc, char** argv) { + return example::launch(argc, argv, "particle-motion-example"); } diff --git a/particle-motion-example/src/ofApp.cpp b/particle-motion-example/src/ofApp.cpp new file mode 100644 index 0000000..b9176e8 --- /dev/null +++ b/particle-motion-example/src/ofApp.cpp @@ -0,0 +1,144 @@ +#include "ofApp.h" + +void ofApp::Trail::update() { + samples.emplace_front(joint->getPosition()); + if (samples.size() > 10) samples.pop_back(); + float distance = 0.0f; + float alignment = 0.0f; + for (size_t i = 0; i + 1 < samples.size(); ++i) distance += glm::distance(samples[i], samples[i + 1]); + for (size_t i = 1; i + 1 < samples.size(); ++i) { + const auto a = samples[i - 1] - samples[i]; + const auto b = samples[i] - samples[i + 1]; + if (glm::dot(a, a) > 1e-12f && glm::dot(b, b) > 1e-12f) alignment += glm::dot(glm::normalize(a), glm::normalize(b)); + } + if (samples.size() > 2) alignment /= float(samples.size() - 2); + const float brightness = ofMap(distance, 30, 40, 0, 1, true) * ofClamp(alignment, 0.0f, 1.0f); + mesh.clear(); + mesh.setMode(OF_PRIMITIVE_LINE_STRIP); + for (size_t i = 0; i < samples.size(); ++i) { + const float age = samples.size() > 1 ? float(i) / float(samples.size() - 1) : 0.0f; + mesh.addVertex(samples[i]); + mesh.addColor(ofFloatColor(brightness, brightness, brightness, 0.55f * (1.0f - age))); + } +} + +void ofApp::ParticleShape::setup(ofxBvh& source) { + motion = &source; + for (int i = 1; i < source.getNumJoints(); ++i) { + const auto* joint = source.getJoint(i); + if (joint->getName().find("Chest") == std::string::npos) { + Trail trail; + trail.joint = joint; + trails.push_back(std::move(trail)); + } + } + particles.resize(15000); + points.setMode(OF_PRIMITIVE_POINTS); + points.getVertices().resize(particles.size()); +} + +void ofApp::ParticleShape::update(float deltaSeconds) { + const float steps = std::min(deltaSeconds * 60.0f, 4.0f); + for (auto& particle : particles) particle.force = glm::vec3(0); + if (motion->isFrameNew()) { + for (auto& trail : trails) { + trail.update(); + const glm::vec3 position(trail.joint->getPosition()); + for (auto& particle : particles) { + auto direction = particle.position - position; + const float squaredDistance = glm::dot(direction, direction); + if (squaredDistance > 1e-4f && squaredDistance < 900.0f) { + const float distance = std::sqrt(squaredDistance); + particle.force += (0.4f / squaredDistance - 1.6f / std::pow(distance, 1.1f)) * direction / distance * 2.0f; + } + } + for (int i = 0; i < 10; ++i) { + particles[nextParticle] = {position, glm::vec3(0), glm::vec3(0)}; + nextParticle = (nextParticle + 1) % particles.size(); + } + } + } + for (size_t i = 0; i < particles.size(); ++i) { + auto& particle = particles[i]; + particle.force.y -= 0.1f; + particle.velocity *= std::pow(0.98f, steps); + particle.velocity += particle.force * 0.9f * steps; + particle.position += particle.velocity * 0.9f * steps; + if (particle.position.y < 0) { + particle.position.y = 0; + particle.velocity *= std::pow(0.95f, steps); + } + points.setVertex(i, particle.position); + } +} + +void ofApp::setup() { + runtime.setup(); + ofBackground(0); + playback.load("particle-motion-example", false, runtime.isSmokeTest()); + if (playback.ready) { + for (size_t i = 0; i < shapes.size(); ++i) shapes[i].setup(playback.motions[i]); + } + camera.setDistance(450); + // gl_PointCoord supplies a soft circular sprite without a texture dependency. + shaderReady = pointShader.setupShaderFromSource(GL_VERTEX_SHADER, R"GLSL(#version 150 +uniform mat4 modelViewProjectionMatrix; +uniform mat4 modelViewMatrix; +in vec4 position; +void main() { + vec4 viewPosition = modelViewMatrix * position; + gl_Position = modelViewProjectionMatrix * position; + gl_PointSize = clamp(1500.0 / max(1.0, -viewPosition.z), 1.0, 12.0); +} +)GLSL"); + shaderReady = pointShader.setupShaderFromSource(GL_FRAGMENT_SHADER, R"GLSL(#version 150 +out vec4 fragColor; +void main() { + float radius = length(gl_PointCoord * 2.0 - 1.0); + float alpha = 0.12 * (1.0 - smoothstep(0.0, 1.0, radius)); + fragColor = vec4(vec3(1.0), alpha); +} +)GLSL") && shaderReady; + pointShader.bindDefaults(); + shaderReady = pointShader.linkProgram() && shaderReady; + GLint linked = GL_FALSE; + glGetProgramiv(pointShader.getProgram(), GL_LINK_STATUS, &linked); + shaderReady = shaderReady && linked == GL_TRUE; +} + +void ofApp::update() { + const float dt = paused ? 0.0f : runtime.deltaSeconds(); + playback.update(dt, paused ? 0.0f : 1.0f); + if (!playback.ready) return; + glm::vec3 average{0}; + for (auto& shape : shapes) { + if (!paused) shape.update(dt); + average += glm::vec3(shape.motion->getJoint(0)->getPosition()); + } + center = glm::mix(center, average / 3.0f, 1.0f - std::exp(-6.3f * dt)); +} + +void ofApp::draw() { + ofDisableDepthTest(); + ofEnableBlendMode(OF_BLENDMODE_ADD); + camera.begin(); + ofPushMatrix(); + ofRotateYDeg(runtime.elapsedSeconds() * 10.0f); + ofTranslate(-center); + for (auto& shape : shapes) for (auto& trail : shape.trails) trail.mesh.draw(); + if (shaderReady) { + glEnable(GL_PROGRAM_POINT_SIZE); + pointShader.begin(); + for (auto& shape : shapes) shape.points.draw(); + pointShader.end(); + glDisable(GL_PROGRAM_POINT_SIZE); + } + ofPopMatrix(); + camera.end(); + ofEnableBlendMode(OF_BLENDMODE_ALPHA); + ofSetColor(255); + playback.drawStatus(); + runtime.finishFrame(playback.ready && shaderReady, "particle-motion-example", shapes.front().points.getNumVertices()); +} + +void ofApp::keyPressed(int) { paused = !paused; } diff --git a/particle-motion-example/src/ofApp.h b/particle-motion-example/src/ofApp.h new file mode 100644 index 0000000..6630a90 --- /dev/null +++ b/particle-motion-example/src/ofApp.h @@ -0,0 +1,43 @@ +#pragma once + +#include "../../shared/ExampleRuntime.h" +#include + +class ofApp : public ofBaseApp { +public: + explicit ofApp(const example::RunOptions& options) : runtime(options) {} + void setup() override; + void update() override; + void draw() override; + void keyPressed(int key) override; + +private: + struct Particle { + glm::vec3 position{0}; + glm::vec3 velocity{0}; + glm::vec3 force{0}; + }; + struct Trail { + const ofxBvhJoint* joint = nullptr; + std::deque samples; + ofVboMesh mesh; + void update(); + }; + struct ParticleShape { + ofxBvh* motion = nullptr; + std::vector trails; + std::vector particles; + ofVboMesh points; + size_t nextParticle = 0; + void setup(ofxBvh& motion); + void update(float deltaSeconds); + }; + example::Runtime runtime; + example::MotionPlayback playback; + std::array shapes; + ofEasyCam camera; + ofShader pointShader; + glm::vec3 center{0}; + bool paused = false; + bool shaderReady = false; +}; diff --git a/particle-motion-example/src/testApp.cpp b/particle-motion-example/src/testApp.cpp deleted file mode 100644 index 1c374d9..0000000 --- a/particle-motion-example/src/testApp.cpp +++ /dev/null @@ -1,393 +0,0 @@ -#include "testApp.h" - -#include -#include - -namespace { - -const std::array kPerfumeMotionFiles = { - "bvhfiles/aachan.bvh", - "bvhfiles/kashiyuka.bvh", - "bvhfiles/nocchi.bvh" -}; - -const std::array kBundledMotionFiles = { - "../../../example-bvh/bin/data/A_test.bvh", - "../../../example-bvh/bin/data/B_test.bvh", - "../../../example-bvh/bin/data/C_test.bvh" -}; - -bool loadMotionFiles(std::array& motions) { - bool loaded = true; - for (size_t i = 0; i < motions.size(); ++i) { - if (!motions[i].load(kPerfumeMotionFiles[i])) { - ofLogWarning("particle-motion-example") - << "Using bundled motion fallback for " << kPerfumeMotionFiles[i]; - loaded = motions[i].load(kBundledMotionFiles[i]) && loaded; - } - } - return loaded; -} - -} // namespace - -class Particle -{ -public: - - ofVec3f pos = {0, 0, 0}; - ofVec3f vel = {0, 0, 0}; - ofVec3f force = {0, 0, 0}; -}; - -class Tracker -{ -public: - - const ofxBvhJoint *joint = nullptr; - deque samples; - - void setup(const ofxBvhJoint *o) - { - joint = o; - } - - void update(vector& particles) - { - const ofVec3f &p = joint->getPosition(); - - // update sample - { - samples.push_front(joint->getPosition()); - while (samples.size() > 10) - samples.pop_back(); - } - - // update particle force - { - const float n = 2.0; - const float A = 0.4; - const float m = 1.1; - const float B = 1.6; - - for (auto& particle : particles) - { - ofVec3f dist = particle.pos - p; - float r = dist.lengthSquared(); - - if (r > 0 && r < 30*30) - { - r = sqrt(r); - dist /= r; - - particle.force += ((A / pow(r, n)) - (B / pow(r, m))) * dist * 2; - } - } - } - } - - float length() - { - if (samples.empty()) return 0; - - float v = 0; - for (size_t i = 0; i + 1 < samples.size(); ++i) - v += samples[i].distance(samples[i + 1]); - - return v; - } - - float dot() - { - if (samples.size() < 3) return 0; - - float v = 0; - - for (size_t i = 1; i + 1 < samples.size(); ++i) - { - const ofVec3f &v0 = samples[i - 1]; - const ofVec3f &v1 = samples[i]; - const ofVec3f &v2 = samples[i + 1]; - - if (v0.squareDistance(v1) == 0) continue; - if (v1.squareDistance(v2) == 0) continue; - - const ofVec3f d0 = (v0 - v1).getNormalized(); - const ofVec3f d1 = (v1 - v2).getNormalized(); - - v += (d0).dot(d1); - } - - return v / ((float)samples.size() - 2); - } - - void draw() - { - if (samples.size() < 2) return; - - float len = length(); - len = ofMap(len, 30, 40, 0, 1, true); - - float d = dot(); - d = ofMap(d, 1, 0, 255, 0, true); - - glBegin(GL_LINE_STRIP); - for (size_t i = 0; i < samples.size(); ++i) - { - float a = ofMap(static_cast(i), 0.0f, - static_cast(samples.size() - 1), 1.0f, 0.0f, true); - ofSetColor(d * len, 140 * a); - glVertex3fv(samples[i].getPtr()); - } - glEnd(); - } -}; - -class ParticleShape -{ -public: - - ofxBvh *bvh = nullptr; - - vector> tracker; - - vector particles; - size_t particle_index = 0; - - void setup(ofxBvh &o) - { - bvh = &o; - - for (int i = 1; i < o.getNumJoints(); ++i) - { - if (bvh->getJoint(i)->getName().find("Chest") == string::npos) - { - auto item = std::make_unique(); - item->setup(bvh->getJoint(i)); - tracker.push_back(std::move(item)); - } - } - - particles.resize(15000); - } - - void update() - { - bvh->update(); - - for (auto& particle : particles) - { - particle.force.set(0, 0, 0); - } - - if (bvh->isFrameNew()) - { - for (auto& item : tracker) - { - // update force - item->update(particles); - - const ofVec3f &p = item->joint->getPosition(); - - // emit 10 particle every frame - for (int i = 0; i < 10; ++i) - { - particles[particle_index].pos.set(p); - - particle_index++; - if (particle_index >= particles.size()) - particle_index = 0; - } - } - } - - // update particle position - for (auto& p : particles) - { - p.force.y += -0.1; - p.vel *= 0.98; - - p.vel += p.force * 0.9; - p.pos += p.vel * 0.9; - - if (p.pos.y <= 0) - { - p.pos.y = 0; - p.vel *= 0.95; - } - } - } - - void draw() - { - // bvh->draw(); - - for (auto& item : tracker) - { - item->draw(); - } - - ofSetColor(255, 15); - glBegin(GL_POINTS); - for (auto& p : particles) - { - glVertex3fv(p.pos.getPtr()); - } - glEnd(); - } -}; - -constexpr size_t NUM_ACTOR = 3; -std::array particle_shapes; -std::array bvh; - -ofSoundPlayer player; - -ofVec3f center; - -//-------------------------------------------------------------- -void testApp::setup() -{ - ofSetFrameRate(60); - ofSetVerticalSync(true); - - ofBackground(0); - - assetsReady = loadMotionFiles(bvh); - - for (size_t i = 0; i < NUM_ACTOR; ++i) - { - bvh[i].setFrame(1); - if (assetsReady) particle_shapes[i].setup(bvh[i]); - } - - audioReady = ofFile::doesFileExist("Perfume_globalsite_sound.wav") - && player.load("Perfume_globalsite_sound.wav"); - if (audioReady) { - player.setLoop(true); - player.play(); - } else { - ofLogNotice("particle-motion-example") - << "Audio file not found; using a silent internal clock."; - } -} - -//-------------------------------------------------------------- -void testApp::update() -{ - if (!assetsReady) return; - const float motionDuration = bvh[0].getDuration(); - const float motionSeconds = audioReady - ? player.getPosition() * player.getDuration() - : std::fmod(ofGetElapsedTimef(), motionDuration); - const float t = ofClamp(motionSeconds / motionDuration, 0.0f, 1.0f); - - ofVec3f avg; - - for (size_t i = 0; i < NUM_ACTOR; ++i) - { - ofxBvh *o = particle_shapes[i].bvh; - - o->setPosition(t); - particle_shapes[i].update(); - - avg += o->getJoint(0)->getPosition(); - } - - avg /= 3; - - center += (avg - center) * 0.1; -} - -//-------------------------------------------------------------- -void testApp::draw() -{ - ofDisableDepthTest(); - ofEnableBlendMode(OF_BLENDMODE_ADD); - - // smooth particle - glEnable(GL_POINT_SMOOTH); - glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); - - static GLfloat distance[] = {0.0, 0.0, 1.0}; - glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, distance); - glPointSize(1500); - - glLineWidth(2); - - cam.begin(); - ofRotateYDeg(ofGetElapsedTimef() * 10); - ofTranslate(-center); - - for (size_t i = 0; i < NUM_ACTOR; ++i) - { - particle_shapes[i].draw(); - } - - cam.end(); - ofEnableBlendMode(OF_BLENDMODE_ALPHA); - ofSetColor(255); - - if (!assetsReady) - ofDrawBitmapString("Motion data is missing. See README.md.", 10, 20); - else if (!audioReady) - ofDrawBitmapString("Audio data is missing; running with a silent clock.", 10, 20); -} - -//-------------------------------------------------------------- -void testApp::keyPressed(int key) -{ - if (!audioReady) return; - if (player.getSpeed() > 0) - player.setSpeed(0); - else - player.setSpeed(1); -} - -//-------------------------------------------------------------- -void testApp::keyReleased(int key) -{ - -} - -//-------------------------------------------------------------- -void testApp::mouseMoved(int x, int y) -{ - -} - -//-------------------------------------------------------------- -void testApp::mouseDragged(int x, int y, int button) -{ - -} - -//-------------------------------------------------------------- -void testApp::mousePressed(int x, int y, int button) -{ - -} - -//-------------------------------------------------------------- -void testApp::mouseReleased(int x, int y, int button) -{ - -} - -//-------------------------------------------------------------- -void testApp::windowResized(int w, int h) -{ - -} - -//-------------------------------------------------------------- -void testApp::gotMessage(ofMessage msg) -{ - -} - -//-------------------------------------------------------------- -void testApp::dragEvent(ofDragInfo dragInfo) -{ - -} diff --git a/particle-motion-example/src/testApp.h b/particle-motion-example/src/testApp.h deleted file mode 100644 index c62abd2..0000000 --- a/particle-motion-example/src/testApp.h +++ /dev/null @@ -1,26 +0,0 @@ -#pragma once - -#include "ofMain.h" -#include "ofxBvh.h" - -class testApp : public ofBaseApp { - -public: - void setup() override; - void update() override; - void draw() override; - - void keyPressed(int key) override; - void keyReleased(int key) override; - void mouseMoved(int x, int y) override; - void mouseDragged(int x, int y, int button) override; - void mousePressed(int x, int y, int button) override; - void mouseReleased(int x, int y, int button) override; - void windowResized(int w, int h) override; - void dragEvent(ofDragInfo dragInfo) override; - void gotMessage(ofMessage msg) override; - - ofEasyCam cam; - bool assetsReady = false; - bool audioReady = false; -}; diff --git a/scripts/build-all.sh b/scripts/build-all.sh index f8b23b1..de0cd59 100755 --- a/scripts/build-all.sh +++ b/scripts/build-all.sh @@ -23,6 +23,8 @@ apps=( "particle-motion-example" "motion-visualization" "marching-cubes" + "example-motion-ribbons" + "example-motion-field" ) for app in "${apps[@]}"; do @@ -34,6 +36,11 @@ echo "==> Building regression test suite" make -C "$repo_root/tests/ofxBvh-boundary" Release OF_ROOT="$of_root" -j"$jobs" echo "==> Running regression test suite" -"$repo_root/tests/ofxBvh-boundary/bin/ofxBvh-boundary.app/Contents/MacOS/ofxBvh-boundary" \ +case "$(uname -s)" in + Darwin) test_binary="$repo_root/tests/ofxBvh-boundary/bin/ofxBvh-boundary.app/Contents/MacOS/ofxBvh-boundary" ;; + Linux) test_binary="$repo_root/tests/ofxBvh-boundary/bin/ofxBvh-boundary" ;; + *) echo "Unsupported platform: $(uname -s)" >&2; exit 2 ;; +esac +"$test_binary" \ "$repo_root/example-bvh/bin/data" \ "$repo_root/tests/ofxBvh-boundary/fixtures" diff --git a/scripts/smoke-all.sh b/scripts/smoke-all.sh new file mode 100755 index 0000000..61d5e4e --- /dev/null +++ b/scripts/smoke-all.sh @@ -0,0 +1,76 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +capture_dir="${1:-}" +if [[ -z "$capture_dir" ]]; then + echo "usage: $0 /path/to/capture-directory" >&2 + exit 2 +fi +mkdir -p "$capture_dir" +capture_dir="$(cd "$capture_dir" && pwd)" + +apps=( + "example-bvh" + "example-sync-sound" + "example-pen-graphics" + "particle-motion-example" + "motion-visualization" + "marching-cubes" + "example-motion-ribbons" + "example-motion-field" +) + +render_app() { + local app="$1" + local artifact="$2" + shift 2 + local binary app_pid watchdog_pid result + case "$(uname -s)" in + Darwin) binary="$repo_root/$app/bin/$app.app/Contents/MacOS/$app" ;; + Linux) binary="$repo_root/$app/bin/$app" ;; + *) echo "Unsupported platform: $(uname -s)" >&2; exit 2 ;; + esac + if [[ ! -x "$binary" ]]; then + echo "Build $app first with scripts/build-all.sh." >&2 + exit 1 + fi + echo "Rendering $app" + # Linux resolves data relative to the working directory; use the same bin layout on both platforms. + ( + cd "$repo_root/$app/bin" + exec "$binary" --smoke-test "--capture=$capture_dir/$artifact.png" "$@" + ) >"$capture_dir/$artifact.log" 2>&1 & + app_pid=$! + # Bound an unresponsive renderer without terminating unrelated processes. + ( + sleep 90 + kill -TERM "$app_pid" 2>/dev/null || true + ) & + watchdog_pid=$! + result=0 + wait "$app_pid" || result=$? + kill "$watchdog_pid" 2>/dev/null || true + wait "$watchdog_pid" 2>/dev/null || true + if [[ "$result" -ne 0 ]] || ! grep -q 'SMOKE_TEST_OK' "$capture_dir/$artifact.log" \ + || [[ ! -s "$capture_dir/$artifact.png" ]]; then + cat "$capture_dir/$artifact.log" >&2 + echo "$app rendering failed (exit $result)." >&2 + exit 1 + fi + grep 'SMOKE_TEST_OK' "$capture_dir/$artifact.log" +} + +for app in "${apps[@]}"; do + render_app "$app" "$app" +done + +# A second pose verifies that each new shader study produces changing artwork. +for app in example-motion-ribbons example-motion-field; do + render_app "$app" "$app-later" --capture-frame=420 + if cmp -s "$capture_dir/$app.png" "$capture_dir/$app-later.png"; then + echo "$app produced identical captures at different motion frames." >&2 + exit 1 + fi +done diff --git a/scripts/validate-xcode-projects.sh b/scripts/validate-xcode-projects.sh index 99ee0e7..3502c6e 100755 --- a/scripts/validate-xcode-projects.sh +++ b/scripts/validate-xcode-projects.sh @@ -13,6 +13,8 @@ apps=( "particle-motion-example" "motion-visualization" "marching-cubes" + "example-motion-ribbons" + "example-motion-field" ) for app in "${apps[@]}"; do @@ -23,11 +25,11 @@ for app in "${apps[@]}"; do xcodebuild -project "$project" -list >/dev/null grep -Fq '"path": "src/main.cpp"' "$pbxproj" - grep -Fq '"name": "testApp.cpp"' "$pbxproj" - grep -Fq '"name": "testApp.h"' "$pbxproj" + grep -Fq '"name": "ofApp.cpp"' "$pbxproj" + grep -Fq '"name": "ofApp.h"' "$pbxproj" grep -Fq '"path": "../ofxBvh/src/ofxBvh.cpp"' "$pbxproj" - if grep -Eq 'ofApp\.(cpp|h)' "$pbxproj"; then - echo "$app references a missing template ofApp source" >&2 + if grep -Eq 'testApp\.(cpp|h)' "$pbxproj"; then + echo "$app references a removed testApp source" >&2 exit 1 fi diff --git a/shared/ExampleRuntime.h b/shared/ExampleRuntime.h new file mode 100644 index 0000000..9fe181b --- /dev/null +++ b/shared/ExampleRuntime.h @@ -0,0 +1,201 @@ +#pragma once + +#include "ofMain.h" +#include "ofAppGLFWWindow.h" +#include "ofxBvh.h" +#include + +#include +#include +#include + +namespace example { + +struct RunOptions { + bool smokeTest = false; + int frames = 90; + std::string capturePath; + + static RunOptions parse(int argc, char** argv) { + RunOptions options; + for (int i = 1; i < argc; ++i) { + const std::string argument(argv[i]); + if (argument == "--smoke-test") options.smokeTest = true; + else if (argument.rfind("--capture=", 0) == 0) options.capturePath = argument.substr(10); + else if (argument.rfind("--frames=", 0) == 0) options.frames = std::max(2, ofToInt(argument.substr(9))); + } + if (!options.capturePath.empty()) options.smokeTest = true; + return options; + } +}; + +// A hidden GLFW window still renders every frame, without activating a preview. +template +int launch(int argc, char** argv, const std::string& title) { + const auto options = RunOptions::parse(argc, argv); + ofGLFWWindowSettings settings; + settings.setSize(1024, 768); + settings.setGLVersion(3, 2); + settings.windowMode = OF_WINDOW; + settings.title = title; + settings.visible = !options.smokeTest; + auto window = ofCreateWindow(settings); + ofRunApp(window, std::make_shared(options)); + return ofRunMainLoop(); +} + +class Runtime { +public: + explicit Runtime(const RunOptions& options) : options(options) {} + + void setup() const { + ofSetFrameRate(options.smokeTest ? 0 : 60); + ofSetVerticalSync(!options.smokeTest); + if (options.smokeTest) ofSetRandomSeed(2012); + } + + float deltaSeconds() const { return options.smokeTest ? 1.0f / 60.0f : ofGetLastFrameTime(); } + float elapsedSeconds() const { return options.smokeTest ? ofGetFrameNum() / 60.0f : ofGetElapsedTimef(); } + bool isSmokeTest() const { return options.smokeTest; } + + void finishFrame(bool ready, const std::string& name, size_t geometryVertices) const { + if (!options.smokeTest) return; + auto* window = dynamic_cast(ofGetWindowPtr()); + auto* nativeWindow = window ? window->getGLFWWindow() : nullptr; + const bool passive = nativeWindow && !glfwGetWindowAttrib(nativeWindow, GLFW_VISIBLE) + && !glfwGetWindowAttrib(nativeWindow, GLFW_FOCUSED); + const GLenum error = glGetError(); + if (!ready || !passive || error != GL_NO_ERROR) { + ofLogError(name) << "SMOKE_TEST_FAILED ready=" << ready << " passive=" << passive << " glError=" << error; + ofExit(1); + return; + } + const bool lastFrame = ofGetFrameNum() + 1 >= options.frames; + if (ofGetFrameNum() != 0 && !lastFrame) return; + + ofImage capture; + capture.grabScreen(0, 0, ofGetWidth(), ofGetHeight()); + const auto& pixels = capture.getPixels(); + if (!pixels.isAllocated() || pixels.getWidth() < 16 || pixels.getHeight() < 128) { + ofLogError(name) << "SMOKE_TEST_FAILED framebuffer capture unavailable"; + ofExit(1); + return; + } + unsigned char minimum = 255; + unsigned char maximum = 0; + uint64_t hash = 1469598103934665603ull; + size_t variedPixels = 0; + const int left = pixels.getWidth() / 10; + const int right = pixels.getWidth() * 9 / 10; + const int top = std::max(100, int(pixels.getHeight() / 10)); + const int bottom = pixels.getHeight() * 9 / 10; + const auto reference = pixels.getColor(left, top); + for (int y = top; y < bottom; ++y) { + for (int x = left; x < right; ++x) { + const auto color = pixels.getColor(x, y); + for (const auto channel : {color.r, color.g, color.b}) { + minimum = std::min(minimum, channel); + maximum = std::max(maximum, channel); + hash = (hash ^ channel) * 1099511628211ull; + } + if (std::abs(int(color.r) - reference.r) + std::abs(int(color.g) - reference.g) + + std::abs(int(color.b) - reference.b) > 24) ++variedPixels; + } + } + if (!lastFrame) { + initialPixelsHash = hash; + return; + } + if (!pixels.isAllocated() || geometryVertices == 0 || maximum - minimum < 8 + || variedPixels < size_t((right - left) * (bottom - top)) / 500 || hash == initialPixelsHash) { + ofLogError(name) << "SMOKE_TEST_FAILED empty, flat, or unchanged content region" + << " vertices=" << geometryVertices << " variedPixels=" << variedPixels; + ofExit(1); + return; + } + if (!options.capturePath.empty() && !capture.save(options.capturePath)) { + ofLogError(name) << "SMOKE_TEST_FAILED unable to save capture"; + ofExit(1); + return; + } + ofLogNotice(name) << "SMOKE_TEST_OK frames=" << options.frames + << " renderer=GL3 pixelRange=" << int(maximum - minimum) << " geometryVertices=" << geometryVertices + << " variedPixels=" << variedPixels << " motionChanged=true hidden=true focused=false"; + ofExit(0); + } + +private: + RunOptions options; + mutable uint64_t initialPixelsHash = 0; +}; + +class MotionPlayback { +public: + std::array motions; + bool ready = false; + bool audioReady = false; + bool usingBundledMotion = false; + + void load(const std::string& name, bool bundledOnly, bool silent) { + constexpr std::array original = { + "bvhfiles/aachan.bvh", "bvhfiles/kashiyuka.bvh", "bvhfiles/nocchi.bvh"}; + constexpr std::array bundled = {"A_test.bvh", "B_test.bvh", "C_test.bvh"}; + ready = true; + usingBundledMotion = false; + for (size_t i = 0; i < motions.size(); ++i) { + const bool originalExists = !bundledOnly && ofFile::doesFileExist(original[i]); + usingBundledMotion = usingBundledMotion || !originalExists; + const auto path = originalExists ? std::string(original[i]) + : (bundledOnly ? std::string(bundled[i]) : "../../../example-bvh/bin/data/" + std::string(bundled[i])); + ready = motions[i].load(path) && ready; + motions[i].setLoop(true); + } + audioReady = !bundledOnly && !silent && ofFile::doesFileExist("Perfume_globalsite_sound.wav") + && audio.load("Perfume_globalsite_sound.wav"); + if (audioReady) { + audio.setLoop(true); + audio.play(); + } else if (!bundledOnly) { + ofLogNotice(name) << (usingBundledMotion ? "Bundled motion" : "Project motion") << "; using a silent internal clock."; + } + playhead = silent ? 4.0f : 0.0f; + } + + void update(float deltaSeconds, float speed = 1.0f) { + if (!ready) return; + const float duration = motions.front().getDuration(); + if (audioReady) { + audio.setSpeed(speed); + playhead = audio.getPosition() * audio.getDuration(); + } else { + playhead += deltaSeconds * speed; + } + playhead = std::fmod(playhead, duration); + if (playhead < 0.0f) playhead += duration; + position = playhead / duration; + for (auto& motion : motions) { + motion.setPosition(position); + motion.update(0.0f); + } + } + + float normalizedPosition() const { return position; } + + void drawStatus(float x = 10.0f, float y = 20.0f) const { + if (!ready) ofDrawBitmapString("Motion data is missing. See README.md.", x, y); + else if (!audioReady) ofDrawBitmapString(usingBundledMotion ? "Bundled motion / silent clock" : "Project motion / silent clock", x, y); + else if (usingBundledMotion) ofDrawBitmapString("Bundled motion / audio clock", x, y); + } + +private: + ofSoundPlayer audio; + float playhead = 0.0f; + float position = 0.0f; +}; + +inline glm::vec3 safeNormalize(const glm::vec3& value, const glm::vec3& fallback = {0, 1, 0}) { + const float lengthSquared = glm::dot(value, value); + return lengthSquared > 1e-12f ? value / std::sqrt(lengthSquared) : fallback; +} + +} // namespace example diff --git a/shared/LICENSE b/shared/LICENSE new file mode 100644 index 0000000..1816353 --- /dev/null +++ b/shared/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2026 Daito Manabe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.