…Or not.

The title is referring to the fact that I didn’t actually finish my channel swizzler. I asked about this on twitter and realised that this tool is useful for others, so would be pretty cool to release properly. Unfortunately that means it needs to be flexible, robust and not just work for my one use case.

Had a big battle with directory syntax and json, but setup a number of different scripts for different use cases and a json config where the user can specify their own paths. Bit better than adding 4 environment vars!

To hide my supporting scripts I had to put them in a folder with Only at the end of the name – this meaning it was hidden. Not sure this is the most sensible way of hiding scripts, but sure.

Will wrap this up and distribute it soon – link incoming! Until then, some images of the tool in use below.

create channel textureswizzle


Finished Channel Swizzle

I decided to finish off my channel swizzler by creating a working files and Unity asset folder connection, so that the file in the asset folder is never actually edited, and there’s no confusion with which is which. It also means Unity will never import workfiles which will happen if they are stored in the project directory.

Because my hypthothetical users might store their project folder on different drives, locations etc, I decided to control the path through an environment variable. Editing things on the user’s pc like this isn’t always the best course of action, but works well for situations when you can’t bank on getting the user’s documents or another folder that windows knows.


To get photoshop to use this, I used $.getenv (which I thought was broken for a while, until realizing I’d made the variable with photoshop open – oops!), and then replaced all the backslashes with forward ones. This is because backslash is an escape character, used for declaring newlines.  Before I did this, the string was broken, and photoshop kept defaulting to its own path instead.

I then wanted to match the folder within workfiles to the folder within assets/textures. This meant I could manage the Unity folder structure via my saved workfile and didn’t need to resave/move/copy anything to get a nice organised folder. To get the last element in the path.split, I had to use string.pop(), which returns the last element of the list while removing it from said list. (or should I say array? We’re not in Python anymore Toto!) This worked nicley for this purpose, but I would like to find the equivalent of Python’s list[-1] as its really handy for this sort of thing.

var myfolderPath = $.getenv('PROJECT_PATH').replace("\\", "/") + '/' + 
     'Assets/Textures' + '/' + String(currentDoc.path).split("/").pop();
var filePath = new File(myfolderPath + '/' + 
     currentDoc.name.replace(/\.[^\.]+$/, '.psd'));


Channel Swizzle Script

Following on from my last blog post, I’ve finished most of my channel swizzle scripts. These generate a file with layers for each channel, then export as a new file with the layers combined into one layer as channels. I channel pack all my VFX textures so it will make my workflow a lot faster!


If you want to use this, feel free to save it as a .jsx file in C:\Program Files\Adobe\Adobe Photoshop CC 2019\Presets\Scripts, but I’d appreciate if you drop me a comment to let me know. Code below if you want to skip my rambling!

I had a couple of issues related to understanding the class hierarchy – for example, I thought that I could save select all as an object, and then perform copy on that object, but actually I needed to have a selection object, of which selectAll and copy are both methods – but I got there in the end!

I had a very odd “has no constructor” error when trying to make a new SolidColor instance, but the only way I solved it was by copying what looked like the same code from the scripting reference documents. I have no idea why mine didn’t work – an extra space or bad character perhaps? I haven’t found as many resources though google/stack overflow as I normally find for python and maya related topics.

EDIT: The no constructor error comes from forgetting to set Photoshop as your connected target application – just a silly mistake!

This one does currently save as a psd in the same location, with the same name, so will write over the source file. I’m not sure exactly what I want to do in terms of work file and export file right now, so I’ll change that once I’ve decided on a nice workflow.

Also, found a nice way to preserve the whitespace when copy pasting code onto this blog – the <pre></pre> HTML tag allows the text to be pasted exactly as is, which is super helpful and makes it much more readable.

function main()
	var currentDoc = app.activeDocument 

	//make duplicate
	fname = currentDoc.name.split(".")[0]
	newDoc = currentDoc.duplicate(fname)

	savePSD(currentDoc, newDoc)

function swizzle(doc)
	//make new layer to recive contents
	newLayer = doc.artLayers.add()
	newLayer.kind = LayerKind.NORMAL
	newLayer.name = "Channel Swizzle"
	//Fill layer with white
	var myWhite = new SolidColor()
	myWhite.rgb.red = 255
	myWhite.rgb.green = 255
	myWhite.rgb.blue = 255
	doc.selection. fill(myWhite)
	var channels = ["Red", "Green", "Blue"]
	var allLayers = doc.artLayers
	var i 
	for (i = 0; i < channels.length; i++)
		var myChannel = doc.channels.getByName(channels[i])
		var myLayer = allLayers.getByName(channels[i])
		doc.activeLayer = myLayer
		//select all
		//copy without merging
		//set active layer
		doc.activeLayer = newLayer
		//select channel
		doc.activeChannels = [myChannel]
		doc.paste() //needs a try catch for if there's nothing on the layer

	//put active channel back to all channels
	doc.activeChannels = doc.componentChannels

function savePSD(currentDoc, newDoc)
	//set png settings and file path
	var psdSettings = new PhotoshopSaveOptions()
	psdSettings.alphaChannels = false
	psdSettings.annotations = false
	psdSettings.embedColorProfile = true
	psdSettings.layers = false
	psdSettings.spotColors = false

	//set file path - careful not to echo anything in built here!
	filePath = new File(currentDoc.path + '/' + currentDoc.name.replace(/\.[^\.]+$/, '.psd'))

	//save new doc
	newDoc.saveAs(filePath, psdSettings, true, Extension.LOWERCASE)

	//close new doc and return focus to original
	app.activeDocument = currentDoc



Photoshop Scripting with JavaScript

I started having a look at java script for photoshop, as there’s a couple of repetitive tasks in there that frustrate me, such as saving non-psds and channel swizzling.

I chose js rather than apple script or visual basic as it can be applied to other platforms, such as web and is a more useful general skill.

To get started, I did the first “hello world” exercise from Adobe’s scripting guide. This was pretty fun to do, and js reminds me of a python/c# hybrid, so its not too unfamiliar. This gave me a new layer with “hello world” written on it.

//set units to inches
var originalUnit = preferences.rulerUnits //remebering this so we can set it back
preferences.rulerUnits = Units.INCHES

//create new doc
var docRef = app.documents.add(2,4) //2,4 = size

//new layer with text
var artLayerRef = docRef.artLayers.add()
artLayerRef.kind = LayerKind.TEXT

//set text value
var textItemRef = artLayerRef.textItem
textItemRef.contents = “Hello World”

//release references – why do I need to do this?
docRef = null
artLayerRef = null
textItemRef = null

//restore units
app.preferences.rulerUnits = originalUnit

Next, I used what I’d learned in that script, along with the methods listed in Adobe’s javascript reference document to write a script that creates three layers, named R, G, and B. This is the start of a channel swizzle script – one that will take the contents of each of these layers, then move them onto one layer as composite channels.  This will allow for far easier creation, editing and saving of channel packed textures – something I use ALL THE TIME for shaders and VFX.

var artLayerRefR = docRef.artLayers.add()
artLayerRefR.kind = LayerKind.NORMAL
//new layer with text
var artLayerRefG = docRef.artLayers.add()
artLayerRefG.kind = LayerKind.NORMAL
//new layer with text
var artLayerRefB = docRef.artLayers.add()
artLayerRefB.kind = LayerKind.NORMAL

artLayerRefR.name = “R”
artLayerRefG.name = “G”
artLayerRefB.name = “B”

Once I’d done this, I tidied it up a bit, the syntax for for loops in js (not as nice as python! ) and making the script a bit more readable.

var layerNames = [“R”, “G”, “B”]
var i
for (i = 0; i < layerNames.length; i++)
var artLayerRef = docRef.artLayers.add()
artLayerRef.kind = LayerKind.NORMAL
artLayerRef.name = layerNames[i]

I then used the reference doc again to try making a save script. This was broken for ages despite looking correct, until I realised I’d named my file path variable “path”. I really should have known from python that this is a bad idea! (Going to blame the fact that normally PyCharm picks this up for me – I need to get a nice javascript IDE.) It echoes the inbuilt path var and therefor was either returning an error, or saving where photoshop is installed, depending on admin permissions.

function savePNG()
//get current doc
currentDoc = app.activeDocument

//set png settings and file path
var pngSettings = new PNGSaveOptions()
pngSettings.compression = 0
pngSettings.interlaced = false

//set file path – careful not to echo anything in built here!
filePath = new File(currentDoc.path + ‘/’ + currentDoc.name.replace(/\.[^\.]+$/, ‘.png’))

//make duplicate to fuck about with
fname = currentDoc.name.split(“.”)[0]
newDoc = currentDoc.duplicate(fname)

//save new doc
newDoc.saveAs(filePath, pngSettings, true, Extension.LOWERCASE)

//close new doc and return focus to original
app.activeDocument = currentDoc



I combined the last two scripts to make the output part of the channel swizzle script, learning how javascript does try catch in the process. Photoshop’s alert function is very nice – allowing me to provide the user an error without having to write up any ui!

function scanLayers()
//get all layers
var allLayers = copyDoc.artLayers

//loop through layers
var r
var g
var b

//g = allLayers.getByName(“G”)

r = allLayers.getByName(“R”)
g = allLayers.getByName(“G”)
b = allLayers.getByName(“B”)

var errorMsg = “Please make sure your document has layers named R, G and B”
CloseAndReturn(copyDoc, docRef)

CloseAndReturn(copyDoc, docRef)


Next step isto get this copying the layer contents into channels.

Sending Text Messages with Python and Twilo

So this is a major departure from shaders, vfx and art tools but I’ve started playing around with sending SMS messages from python and wanted to keep a record.

I have a love of spooky internet mysteries, conspiracies and ARGs, and have been thinking about how I would make my own game like this. Being able to send texts to a users real phone seemed like a much cooler method of delivery than an android game that looked like an SMS app (though a spooky app as part of the game would not go remiss!).


A quick google search turned up that my best bet for something like this is Twilio, a service that gives you a phone number and allows you to send messages directly from their web page, or using their python module with your account’s authentication in your own scripts.

I was able to use the sample script they provide in their python quickstart tutorial to send myself a message! Numbers that you can send to must be registered, and there is a charge for each text. Thankfully their trial version gives you $15 of credit, and a text is $0.08, so I’m good for now.


from twilio.rest import Client
# Your Account Sid and Auth Token from twilio.com/console
account_sid = ‘ACb430d70d81ec0db4a6d563726a2cb121’
auth_token = ‘your_auth_token’
client = Client(account_sid, auth_token)

message = client.messages \


Virtual Environment 

A virtual environment allows you to keep your base python instillation vanilla, while installing modules and updates per project. This prevents updates for new projects affecting older ones still in use.

Twilio suggests setting this up manually, but luckily for me, PyCharm does this by default when creating a new project.



Flask is a web application module for python and I used this to setup a tiny web app that will be used to take incoming requests. This allows me to see when a reply to an sms has come through, and do something in return.

I started off by just making the app.


from flask import Flask

app = Flask(__name__)

def hello():
    return "Hello World!"

if __name__ == "__main__":
    app.run(debug = True)


In order to have my flask application and twilio talk, I needed to make my app available over the internet. To do this I used a tool called Ngrok.

To get this working, I had to edit my PATH environment var to include the folder where Ngrok lives. Once that was done, I could access it via command line.


Running this with the port that I was using with flask, I got a public tunnel to the server. Accessing this showed me the hello world script I’d written earlier.


Sending a Reply

Now that all the web stuff was setup, all I needed to do was use the twilio module to send a reply!

They have a message response class already setup, so all I had to do was import that and extend the original script to give it a string to reply with.


from flask import Flask
from twilio.twiml.messaging_response import MessagingResponse

app = Flask(__name__)

@app.route(“/sms”, methods=[‘GET’, ‘POST’])
def sms_reply():
reply = MessagingResponse()
reply.message(“Thankyou. It has been noted.”)
return str(reply)

if __name__ == “__main__”:
app.run(debug = True)

This is super cool! Hopefully I can use this to do something fun and ARG like!

Armor of Agathys VFX

This is the final effect created with the subsurface scattering effect I posted about earlier.
I started by creating an animation clip that changed the ice amount parameter of the shader over time.



I then added a quick particle effect that consisted of a glow, shiny dots and swirls. (If anyone thinks of a more professional way to talk about VFX do let me know! “Shiny Dots” is a phrase I use far too often at work!)


The glow used fairly high emission and low lifetime, plus size over life, to give the impression of light flickering. It just uses the default particle material, which works surprisingly well for effects like this.

The dots use radial velocity to pull them in, with orbit added on a single axis to give additional unnatural motion.


The trails used the texture below. Initially designed to be chunks that flew outwards, more similar to my concept image above, it worked nicely as a trial instead so I kept it. The gaps give me the nice tearing sort of effect in the swirl, and the details look far less blocky and messy when stretched out. This particle system also uses orbital and radial velocity.


Subsurface Scattering

For an ice effect I worked on recently, I used this tutorial by Alan Zucconi to learn about the implementation of subsurface scattering in Unity shaders.

The concept and mathematics are explained properly in his tutorial, but in brief, subsurface scattering is when light hits a translucent material and instead of exiting the other side like with a transparent one, it bounces around inside until it finds its way out. Therefore, light absorbed at one point is not transmitted at the same point.

This can be seen when things appear to glow slightly in real life – think skin, milk and marble.

In code, we take the dot product of the view direction and the negative light vector plus distortion amount multiplied by surface normal. We then alter this using a texture, scale and power. Essentially what we are doing is looking at light as if it had come directly out of the other side, checking how much of this the player can see due to the angle between their view direction and the vector, and distorting the vector based on some user parameters. This distortion is the fake subsurface scattering. In my example, it mostly relies on a thickness texture to allow artist control, which is shown below.


I also added ambiance and intensity controls, which Zucconi talks about in the second part of his tutorial. This is an artificial scalar of light propagation across the surface, and the overall intensity of that light.

This is all done in a function that extends the unity’s PBR translucent lighting. Code below!

#include “UnityPBSLighting.cginc”
inline fixed4 LightingStandardTranslucent(SurfaceOutputStandard s, fixed3 viewDir, UnityGI gi)
//Original Color
fixed4 pbr = LightingStandard(s, viewDir, gi); //inbuilt lighting function

//Swap out thickness value if area is not ice
thickness *= smoothstep(0, 0.1, (1 – iceAmount));

float3 lightVector = gi.light.dir;
float3 viewDirection = viewDir;
float3 surfaceNormal = s.Normal;

float3 halfPoint = normalize(lightVector + surfaceNormal * _Distortion);
float intensity = pow(saturate(dot(lightVector, -halfPoint)), _Power) * _Scale;
float subsurface = _Attenuation * (intensity + _Ambient) * thickness;

//Add to PBR
pbr.rgb += gi.light.color * subsurface;
return pbr;

void LightingStandardTranslucent_GI(SurfaceOutputStandard s, UnityGIInput data, inout UnityGI gi)
LightingStandard_GI(s, data, gi);


The other shader effects were a mixture of combining different channel packed textures and the alpha erosion and normal recalculation techniques I’ve talked about before on this blog.

The texture below was the main texture, with each channel scrolling seperatley to create the magical under-surface effect. This looks like it is below the surface of the ice because the details are not present on the thickness map, and therefore is independent of the subsurface scattering light function.


This is the thickness map, and is largely used to say which parts of the ice are thicker than others, thus how much light exits when passing through the surface. By using the pixelate -> cells filter in photoshop, I changed a clouds render into a voronoi noise pattern. This gave me a nice faceted sort of look that I wanted for my stylized ice.


The normal map was created from the thickness map, to make sure that reflected light was consistent with transmitted light.


The alpha erosion used a cloud render with levels used to increase the value range. Here, black areas erode sooner than white areas due to a subtractive method being used to control coverage.