This is a part of my Audiosurf 2 scripting documentation.
Audiosurf 2 is built on top of the paid version of the Unity generic game engine. It started under Unity 4 (early access and 1.0), was later upgraded to Unity 5, and the latest version is Unity 2017.4.37f1. The Audiosurf 2 game engine embeds Lua 5.1 to implement modding and skinning support. Some basic Unity concepts are relevant to Lua scripting in Audiosurf 2.
The only mesh file format Audiosurf 2 supports is Wavefront .OBJ, via the Unity addon "ObjReader". It does not support MTL files which often come with OBJ files; material settings must be specified by the script when it uses the model. ObjReader has internal support for MTL files so support for this could be added in the future. Dylan's skins often include MTL files anyways, presumably because he didn't bother to remove them or wanted to make it easier for us to examine his OBJ files in an external program. (If you are using an exporter in a program like Blender, leave the export MTL setting checked because that also puts something inside the OBJ file that ObjReader requires.) OBJ is a very simple and commonly supported format. The downside is there are limitations on the kind of data they can contain. Unity internally supports vertex colors, normals, tangents, two sets of UV coordinates, sub-meshes (one material per sub-mesh), and a custom bounding box. OBJ files cannot contain vertex colors, tangents, more than one set of UVs, or a custom bounding box. Also, ObjReader only supports faces composed of 3 or 4 vertices and ignores larger ones, so be sure to configure your OBJ exporter to export faces as triangles.
In skins, models can also be dynamically generated or modified with the BuildMesh function. BuildMesh is an advanced tool that allows skin scripts to directly control vertex positions, triangle definitions, the first set of UV coordinates, and vertex colors. It can also request automatically generated vertex normals. It is incapable of generating sub-meshes. BuildMesh is capable of applying changes to a model that was previously generated with BuildMesh, and such changes will be immediately applied to all instances of that model in the scene.
In addition to generating a mesh from scratch, BuildMesh can also load a model file into a mesh object. When loading a model in this way, it's possible to specify some special options to the OBJ importer: calculateNormals, calculateTangents, submeshesWhenCombining. These options are the only way to get models with tangents and/or sub-meshes into Audiosurf 2.
For advanced users, it is possible to directly modify the UnityEngine.Mesh
object returned by BuildMesh, which enables scripts to access the full capabilities of Unity meshes.
Each mesh can contain multiple sub-meshes, each of which can be assigned a different material. Audiosurf 2 currently only supports multiple materials in the materials
setting of gameobject
definitions. The first material in the materials list will go to the first sub-mesh and so on. Audiosurf 2's OBJ importer merges OBJ contents into a single mesh by default. To enable sub-mesh support you must load the model with BuildMesh's submeshesWhenCombining
option:
-- replace
mesh = "example.obj",
-- with
mesh = BuildMesh{mesh="example.obj", submeshesWhenCombining=true},
With this enabled, different OBJ groups will go to different sub-meshes. If you aren't sure what order the groups are in, open the OBJ file with Notepad++ and look for lines starting with "g" followed by a group name. All face definitions ("f") after a group definition go in that group.
A texture is an image file loaded into main memory and uploaded to the GPU, typically mapped onto a mesh with UV mapping and rendered by a material's shader. Textures have their own formats such as RGB24/RGBA32 (lossless and large, like bmp) or DXT1/DXT5 (lossy and compressed, like jpeg) that images get converted to when they are imported by Unity or Audiosurf 2.
If you specify a texture name containing <ALBUMART
it will load a texture containing the album art or, if there is no album art, the placeholder image (the old Audiosurf 2 logo in a black square). If the texture name starts with <ALBUMART_DOUBLEWIDTH
the texture will be twice as wide with two side-by-side images of the album art. If it starts with <ALBUMART_DOUBLEWIDTHMIRRORED
, the right side copy will be flipped. If you add GREY
or GRAY
, the texture will be modified to be greyscale (aka black and white). All of the special textures except <ALBUMART>
currently have critical bugs and aren't useful for the purpose of displaying album art in the game. See below for a way to fix this.
Unlike most other textures loaded by AS2, this one will be marked "readable" which means if you retrieve it from the material later you can read the pixels and modify them. If you use this image in multiple places it will normally reuse the same texture object to improve performance unless the name contains the <ALBUMART_DOUBLEWIDTHMIRRORED
or <ALBUMART_DOUBLEWIDTH
modifiers. (Bug: The GREY
/GRAY
modifier alters the original texture object unless another modifier is also used!)
Album art textures ignore any texture settings provided by the texture loader; they are always created with the below settings.
Format | DXT5 (RGB24 for the placeholder image) |
Wrap Mode | Repeat |
Mipmaps | (See Below) |
Filter Mode | Bilinear |
Aniso Level | 1 |
Read/Write Enabled | Yes |
Texture | Mipmaps | Unique Object | OK | Comments |
---|---|---|---|---|
<ALBUMART> | No | No | ✔️ | |
<ALBUMART_GREY> | No | No | ❌ | With the default art or the bug fix, permanently turns the game's internal copy of the texture grey until the game is closed. Otherwise, has no effect or crashes the game. |
<ALBUMART_DOUBLEWIDTH> | Yes | Yes | ⚠️ | Double width textures produce a corrupted image unless it's the default album art or the bug fix is applied. Still useful if you want to produce a new unrestricted texture object to use for other purposes. |
<ALBUMART_DOUBLEWIDTH_GREY> | Yes | Yes | ⚠️ | Corrupted image, needs bug fix. |
<ALBUMART_DOUBLEWIDTHMIRRORED> | Yes | Yes | ⚠️ | Dylan suggested this for skyspheres. Corrupted image, needs bug fix. |
<ALBUMART_DOUBLEWIDTHMIRRORED_GREY> | Yes | Yes | ⚠️ | Corrupted image, needs bug fix. |
If you want to use the ALBUMART_DOUBLEWIDTH
textures as they were designed, here is a Lua hack to make them work again. Put it in your skin anywhere above the first place you want to use a DOUBLEWIDTH
texture.
do -- Fix album art variation textures. local function getTextureFormatARGB32() local TextureFormat = import_type 'UnityEngine.TextureFormat' if TextureFormat then -- currently not whitelisted for importing return TextureFormat.ARGB32 end local cubemap_mat = BuildMaterial{ shader = "VertexColorUnlitTintedAddSmooth", textures = { _MainTex = { -- Any texture that exists (other than <ALBUMART>) will work here. Something small like white.png is best. front = "<ALBUMART_DOUBLEWIDTH>", --front = "white.png", }, }, } local result = cubemap_mat:GetTexture("_MainTex").format local name = tostring(result) if name ~= 'ARGB32' then error('Failed to get enum value TextureFormat.ARGB32, got "' .. name .. '" instead.', 2) end return result end local albumart_mat = BuildMaterial{ shader = "VertexColorUnlitTintedAddSmooth", texture = "<ALBUMART>", } local albumart = albumart_mat:GetTexture("_MainTex") --print('album art texture format: ' .. albumart.format:ToString()) -- https://docs.unity3d.com/2017.4/Documentation/ScriptReference/Texture2D.SetPixels32.html -- "This function works only on RGBA32, ARGB32 texture formats" -- Also, the RGB24 format currently used by the default album art seems to work fine. local format_name = albumart.format:ToString() if format_name ~= 'RGB24' and format_name ~= 'RGBA32' and format_name ~= 'ARGB32' then -- We are going to exploit the fact that Audiosurf 2 handed us the original -- texture rather than a copy in order to fix an internal bug in the album -- art variation textures. Unity only allows editing the pixels of -- uncompressed textures, and Audiosurf 2 is trying to do edits with copies -- of the compressed album art. local ARGB32 = getTextureFormatARGB32() local albumart_pixels = albumart:GetPixels32() albumart:Resize(albumart.width, albumart.height, ARGB32, true) albumart:SetPixels32(albumart_pixels) albumart:Apply() -- This change will persist in future reloads, main menus, and other skins. end end
Textures loaded from local files will be cached and reused across skin reloads until the game closes or a different skin is chosen. The texture's settings like clamping and mipmaps will also be reused, which might cause problems if a single file is used for multiple purposes. (Some settings will work but they will also retroactively apply to the first use of the texture.) The cache is case-sensitive on all platforms but it is not a good idea to exploit this for bypassing the cache because the game is available on Linux, which has case-sensitive paths.
There are multiple internal functions that Audiosurf uses to load textures. They are documented below. Check the documentation of individual texture settings to see which one is used and how it is configured.
This is the most commonly used texture loader. Loads a texture then directly adds it to a material. Although it has asynchronous capabilities, it doesn't use async I/O for reading any local files. (Asynchronous means that when the function returns it appears to not have loaded the texture into a material, but it will be added to the material on some later frame. For local files, it will be completely synchronous and the material will have the texture in it as soon as the function returns.)
Setting | Cached | Special | Skin/Mod Folder | HTTP |
---|---|---|---|---|
format | ? | Art: DXT5, Logo: RGB24 | JPEG: DXT1, PNG: DXT5 | JPEG: RGB24, PNG: ARGB32 |
wrapMode | Repeat** | Repeat** | Repeat** | Repeat** |
mipmap | ? | Normal: No, Wide: Yes | Yes* | Yes |
filterMode | Bilinear** | Bilinear** | Bilinear** | Bilinear** |
anisoLevel | 1** | 1** | 1** | 1* |
Asynchronous Load | No | No | No | Yes |
Read/Write Enabled | ? | Yes | No | No |
Synchronously loads a texture and returns it to the function that invoked it. This theoretically allows the calling function to overwrite most of these settings but in practice none of them do this at the time of writing. (Not to be confused with overrides, below, which are used.) The caller specifies whether the texture should be read/write enabled or not, and this affects the way downloaded textures are loaded. (Special textures will always be R/W enabled, as usual.)
Setting | Cached | Special | Skin/Mod Folder | HTTP (R/W Disabled)* | HTTP (R/W Enabled) |
---|---|---|---|---|---|
format | ? | Art: DXT5, Logo: RGB24 | * JPEG: DXT1, PNG: DXT5 | * JPEG: RGB24, PNG: ARGB32 | * JPEG: DXT1, PNG: DXT5 |
wrapMode | ? | Repeat | Repeat | Repeat* | Repeat* |
mipmap | ? | Normal: No, Wide: Yes | Yes* | Yes | Yes* |
filterMode | ? | Bilinear | Bilinear | Bilinear | Bilinear |
anisoLevel | ? | 1 | 1 | 1 | 1 |
Asynchronous Load | No | No | No | No | No |
Read/Write Enabled | ? | Yes | No* | No | Yes |
Similar to AsyncLoadTextureInto, but instead of directly adding the texture to a material it passes it to a setting-specific function that handles adding the texture to one or more materials and applying the setting. This loader also takes a wrapMode
override but, due to a bug, never actually applies it.
Setting | Cached | Special | Skin/Mod Folder | HTTP |
---|---|---|---|---|
format | ? | Art: DXT5, Logo: RGB24 | JPEG: DXT1, PNG: DXT5 | JPEG: RGB24, PNG: ARGB32 |
wrapMode | ? | Repeat | Repeat | Repeat |
mipmap | ? | Normal: No, Wide: Yes | * | Yes |
filterMode | ? | Bilinear | Bilinear | Bilinear |
anisoLevel | ? | 1 | 1 | 1 |
Asynchronous Load | No | No | No | Yes |
Read/Write Enabled | ? | Yes | No | No |
numvisiblewaterchunks
) there are up to 8 chunks.)Layers determine which camera an object will be rendered on, and can also sometimes be given other special functions.
According to Dylan there are 3 cameras: background, foreground, and close. They are drawn to the framebuffer in that order, and the depth buffer is cleared before the next one, so objects on the later cameras will always cover up objects on the previous camera no matter where they are in the world. If you have the standard glow effect enabled, all objects drawn to the background camera will be part of the glow effect.
You can see Dylan's official documentation here ("Advanced Editing" section, "Rendering Layers" subsection).
SetScene{airdebris_layer}
default.gameobject={layer}
default.CreateRail{layer}
default.SendCommand{command='SetRenderLayerRecursively'}
default if called without specifying a layer.SetSkybox{skyscreenlayer}
is forced to be this layer if the background camera is disabled.SetSkybox{skysphere}
is on this layer if the background camera is disabled.SetWake{layer = 13} -- looks better not rendered in background when water surface is not type 2
GetQualityLevel() <= 1
SetScene{closecam_near,closecam_far}
.CreateObject{gameobject={layer=ifhifi(18,13)}} -- in low detail the glow camera (layer 18) is disabled, so move the skywires to the main camera's layer (13)
SetScene{hide_default_background_verticals}
to get rid of the colored pillars that draw next to the track over objects in this layer.
Camera | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 27 |
---|---|---|---|---|---|---|---|---|
Background | O | O | O | O | O | |||
Foreground | O | O | O | O | O | |||
Close | O | O |
These are layers that currently exist in Audiosurf 2 but have not been documented by Dylan. I advise you not to use them at all unless you have to because the Unity engine is limited to 32 layers and so Dylan may be forced to repurpose some of these in future updates.
gameobject={thrusters={{layer}}
default.Default |
TransparentFX |
Ignore Raycast |
(reserved) |
Water |
UI |
(reserved) |
(reserved) |
UniSky - Droplet Effect |
UniSky - Offscreen Particles |
Unisky - Normal |
BothNormalAndBackground |
Both_IgnoreLights |
NormalOnly |
CloseOnly |
NormalBackgroundClose |
PostProcessing |
PostNavBar |
SkyWires |
SkyWireOverlays |
FogVolumeShadowCaster |
ClippedUIScrollingContent |
Popup_UI_2 |
PostMainOnly |
Minimap_Real3D |
Popup_UI_1 |
ModalDialogs_AboveNav |
LastGameplayCamOnly |
SkyScattering |
UI |
ALL (Even LastCamera) |
NavFrameTopLevel |
||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Camera | PostEffects Name | Clear | FOV | Dynamic FOV | Near Clip | Far Clip | Enabled | ||||||||||||||||||||||||||||||||
UI Webpage | Color32(255,255,255,5) | 60 | 0.3 | 1000 | |||||||||||||||||||||||||||||||||||
UI | Depth Only | Ortho, size 100 | 0.3 | 1000 | |||||||||||||||||||||||||||||||||||
UI Pause | Depth Only | Ortho, size 100 | 0.3 | 1000 | Always Enabled? | ||||||||||||||||||||||||||||||||||
UI Background | Color32(0,0,0,5) | Ortho, size 100 | 0.3 | 1000 | Loading screen. | ||||||||||||||||||||||||||||||||||
Close Blitter | topmost_exclusive | Don't Clear | 60 | 0.3 | 1000 | ||||||||||||||||||||||||||||||||||
Close | foreground | Depth Only | SetPlayer{fov} | 80 + (15 * bias) | 0.05 | 50 | After intro fly-in. | ||||||||||||||||||||||||||||||||
Foreground | middle | Depth Only | SetPlayer{fov} | 70 + (50 * bias) | 0.75 | 100000 | Always Enabled? | ||||||||||||||||||||||||||||||||
Background | background | Depth Only | SetPlayer{fov} | 70 + (50 * bias) | 1 | 100000 | Configurable. | ||||||||||||||||||||||||||||||||
SkyScattering | Color32(0,0,0,0) | SetPlayer{fov} | 70 + (50 * bias) | 1 | 100000 | Always Enabled? |
These are the cameras in the scene that seem to be currently in use, in reverse rendering order. (Topmost cameras first.) The parent of the gameplay cameras moves all of them together to follow the player (internals: AttachToHighway_NoRotation
, CameraTricky
). The UI cameras seem to have constant positions in world space. UI Webpage, UI Background, and Close Blitter have empty culling masks and so they do not draw any game objects; various scripts draw directly into them.
Setting | Initial Value |
---|---|
Parent | HUD_Lobby/WebViewer |
GameObject.name | "WebViewCamera" |
GameObject.tag | "Untagged" |
Transform.localPosition | {x=0, y=0, z=0} |
Transform.localEulerAngles | {x=0, y=0, z=0} |
Camera.clearFlags | CameraClearFlags.SolidColor |
Camera.backgroundColor | {r=1, g=1, b=1, a=0.01960784} |
Camera.rect | {x=0.1, y=0.15, width=0.8, height=0.8} |
Camera.nearClipPlane | 0.3 |
Camera.farClipPlane | 1000 |
Camera.fieldOfView | 60 |
Camera.orthographic | false |
Camera.orthographicSize | 100 |
Camera.depth | 55 |
Camera.cullingMask | 0x00000000 (see table) |
Camera.allowHDR | false |
Camera.useOcclusionCulling | true |
Setting | Initial Value |
---|---|
Parent | HUD |
GameObject.name | "UI_Cam" |
GameObject.tag | "uicamera" |
Transform.localPosition | {x=0, y=0, z=-10} |
Transform.localEulerAngles | {x=0, y=0, z=0} |
Camera.clearFlags | CameraClearFlags.Depth |
Camera.backgroundColor | {r=0.1921569, g=0.3019608, b=0.4745098, a=0.01960784} |
Camera.rect | {x=0, y=0, width=1, height=1} |
Camera.nearClipPlane | 0.3 |
Camera.farClipPlane | 1000 |
Camera.fieldOfView | 60 |
Camera.orthographic | true |
Camera.orthographicSize | 100 |
Camera.depth | 13 |
Camera.cullingMask | 0x20000000 (see table) |
Camera.allowHDR | false |
Camera.useOcclusionCulling | true |
Setting | Initial Value |
---|---|
Parent | HUD_PauseMenu |
GameObject.name | "Camera" |
GameObject.tag | "Untagged" |
Transform.localPosition | {x=0, y=0, z=-10} |
Transform.localEulerAngles | {x=0, y=0, z=0} |
Camera.clearFlags | CameraClearFlags.Depth |
Camera.backgroundColor | {r=0.1921569, g=0.3019608, b=0.4745098, a=0.01960784} |
Camera.rect | {x=0, y=0, width=1, height=1} |
Camera.nearClipPlane | 0.3 |
Camera.farClipPlane | 1000 |
Camera.fieldOfView | 60 |
Camera.orthographic | true |
Camera.orthographicSize | 100 |
Camera.depth | 12 |
Camera.cullingMask | 0x80000000 (see table) |
Camera.allowHDR | false |
Camera.useOcclusionCulling | true |
Setting | Initial Value |
---|---|
Parent | AA_ScreenBlockerCurtain |
GameObject.name | "Camera" |
GameObject.tag | "Untagged" |
Transform.localPosition | {x=0, y=0, z=0} |
Transform.localEulerAngles | {x=0, y=0, z=0} |
Camera.clearFlags | CameraClearFlags.SolidColor |
Camera.backgroundColor | {r=0, g=0, b=0, a=0.01960784} |
Camera.rect | {x=0, y=0, width=1, height=1} |
Camera.nearClipPlane | 0.3 |
Camera.farClipPlane | 1000 |
Camera.fieldOfView | 60 |
Camera.orthographic | true |
Camera.orthographicSize | 100 |
Camera.depth | 11 |
Camera.cullingMask | 0x00000000 (see table) |
Camera.allowHDR | false |
Camera.useOcclusionCulling | true |
Setting | Initial Value |
---|---|
Parent | CameraHolder1/PlayerFollower/[CameraRig]/Camera (head)/GameplayCameras |
GameObject.name | "BlitterCam_CloseCam" |
GameObject.tag | "Untagged" |
Transform.localPosition | {x=-1576.788, y=-4749.838, z=6129.216} |
Transform.localEulerAngles | {x=0, y=0, z=0} |
Camera.clearFlags | CameraClearFlags.Nothing |
Camera.backgroundColor | {r=0.1921569, g=0.3019608, b=0.4745098, a=0.01960784} |
Camera.rect | {x=0, y=0, width=1, height=1} |
Camera.nearClipPlane | 0.3 |
Camera.farClipPlane | 1000 |
Camera.fieldOfView | 60 |
Camera.orthographic | false |
Camera.orthographicSize | 5 |
Camera.depth | 12 |
Camera.cullingMask | 0x00000000 (see table) |
Camera.allowHDR | false |
Camera.useOcclusionCulling | false |
Setting | Initial Value | Blitter Mode |
---|---|---|
Parent | CameraHolder1/PlayerFollower/[CameraRig]/Camera (head)/GameplayCameras | |
GameObject.name | "CloseCam" | |
GameObject.tag | "Untagged" | |
Transform.localPosition | {x=0, y=0, z=100} | |
Transform.localEulerAngles | {x=0, y=0, z=0} | |
Camera.clearFlags | CameraClearFlags.Depth | CameraClearFlags.SolidColor |
Camera.backgroundColor | {r=0.1921569, g=0.3019608, b=0.4745098, a=0.01960784} | {r=0, g=0, b=0, a=0} |
Camera.rect | {x=0, y=0, width=1, height=1} | |
Camera.nearClipPlane | 0.05 | |
Camera.farClipPlane | 50 | |
Camera.fieldOfView | 90 | |
Camera.orthographic | false | |
Camera.orthographicSize | 100 | |
Camera.depth | 11 | |
Camera.cullingMask | 0x0000C000 (see table) | |
Camera.allowHDR | false | |
Camera.useOcclusionCulling | true | |
Camera.targetTexture | null | {width=Screen.width, height=Screen.height, depth=24, format=RenderTextureFormat.ARGB32, readWrite=RenderTextureReadWrite.Linear} |
Setting | Initial Value |
---|---|
Parent | CameraHolder1/PlayerFollower/[CameraRig]/Camera (head)/GameplayCameras |
GameObject.name | "All Plain Camera" |
GameObject.tag | "MainCamera" |
Transform.localPosition | {x=0, y=0, z=0} |
Transform.localEulerAngles | {x=0, y=0, z=0} |
Camera.clearFlags | CameraClearFlags.Depth |
Camera.backgroundColor | {r=0, g=0, b=0.0001775152, a=0} |
Camera.rect | {x=0, y=0, width=1, height=1} |
Camera.nearClipPlane | 0.75 |
Camera.farClipPlane | 100000 |
Camera.fieldOfView | 90 |
Camera.orthographic | false |
Camera.orthographicSize | 100 |
Camera.depth | 0 |
Camera.cullingMask | 0x4800B817 (see table) |
Camera.allowHDR | false |
Camera.useOcclusionCulling | true |
Setting | Initial Value |
---|---|
Parent | CameraHolder1/PlayerFollower/[CameraRig]/Camera (head)/GameplayCameras |
GameObject.name | "SkywireCam" |
GameObject.tag | "SkyBox" |
Transform.localPosition | {x=0, y=0, z=0} |
Transform.localEulerAngles | {x=0, y=0, z=0} |
Camera.clearFlags | CameraClearFlags.Depth |
Camera.backgroundColor | {r=0, g=0, b=0, a=0} |
Camera.rect | {x=0, y=0, width=1, height=1} |
Camera.nearClipPlane | 1 |
Camera.farClipPlane | 100000 |
Camera.fieldOfView | 90 |
Camera.orthographic | false |
Camera.orthographicSize | 100 |
Camera.depth | -1 |
Camera.cullingMask | 0x467F9CFE (see table) |
Camera.allowHDR | false |
Camera.useOcclusionCulling | true |
Setting | Initial Value |
---|---|
Parent | CameraHolder1/PlayerFollower/[CameraRig]/Camera (head)/GameplayCameras |
GameObject.name | "SkyScatteringCam_FirstCam" |
GameObject.tag | "Untagged" |
Transform.localPosition | {x=0, y=0, z=0} |
Transform.localEulerAngles | {x=0, y=0, z=0} |
Camera.clearFlags | CameraClearFlags.SolidColor |
Camera.backgroundColor | {r=0, g=0, b=0, a=0} |
Camera.rect | {x=0, y=0, width=1, height=1} |
Camera.nearClipPlane | 1 |
Camera.farClipPlane | 100000 |
Camera.fieldOfView | 90 |
Camera.orthographic | false |
Camera.orthographicSize | 100 |
Camera.depth | -2 |
Camera.cullingMask | 0x10000000 (see table) |
Camera.allowHDR | false |
Camera.useOcclusionCulling | true |