diff --git a/CMakeLists.txt b/CMakeLists.txt index 2864877f..1a76aaa5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -353,6 +353,10 @@ set(LOVR_SRC src/api/l_lovr.c ) +if(LOVR_USE_PICO) + list(REMOVE_ITEM LOVR_SRC src/main.c) +endif() + if(LOVR_BUILD_SHARED) add_library(lovr SHARED ${LOVR_SRC}) elseif(LOVR_BUILD_EXE) diff --git a/src/api/l_headset.c b/src/api/l_headset.c index 53184c17..80f67f76 100644 --- a/src/api/l_headset.c +++ b/src/api/l_headset.c @@ -87,7 +87,7 @@ static HeadsetRenderData headsetRenderData; static void renderHelper(void* userdata) { HeadsetRenderData* renderData = userdata; lua_State* L = renderData->L; -#ifdef EMSCRIPTEN +#if defined(EMSCRIPTEN) || defined(LOVR_USE_PICO) luax_geterror(L); if (lua_isnil(L, -1)) { lua_pushcfunction(L, luax_getstack); @@ -579,7 +579,7 @@ static int l_lovrHeadsetRenderTo(lua_State* L) { lua_settop(L, 1); luaL_checktype(L, 1, LUA_TFUNCTION); -#ifdef EMSCRIPTEN +#if defined(EMSCRIPTEN) || defined(LOVR_USE_PICO) if (headsetRenderData.ref != LUA_NOREF) { luaL_unref(L, LUA_REGISTRYINDEX, headsetRenderData.ref); } diff --git a/src/modules/graphics/graphics.c b/src/modules/graphics/graphics.c index 0291edac..f00f7017 100644 --- a/src/modules/graphics/graphics.c +++ b/src/modules/graphics/graphics.c @@ -310,7 +310,7 @@ void lovrGraphicsSetCamera(Camera* camera, bool clear) { if (!state.camera.canvas) { state.camera.canvas = state.defaultCanvas; - lovrCanvasSetStereo(state.camera.canvas, camera->stereo); + lovrCanvasSetStereo(state.camera.canvas, camera->stereo); } } @@ -364,7 +364,7 @@ Color lovrGraphicsGetBackgroundColor() { void lovrGraphicsSetBackgroundColor(Color color) { state.backgroundColor = state.linearBackgroundColor = color; -#ifndef LOVR_WEBGL +#if !defined(LOVR_WEBGL) && !defined(LOVR_USE_PICO) gammaCorrect(&state.linearBackgroundColor); #endif } @@ -789,7 +789,7 @@ void lovrGraphicsFlushMesh(Mesh* mesh) { } void lovrGraphicsClear(Color* color, float* depth, int* stencil) { -#ifndef LOVR_WEBGL +#if !defined(LOVR_WEBGL) && !defined(LOVR_USE_PICO) if (color) gammaCorrect(color); #endif if (color || depth || stencil) lovrGraphicsFlush(); diff --git a/src/modules/graphics/graphics.h b/src/modules/graphics/graphics.h index 82ac75fa..3ae22d19 100644 --- a/src/modules/graphics/graphics.h +++ b/src/modules/graphics/graphics.h @@ -228,6 +228,7 @@ void lovrGpuDraw(DrawCommand* draw); void lovrGpuStencil(StencilAction action, int replaceValue, StencilCallback callback, void* userdata); void lovrGpuPresent(void); void lovrGpuDirtyTexture(void); +void lovrGpuResetState(void); void lovrGpuTick(const char* label); double lovrGpuTock(const char* label); const GpuFeatures* lovrGpuGetFeatures(void); diff --git a/src/modules/graphics/opengl.c b/src/modules/graphics/opengl.c index bb7ed6c3..268f69e7 100644 --- a/src/modules/graphics/opengl.c +++ b/src/modules/graphics/opengl.c @@ -1495,6 +1495,32 @@ void lovrGpuDirtyTexture() { state.textures[state.activeTexture] = NULL; } +// This doesn't actually reset all state, just state that is known to be changed externally +void lovrGpuResetState() { + if (state.vertexArray) { + glBindVertexArray(state.vertexArray->vao); + } + + for (size_t i = 0; i < MAX_BUFFER_TYPES; i++) { + glBindBuffer(convertBufferType(i), state.buffers[i]); + } + + glBindFramebuffer(GL_FRAMEBUFFER, state.framebuffer); + glUseProgram(state.program); + + if (state.blendEnabled) { + glEnable(GL_BLEND); + } else { + glDisable(GL_BLEND); + } + + if (state.depthEnabled) { + glEnable(GL_DEPTH_TEST); + } else { + glDisable(GL_DEPTH_TEST); + } +} + void lovrGpuTick(const char* label) { #ifndef LOVR_WEBGL lovrAssert(state.activeTimer == ~0u, "Attempt to start a new GPU timer while one is already active!"); diff --git a/src/modules/headset/headset.c b/src/modules/headset/headset.c index ea0dedcf..2c36c8cf 100644 --- a/src/modules/headset/headset.c +++ b/src/modules/headset/headset.c @@ -33,6 +33,9 @@ bool lovrHeadsetInit(HeadsetDriver* drivers, size_t count, float offset, uint32_ #ifdef LOVR_USE_VRAPI case DRIVER_VRAPI: interface = &lovrHeadsetVrApiDriver; break; #endif +#ifdef LOVR_USE_PICO + case DRIVER_PICO: interface = &lovrHeadsetPicoDriver; break; +#endif #ifdef LOVR_USE_WEBVR case DRIVER_WEBVR: interface = &lovrHeadsetWebVRDriver; break; #endif diff --git a/src/modules/headset/headset_pico.c b/src/modules/headset/headset_pico.c index a76e8090..eb37b91a 100644 --- a/src/modules/headset/headset_pico.c +++ b/src/modules/headset/headset_pico.c @@ -1,5 +1,13 @@ #include "headset/headset.h" +#include "event/event.h" +#include "graphics/graphics.h" +#include "graphics/canvas.h" +#include "resources/boot.lua.h" +#include "api/api.h" +#include "core/arr.h" +#include "core/maf.h" #include "core/os.h" +#include "core/util.h" #include #include #include @@ -8,6 +16,7 @@ #include #include #include +#include // Platform @@ -177,34 +186,70 @@ bool lovrPlatformIsKeyDown(KeyCode key) { // Headset backend +typedef struct { + GLint id; + Canvas* instance; +} NativeCanvas; + static struct { float offset; + float clipNear; + float clipFar; + uint32_t displayWidth; + uint32_t displayHeight; + float headPosition[4]; + float headOrientation[4]; + float fov; + float ipd; + struct { + bool active; + uint16_t buttons; + uint16_t changed; + float trigger; + float thumbstick[2]; + float position[4]; + float orientation[4]; + float hapticStrength; + float hapticDuration; + } controllers[2]; + arr_t(NativeCanvas) canvases; + void (*renderCallback)(void*); + void* renderUserdata; } state; static bool pico_init(float offset, uint32_t msaa) { state.offset = offset; + state.clipNear = .1f; + state.clipFar = 100.f; return true; } static void pico_destroy(void) { + arr_free(&state.canvases); memset(&state, 0, sizeof(state)); } +// TODO use presence of controllers to determine G2 vs Neo (isControllerServiceExisted) static bool pico_getName(char* name, size_t length) { - return false; + strncpy(name, "Pico", length - 1); + name[length - 1] = '\0'; + return true; } +// The Unity/Unreal SDKs expose true origin types (Pvr_SetTrackingOrigin) but there does not appear +// to be a way to access this from the Native SDK. Pose information appears to be relative to the +// initial head pose. static HeadsetOrigin pico_getOriginType(void) { return ORIGIN_HEAD; } static double pico_getDisplayTime(void) { - return 0.; + return lovrPlatformGetTime(); } static void pico_getDisplayDimensions(uint32_t* width, uint32_t* height) { - *width = 0; - *height = 0; + *width = state.displayWidth; + *height = state.displayHeight; } static const float* pico_getDisplayMask(uint32_t* count) { @@ -217,22 +262,28 @@ static uint32_t pico_getViewCount(void) { } static bool pico_getViewPose(uint32_t view, float* position, float* orientation) { - return false; + // TODO use HmdState pose info, offset view by half ipd + quat_init(orientation, state.headOrientation); + return view < 2; } static bool pico_getViewAngles(uint32_t view, float* left, float* right, float* up, float* down) { - return false; + *left = *right = *up = *down = state.fov; + return view < 2; } static void pico_getClipDistance(float* clipNear, float* clipFar) { - *clipNear = 0.f; - *clipFar = 0.f; + *clipNear = state.clipNear; + *clipFar = state.clipFar; } static void pico_setClipDistance(float clipNear, float clipFar) { - // + state.clipNear = clipNear; + state.clipFar = clipFar; } +// The Unity/Unreal SDKs expose something called "SeeThrough" that is very similar to the Oculus +// Guardian API, but this does not appear to be in the Native SDK static void pico_getBoundsDimensions(float* width, float* depth) { *width = *depth = 0.f; } @@ -243,15 +294,56 @@ static const float* pico_getBoundsGeometry(uint32_t* count) { } static bool pico_getPose(Device device, float* position, float* orientation) { + if (device == DEVICE_HEAD) { + vec3_init(position, state.headPosition); + quat_init(orientation, state.headOrientation); + position[1] += state.offset; + return true; + } + + if (device == DEVICE_HAND_LEFT || device == DEVICE_HAND_RIGHT) { + uint32_t index = device - DEVICE_HAND_LEFT; + vec3_init(position, state.controllers[index].position); + quat_init(orientation, state.controllers[index].orientation); + return state.controllers[index].active; + } + return false; } static bool pico_getVelocity(Device device, float* velocity, float* angularVelocity) { - return false; + return false; // Controllers only expose acceleration and angular velocity, so we skip it } static bool pico_isDown(Device device, DeviceButton button, bool* down, bool* changed) { - return false; + if (device != DEVICE_HAND_LEFT && device != DEVICE_HAND_RIGHT) { + return false; + } + + uint32_t index = device - DEVICE_HAND_LEFT; + + if (!state.controllers[index].active) { + return false; + } + + bool active = true; + uint16_t mask = 0; + + switch (button) { + case BUTTON_TRIGGER: mask = 1 << 0; break; + case BUTTON_THUMBSTICK: mask = 1 << 1; break; + case BUTTON_GRIP: mask = 1 << 2; break; + case BUTTON_MENU: mask = 1 << 3; break; + case BUTTON_A: mask = 1 << 4; active = index == 1; break; + case BUTTON_X: mask = 1 << 4; active = index == 0; break; + case BUTTON_B: mask = 1 << 5; active = index == 1; break; + case BUTTON_Y: mask = 1 << 5; active = index == 0; break; + default: return false; + } + + *down = state.controllers[index].buttons & mask; + *changed = state.controllers[index].changed & mask; + return active; } static bool pico_isTouched(Device device, DeviceButton button, bool* touched) { @@ -259,11 +351,39 @@ static bool pico_isTouched(Device device, DeviceButton button, bool* touched) { } static bool pico_getAxis(Device device, DeviceAxis axis, float* value) { - return false; + if (device != DEVICE_HAND_LEFT && device != DEVICE_HAND_RIGHT) { + return false; + } + + uint32_t index = device - DEVICE_HAND_LEFT; + + if (!state.controllers[index].active) { + return false; + } + + switch (axis) { + case AXIS_TRIGGER: + *value = state.controllers[index].trigger; + return true; + case AXIS_THUMBSTICK: + value[0] = state.controllers[index].thumbstick[0]; + value[1] = state.controllers[index].thumbstick[1]; + return true; + default: + return false; + } } static bool pico_vibrate(Device device, float strength, float duration, float frequency) { - return false; + if (device != DEVICE_HAND_LEFT && device != DEVICE_HAND_RIGHT) { + return false; + } + + uint32_t index = device - DEVICE_HAND_LEFT; + + state.controllers[index].hapticStrength = strength; + state.controllers[index].hapticDuration = duration; + return true; } static struct ModelData* pico_newModelData(Device device) { @@ -271,7 +391,8 @@ static struct ModelData* pico_newModelData(Device device) { } static void pico_renderTo(void (*callback)(void*), void* userdata) { - // + state.renderCallback = callback; + state.renderUserdata = userdata; } static void pico_update(float dt) { @@ -304,3 +425,155 @@ HeadsetInterface lovrHeadsetPicoDriver = { .renderTo = pico_renderTo, .update = pico_update }; + +// Activity callbacks + +static lua_State* L; +static lua_State* T; +static Variant cookie; + +static void lovrPicoBoot(void) { + lovrAssert(lovrPlatformInit(), "Failed to initialize platform"); + lovrPlatformSetTime(0.); + + L = luaL_newstate(); + luax_setmainthread(L); + luaL_openlibs(L); + + lua_getglobal(L, "package"); + lua_getfield(L, -1, "preload"); + luaL_register(L, NULL, lovrModules); + lua_pop(L, 2); + + lua_pushcfunction(L, luax_getstack); + if (luaL_loadbuffer(L, (const char*) src_resources_boot_lua, src_resources_boot_lua_len, "@boot.lua") || lua_pcall(L, 0, 1, -2)) { + fprintf(stderr, "%s\n", lua_tostring(L, -1)); + return; + } + + T = lua_newthread(L); + lua_pushvalue(L, -2); + lua_xmove(L, T, 1); + + lovrSetErrorCallback(luax_vthrow, T); + lovrSetLogCallback(luax_vlog, T); +} + +JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPicoOnCreate(JNIEnv* jni, jobject activity, jstring apk) { + const char* path = (*jni)->GetStringUTFChars(jni, apk, NULL); + size_t length = strlen(path); + if (length < sizeof(apkPath)) { + memcpy(apkPath, path, length); + } + lovrPicoBoot(); +} + +JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPicoSetDisplayDimensions(JNIEnv* jni, jobject activity, int width, int height) { + state.displayWidth = width; + state.displayHeight = height; +} + +JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPicoUpdateControllerPose(JNIEnv* jni, jobject activity, int hand, bool active, float x, float y, float z, float qx, float qy, float qz, float qw) { + state.controllers[hand].active = active; + vec3_set(state.controllers[hand].position, x, y, z); + quat_set(state.controllers[hand].orientation, -qx, -qy, qz, qw); +} + +JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPicoUpdateControllerInput(JNIEnv* jni, jobject activity, int hand, int buttons, float trigger, float thumbstickX, float thumbstickY) { + state.controllers[hand].changed = state.controllers[hand].buttons ^ buttons; + state.controllers[hand].buttons = (uint16_t) buttons; + state.controllers[hand].trigger = trigger; + state.controllers[hand].thumbstick[0] = thumbstickX; + state.controllers[hand].thumbstick[1] = thumbstickY; +} + +JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPicoOnFrame(JNIEnv* jni, jobject activity, float x, float y, float z, float qx, float qy, float qz, float qw, float fov, float ipd) { + vec3_set(state.headPosition, x, y, z); + quat_set(state.headOrientation, qx, qy, qz, qw); + state.fov = fov * M_PI / 180.f; + state.ipd = ipd; + + // Haptics + for (uint32_t i = 0; i < 2; i++) { + if (state.controllers[i].hapticStrength > 0.f) { + float strength = state.controllers[i].hapticStrength; + float duration = state.controllers[i].hapticDuration; + jclass class = (*jni)->GetObjectClass(jni, activity); + jmethodID vibrate = (*jni)->GetMethodID(jni, class, "vibrate", "(IFF)V"); + (*jni)->CallObjectMethod(jni, activity, vibrate, i, strength, duration); + state.controllers[i].hapticStrength = 0.f; + } + } + + // Resume the lovr.run coroutine, and if it returns (doesn't yield) then either reboot or exit + if (L && T) { + luax_geterror(T); + luax_clearerror(T); + if (lua_resume(T, 1) != LUA_YIELD) { + bool restart = lua_type(T, 1) == LUA_TSTRING && !strcmp(lua_tostring(T, 1), "restart"); + if (restart) { + luax_checkvariant(T, 2, &cookie); + if (cookie.type == TYPE_OBJECT) { + cookie.type = TYPE_NIL; + memset(&cookie.value, 0, sizeof(cookie.value)); + } + lua_close(L); + lovrPicoBoot(); + } else { + lua_close(L); + L = NULL; + T = NULL; + + // Call 'finish' method on the Activity + jclass class = (*jni)->GetObjectClass(jni, activity); + jmethodID finish = (*jni)->GetMethodID(jni, class, "finish", "()V"); + (*jni)->CallObjectMethod(jni, activity, finish); + } + } + } +} + +JNIEXPORT void JNICALL Java_org_lovr_app_Activity_lovrPicoDrawEye(JNIEnv* jni, jobject object, int eye) { + if (!state.renderCallback) return; + + // Pico modifies a lot of global OpenGL state, including the framebuffer binding, VAO binding, + // buffer bindings, blending, and depth test settings. Since there is no swapchain or texture + // submission API, we have to render into the currently active OpenGL framebuffer, so a cache of + // native Canvas objects is used for that. For the rest of the states, there is a new "nuke all + // OpenGL state" function added to clear any changes made by Pico (lovrGpuResetState) :( + + GLint framebuffer; + glGetIntegerv(GL_FRAMEBUFFER_BINDING, &framebuffer); + + Canvas* canvas = NULL; + for (uint32_t i = 0; i < state.canvases.length; i++) { + if (state.canvases.data[i].id == framebuffer) { + canvas = state.canvases.data[i].instance; + break; + } + } + + if (!canvas) { + CanvasFlags flags = { .depth.enabled = true }; + canvas = lovrCanvasCreateFromHandle(state.displayWidth, state.displayHeight, flags, framebuffer, 0, 0, 1, true); + arr_push(&state.canvases, ((NativeCanvas) { .id = framebuffer, .instance = canvas })); + } + + Camera camera; + camera.stereo = false; + camera.canvas = canvas; + for (uint32_t i = 0; i < 2; i++) { + float fov = tanf(state.fov); + mat4_fov(camera.projection[i], -fov, fov, fov, -fov, state.clipNear, state.clipFar); + mat4_identity(camera.viewMatrix[i]); + mat4_translate(camera.viewMatrix[i], state.headPosition[0], state.headPosition[1] + state.offset, state.headPosition[2]); + mat4_rotateQuat(camera.viewMatrix[i], state.headOrientation); + mat4_translate(camera.viewMatrix[i], state.ipd * (eye == 0 ? -.5f : .5f), 0.f, 0.f); + mat4_invert(camera.viewMatrix[i]); + } + + lovrGpuResetState(); + lovrGraphicsSetCamera(&camera, true); + state.renderCallback(state.renderUserdata); + lovrGraphicsSetCamera(NULL, false); +} diff --git a/src/resources/Activity_pico.java b/src/resources/Activity_pico.java index acb20169..b7ed72e4 100644 --- a/src/resources/Activity_pico.java +++ b/src/resources/Activity_pico.java @@ -6,35 +6,93 @@ import com.picovr.vractivity.Eye; import com.picovr.vractivity.HmdState; import com.picovr.vractivity.RenderInterface; import com.picovr.vractivity.VRActivity; +import com.picovr.cvclient.ButtonNum; +import com.picovr.cvclient.CVController; +import com.picovr.cvclient.CVControllerListener; +import com.picovr.cvclient.CVControllerManager; +import com.picovr.picovrlib.cvcontrollerclient.ControllerClient; +import com.psmart.vrlib.PicovrSDK; -public class Activity extends VRActivity implements RenderInterface { +public class Activity extends VRActivity implements RenderInterface, CVControllerListener { + CVControllerManager controllerManager; + boolean controllersActive; // Activity public void onCreate(Bundle bundle) { super.onCreate(bundle); + + if (ControllerClient.isControllerServiceExisted(this)) { + controllerManager = new CVControllerManager(this); + controllerManager.setListener(this); + } + + lovrPicoOnCreate(getPackageCodePath()); } public void onPause() { super.onPause(); + if (controllerManager != null) { + controllerManager.unbindService(); + } } public void onResume() { super.onResume(); + PicovrSDK.SetEyeBufferSize(1920, 1920); + if (controllerManager != null) { + controllerManager.bindService(); + } } // RenderInterface public void initGL(int width, int height) { - // + lovrPicoSetDisplayDimensions(width, height); } public void onFrameBegin(HmdState state) { - // + if (controllersActive) { + for (int i = 0; i < 2; i++) { + CVController controller = (i == 0) ? + controllerManager.getMainController() : + controllerManager.getSubController(); + + if (controller == null || controller.getConnectState() == 0) { + lovrPicoUpdateControllerPose(i, false, 0, 0, 0, 0, 0, 0, 0); + continue; + } + + float p[] = controller.getPosition(); + float q[] = controller.getOrientation(); + lovrPicoUpdateControllerPose(i, true, p[0], p[1], p[2], q[0], q[1], q[2], q[3]); + + int thumbstick[] = controller.getTouchPad(); + float trigger = (float) controller.getTriggerNum() / 255.f; + float thumbstickX = ((float) thumbstick[1] - 128.f) / (thumbstick[1] > 128 ? 127.f : 128.f); + float thumbstickY = ((float) thumbstick[0] - 128.f) / (thumbstick[0] > 128 ? 127.f : 128.f); + + int buttons = 0; + ButtonNum gripButton = (i == 0) ? ButtonNum.buttonRG : ButtonNum.buttonLG; // Yes I know + buttons |= trigger >= .9f ? (1 << 0) : 0; + buttons |= controller.getButtonState(ButtonNum.click) ? (1 << 1) : 0; + buttons |= controller.getButtonState(gripButton) ? (1 << 2) : 0; + buttons |= controller.getButtonState(ButtonNum.app) ? (1 << 3) : 0; + buttons |= controller.getButtonState(ButtonNum.buttonAX) ? (1 << 4) : 0; + buttons |= controller.getButtonState(ButtonNum.buttonBY) ? (1 << 5) : 0; + lovrPicoUpdateControllerInput(i, buttons, trigger, thumbstickX, thumbstickY); + } + } + + float p[] = state.getPos(); + float q[] = state.getOrientation(); + float fov = state.getFov(); + float ipd = state.getIpd(); + lovrPicoOnFrame(p[0], p[1], p[2], q[0], q[1], q[2], q[3], fov, ipd); } public void onDrawEye(Eye eye) { - // + lovrPicoDrawEye(eye.getType()); } public void onFrameEnd() { @@ -65,6 +123,47 @@ public class Activity extends VRActivity implements RenderInterface { // } + // CVControllerListener + + public void onBindSuccess() { + // + } + + public void onBindFail() { + controllersActive = false; + } + + public void onThreadStart() { + controllersActive = true; + } + + public void onConnectStateChanged(int serial, int state) { + // + } + + public void onMainControllerChanged(int serial) { + // + } + + public void onChannelChanged(int device, int channel) { + // + } + + // Native + protected native void lovrPicoOnCreate(String apkPath); + protected native void lovrPicoSetDisplayDimensions(int width, int height); + protected native void lovrPicoUpdateControllerPose(int hand, boolean active, float x, float y, float z, float qx, float qy, float qz, float qw); + protected native void lovrPicoUpdateControllerInput(int hand, int buttons, float trigger, float thumbstickX, float thumbstickY); + protected native void lovrPicoOnFrame(float x, float y, float z, float qx, float qy, float qz, float qw, float fov, float ipd); + protected native void lovrPicoDrawEye(int eye); + + public void vibrate(int hand, float strength, float duration) { + if (controllerManager != null) { + int ms = (int) (duration * 1000.f); + ControllerClient.vibrateCV2ControllerStrength(strength, ms, hand); + } + } + static { System.loadLibrary("lovr"); } diff --git a/src/resources/boot.lua b/src/resources/boot.lua index ed0148c3..94b2d632 100644 --- a/src/resources/boot.lua +++ b/src/resources/boot.lua @@ -96,7 +96,7 @@ function lovr.boot() timer = true }, headset = { - drivers = { 'leap', 'openxr', 'oculus', 'vrapi', 'openvr', 'webxr', 'webvr', 'desktop' }, + drivers = { 'leap', 'openxr', 'oculus', 'vrapi', 'pico', 'openvr', 'webxr', 'webvr', 'desktop' }, offset = 1.7, msaa = 4 }, diff --git a/src/resources/shaders.c b/src/resources/shaders.c index a5114d73..f2ce0b53 100644 --- a/src/resources/shaders.c +++ b/src/resources/shaders.c @@ -126,7 +126,7 @@ const char* lovrShaderFragmentSuffix = "" " discard; \n" " } \n" "#endif \n" -#ifdef LOVR_WEBGL +#if defined(LOVR_WEBGL) || defined(LOVR_USE_PICO) " lovrCanvas[0].rgb = pow(lovrCanvas[0].rgb, vec3(.4545)); \n" #endif "#endif \n"