Monday, July 22, 2019

Customizing the Lit Shader in Unity's Lightweight Render Pipeline

Unity used to have a "Surface Shader" system by which you could write a function that would specify certain material properties based on various textures, slider settings, etc., and then Unity would compile the numerous shaders needed to support various functions like shadows, deferred rendering, etc. without you having to manually write each one. It also let you write custom lighting functions for that Surface Shader framework.

Surface Shaders were part of the original Unity built-in render pipeline. But, the new Scriptable Render Pipelines, namely the High Definition RP and the Lightweight RP, don't have Surface Shaders. They do have a graphical node-based shader programming facility called Shader Graph. Besides the usual problems with graphical programming paradigms (namely, a few lines of textual code can into verbose visual spaghetti code, with confusing lines connecting many blocks spread out over multiple screens), Shader Graph restricts you to the particular lighting models Unity provides in the form of Master Nodes. Some have tried to work around this by doing lighting calculations in the blocks and feeding the final result into the "emission" input of a master node; however, getting lighting info into the Shader Graph is tough. Another options is to save the HLSL generated by the Shader Graph compiler and modify that; unfortunately, the compiler assigns the links between nodes arbitrary variable name that look like license plate numbers, making the generated HLSL difficult to follow.

These problems with Shader Graph lead to the sad conclusion that if you want to create your own lighting models, you need to copy and modify one of Unity's hand-crafted ubershaders, even if that means duplicating a lot of effort in different shader passes. 

If you want to implement a custom lighting model, you'll be spending a lot of time trying to understand Lighting.hlsl. It contains LightweightFragmentPBR (used by Lit shader) and LightweightFragmentBlinnPhong (used by Simple Lit shader). Lighting.hlsl imports from Common.hlsl, EntityLighting.hlsl, ImageBasedLighting.hlsl, Core.hlsl, and Shadows.hlsl.

We'll also talk explore dynamic vertex and texture coordinate modifications. These are easily implemented with Shader Graph, but we might as well address them while we're here.

LWRP Lit Shader Passes

Lit.shader consists of four passes: ForwardLit, ShadowCaster, DepthOnly, and Meta; they import LitForwardPass.hlsl, ShadowCasterPass.hlsl, DepthOnlyPass.hlsl, and LitMetaPass.hlsl, respectively. All four passes import from LitInput.hlsl.

Forward Lit Pass: The forward lit pass vertex shader is LitPassVertex and its fragment shader is LitPassFragment. These are defined in LitForwardPass.html, which imports from Lighting.hlsl. LitPassFragment calls InitializeStandardLitSurfaceData (defined in LitInput.hlsl), InitializeInputData (defined in LitForwardPass.hlsl), MixFog (defined in Core.hlsl), and LightweightFragmentPBR (defined in Lighting.hlsl).

Shadow Caster Pass: The shadow caster pass vertex shader is ShadowPassVertex and its fragment shader is ShadowPassFragment. These are defined in ShadowCasterPass.hlsl, which imports from Core.hlsl and Shadows.hlsl. ShadowPassFragment calls SampleAlbedoAlpha and Alpha (defined in SurfaceInput.hlsl). The Alpha function contains a clip command; it looks like ShadowCasterPass uses BaseMap only to the extent that it provides alpha clipping. ShadowPassFragment returns 0, so it doesn't really do anything else interesting.

Depth Only Pass: The depth only pass vertex shader is DepthOnlyVertex, and its fragment shader is DepthOnlyFragment. These are defined in DepthOnlyPass.hlsl, which imports Core.hlsl. DepthOnlyFragment calls SampleAlbedoAlpha and Alpha, which are defined in SurfaceInput.hlsl. The Alpha contains a clip command; it looks like DepthOnlyPass uses BaseMap only to the extent that it provides alpha clipping. (This was the case with ShadowCasterPass). DepthOnlyFragment returns 0, so it doesn't really do anything else interesting.

Meta Pass: The meta pass vertex shader is LightweightVertexMeta, and its fragment shader is LightweightFragmentMeta. These are defined in LitMetaPass.hlsl, which imports MetaInput.hlsl. LightweightVertexMeta calls MetaVertexPosition, which is defined in MetaInput.hlsl. LightweightFragmentMeta calls MetaFragment (defined in MetaInput.hlsl), InitializeStandardLitSurfaceData (defined in LitInput.hlsl), and InitializeBRDFData (defined in Lighting.hlsl).

If unity_MetaFragmentControl.x is true, it returns a (possibly tweaked) albedo value. If unity_MetaFragmentControl.y is true, it returns an emission value. This is confusing because the comments at the top say "y = return normal" and I don't see where it's
computing or storing a normal.


Passes for Other LWRP Shaders

SimpleLit.shader consists the same four passes: ForwardLit, ShadowCaster, DepthOnly, and Meta; they import SimpleLitForwardPass.hlsl, ShadowCasterPass.hlsl, DepthOnlyPass.hlsl, and SimpleLitMetaPass.hlsl, respectively. Note that the ShadowCaster and DepthOnly passes import the same hlsl files as the Lit shader, whereas the ForwardLit and Meta passes import hlsl files specialized for the SimpleLit shader. All four passes import from SimpleLitInput.hlsl instead of LitInput.hlsl. The SimpleLit specialized versions of files lack the additional fields and functions needed for a full PBR model. It invokes LightweightFragmentBlinnPhong (in Lighting.hlsl) instead of LightweightFragmentPBR, which is used by Lit.shader.

BakedLit.shader only has three passes: BakedLit, DepthOnly, and Meta.

Unlit.shader only has three passes: Unlit, DepthOnly, and Meta.

I won't explore BakedLit or Unlit shaders further here. I also won't explore the hey have even less functionality; I will not explore the ParticlesLit, ParticlesSimpleLit, ParticlesUnlit, or Terrain shaders.


Dynamic Vertex Modification

Forward Lit Pass: You could insert your vertex, normal, and tangent modification code
just before these lines in LitVertexPass (in LitForwardPass.hlsl), modifying the
positionOS, normalOS, and tangentOS fields in the "input" Attributes structure as desired:

VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
VertexNormalInputs normalInput = GetVertexNormalInputs(input.normalOS, input.tangentOS);

VertexPositionInputs and VertexNormalInputs are defined in Core.hlsl as:

struct VertexPositionInputs {
    float3 positionWS; // World space position
    float3 positionVS; // View space position
    float4 positionCS; // Homogeneous clip space position
    float4 positionNDC;// Homogeneous normalized device coordinates
};

struct VertexNormalInputs {
    real3 tangentWS;
    real3 bitangentWS;
    float3 normalWS;
};

The associated functions are defined as (note VertexNormalInputs is overloaded):

VertexPositionInputs GetVertexPositionInputs(float3 positionOS) {
    VertexPositionInputs input;
    input.positionWS = TransformObjectToWorld(positionOS);
    input.positionVS = TransformWorldToView(input.positionWS);
    input.positionCS = TransformWorldToHClip(input.positionWS);
   
    float4 ndc = input.positionCS * 0.5f;
    input.positionNDC.xy = float2(ndc.x, ndc.y * _ProjectionParams.x) + ndc.w;
    input.positionNDC.zw = input.positionCS.zw;
       
    return input;
}

VertexNormalInputs GetVertexNormalInputs(float3 normalOS) {
    VertexNormalInputs tbn;
    tbn.tangentWS = real3(1.0, 0.0, 0.0);
    tbn.bitangentWS = real3(0.0, 1.0, 0.0);
    tbn.normalWS = TransformObjectToWorldNormal(normalOS);
    return tbn;
}

VertexNormalInputs GetVertexNormalInputs(float3 normalOS, float4 tangentOS) {
    VertexNormalInputs tbn;

    // mikkts space compliant. only normalize when extracting normal at frag.
    real sign = tangentOS.w * GetOddNegativeScale();
    tbn.normalWS = TransformObjectToWorldNormal(normalOS);
    tbn.tangentWS = TransformObjectToWorldDir(tangentOS.xyz);
    tbn.bitangentWS = cross(tbn.normalWS, tbn.tangentWS) * sign;
    return tbn;
}

Shadow Caster Pass: There's two places you could insert your vertex and normal modification code. One possible hack would be to put your code at top of GetShadowPostionHClip (in ShadowCasterPass.hlsl), before the lines:

float3 positionWS = TransformObjectToWorld(input.positionOS.xyz);
float3 normalWS = TransformObjectToWorldNormal(input.normalOS);

A more elegant and clearer approach would be to modify the positionOS ad normalOS fields in the "input" Attributes structure just before this line in ShadowPassVertex:

output.positionCS = GetShadowPositionHClip(input);

Depth Only Pass: You could insert your vertex modification code in DepthOnlyVertex (in DepthOnlyPass.hlsl) just before the line

output.positionCS = TransformObjectToHClip(input.position.xyz);

For clarity, I'd probably write something like:

float3 modifiedPosition = SomeModificationOf(input.position.xyz);
output.positionCS = TransformObjectToHClip(modifiedPosition);

The stock depth only pass doesn't contain incorporate normals, which are often useful in creating interesting vertex deformations. Perhaps you could just add a "float3 normal : NORMAL;" line to the Attributes structure. I haven't tried this, though.

Meta Pass: You shouldn't need to touch the meta pass, since by definition meta passes are only invoked by the lightmapper, and hence are only useful on static geometry.


Dynamic Texture Coordinate Modification

Forward Lit Pass: The best place to modify texture lookups will depend on your particular goals. LitPassFragment has the line:

InitializeStandardLitSurfaceData(input.uv, surfaceData);

You could potentially modify the input.uv argument there, and that would more or less modify every texture lookup accordingly. For more fine-grained control, you could
modify InitializeStandardLitSurfaceData (in LitInput.hlsl), and modify the uv argument
inputs, as desired, in lines like:

half4 albedoAlpha = SampleAlbedoAlpha(uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap));
half4 specGloss = SampleMetallicSpecGloss(uv, albedoAlpha.a);
outSurfaceData.normalTS = SampleNormal(uv, TEXTURE2D_ARGS(_BumpMap, sampler_BumpMap), _BumpScale);
outSurfaceData.occlusion = SampleOcclusion(uv);
outSurfaceData.emission = SampleEmission(uv, _EmissionColor.rgb, TEXTURE2D_ARGS(_EmissionMap, sampler_EmissionMap));

SampleOcclusion and SampleMetallicSpecGloss are defined in LitInput.hlsl. SampleAlbedoAlpha, SampleNormal, and SampleEmission are defined in SurfaceInput.hlsl.

Shadow Caster Pass and Depth Only Pass: ShadowPassFragment (in ShadowCasterPass.html) and DepthOnlyFragment (in DepthOnlyPass.html) both the line:

Alpha(SampleAlbedoAlpha(input.uv, TEXTURE2D_ARGS(_BaseMap, sampler_BaseMap)).a, _BaseColor, _Cutoff);

You could modify the input.uv argument as desired. You would only need to do this if
you are using a clip test with the alpha channel and want to animate it.

Meta Pass: You shouldn't need to touch the meta pass, since by definition meta passes are only invoked by the lightmapper, and hence are only useful on static geometry.


Getting Ready to Customize the Lighting Model

Basically, you'll need to make your own variation of LightweightFragmentPBR, and variations of the macros and functions is calls, if needed.

Make a new "Shader" folder inside your main Assets folder if you haven't done so already.

Via dragging and dropping, copy Packages/Lightweight RP/Shaders/Lit.shader into your new "Shader" folder. Rename your copy of Lit.shader and change the Shader "Lightweight Render Pipeline/Lit" line at the top of the file as desired; I changed it to MyLit.hlsl and Shader "My Custom/My Lit" -- but you can use whatever names you want.

Sadly, the copying will break a bunch of filename include references. Search on #include in MyLit.hlsl, and change the four instances of the lines #include "LitInput.hlsl" to

#include "Packages/com.unity.render-pipelines.lightweight/Shaders/LitInput.hlsl"

Change the instance of #include "DepthOnlyPass.hlsl" to

#include "Packages/com.unity.render-pipelines.lightweight/Shaders/DepthOnlyPass.hlsl"

Change the instance of #include "ShadowCasterPass.hlsl" to

#include "Packages/com.unity.render-pipelines.lightweight/Shaders/ShadowCasterPass.hlsl"

Change the instance of #include "LitMetaPass.hlsl" to

#include "Packages/com.unity.render-pipelines.lightweight/Shaders/LitMetaPass.hlsl"
       
Via dragging and dropping, copy Packages/Lightweight RP/Shaders/LitForwardPass.hlsl into your new "Shader" folder. Rename your copy of LitForwardPass.hlsl. I changed it to MyLitForwardPass.hlsl -- but you can use whatever name you want.

In MyLit.shader, change #include "LitForwardPass.hlsl" to #include "MyLitForwardPass.hlsl"

The above assumes that you're not doing any dynamic vertex modification. If you are, you'll need to create a custom MyDepthOnlyPass.hlsl and MyShadowCasterPass.hlsl and change the various #includes appropriately. The above also assumes you're not doing any dynamic texture modifications that includes alpha clip tests; these will also necessitate custom MyDepthOnlyPass.hlsl and MyShadowCasterPass.hlsl files.

At this point, you should be able to take a material with the original LWRP Lit shader, switch it to your custom shader, and it should maintain all of its various textures and parameter settings and perform identically.

There's a couple of minor tweaks you may want to make before doing some major shader hacking:

1) To keep the vertex and fragment shader naming conventions consistent, in MyLit.shader, I'd recommend changing

#pragma vertex ShadowPassVertex
#pragma fragment ShadowPassFragment

to something like

#pragma vertex MyShadowPassVertex
#pragma fragment MyShadowPassFragment

2) If you want to modify the custom material inspector, make a new "Editor" folder inside your main Assets folder if you haven't done so already. Copy (via drag and drop) Packages/Lightweight RP/Editor/ShaderGUI/Shaders/LitShader.cs and Packages/Lightweight RP/Editor/ShaderGUI/ShadingModels/LitGUI.cs. Change the names of the files to MyLitShader.cs and MyLitGUI.cs (or whatever else you'd like).

In MyLitShader.cs and MyLitGUI.cs, change all the instances of LitGUI to MyLitGUI. In MyLitShader.cs, and LitShader in the class declaration to MyLitShader.

At the bottom of MyLit.shader, change the CustomEditor line to read

CustomEditor "UnityEditor.Rendering.LWRP.ShaderGUI.MyLitShader"

Coming up with a custom material inspector from scratch would be extremely painful, but it's not too taxing to make minor tweaks to an already working custom inspector.


Actually Customizing the Lighting Model

I recommend leaving the original Lighting.hlsl file, as well as the files it includes, intact. You can copy structures, functions, macros, etc. from those files, paste them into your MyLitForwardPass.hlsl, change the names, and then make your modifications; it seems easiest to have everything in one spot. If you wind up developing a lot of different shaders that share various bits of custom lighting code, you might want to break those out into a separate MyLighting.hlsl include file, or something like that.

Digging into MyLitPassFragment in MyLitForwardPass.hlsl, you'll want to drill down into InitializeStandardLitSurfaceData (defined in LitInput.hlsl), InitializeInputData (defined in MyLitForwardPass.hlsl), MixFog (defined in Core.hlsl), and LightweightFragmentPBR (defined in Lighting.hlsl).

MixFog (defined in Core.hlsl) is a one-line function that calls MixFogColor. It interpolates between the color computed by the shader and unity_FogColor.rgb according to fogFactor and some defines (FOG_LINEAR, FOG_EXP, FOD_EXP2). If you want to inject more complicated fog models, this would be the place to do it; otherwise, you probably don't need to change this. (That all said: I can't figure out where to turn on fog in the LWRP anyway! I will save that for another day).

InitializeStandardLitSurfaceData reads from various textures to populate a SurfaceData structure. If you want dynamic texture coordinate modifications that are particular to each texture, this would be a good place to do it. If you want to apply the same texture coordinate modification to everything, you might want to do it in the call to InitializeStandardLitSurfaceData (in MyLitPassFragment) instead, and leave the InitializeStandardLitSurfaceData function itself alone.

SurfaceData is defined in SurfaceInput.hlsl as:

struct SurfaceData {
    half3 albedo;
    half3 specular;
    half  metallic;
    half  smoothness;
    half3 normalTS;
    half3 emission;
    half  occlusion;
    half  alpha;
};

This definition is preceded has the comment "Must match Lightweight ShaderGraph master node." This is interesting, because it represents a connection point between the LWRP ShaderGraph framework and the primary hand-optimized, heavy-on-the-multicompile LWRP Lit shader. SurfaceData is quite reminiscent of the SurfaceOutput, SurfaceOutputStandard, and SurfaceOutputStandardSpecular structures of the Unity's old text-based Surface Shader system, which would make InitializeStandardLitSurfaceData analogous to the Surface Shader's surface functions.

InitializeInputData populates an InputData structure (defined in Input.hlsl) containing various quantities needed for lighting calculations:

struct InputData {
    float3  positionWS;
    half3   normalWS;
    half3   viewDirectionWS;
    float4  shadowCoord;
    half    fogCoord;
    half3   vertexLighting;
    half3   bakedGI;
};


LightweightFragmentPBR

Now we're at the main guts of MyLitPassFragment, namely the call to LightweightFragmentPBR, which is found in lighting.hlsl. It first calls InitializeBRDFData (in Lighting.hlsl), which pre-computes a bunch of values and stores them in a BRDFData struct:

struct BRDFData {
    half3 diffuse;
    half3 specular;
    half perceptualRoughness;
    half roughness;
    half roughness2;
    half grazingTerm;

    // We save some light invariant BRDF terms so we don't have to recompute
    // them in the light loop. Take a look at DirectBRDF function for detailed explaination.
    half normalizationTerm;     // roughness * 4.0 + 2.0
    half roughness2MinusOne;    // roughness² - 1.0
};

InitializeBRDFData sets the grazingTerm to saturate(smoothness + reflectivity); I confess I have no idea where that comes from.

The lighting calculation goes through four steps:

1) It adds the effect of global illumination (via a call to GlobalIllumination, found in Lighting.hlsl) and the effect of the "main light" (via a call to LightingPhysicallyBased, also found in Lighting.hlsl). In the process, it calls MixRealTimeAndBakedGI, which calls SubtractDirectMainLightFromLightmap (both defined in Lighting.hlsl); this implements an interesting but really complicated hack that I'm not going to explore further here.

2) It loops through the remaining lights, calling LightingPhysicallyBased on each one.

3) Unity's LWRP and built-in pipeline have a fairly complicated light hierarchy, in which up to four "unimportant lights" can be calculated with per-vertex lighting. It adds the results of those unimportant lights via a vertexLighting field in the inputData structure. This is assigned by the line

inputData.vertexLighting = input.fogFactorAndVertexLight.yzw;

in InitializeInputData (found in MyLitForwardPass.hlsl).

fogFactorAndVertexLight, which is part of the Varying structure that connects the vertex shader and the fragment shader through interpolation, is assigned in MyLitPassVertex, which calls VertexLighting, which in turn calls LightingLambert (both are defined in Lighting.hlsl).

4) It adds the emissive color.


Global Illumination

The GlobalIlumination function is short enough that I'll include the code here:

half3 GlobalIllumination(BRDFData brdfData, half3 bakedGI, half occlusion, half3 normalWS, half3 viewDirectionWS) {
    half3 reflectVector = reflect(-viewDirectionWS, normalWS);
    half fresnelTerm = Pow4(1.0 - saturate(dot(normalWS, viewDirectionWS)));

    half3 indirectDiffuse = bakedGI * occlusion;
    half3 indirectSpecular = GlossyEnvironmentReflection(reflectVector, brdfData.perceptualRoughness, occlusion);

    return EnvironmentBRDF(brdfData, indirectDiffuse, indirectSpecular, fresnelTerm);
}

Here, the fresnelTerm uses dot(normalWS, viewDirectionWS), which is what is customary for indirect light, in contrast with direct light, which dot products the half vector with the view vector, or equivalently, the half vector with the light vector.

GlobalIllumination is called from this line in LightweightFragmentPBR (in Lighting.hlsl):

half3 color = GlobalIllumination(brdfData, inputData.bakedGI, occlusion, inputData.normalWS, inputData.viewDirectionWS);

In InitializeInputData (in MyLitForwardPass.hlsl), we find the line:

inputData.bakedGI = SAMPLE_GI(input.lightmapUV, input.vertexSH, inputData.normalWS);

SAMPLE_GI is a macro that invokes SampleLightmap on objects with lightmaps and SampleSHPixel (for spherical harmonic lighting) on objects without lightmaps.

GlossyEnvironmentReflection does a lookup into a cube map, choosing a mip level appropriate with to the roughness.

EnvironmentBRDF is short enough that I will include it here:

half3 EnvironmentBRDF(BRDFData brdfData, half3 indirectDiffuse, half3 indirectSpecular, half fresnelTerm) {
    half3 c = indirectDiffuse * brdfData.diffuse;
    float surfaceReduction = 1.0 / (brdfData.roughness2 + 1.0);
    c += surfaceReduction * indirectSpecular * lerp(brdfData.specular, brdfData.grazingTerm, fresnelTerm);
    return c;
}

I have no idea where the surfaceReduction term comes from.

Direct Illumination

LightingPhysicallyBased (in Lighting.hlsl) has two overloaded variations:

half3 LightingPhysicallyBased(BRDFData brdfData, half3 lightColor, half3 lightDirectionWS, half lightAttenuation, half3 normalWS, half3 viewDirectionWS) {
    half NdotL = saturate(dot(normalWS, lightDirectionWS));
    half3 radiance = lightColor * (lightAttenuation * NdotL);
    return DirectBDRF(brdfData, normalWS, lightDirectionWS, viewDirectionWS) * radiance;
}

half3 LightingPhysicallyBased(BRDFData brdfData, Light light, half3 normalWS, half3 viewDirectionWS) {
    return LightingPhysicallyBased(brdfData, light.color, light.direction, light.distanceAttenuation * light.shadowAttenuation, normalWS, viewDirectionWS);
}

It looks like most of the work is done in the curiously misspelled DirectBDRF (it should be DirectBRDF), which implements an approximate Cook-Torrance model.


Where to Start Putting In a Custom Lighting Model

LightingPhysicallyBased handles direct lights; that's the easiest place to put in modified diffuse models (Oren-Nayer, Minnaert, Disney Diffuse, etc.), modified specular models, or completely weird lighting models. A good exercise would be to replace the optimized-for-mobile LWRP Lit model and replace it with as much as you can of the HDRP Lit model.

Weird lighting models are less likely to make sense with any of the global illumination facilities like lightmaps, reflection probes, and light probes. But it's possible that you could partially incorporate modified diffuse and modified specular models. Unity's lightmappers are largely black boxes, so it would be difficult to calculate lightmaps under anything except for the usual Lambertian light assumption. Similarly, Unity computes the convolved mipmaps for reflection probes using its particular specular BDRF assumptions. One could potentially explore ImageBasedLighting.hlsl and try creating convolved mipmaps for other functions, but I except that it would be a ton of work, and few people would be able to tell the difference for most conventional specular models. 

One could try replacing the use of the spherical harmonic light probes with reflection probes with mipmaps crafted for diffuse lighting instead of specular lighting. If you set Texture Shape in the texture inspector to Cube, a Convolute Type dropdown menu appears with the options Specular (Glossy Reflections) and Diffuse (Irradiance). The Diffuse option results in much more blurring per mipmap level, as expected. Interestingly, that option doesn't seem to be used anywhere in stock Unity.

Monday, January 21, 2019

Running CodeWarrior for Playstation Net Yaroze via SheepShaver

The Net Yaroze was a special version of the original Sony Playstation that let consumers write their own games. To prevent Net Yaroze games from competing with official Playstation releases created by licensed companies with professional development kits, a Net Yaroze game couldn't access the CD drive, and it could usually only be played by people who had a Net Yaroze system. (Occasionally, Sony would put Net Yaroze games on demo CDs that came with magazines).

I bought a Net Yaroze system from eBay over decade ago, and in that time have been looking for students that would be interested in experimenting with it, with no success. Now, a decade later, I finally found some students on my Retrofuturistic Hardware Vertically Integrated Project (VIP) team interested in taking it on. Before handing it off, I thought I'd take a crack at getting the development environment running.

My Net Yaroze came with a CD labeled "Metrowerks Codewarrior for Net Yaroze," containing both Windows and Mac versions. I can't seem to find that particular CD at the moment, but I believe it corresponds to this disk image. It came out in 1996, which puts in in the zone of Windows 95 and Classic Mac OS System 7. As expected, the installer programs didn't seem to want run on my Windows 10 desktop machine from 2014 or my Mac OS X laptop from 2015.

SheepShaver emulates PowerPC-based Macintoshes. I presumed that Mac software that works on System 7 will likely work on System 9, so I installed System 9. This video by AlexElectronics was particularly helpful in getting SheepShaver up and running. I went with the "New World PPC ROM" and the "Mac OS 9 Boot Image" available from this page by Redundant Robot.

Click on the images to see the full-size screenshots. We're up and running:


Let's first install Adobe Acrobat Reader, which is conveniently available on the install CD:


Now let's run the installer!




OK, let's run CodeWarrior:


Let's try to compile this Hello World program:



We got some errors, but I'll save looking into those for another day.

At this point, I noticed there were folders on the CD labeled "Unsupported Software" and "Misc Goodies" that weren't copied over as part of the installation procedure, so I copied those over manually.


I didn't have the Net Yaroze on me at the time of doing these experiments, but I thought it would be fun to try running PSComUtil, the app that connects to the Net Yaroze.



Unsurprisingly, I got some errors.

Net Yaroze connects to its host computer via a DB9 RS232 serial port, so to even try to connect my 2015 MacBook Pro would require a USB-to-RS232 adapter. I'm also not sure too what extent SheepShaver supports serial communication through such an adapter.

Friday, January 5, 2018

Fix your car's AC with 3 zip ties and save $900

Last summer, it seemed like the AC in my 2003 Nissan Sentra stopped working. I would turn up the fan, and I would hear the fan become louder, but air no additional air (of any temperature) came out. Atlanta in the Summer without AC is a markedly unpleasant experience. For a while I just rolled the windows down, but that has limited utility once July rolls around. 

I took my car in for repair. I had thought that maybe a hose had come loose and the CA (conditioned air, being the thing created by the air conditioning) was being dumped into the engine compartment, and hoped that would be a relatively inexpensive fix. The Nissan folks told me that the entire HVAC "computer unit" would need replaced, for a cost of $900. They then came back and said that they couldn't do the repair anyway since they couldn't order the part from Nissan, who had discontinued manufacturing that unit. They told me that if I managed to acquire such a unit, they could install it and just charge me for the install.

While I was looking at eBay and other sources to see how cheaply I could acquire the unit, I noticed that air was coming out of the defrost vents -- cold air, if I had the AC on. I realized that the core AC and fan components were working fine, but the knob that controls where the air comes out had stopped functioning and stuck on defrost regardless of where the knob was set. A bit of web searching discovered that this was not an uncommon problem, so I dived in. 

Alas, the lighting conditions for these photographs were suboptimal; I had to perform the procedure at night so I wouldn't be sweltering.

First, snap off the big plastic bit that covers the lower control panels:


Second, you need to remove two screws to remove the HVAC's bezel:




Third, you need to remove the plastic covering the middle vents:




I had to unplug the radio to be able to maneuver:



Now we can see the culprit. The rear of the vent selector knob is the big chunk of white plastic, with a metal rod that slides in and out of the black sheath with the shiny end. In the defrost setting, the big chunk of white plastic moves far enough to activate the microswitch on the lower right of the photo. There's supposed to be a clip that holds the shiny end of the black sheath in place. You can see it's broken off, with the result that turning the knob makes the metal rod push and pull the black sheath instead of sliding in and out of it, so the actual setting stays the same:



I used three zip ties and some epoxy/glue/sticky-something (I don't remember exactly what) to keep the shiny end in its place:

 

Unfortunately, at some point in this process I accidentally broke the clip that holds the corresponding black sheath for the temperature setting in place:



The clip was still in place, it just wasn't snapped on all the way, so I was able to use the temperature control as normal for about six months -- I just tried to avoid turning it unless I absolutely had to, and I turned it slowly when I did. It finally broke a couple days ago and is now stuck on the full heat setting. Luckily this isn't too terrible since it's freezing out and I generally want the heat on anyway, but when Spring comes around I'll need to dig back in there again and fix it.

There was another problem. When I plugged the radio back in and started to screw it back into place, I got disturbing sparks between the radio chassis and the car chassis. (Yes, I should have disconnected the car battery before beginning my expedition). I got out my multimeter and measured an 8 volt difference between the radio chassis and the car chassis. Needless to say, this was a BAD THING. The rest of the car appeared to be in working order, so I presumed the fault was in the radio. It had been working fine that day; I have no idea what went wrong. I took this opportunity to replace the radio (whose CD player had died years ago) with an aftermarket radio with an aux input jack and USB and SD card readers that I got from Fry's on clearance for around $8 -- but that is the topic for another blog post.

Here is the radio that bit the dust:

Sunday, September 17, 2017

Running Energia on Mac OS X

Energia is a programming environment for the Texas Instruments LaunchPads, based on the Wiring and Arduino frameworks. It took a bit of effort to get it working on my Mac. I'm running OS X 10.11.6, El Capitan; hopefully these instructions will work for the more recent OS X 10.12, Sierra.

1) The most obvious thing is to go to energia.nu/guide/guide_macosx instead of the windows page.

2) After starting Energia, go to the Tools -> Board -> Board Manager menu item and choose your target board. You should see a list of boards. For instance, you can choose

Energia MSP432 boards by Energia
Boards included in this package:
MSP_EXP432P401R

Clink Install for the entry corresponding to your target board. (I believe the entry listed above is for the black MSP432 Launchpad. If you have a red board, you should probably install the "MSP432 EMT Red boards" instead -- just a guess.)

3) Now quit Energia, since we'll need to implement an annoying workaround. When I first tried compiling a program, I got a dialog box with this error:

To use the "java" command-line tool you need to install a JDK.

Some discussion about this error may be found here. After doing a lot of google searching and hitting my head against the table a lot, I came up with a somewhat annoying but not utterly dreadful workaround.

If you open up a Terminal and type which java, it will tell you: /usr/bin/java

If you type java -version, it will tell you: No Java runtime present, requesting install.

Based on this webpage, I discovered /usr/bin/java is a link to

/System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java

Instead, it should be a link to

/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java

You can't readily change the /usr/bin/java link without disabling something called System Integrity Protection (SIP), which isn't really something I wanted to do.

So, here's my hack:

3A) In the Terminal, type

sudo ln -s "/Library/Internet Plug-Ins/JavaAppletPlugin.plugin/Contents/Home/bin/java” /usr/local/bin/java

That puts a link in /usr/local/bin, which SIP doesn't complain about.

3B) We need to get into the "package" of Energia.app. (I'm assuming you put it in your main Applications folder.) In the Terminal, type

cd /Applications/Energia.app/Contents/MacOS

In that folder, you'll find a file called Energia, which starts up the program. Let's rename it something else that we can call from our own Energia script. Type:

mv Energia Energia_main

3C) Then create a text file called Energia that contains these three lines:

#!/bin/bash
export PATH=/usr/local/bin:$PATH
./Applications/Energia.app/Contents/MacOS/Energia_main

Now, when you double-click on the Energia icon, it will change your path to put /usr/local/bin before /usr/bin so Energia will find the correct java command line program. This path change is restricted to this running of Energia, so hopefully this won't mess up any other program that for whatever reason really should have /usr/bin before /usr/local/bin in the path.

Usual caveats apply; your mileage may vary. Use the above information at your own risk (although I don't think I'm suggesting that you do anything particularly risky.)

Saturday, September 16, 2017

Teardown of SNES and Genesis USB Gamepads

I picked up these incredibly cheap USB gamepads on eBay: Two SNES-style for $8.90 total, and two Genesis style for $15.24 total. They work reasonably well.
Let's take them apart, starting with the SNES-style controller. Removing the screws from the back reveals the boring side of the PCB, showing the crystal and the four standard USB wires.

Flipping over the board reveals a glop top and ten nice pads and two through-hole connections for the shoulder buttons that you could solder to, giving you twelve inputs if you wanted to use the PCB as general button-like-thing-to-USB converter.

Here's what the SNES-style controller looks like with the buttons and D-pad removed.
Now let's take look at the Genesis-style controller. Removing the screws from the back once again reveals a crystal and the four standard USB wires. Also note that the shoulder buttons are tactile switches.

Flipping over the board reveals a glop top, eleven nice pads and two through-hole connections for the shoulder buttons that you could solder to, giving you thirteen inputs if you wanted to use the PCB as general button-like-thing-to-USB converter.
Here's what it looks like with the rubber membranes removed.
For completeness, here's what the individual buttons look like, along with the inside of the back of the controller: 


Tuesday, September 12, 2017

Let's put those electrons to work!

I'm Aaron Lanterman, a Professor of Electrical and Computer Engineering at Georgia Tech.* This is my blog on DIY electronics**, with an aesthetic bent towards the retrofuristic: steampunk, dieselpunk, and games such as Bioshock and Fallout. Over the next few months, we'll design and construct a custom bass guitar pedal and a R2-D2 hat that bleeps and blinks. We'll hack together some custom game controllers and portable gaming consoles. We'll play around with analog circuits for music synthesis. We'll fix some old tube radios. We'll fix a car's air conditioning using three zip ties and install a new car stereo.

Along the way, we'll do some reverse engineering, and experiment with "single-board computers" like the Raspberry Pi and "microcontroller boards" like Arduinos and the Texas Instruments Launchpads.

I have another blog, focused on education: Edupocalypse Now: Education and Innovation in the End Times.

Like everyone else in 2006, I had a Livejournal where I posted political rants and personal life observations. (I technically still have it, although I don't post much there nowadays.)

*Opinions expressed here are my own, and not those of my employer. I may at times provide commentary on equipment that was donated to me or  Georgia Tech; I will disclose such situations in the interest of fairness (although I believe I can offer unbiased advise).

**Any electronic project may present a risk of injury or death, particularly when dealing with mains voltages. It is important to follow appropriate safety practices. The author of these pages, Aaron Lanterman, disclaims any liability for injury, death, or other damage caused in using any of the information contained on these webpages.