About

Author: DeathByNukes

This is a part of my Audiosurf 2 scripting documentation.

Contents

Overview

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.

Meshes

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.

Sub-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.

Textures

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.

Special Textures

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.

FormatDXT5 (RGB24 for the placeholder image)
Wrap ModeRepeat
Mipmaps(See Below)
Filter ModeBilinear
Aniso Level1
Read/Write EnabledYes

All Combinations

TextureMipmapsUnique ObjectOKComments
<ALBUMART>NoNo✔️
<ALBUMART_GREY>NoNoWith 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>YesYes⚠️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>YesYes⚠️Corrupted image, needs bug fix.
<ALBUMART_DOUBLEWIDTHMIRRORED>YesYes⚠️Dylan suggested this for skyspheres. Corrupted image, needs bug fix.
<ALBUMART_DOUBLEWIDTHMIRRORED_GREY>YesYes⚠️Corrupted image, needs bug fix.

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

Texture Cache

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.

Texture Loader

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.

AsyncLoadTextureInto

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.)

SettingCachedSpecialSkin/Mod FolderHTTP
format?Art: DXT5, Logo: RGB24JPEG: DXT1, PNG: DXT5JPEG: RGB24, PNG: ARGB32
wrapModeRepeat**Repeat**Repeat**Repeat**
mipmap?Normal: No, Wide: YesYes*Yes
filterModeBilinear**Bilinear**Bilinear**Bilinear**
anisoLevel1**1**1**1*
Asynchronous LoadNoNoNoYes
Read/Write Enabled?YesNoNo
* This is a default value that might be overridden by the setting that uses this loader. Check the documentation of specific texture settings.
** Same as above but, in rare cases where the loader only puts it into the cache without loading it into a material, this setting will not be applied.

SyncLoadTexture

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.)

SettingCachedSpecialSkin/Mod FolderHTTP (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?RepeatRepeatRepeat*Repeat*
mipmap?Normal: No, Wide: YesYes*YesYes*
filterMode?BilinearBilinearBilinearBilinear
anisoLevel?1111
Asynchronous LoadNoNoNoNoNo
Read/Write Enabled?YesNo*NoYes
* This is a default value that might be overridden by the setting that uses this loader. Check the documentation of specific texture settings.

AsyncLoadTextureThenBroadcastTo

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.

SettingCachedSpecialSkin/Mod FolderHTTP
format?Art: DXT5, Logo: RGB24JPEG: DXT1, PNG: DXT5JPEG: RGB24, PNG: ARGB32
wrapMode?RepeatRepeatRepeat
mipmap?Normal: No, Wide: Yes*Yes
filterMode?BilinearBilinearBilinear
anisoLevel?111
Asynchronous LoadNoNoNoYes
Read/Write Enabled?YesNoNo
* This is specified by the setting that uses this loader.

Render Queue

  1. Background, skywires (neon, sandstorm, classic)
  2. Geometry (solid objects)
  3. Stars
  4. Cirrus clouds
  5. AlphaTest
  6. Water (The front water chunk gets 2998 and other chunks are lower numbers, in order. By default (see numvisiblewaterchunks) there are up to 8 chunks.)
  7. Transparent
  8. Rings
  9. Block flashes
  10. Air debris
  11. Water wakes
  12. Surfboard (neon, sandstorm, classic), Vehicle thrusters (classic)
  13. Overlay

Layers

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).

11 BothNormalAndBackground (BG+FG)
According to Dylan: "Renders the material to the background, and also in the foreground. Rendering foreground objects to the background can be useful for getting a glow halo that doesn't obscure gameplay."
12 Both_IgnoreLights (BG+FG)
Dylan: "Same as 11, but lights are ignored"
13 NormalOnly (FG)
Foreground.
The foreground layer. Objects in this layer "won't be part of the glow effect".
Observed uses:
14 CloseOnly (C)
The close camera layer. This is for important gameplay objects that are close to the camera like the player model. Objects in this layer will always cover up objects drawn in the background and foreground cameras and the close camera appears to have a much smaller far culling plane but that can be altered with SetScene{closecam_near,closecam_far}.
Observed uses:
15 NormalBackgroundClose (BG+FG+C)
Renders on all 3 main cameras: Background, then Foreground, then Close.
18 SkyWires (BG)
Draws only on the background camera AKA the "glow camera". Dylan says this camera is disabled in quality levels less than 3.
Used for skywires: 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)
Use SetScene{hide_default_background_verticals} to get rid of the colored pillars that draw next to the track over objects in this layer.
19 SkyWireOverlays (BG)
Dylan's documentation mentions this layer but gives it the exact same description as layer 18. I haven't found a difference in my investigations either.
27 LastGameplayCamOnly (FG)
According to Dylan: "renders after eveything else (even close foreground). can be used for post-effects". What I have seen in the game files disagrees with this, but I haven't tested it.

Layers Table

Camera1112131415181927
Background O O O O O
Foreground O O O O O
Close O O

Undocumented Layers

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.

0 Builtin Layer: Default
Foreground camera only, like layer 13.
1 Builtin Layer: TransparentFX
2 Builtin Layer: Ignore Raycast
3 Builtin Layer (reserved for future use)
4 Builtin Layer: Water
5 Builtin Layer: UI
6 Builtin Layer (reserved for future use)
7 Builtin Layer (reserved for future use)
8 UniSky - Droplet Effect
9 UniSky - Offscreen Particles
10 Unisky - Normal
11 BothNormalAndBackground
12 Both_IgnoreLights
13 NormalOnly
14 CloseOnly
15 NormalBackgroundClose
16 PostProcessing
17 PostNavBar
18 SkyWires
19 SkyWireOverlays
20 FogVolumeShadowCaster
21 ClippedUIScrollingContent
22 Popup_UI_2
23 PostMainOnly
24 Minimap_Real3D
25 Popup_UI_1
26 ModalDialogs_AboveNav
27 LastGameplayCamOnly
28 SkyScattering
29 UI
30 ALL (Even LastCamera)
31 NavFrameTopLevel
The pause screen uses this layer.

Cameras

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
CameraPostEffects NameClearFOVDynamic FOVNear ClipFar ClipEnabled
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.

UI Webpage Camera

SettingInitial 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

UI Camera

SettingInitial 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

UI Pause Camera

SettingInitial 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

UI Background Camera

SettingInitial 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

Close Blitter Camera

SettingInitial 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

Close Camera

SettingInitial ValueBlitter 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}

Foreground Camera

SettingInitial 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

Background Camera

SettingInitial 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

SkyScattering Camera

SettingInitial 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