Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 12 additions & 17 deletions .github/workflows/processing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,25 +55,20 @@ jobs:
mkdir -p "${RUNNER_TEMP}/processing-4.5.6"
unzip -q "${RUNNER_TEMP}/processing.zip" -d "${RUNNER_TEMP}/processing-4.5.6"

- name: Compile the PDE sketch with Processing 4.5.6
- name: Compile and render all sketches; reject invalid and missing shaders
run: |
PROCESSING_BIN=$(find "${RUNNER_TEMP}/processing-4.5.6" -type f \( -name processing -o -name Processing \) -print -quit)
test -n "${PROCESSING_BIN}"
chmod +x "${PROCESSING_BIN}"
"${PROCESSING_BIN}" cli \
--sketch="${GITHUB_WORKSPACE}/p5f_sample" \
--output="${RUNNER_TEMP}/p5f-sample-build" \
--force \
--build
xvfb-run --auto-servernum ./scripts/verify-sketches.sh \
"${PROCESSING_BIN}" "${RUNNER_TEMP}/sketch-verification"

- name: Run a ten-frame P3D smoke test
run: |
set -o pipefail
PROCESSING_BIN=$(find "${RUNNER_TEMP}/processing-4.5.6" -type f \( -name processing -o -name Processing \) -print -quit)
timeout 30s xvfb-run --auto-servernum "${PROCESSING_BIN}" cli \
--sketch="${GITHUB_WORKSPACE}/p5f_sample" \
--output="${RUNNER_TEMP}/p5f-sample-run" \
--force \
--run \
--smoke-test | tee "${RUNNER_TEMP}/p5f-sample-smoke.log"
grep -q "SMOKE_TEST_OK" "${RUNNER_TEMP}/p5f-sample-smoke.log"
- name: Preserve rendered frames and runtime logs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: processing-render-verification
path: |
${{ runner.temp }}/sketch-verification/**/frame.png
${{ runner.temp }}/sketch-verification/**/runtime.log
if-no-files-found: warn
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
target/
.DS_Store
*.class
**/captures/
28 changes: 28 additions & 0 deletions LICENSE-new-examples
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
MIT License

Copyright (c) 2026 Daito Manabe

This license applies only to the newly written .pde and GLSL shader files
under motion_ribbons/ and motion_field/, the SketchRun.pde helper in
p5f_sample/, scripts/verify-sketches.sh, and tests/shaders/. It does not apply to the
pre-existing p5f_sample source, BVHParser JAR/source/API documentation,
or original BVH motion data. Copies of that existing parser in each
new sketch's code/ directory retain their existing copyright status.

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.
55 changes: 46 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,43 @@
# Perfume Global Site Project — Processing example
# Perfume Global Site Project — Processing examples

[![Processing 4](https://github.com/perfume-dev/example-processing/actions/workflows/processing.yml/badge.svg)](https://github.com/perfume-dev/example-processing/actions/workflows/processing.yml)

A Processing sketch that visualizes the three bundled BVH motion-capture files as animated point skeletons. This repository originated with the 2012 Perfume Global Site Project and is preserved as a small, runnable creative-coding example.
Three small, runnable motion-visualization sketches for Processing 4.5.6. The original 2012 Perfume Global Site Project example now uses retained `PShape` geometry, `PVector` coordinates, typed collections, and an explicit playback clock. Two new shader studies turn the same BVH recordings into ribbons and contour fields.

| Sketch | What to learn | Renderer |
| --- | --- | --- |
| [`p5f_sample`](p5f_sample/p5f_sample.pde) | BVH playback; update a retained skeleton with `PShape.setVertex()` | P3D |
| [`motion_ribbons`](motion_ribbons/) | Cached joint trajectories; triangle strips; vertex + fragment shaders | P3D |
| [`motion_field`](motion_field/) | Send joint positions as uniforms; draw isocontours and interference in one fragment pass | P2D |

![Motion ribbons, rendered by Processing](docs/images/motion-ribbons.png)

![Motion field, rendered by Processing](docs/images/motion-field.png)

These are actual rendered frames from the bundled recordings, at 16 seconds and 8 seconds respectively.

## Compatibility

- [Processing 4.5.6](https://processing.org/download/) (Java mode)
- macOS, Windows, or Linux with OpenGL support for the `P3D` renderer
- macOS, Windows, or Linux with OpenGL support for the `P2D` and `P3D` renderers
- No separately installed Processing libraries

The bundled `p5f_sample/code/BVHParser.jar` is built from the source in `p5f_sample/lib/src` against Processing Core 4.5.6 and Java 17.
The bundled `code/BVHParser.jar` in each sketch is the same small library built from `p5f_sample/lib/src` against Processing Core 4.5.6 and Java 17. CI checks all copies against the reproducible Maven build. [Processing's `PShader`](https://processing.org/reference/PShader) exposes vertex and fragment stages; the ribbon topology is generated on the CPU. These sketches do not require a geometry-shader extension or a separately installed library.

## Run the sketch

1. Install Processing 4.5.6.
2. Open `p5f_sample/p5f_sample.pde` in Processing.
3. Press **Run**.
2. Download or clone this entire repository.
3. Open `p5f_sample/p5f_sample.pde`, `motion_ribbons/motion_ribbons.pde`, or `motion_field/motion_field.pde` in Processing.
4. Press **Run**.

The sketch loads `A_test.bvh`, `B_test.bvh`, and `C_test.bvh` from its `data` directory and plays all three motions in a continuously orbiting 3D view.
The original BVH files remain in `p5f_sample/data/`. The new sketches resolve this directory relative to their own sketch directory, so the repository can live anywhere. For **Export Application** or a standalone sketch folder, copy `A_test.bvh`, `B_test.bvh`, and `C_test.bvh` into that sketch's own `data/` directory first; local files take priority. The original data has not been modified or duplicated in Git.

All views recenter each dancer's root X/Z position and place the dancers side by side, retaining the recorded joint poses and vertical movement. This is a visualization layout, not the original stage formation. `MotionData.position()` (or `PBvh.stagePosition()`) is the small coordinate-conversion function to change when preserving the original translation is desired.

`Space` pauses; `R` restarts; `H` toggles labels; `S` saves a PNG under the sketch's `captures/` folder. Drag to orbit the 3D views. In the field sketch, `+` / `-` change contour density and `J` displays the driving joints. The views adapt to window size, and playback continues while another application has focus.

日本語: Processingで各フォルダーの同名 `.pde` を開けば実行できます。リボンは「関節の軌跡 → 三角形ストリップ → 頂点・フラグメントシェーダー」、フィールドは「関節座標 → uniform → 等高線」という最小構成です。元のモーションデータを共有するため、最初はリポジトリ全体をダウンロードしてください。

## Development and verification

Expand All @@ -33,14 +52,32 @@ After changing the Java library source, rebuild the bundled Processing JAR and i
```sh
mvn -B package javadoc:javadoc
cp target/BVHParser.jar p5f_sample/code/BVHParser.jar
cp target/BVHParser.jar motion_ribbons/code/BVHParser.jar
cp target/BVHParser.jar motion_field/code/BVHParser.jar
rm -rf p5f_sample/lib/doc
cp -R target/reports/apidocs p5f_sample/lib/doc
find p5f_sample/lib/doc -type f \( -name '*.html' -o -name '*.css' -o -name '*.js' \
-o -name 'ADDITIONAL_LICENSE_INFO' \) \
-exec perl -pi -e 's/[ \t]+$//' {} +
```

CI additionally compiles the complete PDE sketch with the official Processing 4.5.6 command-line tool and verifies that the committed JAR is identical to the reproducible Maven build.
Use the official Processing executable (`Processing` on macOS, `processing` on Linux) to compile and render all sketches:

```sh
./scripts/verify-sketches.sh /path/to/Processing /path/to/test-output
```

On Linux without a desktop, prefix the command with `xvfb-run --auto-servernum`. Each run has a 60-second timeout. CI uses this same script, renders all three sketches at 8 and 16 seconds, checks that the two images differ, verifies nonempty framebuffers and shader link status, and confirms that missing or malformed shaders fail. PNGs and runtime logs are retained as workflow artifacts. The two helper tabs (`SketchRun.pde` and `MotionData.pde`) are kept byte-identical across their copies so that each sketch opens directly in the PDE without preprocessing or extra setup.

For one deterministic capture (the parent output directory must exist):

```sh
/path/to/Processing cli --sketch="$(pwd)/motion_ribbons" \
--output=/path/to/a-new-build-directory --run \
--smoke-test --time=16 --capture=/path/to/ribbons.png
```

`--smoke-test` / `--capture` use a fixed timeline, hide labels, suppress window focus requests, and exit after the captured frame. Normal interactive playback uses elapsed time. For automated captures on macOS, `JAVA_TOOL_OPTIONS=-Dapple.awt.UIElement=true` also prevents a transient Dock application from activating.

## Project stewardship and credits

Expand All @@ -50,4 +87,4 @@ The repository history credits [Satoru Higa](https://github.com/satoruhiga) for

## License

No license file was included in the original repository. Unless and until the maintainers add one, the source and bundled motion data remain subject to their respective owners' copyright; public availability does not by itself grant reuse rights.
The newly written shader examples and test helper are MIT-licensed as scoped explicitly in [`LICENSE-new-examples`](LICENSE-new-examples), copyright 2026 Daito Manabe. This does **not** relicense the pre-existing source, original parser (including its copied JARs), or motion data. No license file was included with those original materials; they remain subject to their respective owners' copyright, and public availability does not by itself grant reuse rights.
Binary file added docs/images/motion-field.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/motion-ribbons.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions motion_field/MotionData.pde
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) 2026 Daito Manabe
// SPDX-License-Identifier: MIT; see ../LICENSE-new-examples
class MotionData {
final BvhParser parser = new BvhParser();

MotionData(String filename) {
// Prefer local data when exporting; share the original files in a checkout.
java.io.File local = new java.io.File(dataPath(filename));
java.io.File shared = new java.io.File(sketchPath("../p5f_sample/data/" + filename));
java.io.File source = local.isFile() ? local : shared;
if (!source.isFile()) throw new IllegalStateException(
"Missing " + filename + ". Keep the full repository together, or copy the BVH files into this sketch's data folder.");
parser.parse(loadStrings(source));
}

void update(float seconds) {
parser.moveMsTo(round(seconds * 1000));
parser.update();
}

BvhBone joint(String name) {
for (BvhBone bone : parser.getBones()) if (bone.getName().equals(name)) return bone;
throw new IllegalArgumentException("Required BVH joint not found: " + name);
}

PVector position(PVector point) {
PVector root = parser.getBones().get(0).absPos;
return new PVector(-(point.x - root.x) * 1.55, (85 - point.y) * 1.55,
-(point.z - root.z) * 1.55);
}
}
16 changes: 16 additions & 0 deletions motion_field/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Motion field

![Actual Processing render at 8 seconds](../docs/images/motion-field.png)

Open `motion_field.pde` in Processing 4.5.6 and press Run. Keep the full repository together so the sketch can load the shared files from `../p5f_sample/data/`. No additional Processing library is required. For export, copy those three BVH files into this folder's `data/` directory first.

Six joints from each of the three recordings become an array of eighteen `vec3` uniforms: X, Y, and dancer number. One fullscreen rectangle runs `data/field.frag`; its pixels sum joint influence, draw logarithmic isocontours, and mix a small interference term. Screen derivatives keep contour edges smooth at different resolutions. The third uniform component chooses the restrained cyan/coral/ivory palette.

- `motion_field.pde`: BVH-to-uniform conversion, shader pass, controls.
- `data/field.frag`: field, contour spacing, antialiasing, and palette.
- `MotionData.pde`: legacy BVH adapter; recenters root X/Z for the side-by-side layout.
- `SketchRun.pde`: shader validation, deterministic capture, and smoke test.

Space pauses; + / - change contour density; J reveals the driving joints; R resets; H toggles labels; S saves a screenshot. Window resizing preserves the shape proportions. Change the six entries in `jointNames` to try different body landmarks, or change the `influence` and `wave` expressions to explore another field.

See the [root README](../README.md#development-and-verification) for deterministic CLI captures. Only the new sketch/shader code is covered by [`LICENSE-new-examples`](../LICENSE-new-examples); the original BVH data and parser JAR retain their existing copyright status.
99 changes: 99 additions & 0 deletions motion_field/SketchRun.pde
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) 2026 Daito Manabe
// SPDX-License-Identifier: MIT; see ../LICENSE-new-examples
import java.nio.IntBuffer;
import processing.opengl.PGL;
import processing.opengl.PShader;

// Automated runs retain an OpenGL window, but suppress its focus request.
protected PSurface initSurface() {
PSurface result = super.initSurface();
if (args != null) for (String arg : args) {
if (arg.equals("--smoke-test") || arg.startsWith("--capture=")) {
((com.jogamp.newt.opengl.GLWindow) result.getNative()).setFocusAction(() -> true);
}
}
return result;
}

class SketchRun {
boolean automated = false;
boolean paused = false;
String capturePath;
float captureTime = 8;
float elapsed = 0;
int previousMillis = millis();
final int captureFrame = 12;

SketchRun() {
if (args != null) for (String arg : args) {
if (arg.equals("--smoke-test")) automated = true;
if (arg.startsWith("--capture=")) {
capturePath = arg.substring("--capture=".length());
if (!new java.io.File(capturePath).isAbsolute()) {
throw new IllegalArgumentException("--capture requires an absolute output path");
}
automated = true;
}
if (arg.startsWith("--time=")) captureTime = Float.parseFloat(arg.substring(7));
}
if (!Float.isFinite(captureTime) || captureTime < 0 || captureTime > 3600) {
throw new IllegalArgumentException("--time must be finite, between 0 and 3600 seconds");
}
}

float seconds() {
int now = millis();
if (!paused) elapsed += min((now - previousMillis) / 1000.0, 0.1);
previousMillis = now;
return automated ? max(0, captureTime + (frameCount - captureFrame) / 60.0) : elapsed;
}

void togglePause() { paused = !paused; }
void reset() { elapsed = 0; paused = false; previousMillis = millis(); }

PShader checkedShader(String fragment, String vertex) {
try {
if (!new java.io.File(dataPath(fragment)).isFile() ||
(vertex != null && !new java.io.File(dataPath(vertex)).isFile())) {
throw new IllegalStateException("A required shader file is missing");
}
PShader program = vertex == null ? loadShader(fragment) : loadShader(fragment, vertex);
// Processing can print a shader error and continue. Make that a test failure.
program.init();
PGL gl = beginPGL();
IntBuffer linked = IntBuffer.allocate(1);
gl.getProgramiv(program.glProgram, PGL.LINK_STATUS, linked);
String log = gl.getProgramInfoLog(program.glProgram);
endPGL();
if (linked.get(0) != 1) throw new IllegalStateException("Shader did not link: " + log);
return program;
} catch (RuntimeException failure) {
System.err.println("SHADER_ERROR: " + failure.getMessage());
// JOGL shutdown hooks can deadlock when compilation fails on its draw
// thread. Test failures exit immediately; normal runs use exit().
if (automated) Runtime.getRuntime().halt(1);
throw failure;
}
}

void finishFrame(String name, int motions) {
if (!automated || frameCount < captureFrame) return;
loadPixels();
int litPixels = 0;
for (int pixel : pixels) {
if (max((pixel >> 16) & 255, max((pixel >> 8) & 255, pixel & 255)) > 65) litPixels++;
}
if (litPixels < 600) {
System.err.println("SMOKE_TEST_FAILED: rendered frame is unexpectedly empty");
Runtime.getRuntime().halt(1);
}
if (capturePath != null) {
save(capturePath);
java.io.File output = new java.io.File(capturePath);
if (!output.isFile() || output.length() < 1000) throw new IllegalStateException("Capture was not saved");
}
println("SMOKE_TEST_OK sketch=" + name + " frames=" + frameCount +
" motions=" + motions + " litPixels=" + litPixels + " focused=" + focused);
exit();
}
}
Binary file added motion_field/code/BVHParser.jar
Binary file not shown.
47 changes: 47 additions & 0 deletions motion_field/data/field.frag
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) 2026 Daito Manabe
// SPDX-License-Identifier: MIT
#ifdef GL_ES
#extension GL_OES_standard_derivatives : enable
precision highp float;
precision mediump int;
#endif
#define PROCESSING_COLOR_SHADER
uniform vec2 resolution;
uniform float time;
uniform float density;
uniform vec3 sources[18];

vec3 palette(float id) {
if (id < 0.5) return vec3(0.24, 0.86, 0.84);
if (id < 1.5) return vec3(1.0, 0.38, 0.30);
return vec3(0.94, 0.86, 0.66);
}

void main() {
float fieldScale = min(resolution.y, resolution.x / 1.6);
vec2 p = (gl_FragCoord.xy - resolution * 0.5) / fieldScale;
vec3 ink = vec3(0.0);
float total = 0.0;
float wave = 0.0;
float nearest = 10.0;
for (int i = 0; i < 18; i++) {
float d = length(p - sources[i].xy);
float influence = 0.007 / (d * d + 0.006);
total += influence;
ink += palette(sources[i].z) * influence;
wave += sin(d * 49.0 - time * 1.6 + float(i) * 0.36) * exp(-d * 7.0);
nearest = min(nearest, d);
}
ink /= max(total, 0.001);
float level = log(1.0 + total) * 14.0 * density + wave * 0.24;
float band = abs(fract(level) - 0.5);
float pixelWidth = max(fwidth(level), 0.004);
float line = 1.0 - smoothstep(pixelWidth * 0.45, pixelWidth * 1.4, band);
float envelope = smoothstep(0.14, 0.9, total);
float halo = exp(-nearest * 17.0);
vec3 base = vec3(0.022, 0.037, 0.060);
vec3 color = base + ink * (line * 0.58 * envelope + halo * 0.07);
color += vec3(0.50, 0.64, 0.69) * pow(halo, 9.0) * 0.25;
float vignette = 1.0 - smoothstep(0.42, 1.0, length(p * vec2(0.65, 1.0)));
gl_FragColor = vec4(color * (0.4 + 0.6 * vignette), 1.0);
}
Loading