samples: fixes and improvements to gamepad and mappingGUI sample

This commit is contained in:
rdb 2018-09-28 20:48:50 +02:00
parent 85752b06f5
commit c4b3b558c9
4 changed files with 714 additions and 209 deletions

View File

@ -9,21 +9,51 @@ move the camera where the right stick will rotate the camera.
from direct.showbase.ShowBase import ShowBase
from panda3d.core import TextNode, InputDevice, loadPrcFileData, Vec3
from panda3d.core import TextPropertiesManager
from direct.gui.OnscreenText import OnscreenText
loadPrcFileData("", "notify-level-device debug")
loadPrcFileData("", """
default-fov 60
notify-level-device debug
""")
# Informational text in the bottom-left corner. We use the special \5
# character to embed an image representing the gamepad buttons.
INFO_TEXT = """Move \5lstick\5 to strafe, \5rstick\5 to turn
Press \5ltrigger\5 and \5rtrigger\5 to go down/up
Press \5face_x\5 to reset camera"""
class App(ShowBase):
def __init__(self):
ShowBase.__init__(self)
# print all events sent through the messenger
self.messenger.toggleVerbose()
# Print all events sent through the messenger
#self.messenger.toggleVerbose()
# Load the graphics for the gamepad buttons and register them, so that
# we can embed them in our information text.
graphics = loader.loadModel("models/xbone-icons.egg")
mgr = TextPropertiesManager.getGlobalPtr()
for name in ["face_a", "face_b", "face_x", "face_y", "ltrigger", "rtrigger", "lstick", "rstick"]:
graphic = graphics.find("**/" + name)
graphic.setScale(1.5)
mgr.setGraphic(name, graphic)
graphic.setZ(-0.5)
# Show the informational text in the corner.
self.lblInfo = OnscreenText(
parent = self.a2dBottomLeft,
pos = (0.1, 0.3),
fg = (1, 1, 1, 1),
bg = (0.2, 0.2, 0.2, 0.9),
align = TextNode.A_left,
text = INFO_TEXT)
self.lblInfo.textNode.setCardAsMargin(0.5, 0.5, 0.5, 0.2)
self.lblWarning = OnscreenText(
text = "No devices found",
fg=(1,0,0,1),
scale = .25)
self.lblWarning.hide()
self.lblAction = OnscreenText(
text = "Action",
@ -31,99 +61,133 @@ class App(ShowBase):
scale = .15)
self.lblAction.hide()
self.checkDevices()
# Is there a gamepad connected?
self.gamepad = None
devices = self.devices.getDevices(InputDevice.DC_gamepad)
if devices:
self.connect(devices[0])
# Accept device dis-/connection events
# NOTE: catching the events here will overwrite the accept in showbase, hence
# we need to forward the event in the functions we set here!
self.accept("connect-device", self.connect)
self.accept("disconnect-device", self.disconnect)
self.accept("escape", exit)
self.accept("gamepad0-start", exit)
self.accept("flight_stick0-start", exit)
# Accept button events of the first connected gamepad
self.accept("gamepad0-action_a", self.doAction, extraArgs=[True, "Action"])
self.accept("gamepad0-action_a-up", self.doAction, extraArgs=[False, "Release"])
self.accept("gamepad0-action_b", self.doAction, extraArgs=[True, "Action 2"])
self.accept("gamepad0-action_b-up", self.doAction, extraArgs=[False, "Release"])
self.accept("gamepad-back", exit)
self.accept("gamepad-start", exit)
self.accept("gamepad-face_x", self.reset)
self.accept("gamepad-face_a", self.action, extraArgs=["face_a"])
self.accept("gamepad-face_a-up", self.actionUp)
self.accept("gamepad-face_b", self.action, extraArgs=["face_b"])
self.accept("gamepad-face_b-up", self.actionUp)
self.accept("gamepad-face_y", self.action, extraArgs=["face_y"])
self.accept("gamepad-face_y-up", self.actionUp)
self.environment = loader.loadModel("environment")
self.environment.reparentTo(render)
# disable pandas default mouse-camera controls so we can handle the camera
# movements by ourself
# Disable the default mouse-camera controls since we need to handle
# our own camera controls.
self.disableMouse()
# list of connected gamepad devices
gamepads = base.devices.getDevices(InputDevice.DC_gamepad)
# set the center position of the control sticks
# NOTE: here we assume, that the wheel is centered when the application get started.
# In real world applications, you should notice the user and give him enough time
# to center the wheel until you store the center position of the controler!
self.lxcenter = gamepads[0].findControl(InputDevice.C_left_x).state
self.lycenter = gamepads[0].findControl(InputDevice.C_left_y).state
self.rxcenter = gamepads[0].findControl(InputDevice.C_right_x).state
self.rycenter = gamepads[0].findControl(InputDevice.C_right_y).state
self.reset()
self.taskMgr.add(self.moveTask, "movement update task")
def connect(self, device):
# we need to forward the event to the connectDevice function of showbase
self.connectDevice(device)
# Now we can check for ourself
self.checkDevices()
"""Event handler that is called when a device is discovered."""
# We're only interested if this is a gamepad and we don't have a
# gamepad yet.
if device.device_class == InputDevice.DC_gamepad and not self.gamepad:
print("Found %s" % (device))
self.gamepad = device
# Enable this device to ShowBase so that we can receive events.
# We set up the events with a prefix of "gamepad-".
self.attachInputDevice(device, prefix="gamepad")
# Hide the warning that we have no devices.
self.lblWarning.hide()
def disconnect(self, device):
# we need to forward the event to the disconnectDevice function of showbase
self.disconnectDevice(device)
# Now we can check for ourself
self.checkDevices()
"""Event handler that is called when a device is removed."""
def checkDevices(self):
# check if we have gamepad devices connected
if self.devices.get_devices(InputDevice.DC_gamepad):
# we have at least one gamepad device
self.lblWarning.hide()
if self.gamepad != device:
# We don't care since it's not our gamepad.
return
# Tell ShowBase that the device is no longer needed.
print("Disconnected %s" % (device))
self.detachInputDevice(device)
self.gamepad = None
# Do we have any other gamepads? Attach the first other gamepad.
devices = self.devices.getDevices(InputDevice.DC_gamepad)
if devices:
self.connect(devices[0])
else:
# no devices connected
# No devices. Show the warning.
self.lblWarning.show()
def doAction(self, showText, text):
if showText and self.lblAction.isHidden():
self.lblAction.show()
else:
self.lblAction.hide()
def reset(self):
"""Reset the camera to the initial position."""
self.camera.setPosHpr(0, -200, 10, 0, 0, 0)
def action(self, button):
# Just show which button has been pressed.
self.lblAction.text = "Pressed \5%s\5" % button
self.lblAction.show()
def actionUp(self):
# Hide the label showing which button is pressed.
self.lblAction.hide()
def moveTask(self, task):
dt = globalClock.getDt()
movementVec = Vec3()
gamepads = base.devices.getDevices(InputDevice.DC_gamepad)
if len(gamepads) == 0:
# savety check
if not self.gamepad:
return task.cont
strafe_speed = 85
vert_speed = 50
turn_speed = 100
# If the left stick is pressed, we will go faster.
lstick = self.gamepad.findButton("lstick")
if lstick.pressed:
strafe_speed *= 2.0
# we will use the first found gamepad
# Move the camera left/right
left_x = gamepads[0].findControl(InputDevice.C_left_x)
movementVec.setX(left_x.state - self.lxcenter)
# Move the camera forward/backward
left_y = gamepads[0].findControl(InputDevice.C_left_y)
movementVec.setY(left_y.state - self.lycenter)
# Control the cameras heading
right_x = gamepads[0].findControl(InputDevice.C_right_x)
base.camera.setH(base.camera, 100 * dt * (right_x.state - self.rxcenter))
# Control the cameras pitch
right_y = gamepads[0].findControl(InputDevice.C_right_y)
base.camera.setP(base.camera, 100 * dt * (right_y.state - self.rycenter))
strafe = Vec3(0)
left_x = self.gamepad.findAxis(InputDevice.Axis.left_x)
left_y = self.gamepad.findAxis(InputDevice.Axis.left_y)
strafe.x = left_x.value
strafe.y = left_y.value
# calculate movement
base.camera.setX(base.camera, 100 * dt * movementVec.getX())
base.camera.setY(base.camera, 100 * dt * movementVec.getY())
# Apply some deadzone, since the sticks don't center exactly at 0
if strafe.lengthSquared() >= 0.01:
self.camera.setPos(self.camera, strafe * strafe_speed * dt)
# Use the triggers for the vertical position.
trigger_l = self.gamepad.findAxis(InputDevice.Axis.left_trigger)
trigger_r = self.gamepad.findAxis(InputDevice.Axis.right_trigger)
lift = trigger_r.value - trigger_l.value
self.camera.setZ(self.camera.getZ() + (lift * vert_speed * dt))
# Move the camera forward/backward
right_x = self.gamepad.findAxis(InputDevice.Axis.right_x)
right_y = self.gamepad.findAxis(InputDevice.Axis.right_y)
# Again, some deadzone
if abs(right_x.value) >= 0.1 or abs(right_y.value) >= 0.1:
self.camera.setH(self.camera, turn_speed * dt * -right_x.value)
self.camera.setP(self.camera, turn_speed * dt * right_y.value)
# Reset the roll so that the camera remains upright.
self.camera.setR(0)
return task.cont

View File

@ -16,62 +16,75 @@ from panda3d.core import (
TextNode,
Vec2,
InputDevice,
loadPrcFileData)
loadPrcFileData,
GamepadButton,
KeyboardButton)
# Make sure the textures look crisp on every device that supports
# non-power-2 textures
loadPrcFileData("", "textures-auto-power-2 #t")
class App(ShowBase):
# How much an axis should have moved for it to register as a movement.
DEAD_ZONE = 0.33
class InputMapping(object):
"""A container class for storing a mapping from a string action to either
an axis or a button. You could extend this with additional methods to load
the default mappings from a configuration file. """
# Define all the possible actions.
actions = (
"Move forward", "Move backward", "Move left", "Move right", "Jump",
"Buy", "Use", "Break", "Fix", "Trash", "Change", "Mail", "Upgrade",
)
def __init__(self):
ShowBase.__init__(self)
self.__map = dict.fromkeys(self.actions)
self.setBackgroundColor(0, 0, 0)
# make the font look nice at a big scale
DGG.getDefaultFont().setPixelsPerUnit(100)
def mapButton(self, action, button):
self.__map[action] = ("button", str(button))
# a dict of actions and button/axis events
self.gamepadMapping = {
"Move forward":"Left Stick Y",
"Move backward":"Left Stick Y",
"Move left":"Left Stick X",
"Move right":"Left Stick X",
"Jump":"a",
"Action":"b",
"Sprint":"x",
"Map":"y",
"action-1":"c",
"action-2":"d",
"action-3":"e",
"action-4":"f",
"action-5":"g",
"action-6":"h",
"action-7":"i",
"action-8":"j",
"action-9":"k",
"action-10":"l",
"action-11":"m",
}
# this will store the action that we want to remap
self.actionToMap = ""
# this will store the key/axis that we want to asign to an action
self.newActionKey = ""
# this will store the label that needs to be actualized in the list
self.actualizeLabel = None
def mapAxis(self, action, axis):
self.__map[action] = ("axis", axis.name)
# The geometry for our basic buttons
maps = loader.loadModel("models/button_map")
self.buttonGeom = (
maps.find("**/ready"),
maps.find("**/click"),
maps.find("**/hover"),
maps.find("**/disabled"))
def unmap(self):
self.__map[action] = None
# Create the dialog that asks the user for input on a given
# action to map a key to.
DGG.setDefaultDialogGeom("models/dialog.png")
# setup a dialog to ask for device input
self.dlgInput = OkCancelDialog(
def formatMapping(self, action):
"""Returns a string label describing the mapping for a given action,
for displaying in a GUI. """
mapping = self.__map.get(action)
if not mapping:
return "Unmapped"
# Format the symbolic string from Panda nicely. In a real-world game,
# you might want to look these up in a translation table, or so.
label = mapping[1].replace('_', ' ').title()
if mapping[0] == "axis":
return "Axis: " + label
else:
return "Button: " + label
class ChangeActionDialog(object):
"""Encapsulates the UI dialog that opens up when changing a mapping. It
holds the state of which action is being set and which button is pressed
and invokes a callback function when the dialog is exited."""
def __init__(self, action, button_geom, command):
# This stores which action we are remapping.
self.action = action
# This will store the key/axis that we want to asign to an action
self.newInputType = ""
self.newInput = ""
self.setKeyCalled = False
self.__command = command
# Initialize the DirectGUI stuff.
self.dialog = OkCancelDialog(
dialogName="dlg_device_input",
pos=(0, 0, 0.25),
text="Hit desired key:",
@ -82,7 +95,7 @@ class App(ShowBase):
text_align=TextNode.ACenter,
fadeScreen=0.65,
frameColor=VBase4(0.3, 0.3, 0.3, 1),
button_geom=self.buttonGeom,
button_geom=button_geom,
button_scale=0.15,
button_text_scale=0.35,
button_text_align=TextNode.ALeft,
@ -93,13 +106,73 @@ class App(ShowBase):
button_frameColor=VBase4(0, 0, 0, 0),
button_frameSize=VBase4(-1.0, 1.0, -0.25, 0.25),
button_pressEffect=False,
command=self.closeDialog)
self.dlgInput.setTransparency(True)
self.dlgInput.configureDialog()
scale = self.dlgInput["image_scale"]
self.dlgInput["image_scale"] = (scale[0]/2.0, scale[1], scale[2]/2.0)
self.dlgInput["text_pos"] = (self.dlgInput["text_pos"][0], self.dlgInput["text_pos"][1] + 0.06)
self.dlgInput.hide()
command=self.onClose)
self.dialog.setTransparency(True)
self.dialog.configureDialog()
scale = self.dialog["image_scale"]
self.dialog["image_scale"] = (scale[0]/2.0, scale[1], scale[2]/2.0)
self.dialog["text_pos"] = (self.dialog["text_pos"][0], self.dialog["text_pos"][1] + 0.06)
def buttonPressed(self, button):
if any(button.guiItem.getState() == 1 for button in self.dialog.buttonList):
# Ignore events while any of the dialog buttons are active, because
# otherwise we register mouse clicks when the user is trying to
# exit the dialog.
return
text = str(button).replace('_', ' ').title()
self.dialog["text"] = "New event will be:\n\nButton: " + text
self.newInputType = "button"
self.newInput = button
def axisMoved(self, axis):
text = axis.name.replace('_', ' ').title()
self.dialog["text"] = "New event will be:\n\nAxis: " + text
self.newInputType = "axis"
self.newInput = axis
def onClose(self, result):
"""Called when the OK or Cancel button is pressed."""
self.dialog.cleanup()
# Call the constructor-supplied callback with our new setting, if any.
if self.newInput and result == DGG.DIALOG_OK:
self.__command(self.action, self.newInputType, self.newInput)
else:
# Cancel (or no input was pressed)
self.__command(self.action, None, None)
class MappingGUIDemo(ShowBase):
def __init__(self):
ShowBase.__init__(self)
self.setBackgroundColor(0, 0, 0)
# make the font look nice at a big scale
DGG.getDefaultFont().setPixelsPerUnit(100)
# Store our mapping, with some sensible defaults. In a real game, you
# will want to load these from a configuration file.
self.mapping = InputMapping()
self.mapping.mapAxis("Move forward", InputDevice.Axis.left_y)
self.mapping.mapAxis("Move backward", InputDevice.Axis.left_y)
self.mapping.mapAxis("Move left", InputDevice.Axis.left_x)
self.mapping.mapAxis("Move right", InputDevice.Axis.left_x)
self.mapping.mapButton("Jump", GamepadButton.face_a())
self.mapping.mapButton("Use", GamepadButton.face_b())
self.mapping.mapButton("Break", GamepadButton.face_x())
self.mapping.mapButton("Fix", GamepadButton.face_y())
# The geometry for our basic buttons
maps = loader.loadModel("models/button_map")
self.buttonGeom = (
maps.find("**/ready"),
maps.find("**/click"),
maps.find("**/hover"),
maps.find("**/disabled"))
# Change the default dialog skin.
DGG.setDefaultDialogGeom("models/dialog.png")
# create a sample title
self.textscale = 0.1
@ -135,6 +208,7 @@ class App(ShowBase):
decMaps.find("**/dec_click"),
decMaps.find("**/dec_hover"),
decMaps.find("**/dec_disabled"))
# create the scrolled frame that will hold our list
self.lstActionMap = DirectScrolledFrame(
# make the frame occupy the whole window
@ -167,135 +241,116 @@ class App(ShowBase):
idx = 0
self.listBGEven = base.loader.loadModel("models/list_item_even")
self.listBGOdd = base.loader.loadModel("models/list_item_odd")
for key, value in self.gamepadMapping.items():
item = self.__makeListItem(key, key, value, idx)
self.actionLabels = {}
for action in self.mapping.actions:
mapped = self.mapping.formatMapping(action)
item = self.__makeListItem(action, mapped, idx)
item.reparentTo(self.lstActionMap.getCanvas())
idx += 1
# recalculate the canvas size to set scrollbars if necesary
self.lstActionMap["canvasSize"] = (
base.a2dLeft+0.05, base.a2dRight-0.05,
-(len(self.gamepadMapping.keys())*0.1), 0.09)
-(len(self.mapping.actions)*0.1), 0.09)
self.lstActionMap.setCanvasSize()
def closeDialog(self, result):
self.dlgInput.hide()
if result == DGG.DIALOG_OK:
def closeDialog(self, action, newInputType, newInput):
"""Called in callback when the dialog is closed. newInputType will be
"button" or "axis", or None if the remapping was cancelled."""
self.dlgInput = None
if newInputType is not None:
# map the event to the given action
self.gamepadMapping[self.actionToMap] = self.newActionKey
if newInputType == "axis":
self.mapping.mapAxis(action, newInput)
else:
self.mapping.mapButton(action, newInput)
# actualize the label in the list that shows the current
# event for the action
self.actualizeLabel["text"] = self.newActionKey
self.actionLabels[action]["text"] = self.mapping.formatMapping(action)
# cleanup
self.dlgInput["text"] ="Hit desired key:"
self.actionToMap = ""
self.newActionKey = ""
self.actualizeLabel = None
for bt in base.buttonThrowers:
bt.node().setSpecificFlag(True)
bt.node().setButtonDownEvent("")
for bt in base.deviceButtonThrowers:
bt.node().setSpecificFlag(True)
bt.node().setButtonDownEvent("")
taskMgr.remove("checkControls")
def changeMapping(self, action, label):
# set the action that we want to map a new key to
self.actionToMap = action
# set the label that needs to be actualized
self.actualizeLabel = label
# show our dialog
self.dlgInput.show()
# Now detach all the input devices.
for device in self.attachedDevices:
base.detachInputDevice(device)
self.attachedDevices.clear()
# catch all button events
def changeMapping(self, action):
# Create the dialog window
self.dlgInput = ChangeActionDialog(action, button_geom=self.buttonGeom, command=self.closeDialog)
# Attach all input devices.
devices = base.devices.getDevices()
for device in devices:
base.attachInputDevice(device)
self.attachedDevices = devices
# Disable regular button events on all button event throwers, and
# instead broadcast a generic event.
for bt in base.buttonThrowers:
bt.node().setSpecificFlag(False)
bt.node().setButtonDownEvent("keyListenEvent")
for bt in base.deviceButtonThrowers:
bt.node().setSpecificFlag(False)
bt.node().setButtonDownEvent("deviceListenEvent")
self.setKeyCalled = False
self.accept("keyListenEvent", self.setKey)
self.accept("deviceListenEvent", self.setDeviceKey)
# As there are no events thrown for control changes, we set up
# a task to check if the controls got moved
# This list will help us for checking which controls got moved
self.controlStates = {None:{}}
self.accept("keyListenEvent", self.dlgInput.buttonPressed)
self.accept("deviceListenEvent", self.dlgInput.buttonPressed)
# As there are no events thrown for control changes, we set up a task
# to check if the controls were moved
# This list will help us for checking which controls were moved
self.axisStates = {None: {}}
# fill it with all available controls
for device in base.devices.get_devices():
for ctrl in device.controls:
if device not in self.controlStates.keys():
self.controlStates.update({device: {ctrl.axis: ctrl.state}})
for device in devices:
for axis in device.axes:
if device not in self.axisStates.keys():
self.axisStates.update({device: {axis.axis: axis.value}})
else:
self.controlStates[device].update({ctrl.axis: ctrl.state})
self.axisStates[device].update({axis.axis: axis.value})
# start the task
taskMgr.add(self.watchControls, "checkControls")
def watchControls(self, task):
# move through all devices and all it's controls
for device in base.devices.get_devices():
for ctrl in device.controls:
# if a control got changed more than the given puffer zone
if self.controlStates[device][ctrl.axis] + 0.2 < ctrl.state or \
self.controlStates[device][ctrl.axis] - 0.2 > ctrl.state:
for device in self.attachedDevices:
if device.device_class == InputDevice.DC_mouse:
# Ignore mouse axis movement, or the user can't even navigate
# to the OK/Cancel buttons!
continue
for axis in device.axes:
# if a control got changed more than the given dead zone
if self.axisStates[device][axis.axis] + DEAD_ZONE < axis.value or \
self.axisStates[device][axis.axis] - DEAD_ZONE > axis.value:
# set the current state in the dict
self.controlStates[device][ctrl.axis] = ctrl.state
# check which axis got moved
if ctrl.axis == InputDevice.C_left_x:
self.setKey("Left Stick X")
elif ctrl.axis == InputDevice.C_left_y:
self.setKey("Left Stick Y")
elif ctrl.axis == InputDevice.C_left_trigger:
self.setKey("Left Trigger")
elif ctrl.axis == InputDevice.C_right_x:
self.setKey("Right Stick X")
elif ctrl.axis == InputDevice.C_right_y:
self.setKey("Right Stick Y")
elif ctrl.axis == InputDevice.C_right_trigger:
self.setKey("Right Trigger")
elif ctrl.axis == InputDevice.C_x:
self.setKey("X")
elif ctrl.axis == InputDevice.C_y:
self.setKey("Y")
elif ctrl.axis == InputDevice.C_trigger:
self.setKey("Trigger")
elif ctrl.axis == InputDevice.C_throttle:
self.setKey("Throttle")
elif ctrl.axis == InputDevice.C_rudder:
self.setKey("Rudder")
elif ctrl.axis == InputDevice.C_hat_x:
self.setKey("Hat X")
elif ctrl.axis == InputDevice.C_hat_y:
self.setKey("Hat Y")
elif ctrl.axis == InputDevice.C_wheel:
self.setKey("Wheel")
elif ctrl.axis == InputDevice.C_accelerator:
self.setKey("Acclerator")
elif ctrl.axis == InputDevice.C_brake:
self.setKey("Break")
self.axisStates[device][axis.axis] = axis.value
# Format the axis for being displayed.
if axis.axis != InputDevice.Axis.none:
#label = axis.axis.name.replace('_', ' ').title()
self.dlgInput.axisMoved(axis.axis)
return task.cont
def setKey(self, args):
self.setKeyCalled = True
if self.dlgInput.buttonList[0].guiItem.getState() == 1:
# this occurs if the OK button was clicked. To prevent to
# always set the mouse1 event whenever the OK button was
# pressed, we instantly return from this function
return
self.dlgInput["text"] = "New event will be:\n\n" + args
self.newActionKey = args
def setDeviceKey(self, args):
if not self.setKeyCalled:
self.setKey(args)
self.setKeyCalled = False
def __makeListItem(self, itemName, action, event, index):
def __makeListItem(self, action, event, index):
def dummy(): pass
if index % 2 == 0:
bg = self.listBGEven
else:
bg = self.listBGOdd
item = DirectFrame(
text=itemName,
text=action,
geom=bg,
geom_scale=(base.a2dRight-0.05, 1, 0.1),
frameSize=VBase4(base.a2dLeft+0.05, base.a2dRight-0.05, -0.05, 0.05),
@ -317,6 +372,8 @@ class App(ShowBase):
)
lbl.reparentTo(item)
lbl.setTransparency(True)
self.actionLabels[action] = lbl
buttonScale = 0.15
btn = DirectButton(
text="Change",
@ -333,10 +390,10 @@ class App(ShowBase):
pos=(base.a2dRight-(0.898*buttonScale+0.3), 0, 0),
pressEffect=False,
command=self.changeMapping,
extraArgs=[action, lbl])
extraArgs=[action])
btn.setTransparency(True)
btn.reparentTo(item)
return item
app = App()
app = MappingGUIDemo()
app.run()

View File

@ -0,0 +1,384 @@
<CoordinateSystem> { Y-Up }
<Texture> xbone-icons {
xbone-icons.png
<Scalar> format { rgba }
<Scalar> alpha { dual }
}
<VertexPool> vpool {
<Vertex> 0 {
-0.5 0.5 0
<UV> { 0.00390625 0.9921875 }
}
<Vertex> 1 {
-0.5 -0.5 0
<UV> { 0.00390625 0.7578125 }
}
<Vertex> 2 {
0.5 -0.5 0
<UV> { 0.12109375 0.7578125 }
}
<Vertex> 3 {
0.5 0.5 0
<UV> { 0.12109375 0.9921875 }
}
<Vertex> 4 {
-0.5 0.5 0
<UV> { 0.12890625 0.9921875 }
}
<Vertex> 5 {
-0.5 -0.5 0
<UV> { 0.12890625 0.7578125 }
}
<Vertex> 6 {
0.5 -0.5 0
<UV> { 0.24609375 0.7578125 }
}
<Vertex> 7 {
0.5 0.5 0
<UV> { 0.24609375 0.9921875 }
}
<Vertex> 8 {
-0.5 0.5 0
<UV> { 0.25390625 0.9921875 }
}
<Vertex> 9 {
-0.5 -0.5 0
<UV> { 0.25390625 0.7578125 }
}
<Vertex> 10 {
0.5 -0.5 0
<UV> { 0.37109375 0.7578125 }
}
<Vertex> 11 {
0.5 0.5 0
<UV> { 0.37109375 0.9921875 }
}
<Vertex> 12 {
-0.5 0.5 0
<UV> { 0.37890625 0.9921875 }
}
<Vertex> 13 {
-0.5 -0.5 0
<UV> { 0.37890625 0.7578125 }
}
<Vertex> 14 {
0.5 -0.5 0
<UV> { 0.49609375 0.7578125 }
}
<Vertex> 15 {
0.5 0.5 0
<UV> { 0.49609375 0.9921875 }
}
<Vertex> 16 {
-0.5 0.5 0
<UV> { 0.50390625 0.9921875 }
}
<Vertex> 17 {
-0.5 -0.5 0
<UV> { 0.50390625 0.7578125 }
}
<Vertex> 18 {
0.5 -0.5 0
<UV> { 0.62109375 0.7578125 }
}
<Vertex> 19 {
0.5 0.5 0
<UV> { 0.62109375 0.9921875 }
}
<Vertex> 20 {
-0.5 0.5 0
<UV> { 0.62890625 0.9921875 }
}
<Vertex> 21 {
-0.5 -0.5 0
<UV> { 0.62890625 0.7578125 }
}
<Vertex> 22 {
0.5 -0.5 0
<UV> { 0.74609375 0.7578125 }
}
<Vertex> 23 {
0.5 0.5 0
<UV> { 0.74609375 0.9921875 }
}
<Vertex> 24 {
-0.5 0.5 0
<UV> { 0.75390625 0.9921875 }
}
<Vertex> 25 {
-0.5 -0.5 0
<UV> { 0.75390625 0.7578125 }
}
<Vertex> 26 {
0.5 -0.5 0
<UV> { 0.87109375 0.7578125 }
}
<Vertex> 27 {
0.5 0.5 0
<UV> { 0.87109375 0.9921875 }
}
<Vertex> 28 {
-0.5 0.5 0
<UV> { 0.87890625 0.9921875 }
}
<Vertex> 29 {
-0.5 -0.5 0
<UV> { 0.87890625 0.7578125 }
}
<Vertex> 30 {
0.5 -0.5 0
<UV> { 0.99609375 0.7578125 }
}
<Vertex> 31 {
0.5 0.5 0
<UV> { 0.99609375 0.9921875 }
}
<Vertex> 32 {
-0.5 0.5 0
<UV> { 0.12890625 0.7421875 }
}
<Vertex> 33 {
-0.5 -0.5 0
<UV> { 0.12890625 0.5078125 }
}
<Vertex> 34 {
0.5 -0.5 0
<UV> { 0.24609375 0.5078125 }
}
<Vertex> 35 {
0.5 0.5 0
<UV> { 0.24609375 0.7421875 }
}
<Vertex> 36 {
-0.5 0.5 0
<UV> { 0.00390625 0.7421875 }
}
<Vertex> 37 {
-0.5 -0.5 0
<UV> { 0.00390625 0.5078125 }
}
<Vertex> 38 {
0.5 -0.5 0
<UV> { 0.12109375 0.5078125 }
}
<Vertex> 39 {
0.5 0.5 0
<UV> { 0.12109375 0.7421875 }
}
<Vertex> 40 {
-0.5 0.5 0
<UV> { 0.25390625 0.7421875 }
}
<Vertex> 41 {
-0.5 -0.5 0
<UV> { 0.25390625 0.5078125 }
}
<Vertex> 42 {
0.5 -0.5 0
<UV> { 0.37109375 0.5078125 }
}
<Vertex> 43 {
0.5 0.5 0
<UV> { 0.37109375 0.7421875 }
}
<Vertex> 44 {
-0.5 0.5 0
<UV> { 0.37890625 0.7421875 }
}
<Vertex> 45 {
-0.5 -0.5 0
<UV> { 0.37890625 0.5078125 }
}
<Vertex> 46 {
0.5 -0.5 0
<UV> { 0.49609375 0.5078125 }
}
<Vertex> 47 {
0.5 0.5 0
<UV> { 0.49609375 0.7421875 }
}
<Vertex> 48 {
-0.5 0.5 0
<UV> { 0.62890625 0.7421875 }
}
<Vertex> 49 {
-0.5 -0.5 0
<UV> { 0.62890625 0.5078125 }
}
<Vertex> 50 {
0.5 -0.5 0
<UV> { 0.74609375 0.5078125 }
}
<Vertex> 51 {
0.5 0.5 0
<UV> { 0.74609375 0.7421875 }
}
<Vertex> 52 {
-0.5 0.5 0
<UV> { 0.50390625 0.7421875 }
}
<Vertex> 53 {
-0.5 -0.5 0
<UV> { 0.50390625 0.5078125 }
}
<Vertex> 54 {
0.5 -0.5 0
<UV> { 0.62109375 0.5078125 }
}
<Vertex> 55 {
0.5 0.5 0
<UV> { 0.62109375 0.7421875 }
}
<Vertex> 56 {
-0.5 0.5 0
<UV> { 0.75390625 0.7421875 }
}
<Vertex> 57 {
-0.5 -0.5 0
<UV> { 0.75390625 0.5078125 }
}
<Vertex> 58 {
0.5 -0.5 0
<UV> { 0.87109375 0.5078125 }
}
<Vertex> 59 {
0.5 0.5 0
<UV> { 0.87109375 0.7421875 }
}
<Vertex> 60 {
-0.5 0.5 0
<UV> { 0.87890625 0.7421875 }
}
<Vertex> 61 {
-0.5 -0.5 0
<UV> { 0.87890625 0.5078125 }
}
<Vertex> 62 {
0.5 -0.5 0
<UV> { 0.99609375 0.5078125 }
}
<Vertex> 63 {
0.5 0.5 0
<UV> { 0.99609375 0.7421875 }
}
<Vertex> 64 {
-0.5 0.5 0
<UV> { 0.00390625 0.4921875 }
}
<Vertex> 65 {
-0.5 -0.5 0
<UV> { 0.00390625 0.2578125 }
}
<Vertex> 66 {
0.5 -0.5 0
<UV> { 0.12109375 0.2578125 }
}
<Vertex> 67 {
0.5 0.5 0
<UV> { 0.12109375 0.4921875 }
}
}
<Group> face_a {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 0 1 2 3 <Ref> { vpool } }
}
}
<Group> face_b {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 4 5 6 7 <Ref> { vpool } }
}
}
<Group> dpad {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 8 9 10 11 <Ref> { vpool } }
}
}
<Group> dpad_down {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 12 13 14 15 <Ref> { vpool } }
}
}
<Group> dpad_left {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 16 17 18 19 <Ref> { vpool } }
}
}
<Group> dpad_right {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 20 21 22 23 <Ref> { vpool } }
}
}
<Group> dpad_up {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 24 25 26 27 <Ref> { vpool } }
}
}
<Group> XboxOne_LB {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 28 29 30 31 <Ref> { vpool } }
}
}
<Group> lstick {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 32 33 34 35 <Ref> { vpool } }
}
}
<Group> ltrigger {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 36 37 38 39 <Ref> { vpool } }
}
}
<Group> start {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 40 41 42 43 <Ref> { vpool } }
}
}
<Group> rshoulder {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 44 45 46 47 <Ref> { vpool } }
}
}
<Group> rstick {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 48 49 50 51 <Ref> { vpool } }
}
}
<Group> rtrigger {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 52 53 54 55 <Ref> { vpool } }
}
}
<Group> back {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 56 57 58 59 <Ref> { vpool } }
}
}
<Group> face_x {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 60 61 62 63 <Ref> { vpool } }
}
}
<Group> face_y {
<Polygon> {
<TRef> { xbone-icons }
<VertexRef> { 64 65 66 67 <Ref> { vpool } }
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB