ANDROID: Support swipe-based virtcontrols

Touching and holding the left side of the screen functions as a
joystick.
Swiping the middle side allows fine control over arrow keys (eg.
dialogues), tapping it sends an enter key.
Swiping the right side up/down sends pageup/pagedown.
This commit is contained in:
Dries Harnie 2013-05-24 01:40:23 +02:00
parent dcfade91e9
commit 471e4748b3
11 changed files with 243 additions and 119 deletions

View file

@ -150,7 +150,9 @@ OSystem_Android::OSystem_Android(int audio_sample_rate, int audio_buffer_size) :
_touchpad_scale(66),
_dpad_scale(4),
_fingersDown(0),
_trackball_scale(2) {
_trackball_scale(2),
_joystickPressing(Common::KEYCODE_INVALID),
_arrows_texture(0) {
_fsFactory = new POSIXFilesystemFactory();
@ -180,8 +182,6 @@ OSystem_Android::OSystem_Android(int audio_sample_rate, int audio_buffer_size) :
for (int i = 0; i < 4; ++i)
_activePointers[i] = -1;
_joystickPressing = Common::KEYCODE_INVALID;
}
OSystem_Android::~OSystem_Android() {
@ -194,6 +194,11 @@ OSystem_Android::~OSystem_Android() {
delete _timerManager;
_timerManager = 0;
if (_arrows_texture) {
delete _arrows_texture;
_arrows_texture = 0;
}
deleteMutex(_event_queue_lock);
}

View file

@ -252,18 +252,10 @@ private:
kTouchAreaCenter = 0xfffe,
kTouchAreaRight = 0xfffd,
kTouchAreaNone = 0xfffc,
};
uint16 getTouchArea(int x, int y);
struct VirtControl {
int x, y;
bool sticky;
Common::KeyCode keyCode;
bool active;
};
struct Pointer {
uint16 startX, startY;
uint16 currentX, currentY;
@ -271,16 +263,27 @@ private:
bool active;
};
struct CardinalSwipe {
CardinalSwipe(int dX, int dY);
uint16 distance;
enum Direction {
kDirectionLeft,
kDirectionUp,
kDirectionRight,
kDirectionDown
} direction;
};
enum { kNumPointers = 5 };
static int _virt_numControls;
static VirtControl _virtcontrols[];
int _virt_numDivisions;
Pointer _pointers[kNumPointers];
int _activePointers[4];
Common::KeyCode _joystickPressing;
int &pointerFor(TouchArea ta);
const Common::Rect clipFor(const CardinalSwipe &cs);
void initVirtControls();
void drawVirtControls();
GLESTexture *_arrows_texture;
protected:
// PaletteManager API

View file

@ -23,6 +23,7 @@ PATH_DIST = $(srcdir)/dists/android
PATH_RESOURCES = $(PATH_DIST)/res
PORT_DISTFILES = $(PATH_DIST)/README.Android
DIST_ANDROID_CONTROLS = $(PATH_DIST)/assets/arrows.tga
RESOURCES = \
$(PATH_RESOURCES)/values/strings.xml \
@ -121,9 +122,9 @@ $(PATH_STAGE_PREFIX).%/res/drawable/residualvm.png: $(PATH_RESOURCES)/drawable/r
@$(MKDIR) -p $(@D)
$(CP) $< $@
$(FILE_RESOURCES_MAIN): $(FILE_MANIFEST) $(RESOURCES) $(ANDROID_JAR) $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA)
$(FILE_RESOURCES_MAIN): $(FILE_MANIFEST) $(RESOURCES) $(ANDROID_JAR) $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_ANDROID_CONTROLS)
$(INSTALL) -d $(PATH_BUILD_ASSETS)
$(INSTALL) -c -m 644 $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(PATH_BUILD_ASSETS)/
$(INSTALL) -c -m 644 $(DIST_FILES_THEMES) $(DIST_FILES_ENGINEDATA) $(DIST_ANDROID_CONTROLS) $(PATH_BUILD_ASSETS)/
work_dir=`pwd`; \
for i in $(PATH_BUILD_ASSETS)/*.zip; do \
echo "recompress $$i"; \

View file

@ -235,6 +235,25 @@ static Common::KeyCode determineKey(int dX, int dY) {
return Common::KEYCODE_INVALID;
}
OSystem_Android::CardinalSwipe::CardinalSwipe(int dX, int dY) {
if (abs(dX) > abs(dY)) {
if (dX > 0) {
distance = dX;
direction = kDirectionRight;
} else {
distance = abs(dX);
direction = kDirectionLeft;
}
} else {
if (dY > 0) {
distance = dY;
direction = kDirectionDown;
} else {
distance = abs(dY);
direction = kDirectionUp;
}
}
}
static const int kQueuedInputEventDelay = 50;
@ -328,28 +347,7 @@ void OSystem_Android::updateEventScale() {
_eventScaleX = 100 * 640 / tex->width();
}
int OSystem_Android::_virt_numControls = 6;
OSystem_Android::VirtControl OSystem_Android::_virtcontrols[] = {
{-2, 1, false, Common::KEYCODE_u, false},
{-3, 1, false, Common::KEYCODE_p, false},
{-1, 5, false, Common::KEYCODE_i, false},
{-1, 0, false, Common::KEYCODE_PAGEDOWN, false},
{-1, 1, false, Common::KEYCODE_KP_ENTER, false},
{-1, 2, false, Common::KEYCODE_PAGEUP, false},
};
uint16 OSystem_Android::getTouchArea(int x, int y) {
int scaledX = _virt_numDivisions * x / _egl_surface_width;
int scaledY = _virt_numDivisions * (_egl_surface_height - y) / _egl_surface_height;
for (uint16 i = 0; i < _virt_numControls; ++i) {
int testX = _virtcontrols[i].x < 0
? (_virtcontrols[i].x + _virt_numDivisions)
: _virtcontrols[i].x;
if (scaledX == testX && scaledY == _virtcontrols[i].y)
return i;
}
float xPercent = float(x) / _egl_surface_width;
if (xPercent < 0.3)
@ -707,8 +705,6 @@ void OSystem_Android::pushEvent(int type, int arg1, int arg2, int arg3,
if (idx == kTouchAreaCenter) {
e.kbd.keycode = Common::KEYCODE_RETURN;
e.kbd.ascii = Common::ASCII_RETURN;
} else {
e.kbd.keycode = _virtcontrols[idx].keyCode;
}
lockMutex(_event_queue_lock);
@ -813,9 +809,6 @@ void OSystem_Android::pushEvent(int type, int arg1, int arg2, int arg3,
_pointers[arg1].startX = _pointers[arg1].currentX = arg3;
_pointers[arg1].startY = _pointers[arg1].currentY = arg4;
}
if (touchArea == kTouchAreaJoystick)
LOGW("Started joystick move at (%d,%d)", arg3, arg4);
}
return;
@ -839,7 +832,6 @@ void OSystem_Android::pushEvent(int type, int arg1, int arg2, int arg3,
_joystickPressing = newPressing;
}
LOGW("Joystick moved to (%d,%d)", arg3, arg4);
}
}
return;
@ -880,34 +872,36 @@ void OSystem_Android::pushEvent(int type, int arg1, int arg2, int arg3,
unlockMutex(_event_queue_lock);
} else {
_pointers[arg1].active = false;
int dX = _pointers[arg1].currentX - _pointers[arg1].startX;
int dY = _pointers[arg1].currentY - _pointers[arg1].startY;
struct CardinalSwipe cs(dX, dY);
switch (_pointers[arg1].function) {
case kTouchAreaCenter: {
pointerFor(kTouchAreaCenter) = -1;
if (abs(dX) < 100 && abs(dY) < 100) {
sendKey(Common::KEYCODE_u);
} else if (abs(dX) > abs(dY)) {
if (dX > 0) {
sendKey(Common::KEYCODE_RIGHT);
} else {
if (cs.distance < 100) {
sendKey(Common::KEYCODE_RETURN);
break;
}
switch (cs.direction) {
case CardinalSwipe::kDirectionLeft:
sendKey(Common::KEYCODE_LEFT);
}
} else {
if (dY > 0) {
sendKey(Common::KEYCODE_DOWN);
} else {
break;
case CardinalSwipe::kDirectionUp:
sendKey(Common::KEYCODE_UP);
}
break;
case CardinalSwipe::kDirectionRight:
sendKey(Common::KEYCODE_RIGHT);
break;
case CardinalSwipe::kDirectionDown:
sendKey(Common::KEYCODE_DOWN);
break;
}
break;
}
case kTouchAreaJoystick:
pointerFor(kTouchAreaJoystick) = -1;
LOGW("Joystick move ended at (%d,%d)", arg3, arg4);
if (_joystickPressing != Common::KEYCODE_INVALID) {
lockMutex(_event_queue_lock);
e.kbd.keycode = _joystickPressing;
@ -919,13 +913,15 @@ void OSystem_Android::pushEvent(int type, int arg1, int arg2, int arg3,
case kTouchAreaRight:
pointerFor(kTouchAreaRight) = -1;
if (abs(dX) > 50 || abs(dY) < 100) {
if ( cs.direction == CardinalSwipe::kDirectionLeft
|| cs.direction == CardinalSwipe::kDirectionRight
|| cs.distance < 100) {
sendKey(Common::KEYCODE_i);
} else {
if (dY > 0) {
sendKey(Common::KEYCODE_PAGEDOWN);
} else {
if (cs.direction == CardinalSwipe::kDirectionUp) {
sendKey(Common::KEYCODE_PAGEUP);
} else {
sendKey(Common::KEYCODE_PAGEDOWN);
}
}
break;
@ -934,6 +930,7 @@ void OSystem_Android::pushEvent(int type, int arg1, int arg2, int arg3,
default:
break;
}
_pointers[arg1].active = false;
_pointers[arg1].function = kTouchAreaNone;
}
return;

View file

@ -41,6 +41,7 @@
#include "common/endian.h"
#include "graphics/conversion.h"
#include "graphics/decoders/tga.h"
#include "graphics/opengles2/shader.h"
#include "backends/platform/android/android.h"
@ -569,57 +570,82 @@ void OSystem_Android::updateScreen() {
}
static Graphics::Shader *_virtcontrols_shader = NULL;
static const char *_virtVertex =
"#version 100\n"
"attribute vec2 position;\n"
"uniform vec2 offsetXY;\n"
"uniform vec2 sizeWH;\n"
"void main() {\n"
"vec2 pos = offsetXY + position * sizeWH;\n"
"pos.x = pos.x * 2.0 - 1.0;\n"
"pos.y = pos.y * 2.0 - 1.0;\n"
"gl_Position = vec4(pos, 0.0, 1.0);\n"
"}\n";
static GLES8888Texture *loadBuiltinTexture(const char *filename) {
Common::ArchiveMemberPtr member = SearchMan.getMember(filename);
Common::SeekableReadStream *str = member->createReadStream();
Graphics::TGADecoder dec;
dec.loadStream(*str);
void *pixels = dec.getSurface()->pixels;
static const char *_virtFragment =
"#version 100\n"
"precision mediump float;\n"
"uniform bool active;\n"
"void main() {\n"
"gl_FragColor = vec4(1.0, 1.0, 1.0, active ? 0.8 : 0.3);\n"
"}\n";
GLES8888Texture *ret = new GLES8888Texture();
uint16 w = dec.getSurface()->w;
uint16 h = dec.getSurface()->h;
uint16 pitch = dec.getSurface()->pitch;
ret->allocBuffer(w, h);
ret->updateBuffer(0, 0, w, h, pixels, pitch);
delete str;
return ret;
}
void OSystem_Android::initVirtControls() {
if (_virtcontrols_shader)
return;
if (!_arrows_texture)
_arrows_texture = loadBuiltinTexture("arrows.tga");
}
const char* attributes[] = { "position", NULL };
_virtcontrols_shader = Graphics::Shader::fromStrings("key", _virtVertex, _virtFragment, attributes);
_virtcontrols_shader->enableVertexAttribute("position", g_verticesVBO, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), 0);
_virt_numDivisions = _egl_surface_width > 800 ? 12 : 8;
const Common::Rect OSystem_Android::clipFor(const CardinalSwipe &cs) {
switch (cs.direction) {
case CardinalSwipe::kDirectionLeft:
return Common::Rect(0, 128, 128, 256);
case CardinalSwipe::kDirectionUp:
return Common::Rect(0, 0, 128, 128);
case CardinalSwipe::kDirectionRight:
return Common::Rect(128, 128, 256, 256);
case CardinalSwipe::kDirectionDown:
return Common::Rect(128, 0, 256, 128);
default: // unreachable
return Common::Rect(0, 0, 1, 1);
}
}
void OSystem_Android::drawVirtControls() {
if (_show_overlay)
return;
_virtcontrols_shader->use();
float div = 1.0 / _virt_numDivisions;
float divX = div;
float divY = div;
_virtcontrols_shader->setUniform("sizeWH", 0.9 * Math::Vector2d(divX, divY));
glEnable(GL_BLEND);
int joyPtr = pointerFor(kTouchAreaJoystick);
if (joyPtr != -1) {
Pointer &joy = _pointers[joyPtr];
CardinalSwipe cs(joy.currentX - joy.startX, joy.currentY - joy.startY);
for (int control = 0; control < _virt_numControls; ++control) {
float offsetX = (_virtcontrols[control].x + 0.1) * divX;
float offsetY = (_virtcontrols[control].y + 0.1) * divY;
if (offsetX < 0)
offsetX += 1.0;
if (cs.distance >= 50) {
Common::Rect clip = clipFor(cs);
_arrows_texture->drawTexture(2 * _egl_surface_width / 10, _egl_surface_height / 2, 64, 64, clip);
}
}
_virtcontrols_shader->setUniform("offsetXY", Math::Vector2d(offsetX, offsetY));
_virtcontrols_shader->setUniform("active", _virtcontrols[control].active);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
int centerPtr = pointerFor(kTouchAreaCenter);
if (centerPtr != -1) {
Pointer &center = _pointers[centerPtr];
CardinalSwipe cs(center.currentX - center.startX, center.currentY - center.startY);
if (cs.distance >= 100) {
Common::Rect clip = clipFor(cs);
_arrows_texture->drawTexture(_egl_surface_width / 2, _egl_surface_height / 2, 64, 64, clip);
}
}
int rightPtr = pointerFor(kTouchAreaRight);
if (rightPtr != -1) {
Pointer &right = _pointers[rightPtr];
CardinalSwipe cs(right.currentX - right.startX, right.currentY - right.startY);
if (cs.distance >= 100) {
if ( cs.direction == CardinalSwipe::kDirectionDown
|| cs.direction == CardinalSwipe::kDirectionUp) {
Common::Rect clip = clipFor(cs);
_arrows_texture->drawTexture( 8 * _egl_surface_width / 10, _egl_surface_height / 2, 64, 64, clip);
}
}
}
}

View file

@ -97,7 +97,7 @@ void GLESBaseTexture::initGL() {
}
const char* attributes[] = { "position", "texcoord", NULL };
g_box_shader = Graphics::Shader::fromStrings("box", Graphics::BuiltinShaders::boxVertex, Graphics::BuiltinShaders::boxFragment, attributes);
g_box_shader = Graphics::Shader::fromStrings("control", Graphics::BuiltinShaders::controlVertex, Graphics::BuiltinShaders::controlFragment, attributes);
g_verticesVBO = Graphics::Shader::createBuffer(GL_ARRAY_BUFFER, sizeof(vertices), vertices);
g_box_shader->enableVertexAttribute("position", g_verticesVBO, 2, GL_FLOAT, GL_TRUE, 2 * sizeof(float), 0);
g_box_shader->enableVertexAttribute("texcoord", g_verticesVBO, 2, GL_FLOAT, GL_TRUE, 2 * sizeof(float), 0);
@ -186,7 +186,7 @@ void GLESBaseTexture::allocBuffer(GLuint w, GLuint h) {
initSize();
}
void GLESBaseTexture::drawTexture(GLshort x, GLshort y, GLshort w, GLshort h) {
void GLESBaseTexture::drawTexture(GLshort x, GLshort y, GLshort w, GLshort h, const Common::Rect &clip) {
// LOGD("*** Texture %p: Drawing %dx%d rect to (%d,%d)", this, w, h, x, y);
assert(g_box_shader);
@ -197,13 +197,14 @@ void GLESBaseTexture::drawTexture(GLshort x, GLshort y, GLshort w, GLshort h) {
const GLfloat offsetY = float(y) / float(JNI::egl_surface_height);
const GLfloat sizeW = float(w) / float(JNI::egl_surface_width);
const GLfloat sizeH = float(h) / float(JNI::egl_surface_height);
const GLfloat tex_width = float(_surface.w) / float(_texture_width);
const GLfloat tex_height = float(_surface.h) / float(_texture_height);
Math::Vector4d clipV = Math::Vector4d(clip.left, clip.top, clip.right, clip.bottom);
clipV.x() /= _texture_width; clipV.y() /= _texture_height;
clipV.z() /= _texture_width; clipV.w() /= _texture_height;
// LOGD("*** Drawing at (%f,%f) , size %f x %f", float(x) / float(_surface.w), float(y) / float(_surface.h), tex_width, tex_height);
g_box_shader->setUniform("offsetXY", Math::Vector2d(offsetX, offsetY));
g_box_shader->setUniform("sizeWH", Math::Vector2d(sizeW, sizeH));
g_box_shader->setUniform("texcrop", Math::Vector2d(tex_width, tex_height));
g_box_shader->setUniform("clip", clipV);
g_box_shader->setUniform("flipY", !_is_game_texture);
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
@ -281,7 +282,7 @@ void GLESTexture::fillBuffer(uint32 color) {
setDirty();
}
void GLESTexture::drawTexture(GLshort x, GLshort y, GLshort w, GLshort h) {
void GLESTexture::drawTexture(GLshort x, GLshort y, GLshort w, GLshort h, const Common::Rect &clip) {
if (_all_dirty) {
_dirty_rect.top = 0;
_dirty_rect.left = 0;
@ -323,7 +324,7 @@ void GLESTexture::drawTexture(GLshort x, GLshort y, GLshort w, GLshort h) {
dwidth, dheight, _glFormat, _glType, _tex));
}
GLESBaseTexture::drawTexture(x, y, w, h);
GLESBaseTexture::drawTexture(x, y, w, h, clip);
}
GLES4444Texture::GLES4444Texture() :
@ -333,6 +334,13 @@ GLES4444Texture::GLES4444Texture() :
GLES4444Texture::~GLES4444Texture() {
}
GLES8888Texture::GLES8888Texture() :
GLESTexture(GL_RGBA, GL_UNSIGNED_BYTE, pixelFormat()) {
}
GLES8888Texture::~GLES8888Texture() {
}
GLES5551Texture::GLES5551Texture() :
GLESTexture(GL_RGBA, GL_UNSIGNED_SHORT_5_5_5_1, pixelFormat()) {
}
@ -419,8 +427,7 @@ void GLESFakePaletteTexture::updateBuffer(GLuint x, GLuint y, GLuint w,
} while (--h);
}
void GLESFakePaletteTexture::drawTexture(GLshort x, GLshort y, GLshort w,
GLshort h) {
void GLESFakePaletteTexture::drawTexture(GLshort x, GLshort y, GLshort w, GLshort h, const Common::Rect &clip) {
if (_all_dirty) {
_dirty_rect.top = 0;
_dirty_rect.left = 0;
@ -452,7 +459,7 @@ void GLESFakePaletteTexture::drawTexture(GLshort x, GLshort y, GLshort w,
dwidth, dheight, _glFormat, _glType, _buf));
}
GLESBaseTexture::drawTexture(x, y, w, h);
GLESBaseTexture::drawTexture(x, y, w, h, clip);
}
const Graphics::PixelFormat &GLESFakePaletteTexture::getPixelFormat() const {

View file

@ -57,7 +57,11 @@ public:
const void *buf, int pitch_buf) = 0;
virtual void fillBuffer(uint32 color) = 0;
virtual void drawTexture(GLshort x, GLshort y, GLshort w, GLshort h);
virtual void drawTexture(GLshort x, GLshort y, GLshort w, GLshort h) {
drawTexture(x, y, w, h, Common::Rect(0, 0, width(), height()));
}
virtual void drawTexture(GLshort x, GLshort y, GLshort w, GLshort h, const Common::Rect &clip);
inline void setDrawRect(const Common::Rect &rect) {
_draw_rect = rect;
@ -203,13 +207,26 @@ public:
const void *buf, int pitch_buf);
virtual void fillBuffer(uint32 color);
virtual void drawTexture(GLshort x, GLshort y, GLshort w, GLshort h);
virtual void drawTexture(GLshort x, GLshort y, GLshort w, GLshort h) {
drawTexture(x, y, w, h, Common::Rect(0, 0, width(), height()));
}
virtual void drawTexture(GLshort x, GLshort y, GLshort w, GLshort h, const Common::Rect &clip);
protected:
byte *_pixels;
byte *_buf;
};
class GLES8888Texture : public GLESTexture {
public:
GLES8888Texture();
virtual ~GLES8888Texture();
static Graphics::PixelFormat pixelFormat() {
return Graphics::PixelFormat(4, 8, 8, 8, 8, 24, 16, 8, 0);
}
};
// RGBA4444 texture
class GLES4444Texture : public GLESTexture {
public:
@ -256,7 +273,10 @@ public:
const void *buf, int pitch_buf);
virtual void fillBuffer(uint32 color);
virtual void drawTexture(GLshort x, GLshort y, GLshort w, GLshort h);
virtual void drawTexture(GLshort x, GLshort y, GLshort w, GLshort h) {
drawTexture(x, y, w, h, Common::Rect(0, 0, width(), height()));
}
virtual void drawTexture(GLshort x, GLshort y, GLshort w, GLshort h, const Common::Rect &clip);
virtual const byte *palette_const() const {
return (byte *)_palette;

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

View file

@ -23,6 +23,7 @@ MODULE_OBJS := \
pixelbuffer.o \
opengles2/shader.o \
opengles2/box_shaders.o \
opengles2/control_shaders.o \
opengles2/compat_shaders.o \
tinygl/api.o \
tinygl/arrays.o \

View file

@ -0,0 +1,65 @@
/* ResidualVM - A 3D game interpreter
*
* ResidualVM is the legal property of its developers, whose names
* are too numerous to list here. Please refer to the COPYRIGHT
* file distributed with this source distribution.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
*/
#include "common/scummsys.h"
#ifdef USE_GLES2
namespace Graphics {
namespace BuiltinShaders {
const char *controlVertex =
"#version 100\n"
"attribute vec2 position;\n"
"attribute vec2 texcoord;\n"
"uniform vec2 offsetXY;\n"
"uniform vec2 sizeWH;\n"
"uniform vec4 clip;\n"
"uniform bool flipY;\n"
"varying vec2 Texcoord;\n"
"void main() {\n"
"Texcoord = clip.xy + texcoord * (clip.zw - clip.xy);\n"
"vec2 pos = offsetXY + position * sizeWH;\n"
"pos.x = pos.x * 2.0 - 1.0;\n"
"pos.y = pos.y * 2.0 - 1.0;\n"
"if (flipY)\n"
"pos.y *= -1.0;\n"
"gl_Position = vec4(pos, 0.0, 1.0);\n"
"}\n";
const char *controlFragment =
"#version 100\n"
"#ifdef GL_FRAGMENT_PRECISION_HIGH\n"
"precision highp float;\n"
"#else\n"
"precision mediump float;\n"
"#endif\n"
"varying vec2 Texcoord;\n"
"uniform sampler2D tex;\n"
"void main() {\n"
"gl_FragColor = texture2D(tex, Texcoord);\n"
"}\n";
}
}
#endif

View file

@ -34,10 +34,9 @@
namespace Graphics {
namespace BuiltinShaders {
extern const char *boxVertex;
extern const char *boxFragment;
extern const char *compatVertex;
extern const char *compatFragment;
extern const char *boxVertex, *boxFragment;
extern const char *compatVertex, *compatFragment;
extern const char *controlVertex, *controlFragment;
}
struct VertexAttrib {