Skip to content

Commit

Permalink
Merge pull request #349 from nudibranchrecords/feature/postprocessing…
Browse files Browse the repository at this point in the history
…-per-scene

Feature/postprocessing per scene
  • Loading branch information
funwithtriangles authored Feb 3, 2020
2 parents db59d98 + 1867151 commit 9f41039
Show file tree
Hide file tree
Showing 46 changed files with 946 additions and 564 deletions.
26 changes: 13 additions & 13 deletions docs/dev/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ This is the main file for the sketch. Essentially, it's a Javascript class, with
- `camera` - The [three.js camera](https://threejs.org/docs/#api/en/cameras/Camera) the scene is using
- `renderer` - The [three.js renderer](https://threejs.org/docs/#api/en/constants/Renderer)
- `sketchesDir` - The location of the top level sketches directory. Useful for loading in external assets.
- `outputSize` - An object with `width` and `height` properties that match the image output resolution
- `update` - This method is called every frame. It has a single object literal as an argument, with the following properties:
- `params` - A key/value pair of all the params in the sketch
- `elapsedTimeMs` - Elapsed time in milliseconds, since Hedron was started
Expand All @@ -120,6 +121,7 @@ This is the main file for the sketch. Essentially, it's a Javascript class, with
- `deltaFrame` - How many should have passed since the last update was fired. Ideally, this will always be at 1. If the program experiences some lag and the FPS drops below 60, this will be some number greater than 1. Use this to multiply with any incremental values to keep animation speeds consistent
- `tick` - Raw number of updates that have actually been fired since Hedron started
- `allParams` - A key/value pair of all the sketches in the scene, aech with their own key/value pair of params
- `outputSize` - An object with `width` and `height` properties that match the image output resolution
- `destructor` - Fires when a sketch is removed. Use this to clean up anything that may continue to run afterwards. Not usually needed. Has the same argument object as the construtor, except for the `params` property.

### Shots
Expand Down Expand Up @@ -268,20 +270,21 @@ module.exports = Solid
```

## Post Processing
Custom post processing, such as pixel shaders, are handled inside of sketches. Hedron's post processing system is a thin wrapper around the [postprocessing](https://github.com/vanruesc/postprocessing) library, so it's a good idea to understand how that works. Multiple passes can be added to the rendering composer using `initiatePostProcessing` and things are updated every frame using `updatePostProcessing`.
Custom post processing, such as pixel shaders, are handled inside of sketches. Hedron's post processing system is a thin wrapper around the [postprocessing](https://github.com/vanruesc/postprocessing) library, so it's a good idea to understand how that works. Multiple passes can be created inside of `initiatePostProcessing` and returned as an array. `initiatePostProcessing` has a similar argument object to the class constructor (see above), except `renderer` is replaced with `composer` (for experimental usage).

All values are updated with the usual `update` method.

Please note that this feature is still very much under development and so will most likely see many changes to the API in future.

### Simple example
Below is a simple example of how to achieve a bloom effect. A config file is also needed, which is exactly the same as a normal sketch config file.

```javascript
// POSTPROCESSING is a global variable available to Hedron sketches
const { EffectPass, BloomEffect, BlendFunction, KernelSize } = POSTPROCESSING
// THe postprocessing library is available under the HEDRON global variable
const { EffectPass, BloomEffect, BlendFunction, KernelSize } = window.HEDRON.dependencies.postprocessing

class Bloom {
// Here we add our passes to the composer
initiatePostProcessing ({ composer }) {
initiatePostProcessing () {
// Create a bloom effect
this.bloomEffect = new BloomEffect({
blendFunction: BlendFunction.SCREEN,
Expand All @@ -292,16 +295,13 @@ class Bloom {
height: 480,
})

// Add the bloom effect as a new pass to the composer
// Create your passes and return as an array
const pass = new EffectPass(null, this.bloomEffect)
composer.addPass(pass)

// Return the pass that needs to be rendered to the screen
return pass
return [ pass ]
}

// This method will be called every frame, just like the usual update method
updatePostProcessing ({ params }) {
update ({ params }) {
this.bloomEffect.blurPass.scale = params.scale
this.bloomEffect.luminanceMaterial.threshold = params.lumThreshold
this.bloomEffect.luminanceMaterial.smoothing = params.lumSmoothing
Expand All @@ -315,8 +315,8 @@ module.exports = Bloom
### Other examples
There are plenty of other examples that can be found in the [example sketches folder](../../example-projects).

### How to see the output in Hedron
Currently, post processing only works on the final output of Hedron (there are plans for a per-scene option too). To see the output of your passes, the sketch must be added to a scene. This scene must have "Global Post Processing" enabled under the scene settings for the passes to take effect. An icon will appear on the scene thumbnail if this setting is enabled. The scene does not need to be added to any channel, `updatePostProcessing` will always be running with this setting on.
### Global postprocessing
By default, any post processing will only affect the scene that sketch is in. This means that fading the scene out on the crossfader will also fade out any post processing effects you have in that scene. However, by checking "Global Post Processing" enabled under the scene settings, the effect will now work across all scenes. An icon will appear on the scene thumbnail if this setting is enabled. The scene does not need to be added to any channel, `update` will always be running with this setting on.

As a convention, it makes sense to have a post processing scene, with post processing related sketches added to it. This scene does not need to have any 3D objects in it and never needs to be added to a channel.

Expand Down
File renamed without changes.
File renamed without changes.
27 changes: 27 additions & 0 deletions example-projects/sketches/fragment-shader/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module.exports = {
defaultTitle: 'Fragment Shader',
params: [
{
key: 'distanceStep',
title: 'distanceStep',
defaultMax: 20,
defaultValue: 0.1,
},
{
key: 'yMorphAmp',
title: 'Y Morph Amp',
},
{
key: 'yMorphMix',
title: 'Y Morph Mix',
},
{
key: 'xMorphAmp',
title: 'X Morph Amp',
},
{
key: 'xMorphMix',
title: 'X Morph Mix',
},
],
}
26 changes: 26 additions & 0 deletions example-projects/sketches/fragment-shader/frag.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#ifdef GL_ES
precision mediump float;
#endif

uniform float distanceStep;
uniform float xMorphAmp;
uniform float xMorphMix;
uniform float yMorphAmp;
uniform float yMorphMix;

const vec2 center = vec2(0.5);

void mainImage(const in vec4 inputColor, const in vec2 uv, out vec4 outputColor) {
// Remap the space to -1. to 1.
vec2 st = uv - center;
st.x *= resolution.x/resolution.y;

float xMorph = mix(1., st.x * xMorphAmp, xMorphMix);
float yMorph = mix(1., st.y * yMorphAmp, yMorphMix);

// Make the distance field
float d = length( st * yMorph * xMorph );

// Visualize the distance field
outputColor = vec4(vec3(fract(d*distanceStep)),1.0);
}
38 changes: 38 additions & 0 deletions example-projects/sketches/fragment-shader/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
const { postprocessing, glslify, THREE } = window.HEDRON.dependencies
const { Uniform } = THREE
const { EffectPass, Effect } = postprocessing

const fragmentShader = glslify.file('./frag.glsl')

class FragmentShader {
initiatePostProcessing ({ params }) {
// Define all params as uniforms
const paramUniforms = Object.entries(params).map(
([key, value]) => [key, new Uniform(value)]
)

class PatternEffect extends Effect {
constructor () {
super('PatternEffect', fragmentShader, {
uniforms: new Map([
...paramUniforms,
]),
})
}
}

this.effect = new PatternEffect()

return [ new EffectPass(null, this.effect) ]
}

update ({ params }) {
// Update all uniforms using params
for (const [key, value] of Object.entries(params)) {
this.effect.uniforms.get(key).value = value
}
}
}

module.exports = FragmentShader

File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { EffectPass, BloomEffect, BlendFunction, KernelSize } = postprocessing

class Bloom {
// Here we add our passes to the composer
initiatePostProcessing ({ composer }) {
initiatePostProcessing () {
this.bloomEffect = new BloomEffect({
blendFunction: BlendFunction.SCREEN,
kernelSize: KernelSize.LARGE,
Expand All @@ -16,14 +16,13 @@ class Bloom {
// Please refer to the postprocessing documentation to understand how these classes work
// https://github.com/vanruesc/postprocessing
const pass = new EffectPass(null, this.bloomEffect)
composer.addPass(pass)

// Return the pass that needs to be rendered to the screen
return pass
// Return the pass in an array
return [ pass ]
}

// This method will be called every frame, just like the usual update method
updatePostProcessing ({ params: p }) {
update ({ params: p }) {
this.bloomEffect.blurPass.scale = p.scale
this.bloomEffect.luminanceMaterial.threshold = p.lumThreshold
this.bloomEffect.luminanceMaterial.smoothing = p.lumSmoothing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { postprocessing } = window.HEDRON.dependencies
const { EffectPass, BrightnessContrastEffect, GammaCorrectionEffect, HueSaturationEffect } = postprocessing

class Color {
initiatePostProcessing ({ composer }) {
initiatePostProcessing () {
this.brightnessContrastEffect = new BrightnessContrastEffect()
this.gammaCorrectionEffect = new GammaCorrectionEffect()
this.hueSaturationEffect = new HueSaturationEffect()
Expand All @@ -13,13 +13,11 @@ class Color {
this.hueSaturationEffect
)

composer.addPass(pass)

// Return the pass that needs to be rendered to the screen
return pass
// Return the pass in array
return [ pass ]
}

updatePostProcessing ({ params: p }) {
update ({ params: p }) {
this.brightnessContrastEffect.uniforms.get('brightness').value = p.brightness
this.brightnessContrastEffect.uniforms.get('contrast').value = p.contrast
this.brightnessContrastEffect.blendMode.opacity.value = p.brightnessContrastOpacity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const { postprocessing } = window.HEDRON.dependencies
const { EffectPass, ChromaticAberrationEffect, GlitchEffect } = postprocessing

class Glitch {
initiatePostProcessing ({ composer }) {
initiatePostProcessing () {
this.rgbShiftEffect = new ChromaticAberrationEffect()

this.glitchEffect = new GlitchEffect({
Expand All @@ -12,14 +12,11 @@ class Glitch {
const glitchPass = new EffectPass(null, this.glitchEffect)
const rgbPass = new EffectPass(null, this.rgbShiftEffect)

composer.addPass(glitchPass)
composer.addPass(rgbPass)

// Return the pass that needs to be rendered to the screen
return rgbPass
// Return array of passes in correct order
return [ glitchPass, rgbPass ]
}

updatePostProcessing ({ params: p }) {
update ({ params: p }) {
this.glitchEffect.delay.x = p.delayMin * 1000
this.glitchEffect.delay.y = p.delayMax * 1000

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const { EffectPass, SavePass, TextureEffect } = postprocessing
const FeedbackEffect = require('./FeedbackEffect')

class Trails {
initiatePostProcessing ({ composer }) {
initiatePostProcessing () {
this.feedbackEffect = new FeedbackEffect()

const savePass = new SavePass()
Expand All @@ -17,14 +17,10 @@ class Trails {
const feedbackPass = new EffectPass(null, this.feedbackEffect)
const texturePass = new EffectPass(null, textureEffect)

composer.addPass(feedbackPass)
composer.addPass(savePass)
composer.addPass(texturePass)

return texturePass
return [feedbackPass, savePass, texturePass]
}

updatePostProcessing ({ params: p }) {
update ({ params: p }) {
this.feedbackEffect.uniforms.get('scale').value = p.scale
this.feedbackEffect.uniforms.get('rotAngle').value = p.rotAngle
this.feedbackEffect.uniforms.get('mixAmp').value = p.mixAmp
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
10 changes: 6 additions & 4 deletions mockTests/scenes.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ jest.mock('uid', () => () => {

jest.mock('../src/engine/renderer', () => ({
setPostProcessing: jest.fn(),
channelUpdate: jest.fn(),
}))

const rootListener = {
Expand Down Expand Up @@ -95,7 +96,11 @@ test('(mock) Scenes - Add/Delete/Reorder scenes', () => {
shots: [],
},
},
nodes: {},
nodes: {
sceneCrossfader: {
value: 0,
},
},
sketches: {},
scenes: {
currentSceneId: false,
Expand All @@ -113,9 +118,6 @@ test('(mock) Scenes - Add/Delete/Reorder scenes', () => {

state = store.getState()

// 'nodes start empty'
expect(state.nodes).toEqual({})

// 'sketches start empty'
expect(state.sketches).toEqual({})

Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"jsonfile": "^2.4.0",
"lodash": "^4.17.15",
"performance-now": "^2.1.0",
"postprocessing": "^6.6.0",
"postprocessing": "^6.10.0",
"proxyquire": "^1.7.11",
"react": "^16.9.0",
"react-addons-perf": "^15.4.2",
Expand All @@ -95,7 +95,7 @@
"styled-components": "^5.0.0-beta.9",
"tap-diff": "^0.1.1",
"tap-tempo": "^0.1.1",
"three": "^0.109.0",
"three": "^0.112.1",
"tinycolor2": "^1.4.1",
"try-require": "^1.2.1",
"uid": "^0.0.2"
Expand All @@ -121,8 +121,8 @@
"cross-env": "^5.2.0",
"css-loader": "^0.28.7",
"deep-freeze": "^0.0.1",
"electron": "^7.1.0",
"electron-builder": "^20.38.5",
"electron": "^7.1.9",
"electron-builder": "^22.2.0",
"electron-debug": "^1.4.0",
"electron-devtools-installer": "^2.2.4",
"electron-log": "^2.2.14",
Expand Down
50 changes: 42 additions & 8 deletions projectFixScripts/0.5-0.6.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,51 @@ const parseOldOptions = (key, node) => {
fix(data => {
for (const key in data.nodes) {
const node = data.nodes[key]
node.optionIds = []

// option Ids have become a single array
parseOldOptions('lfoOptionIds', node)
parseOldOptions('midiOptionIds', node)
parseOldOptions('audioOptionIds', node)
parseOldOptions('animOptionIds', node)
if (!node.optionIds) {
// option Ids now share the same array property
node.optionIds = []
parseOldOptions('lfoOptionIds', node)
parseOldOptions('midiOptionIds', node)
parseOldOptions('audioOptionIds', node)
parseOldOptions('animOptionIds', node)
}

// We now have valueType, default is "float"
if (node.type === 'param' && !node.valueType) {
node.valueType = 'float'
const becomeFloat = ['param', 'macro', 'macroTargetParamLink']
if (!node.valueType && becomeFloat.includes(node.type)) node.valueType = 'float'

// Shots become shotFloat
if (node.type === 'shot') {
node.valueType = 'shotFloat'
}

// Selects become enums
if (node.type === 'select') {
delete node.type
node.valueType = 'enum'
}

// Convert macro param link start values from "false" to "null"
if (node.type === 'macro' && node.targetParamLinks) {
for (let key in node.targetParamLinks) {
const obj = node.targetParamLinks[key]
if (obj.startValue === false) obj.startValue = null
}
}

// Channel change action payload property from "type" to "channel"
if (
node.type === 'linkableAction' &&
node.action &&
node.action.type === 'U_SCENE_SELECT_CHANNEL' &&
node.action.payload && node.action.payload.type &&
!node.action.payload.channel
) {
const p = node.action.payload
const channel = p.type
delete p.type
p.channel = channel
}
}

Expand Down
Loading

0 comments on commit 9f41039

Please sign in to comment.