mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
1218 lines
45 KiB
C++
1218 lines
45 KiB
C++
/*
|
|
==============================================================================
|
|
|
|
This file is part of the JUCE library - "Jules' Utility Class Extensions"
|
|
Copyright 2004-12 by Raw Material Software Ltd.
|
|
|
|
------------------------------------------------------------------------------
|
|
|
|
JUCE can be redistributed and/or modified under the terms of the GNU General
|
|
Public License (Version 2), as published by the Free Software Foundation.
|
|
A copy of the license is included in the JUCE distribution, or can be found
|
|
online at www.gnu.org/licenses.
|
|
|
|
JUCE 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.
|
|
|
|
------------------------------------------------------------------------------
|
|
|
|
To release a closed-source product which uses JUCE, commercial licenses are
|
|
available: visit www.rawmaterialsoftware.com/juce for more information.
|
|
|
|
==============================================================================
|
|
*/
|
|
|
|
#include "../JuceDemoHeader.h"
|
|
|
|
#if JUCE_OPENGL
|
|
|
|
#include "WavefrontObjParser.h"
|
|
|
|
|
|
//==============================================================================
|
|
struct OpenGLDemoClasses
|
|
{
|
|
/** Vertex data to be passed to the shaders.
|
|
For the purposes of this demo, each vertex will have a 3D position, a colour and a
|
|
2D texture co-ordinate. Of course you can ignore these or manipulate them in the
|
|
shader programs but are some useful defaults to work from.
|
|
*/
|
|
struct Vertex
|
|
{
|
|
float position[3];
|
|
float normal[3];
|
|
float colour[4];
|
|
float texCoord[2];
|
|
};
|
|
|
|
//==============================================================================
|
|
// This class just manages the attributes that the demo shaders use.
|
|
struct Attributes
|
|
{
|
|
Attributes (OpenGLContext& openGLContext, OpenGLShaderProgram& shader)
|
|
{
|
|
position = createAttribute (openGLContext, shader, "position");
|
|
normal = createAttribute (openGLContext, shader, "normal");
|
|
sourceColour = createAttribute (openGLContext, shader, "sourceColour");
|
|
texureCoordIn = createAttribute (openGLContext, shader, "texureCoordIn");
|
|
}
|
|
|
|
void enable (OpenGLContext& openGLContext)
|
|
{
|
|
if (position != nullptr)
|
|
{
|
|
openGLContext.extensions.glVertexAttribPointer (position->attributeID, 3, GL_FLOAT, GL_FALSE, sizeof (Vertex), 0);
|
|
openGLContext.extensions.glEnableVertexAttribArray (position->attributeID);
|
|
}
|
|
|
|
if (normal != nullptr)
|
|
{
|
|
openGLContext.extensions.glVertexAttribPointer (normal->attributeID, 3, GL_FLOAT, GL_FALSE, sizeof (Vertex), (GLvoid*) (sizeof (float) * 3));
|
|
openGLContext.extensions.glEnableVertexAttribArray (normal->attributeID);
|
|
}
|
|
|
|
if (sourceColour != nullptr)
|
|
{
|
|
openGLContext.extensions.glVertexAttribPointer (sourceColour->attributeID, 4, GL_FLOAT, GL_FALSE, sizeof (Vertex), (GLvoid*) (sizeof (float) * 6));
|
|
openGLContext.extensions.glEnableVertexAttribArray (sourceColour->attributeID);
|
|
}
|
|
|
|
if (texureCoordIn != nullptr)
|
|
{
|
|
openGLContext.extensions.glVertexAttribPointer (texureCoordIn->attributeID, 2, GL_FLOAT, GL_FALSE, sizeof (Vertex), (GLvoid*) (sizeof (float) * 10));
|
|
openGLContext.extensions.glEnableVertexAttribArray (texureCoordIn->attributeID);
|
|
}
|
|
}
|
|
|
|
void disable (OpenGLContext& openGLContext)
|
|
{
|
|
if (position != nullptr) openGLContext.extensions.glDisableVertexAttribArray (position->attributeID);
|
|
if (normal != nullptr) openGLContext.extensions.glDisableVertexAttribArray (normal->attributeID);
|
|
if (sourceColour != nullptr) openGLContext.extensions.glDisableVertexAttribArray (sourceColour->attributeID);
|
|
if (texureCoordIn != nullptr) openGLContext.extensions.glDisableVertexAttribArray (texureCoordIn->attributeID);
|
|
}
|
|
|
|
ScopedPointer<OpenGLShaderProgram::Attribute> position, normal, sourceColour, texureCoordIn;
|
|
|
|
private:
|
|
static OpenGLShaderProgram::Attribute* createAttribute (OpenGLContext& openGLContext,
|
|
OpenGLShaderProgram& shader,
|
|
const char* attributeName)
|
|
{
|
|
if (openGLContext.extensions.glGetAttribLocation (shader.getProgramID(), attributeName) < 0)
|
|
return nullptr;
|
|
|
|
return new OpenGLShaderProgram::Attribute (shader, attributeName);
|
|
}
|
|
};
|
|
|
|
//==============================================================================
|
|
// This class just manages the uniform values that the demo shaders use.
|
|
struct Uniforms
|
|
{
|
|
Uniforms (OpenGLContext& openGLContext, OpenGLShaderProgram& shader)
|
|
{
|
|
projectionMatrix = createUniform (openGLContext, shader, "projectionMatrix");
|
|
viewMatrix = createUniform (openGLContext, shader, "viewMatrix");
|
|
texture = createUniform (openGLContext, shader, "texture");
|
|
lightPosition = createUniform (openGLContext, shader, "lightPosition");
|
|
bouncingNumber = createUniform (openGLContext, shader, "bouncingNumber");
|
|
}
|
|
|
|
ScopedPointer<OpenGLShaderProgram::Uniform> projectionMatrix, viewMatrix, texture, lightPosition, bouncingNumber;
|
|
|
|
private:
|
|
static OpenGLShaderProgram::Uniform* createUniform (OpenGLContext& openGLContext,
|
|
OpenGLShaderProgram& shader,
|
|
const char* uniformName)
|
|
{
|
|
if (openGLContext.extensions.glGetUniformLocation (shader.getProgramID(), uniformName) < 0)
|
|
return nullptr;
|
|
|
|
return new OpenGLShaderProgram::Uniform (shader, uniformName);
|
|
}
|
|
};
|
|
|
|
//==============================================================================
|
|
/** This loads a 3D model from an OBJ file and converts it into some vertex buffers
|
|
that we can draw.
|
|
*/
|
|
struct Shape
|
|
{
|
|
Shape (OpenGLContext& openGLContext)
|
|
{
|
|
if (shapeFile.load (BinaryData::teapot_obj).wasOk())
|
|
for (int i = 0; i < shapeFile.shapes.size(); ++i)
|
|
vertexBuffers.add (new VertexBuffer (openGLContext, *shapeFile.shapes.getUnchecked(i)));
|
|
|
|
}
|
|
|
|
void draw (OpenGLContext& openGLContext, Attributes& attributes)
|
|
{
|
|
for (int i = 0; i < vertexBuffers.size(); ++i)
|
|
{
|
|
VertexBuffer& vertexBuffer = *vertexBuffers.getUnchecked (i);
|
|
vertexBuffer.bind();
|
|
|
|
attributes.enable (openGLContext);
|
|
glDrawElements (GL_TRIANGLES, vertexBuffer.numIndices, GL_UNSIGNED_INT, 0);
|
|
attributes.disable (openGLContext);
|
|
}
|
|
}
|
|
|
|
private:
|
|
struct VertexBuffer
|
|
{
|
|
VertexBuffer (OpenGLContext& context, WavefrontObjFile::Shape& shape) : openGLContext (context)
|
|
{
|
|
numIndices = shape.mesh.indices.size();
|
|
|
|
openGLContext.extensions.glGenBuffers (1, &vertexBuffer);
|
|
openGLContext.extensions.glBindBuffer (GL_ARRAY_BUFFER, vertexBuffer);
|
|
|
|
Array<Vertex> vertices;
|
|
createVertexListFromMesh (shape.mesh, vertices, Colours::green);
|
|
|
|
openGLContext.extensions.glBufferData (GL_ARRAY_BUFFER, vertices.size() * sizeof (Vertex),
|
|
vertices.getRawDataPointer(), GL_STATIC_DRAW);
|
|
|
|
openGLContext.extensions.glGenBuffers (1, &indexBuffer);
|
|
openGLContext.extensions.glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
openGLContext.extensions.glBufferData (GL_ELEMENT_ARRAY_BUFFER, numIndices * sizeof (juce::uint32),
|
|
shape.mesh.indices.getRawDataPointer(), GL_STATIC_DRAW);
|
|
}
|
|
|
|
~VertexBuffer()
|
|
{
|
|
openGLContext.extensions.glDeleteBuffers (1, &vertexBuffer);
|
|
openGLContext.extensions.glDeleteBuffers (1, &indexBuffer);
|
|
}
|
|
|
|
void bind()
|
|
{
|
|
openGLContext.extensions.glBindBuffer (GL_ARRAY_BUFFER, vertexBuffer);
|
|
openGLContext.extensions.glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
|
|
}
|
|
|
|
GLuint vertexBuffer, indexBuffer;
|
|
int numIndices;
|
|
OpenGLContext& openGLContext;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VertexBuffer)
|
|
};
|
|
|
|
WavefrontObjFile shapeFile;
|
|
OwnedArray<VertexBuffer> vertexBuffers;
|
|
|
|
static void createVertexListFromMesh (const WavefrontObjFile::Mesh& mesh, Array<Vertex>& list, Colour colour)
|
|
{
|
|
const float scale = 0.2f;
|
|
WavefrontObjFile::TextureCoord defaultTexCoord = { 0.5f, 0.5f };
|
|
WavefrontObjFile::Vertex defaultNormal = { 0.5f, 0.5f, 0.5f };
|
|
|
|
for (int i = 0; i < mesh.vertices.size(); ++i)
|
|
{
|
|
const WavefrontObjFile::Vertex& v = mesh.vertices.getReference (i);
|
|
|
|
const WavefrontObjFile::Vertex& n
|
|
= i < mesh.normals.size() ? mesh.normals.getReference (i) : defaultNormal;
|
|
|
|
const WavefrontObjFile::TextureCoord& tc
|
|
= i < mesh.textureCoords.size() ? mesh.textureCoords.getReference (i) : defaultTexCoord;
|
|
|
|
Vertex vert =
|
|
{
|
|
{ scale * v.x, scale * v.y, scale * v.z, },
|
|
{ scale * n.x, scale * n.y, scale * n.z, },
|
|
{ colour.getFloatRed(), colour.getFloatGreen(), colour.getFloatBlue(), colour.getFloatAlpha() },
|
|
{ tc.x, tc.y }
|
|
};
|
|
|
|
list.add (vert);
|
|
}
|
|
}
|
|
};
|
|
|
|
//==============================================================================
|
|
// These classes are used to load textures from the various sources that the demo uses..
|
|
struct DemoTexture
|
|
{
|
|
virtual ~DemoTexture() {}
|
|
virtual bool applyTo (OpenGLTexture&) = 0;
|
|
|
|
String name;
|
|
};
|
|
|
|
struct DynamicTexture : public DemoTexture
|
|
{
|
|
DynamicTexture() { name = "Dynamically-generated texture"; }
|
|
|
|
Image image;
|
|
BouncingNumber x, y;
|
|
|
|
bool applyTo (OpenGLTexture& texture) override
|
|
{
|
|
const int size = 128;
|
|
|
|
if (! image.isValid())
|
|
image = Image (Image::ARGB, size, size, true);
|
|
|
|
{
|
|
Graphics g (image);
|
|
g.fillAll (Colours::lightcyan);
|
|
|
|
g.setColour (Colours::darkred);
|
|
g.drawRect (0, 0, size, size, 2);
|
|
|
|
g.setColour (Colours::green);
|
|
g.fillEllipse (x.getValue() * size * 0.9f, y.getValue() * size * 0.9f, size * 0.1f, size * 0.1f);
|
|
|
|
g.setColour (Colours::black);
|
|
g.setFont (40);
|
|
g.drawFittedText (String (Time::getCurrentTime().getMilliseconds()), image.getBounds(), Justification::centred, 1);
|
|
}
|
|
|
|
texture.loadImage (image);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
struct BuiltInTexture : public DemoTexture
|
|
{
|
|
BuiltInTexture (const char* nm, const void* imageData, int imageSize)
|
|
: image (resizeImageToPowerOfTwo (ImageFileFormat::loadFrom (imageData, imageSize)))
|
|
{
|
|
name = nm;
|
|
}
|
|
|
|
Image image;
|
|
|
|
bool applyTo (OpenGLTexture& texture) override
|
|
{
|
|
texture.loadImage (image);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
struct TextureFromFile : public DemoTexture
|
|
{
|
|
TextureFromFile (const File& file)
|
|
{
|
|
name = file.getFileName();
|
|
image = resizeImageToPowerOfTwo (ImageFileFormat::loadFrom (file));
|
|
}
|
|
|
|
Image image;
|
|
|
|
bool applyTo (OpenGLTexture& texture) override
|
|
{
|
|
texture.loadImage (image);
|
|
return false;
|
|
}
|
|
};
|
|
|
|
static Image resizeImageToPowerOfTwo (Image image)
|
|
{
|
|
if (! (isPowerOfTwo (image.getWidth()) && isPowerOfTwo (image.getHeight())))
|
|
return image.rescaled (jmin (1024, nextPowerOfTwo (image.getWidth())),
|
|
jmin (1024, nextPowerOfTwo (image.getHeight())));
|
|
|
|
return image;
|
|
}
|
|
|
|
class OpenGLDemo;
|
|
|
|
//==============================================================================
|
|
/**
|
|
This component sits on top of the main GL demo, and contains all the sliders
|
|
and widgets that control things.
|
|
*/
|
|
class DemoControlsOverlay : public Component,
|
|
private CodeDocument::Listener,
|
|
private ComboBox::Listener,
|
|
private Slider::Listener,
|
|
private Button::Listener,
|
|
private Timer
|
|
{
|
|
public:
|
|
DemoControlsOverlay (OpenGLDemo& d)
|
|
: demo (d),
|
|
vertexEditorComp (vertexDocument, nullptr),
|
|
fragmentEditorComp (fragmentDocument, nullptr),
|
|
tabbedComp (TabbedButtonBar::TabsAtLeft),
|
|
showBackgroundToggle ("Draw 2D graphics in background")
|
|
{
|
|
addAndMakeVisible (statusLabel);
|
|
statusLabel.setJustificationType (Justification::topLeft);
|
|
statusLabel.setColour (Label::textColourId, Colours::black);
|
|
statusLabel.setFont (Font (14.0f));
|
|
|
|
addAndMakeVisible (sizeSlider);
|
|
sizeSlider.setRange (0.0, 1.0, 0.001);
|
|
sizeSlider.addListener (this);
|
|
|
|
addAndMakeVisible (zoomLabel);
|
|
zoomLabel.setText ("Zoom:", dontSendNotification);
|
|
zoomLabel.attachToComponent (&sizeSlider, true);
|
|
|
|
addAndMakeVisible (speedSlider);
|
|
speedSlider.setRange (0.0, 0.5, 0.001);
|
|
speedSlider.addListener (this);
|
|
speedSlider.setSkewFactor (0.5f);
|
|
|
|
addAndMakeVisible (speedLabel);
|
|
speedLabel.setText ("Speed:", dontSendNotification);
|
|
speedLabel.attachToComponent (&speedSlider, true);
|
|
|
|
addAndMakeVisible (showBackgroundToggle);
|
|
showBackgroundToggle.addListener (this);
|
|
|
|
Colour editorBackground (Colours::white.withAlpha (0.6f));
|
|
|
|
addAndMakeVisible (tabbedComp);
|
|
tabbedComp.setTabBarDepth (25);
|
|
tabbedComp.setColour (TabbedButtonBar::tabTextColourId, Colours::grey);
|
|
tabbedComp.addTab ("Vertex", editorBackground, &vertexEditorComp, false);
|
|
tabbedComp.addTab ("Fragment", editorBackground, &fragmentEditorComp, false);
|
|
|
|
vertexEditorComp.setColour (CodeEditorComponent::backgroundColourId, editorBackground);
|
|
fragmentEditorComp.setColour (CodeEditorComponent::backgroundColourId, editorBackground);
|
|
|
|
vertexDocument.addListener (this);
|
|
fragmentDocument.addListener (this);
|
|
|
|
textures.add (new BuiltInTexture ("Portmeirion", BinaryData::portmeirion_jpg, BinaryData::portmeirion_jpgSize));
|
|
textures.add (new BuiltInTexture ("Brushed aluminium", BinaryData::brushed_aluminium_png, BinaryData::brushed_aluminium_pngSize));
|
|
textures.add (new BuiltInTexture ("JUCE logo", BinaryData::juce_icon_png, BinaryData::juce_icon_pngSize));
|
|
textures.add (new DynamicTexture());
|
|
|
|
addAndMakeVisible (textureBox);
|
|
textureBox.addListener (this);
|
|
updateTexturesList();
|
|
|
|
addAndMakeVisible (presetBox);
|
|
presetBox.addListener (this);
|
|
|
|
Array<ShaderPreset> presets (getPresets());
|
|
StringArray presetNames;
|
|
|
|
for (int i = 0; i < presets.size(); ++i)
|
|
presetBox.addItem (presets[i].name, i + 1);
|
|
|
|
addAndMakeVisible (presetLabel);
|
|
presetLabel.setText ("Shader Preset:", dontSendNotification);
|
|
presetLabel.attachToComponent (&presetBox, true);
|
|
|
|
addAndMakeVisible (textureLabel);
|
|
textureLabel.setText ("Texture:", dontSendNotification);
|
|
textureLabel.attachToComponent (&textureBox, true);
|
|
}
|
|
|
|
void initialise()
|
|
{
|
|
showBackgroundToggle.setToggleState (false, sendNotification);
|
|
textureBox.setSelectedItemIndex (0);
|
|
presetBox.setSelectedItemIndex (0);
|
|
speedSlider.setValue (0.01);
|
|
sizeSlider.setValue (0.5);
|
|
}
|
|
|
|
void resized() override
|
|
{
|
|
Rectangle<int> area (getLocalBounds().reduced (4));
|
|
|
|
Rectangle<int> top (area.removeFromTop (75));
|
|
|
|
Rectangle<int> sliders (top.removeFromRight (area.getWidth() / 2));
|
|
showBackgroundToggle.setBounds (sliders.removeFromBottom (25));
|
|
speedSlider.setBounds (sliders.removeFromBottom (25));
|
|
sizeSlider.setBounds (sliders.removeFromBottom (25));
|
|
|
|
top.removeFromRight (70);
|
|
statusLabel.setBounds (top);
|
|
|
|
Rectangle<int> shaderArea (area.removeFromBottom (area.getHeight() / 2));
|
|
|
|
Rectangle<int> presets (shaderArea.removeFromTop (25));
|
|
presets.removeFromLeft (100);
|
|
presetBox.setBounds (presets.removeFromLeft (150));
|
|
presets.removeFromLeft (100);
|
|
textureBox.setBounds (presets);
|
|
|
|
shaderArea.removeFromTop (4);
|
|
tabbedComp.setBounds (shaderArea);
|
|
}
|
|
|
|
void mouseDown (const MouseEvent& e) override
|
|
{
|
|
demo.draggableOrientation.mouseDown (e.getPosition());
|
|
}
|
|
|
|
void mouseDrag (const MouseEvent& e) override
|
|
{
|
|
demo.draggableOrientation.mouseDrag (e.getPosition());
|
|
}
|
|
|
|
void mouseWheelMove (const MouseEvent&, const MouseWheelDetails& d) override
|
|
{
|
|
sizeSlider.setValue (sizeSlider.getValue() + d.deltaY);
|
|
}
|
|
|
|
void mouseMagnify (const MouseEvent&, float magnifyAmmount) override
|
|
{
|
|
sizeSlider.setValue (sizeSlider.getValue() + magnifyAmmount - 1.0f);
|
|
}
|
|
|
|
void selectPreset (int preset)
|
|
{
|
|
const ShaderPreset& p = getPresets()[preset];
|
|
|
|
vertexDocument.replaceAllContent (p.vertexShader);
|
|
fragmentDocument.replaceAllContent (p.fragmentShader);
|
|
|
|
startTimer (1);
|
|
}
|
|
|
|
void selectTexture (int itemID)
|
|
{
|
|
#if JUCE_MODAL_LOOPS_PERMITTED
|
|
if (itemID == 1000)
|
|
{
|
|
static File lastLocation = File::getSpecialLocation (File::userPicturesDirectory);
|
|
|
|
FileChooser fc ("Choose an image to open...", lastLocation, "*.jpg;*.jpeg;*.png;*.gif");
|
|
|
|
if (fc.browseForFileToOpen())
|
|
{
|
|
lastLocation = fc.getResult();
|
|
|
|
textures.add (new TextureFromFile (fc.getResult()));
|
|
updateTexturesList();
|
|
|
|
textureBox.setSelectedId (textures.size());
|
|
}
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
if (DemoTexture* t = textures [itemID - 1])
|
|
demo.setTexture (t);
|
|
}
|
|
}
|
|
|
|
void updateTexturesList()
|
|
{
|
|
textureBox.clear();
|
|
|
|
for (int i = 0; i < textures.size(); ++i)
|
|
textureBox.addItem (textures.getUnchecked(i)->name, i + 1);
|
|
|
|
#if JUCE_MODAL_LOOPS_PERMITTED
|
|
textureBox.addSeparator();
|
|
textureBox.addItem ("Load from a file...", 1000);
|
|
#endif
|
|
}
|
|
|
|
Label statusLabel;
|
|
|
|
private:
|
|
void sliderValueChanged (Slider*) override
|
|
{
|
|
demo.scale = (float) sizeSlider.getValue();
|
|
demo.rotationSpeed = (float) speedSlider.getValue();
|
|
}
|
|
|
|
void buttonClicked (Button*)
|
|
{
|
|
demo.doBackgroundDrawing = showBackgroundToggle.getToggleState();
|
|
}
|
|
|
|
enum { shaderLinkDelay = 500 };
|
|
|
|
void codeDocumentTextInserted (const String& /*newText*/, int /*insertIndex*/) override
|
|
{
|
|
startTimer (shaderLinkDelay);
|
|
}
|
|
|
|
void codeDocumentTextDeleted (int /*startIndex*/, int /*endIndex*/) override
|
|
{
|
|
startTimer (shaderLinkDelay);
|
|
}
|
|
|
|
void timerCallback() override
|
|
{
|
|
stopTimer();
|
|
demo.setShaderProgram (vertexDocument.getAllContent(),
|
|
fragmentDocument.getAllContent());
|
|
}
|
|
|
|
void comboBoxChanged (ComboBox* box) override
|
|
{
|
|
if (box == &presetBox)
|
|
selectPreset (presetBox.getSelectedItemIndex());
|
|
else if (box == &textureBox)
|
|
selectTexture (textureBox.getSelectedId());
|
|
}
|
|
|
|
OpenGLDemo& demo;
|
|
|
|
Label speedLabel, zoomLabel;
|
|
|
|
CodeDocument vertexDocument, fragmentDocument;
|
|
CodeEditorComponent vertexEditorComp, fragmentEditorComp;
|
|
TabbedComponent tabbedComp;
|
|
|
|
ComboBox presetBox, textureBox;
|
|
Label presetLabel, textureLabel;
|
|
|
|
Slider speedSlider, sizeSlider;
|
|
ToggleButton showBackgroundToggle;
|
|
|
|
OwnedArray<DemoTexture> textures;
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DemoControlsOverlay)
|
|
};
|
|
|
|
//==============================================================================
|
|
/** This is the main demo component - the GL context gets attached to it, and
|
|
it implements the OpenGLRenderer callback so that it can do real GL work.
|
|
*/
|
|
class OpenGLDemo : public Component,
|
|
private OpenGLRenderer
|
|
{
|
|
public:
|
|
OpenGLDemo()
|
|
: doBackgroundDrawing (false),
|
|
scale (0.5f), rotationSpeed (0.0f), rotation (0.0f),
|
|
textureToUse (nullptr)
|
|
{
|
|
MainAppWindow::getMainAppWindow()->setRenderingEngine (0);
|
|
|
|
setOpaque (true);
|
|
addAndMakeVisible (controlsOverlay = new DemoControlsOverlay (*this));
|
|
|
|
openGLContext.setRenderer (this);
|
|
openGLContext.attachTo (*this);
|
|
openGLContext.setContinuousRepainting (true);
|
|
|
|
controlsOverlay->initialise();
|
|
}
|
|
|
|
~OpenGLDemo()
|
|
{
|
|
openGLContext.detach();
|
|
}
|
|
|
|
void newOpenGLContextCreated() override
|
|
{
|
|
// nothing to do in this case - we'll initialise our shaders + textures
|
|
// on demand, during the render callback.
|
|
}
|
|
|
|
void openGLContextClosing() override
|
|
{
|
|
// When the context is about to close, you must use this callback to delete
|
|
// any GPU resources while the context is still current.
|
|
shape = nullptr;
|
|
shader = nullptr;
|
|
attributes = nullptr;
|
|
uniforms = nullptr;
|
|
texture.release();
|
|
}
|
|
|
|
// This is a virtual method in OpenGLRenderer, and is called when it's time
|
|
// to do your GL rendering.
|
|
void renderOpenGL() override
|
|
{
|
|
jassert (OpenGLHelpers::isContextActive());
|
|
|
|
const float desktopScale = (float) openGLContext.getRenderingScale();
|
|
OpenGLHelpers::clear (Colours::lightblue);
|
|
|
|
if (textureToUse != nullptr)
|
|
if (! textureToUse->applyTo (texture))
|
|
textureToUse = nullptr;
|
|
|
|
// First draw our background graphics to demonstrate the OpenGLGraphicsContext class
|
|
if (doBackgroundDrawing)
|
|
drawBackground2DStuff (desktopScale);
|
|
|
|
updateShader(); // Check whether we need to compile a new shader
|
|
|
|
if (shader == nullptr)
|
|
return;
|
|
|
|
// Having used the juce 2D renderer, it will have messed-up a whole load of GL state, so
|
|
// we need to initialise some important settings before doing our normal GL 3D drawing..
|
|
glEnable (GL_DEPTH_TEST);
|
|
glDepthFunc (GL_LESS);
|
|
glEnable (GL_BLEND);
|
|
glBlendFunc (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
|
openGLContext.extensions.glActiveTexture (GL_TEXTURE0);
|
|
glEnable (GL_TEXTURE_2D);
|
|
|
|
glViewport (0, 0, roundToInt (desktopScale * getWidth()), roundToInt (desktopScale * getHeight()));
|
|
|
|
texture.bind();
|
|
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
|
|
glTexParameteri (GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
|
|
|
|
shader->use();
|
|
|
|
if (uniforms->projectionMatrix != nullptr)
|
|
uniforms->projectionMatrix->setMatrix4 (getProjectionMatrix().mat, 1, false);
|
|
|
|
if (uniforms->viewMatrix != nullptr)
|
|
uniforms->viewMatrix->setMatrix4 (getViewMatrix().mat, 1, false);
|
|
|
|
if (uniforms->texture != nullptr)
|
|
uniforms->texture->set ((GLint) 0);
|
|
|
|
if (uniforms->lightPosition != nullptr)
|
|
uniforms->lightPosition->set (-15.0f, 10.0f, 15.0f, 0.0f);
|
|
|
|
if (uniforms->bouncingNumber != nullptr)
|
|
uniforms->bouncingNumber->set (bouncingNumber.getValue());
|
|
|
|
shape->draw (openGLContext, *attributes);
|
|
|
|
// Reset the element buffers so child Components draw correctly
|
|
openGLContext.extensions.glBindBuffer (GL_ARRAY_BUFFER, 0);
|
|
openGLContext.extensions.glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, 0);
|
|
|
|
if (! controlsOverlay->isMouseButtonDown())
|
|
rotation += (float) rotationSpeed;
|
|
}
|
|
|
|
Matrix3D<float> getProjectionMatrix() const
|
|
{
|
|
float w = 1.0f / (scale + 0.1f);
|
|
float h = w * getLocalBounds().toFloat().getAspectRatio (false);
|
|
return Matrix3D<float>::fromFrustum (-w, w, -h, h, 4.0f, 30.0f);
|
|
}
|
|
|
|
Matrix3D<float> getViewMatrix() const
|
|
{
|
|
Matrix3D<float> viewMatrix (Vector3D<float> (0.0f, 1.0f, -10.0f));
|
|
|
|
viewMatrix *= draggableOrientation.getRotationMatrix();
|
|
|
|
Matrix3D<float> rotationMatrix = viewMatrix.rotated (Vector3D<float> (rotation, rotation, -0.3f));
|
|
|
|
return viewMatrix * rotationMatrix;
|
|
}
|
|
|
|
void setTexture (DemoTexture* t)
|
|
{
|
|
textureToUse = t;
|
|
}
|
|
|
|
void setShaderProgram (const String& vertexShader, const String& fragmentShader)
|
|
{
|
|
newVertexShader = vertexShader;
|
|
newFragmentShader = fragmentShader;
|
|
}
|
|
|
|
void paint (Graphics&) {}
|
|
|
|
void resized() override
|
|
{
|
|
controlsOverlay->setBounds (getLocalBounds());
|
|
draggableOrientation.setViewport (getLocalBounds());
|
|
}
|
|
|
|
Draggable3DOrientation draggableOrientation;
|
|
bool doBackgroundDrawing;
|
|
float scale, rotationSpeed;
|
|
BouncingNumber bouncingNumber;
|
|
|
|
private:
|
|
void drawBackground2DStuff (float desktopScale)
|
|
{
|
|
// Create an OpenGLGraphicsContext that will draw into this GL window..
|
|
ScopedPointer<LowLevelGraphicsContext> glRenderer (createOpenGLGraphicsContext (openGLContext,
|
|
roundToInt (desktopScale * getWidth()),
|
|
roundToInt (desktopScale * getHeight())));
|
|
|
|
if (glRenderer != nullptr)
|
|
{
|
|
Graphics g (*glRenderer);
|
|
g.addTransform (AffineTransform::scale (desktopScale));
|
|
|
|
for (int i = 0; i < numElementsInArray (stars); ++i)
|
|
{
|
|
float size = 0.25f;
|
|
|
|
// This stuff just creates a spinning star shape and fills it..
|
|
Path p;
|
|
p.addStar (Point<float> (getWidth() * stars[i].x.getValue(),
|
|
getHeight() * stars[i].y.getValue()), 7,
|
|
getHeight() * size * 0.5f,
|
|
getHeight() * size,
|
|
stars[i].angle.getValue());
|
|
|
|
float hue = stars[i].hue.getValue();
|
|
|
|
g.setGradientFill (ColourGradient (Colours::green.withRotatedHue (hue).withAlpha (0.8f),
|
|
0, 0,
|
|
Colours::red.withRotatedHue (hue).withAlpha (0.5f),
|
|
0, (float) getHeight(), false));
|
|
g.fillPath (p);
|
|
}
|
|
}
|
|
}
|
|
|
|
OpenGLContext openGLContext;
|
|
|
|
ScopedPointer<DemoControlsOverlay> controlsOverlay;
|
|
|
|
float rotation;
|
|
|
|
ScopedPointer<OpenGLShaderProgram> shader;
|
|
ScopedPointer<Shape> shape;
|
|
ScopedPointer<Attributes> attributes;
|
|
ScopedPointer<Uniforms> uniforms;
|
|
|
|
OpenGLTexture texture;
|
|
DemoTexture* textureToUse;
|
|
|
|
String newVertexShader, newFragmentShader;
|
|
|
|
struct BackgroundStar
|
|
{
|
|
SlowerBouncingNumber x, y, hue, angle;
|
|
};
|
|
|
|
BackgroundStar stars[3];
|
|
|
|
//==============================================================================
|
|
void updateShader()
|
|
{
|
|
if (newVertexShader.isNotEmpty() || newFragmentShader.isNotEmpty())
|
|
{
|
|
ScopedPointer<OpenGLShaderProgram> newShader (new OpenGLShaderProgram (openGLContext));
|
|
String statusText;
|
|
|
|
if (newShader->addVertexShader (newVertexShader)
|
|
&& newShader->addFragmentShader (newFragmentShader)
|
|
&& newShader->link())
|
|
{
|
|
shape = nullptr;
|
|
attributes = nullptr;
|
|
uniforms = nullptr;
|
|
|
|
shader = newShader;
|
|
shader->use();
|
|
|
|
shape = new Shape (openGLContext);
|
|
attributes = new Attributes (openGLContext, *shader);
|
|
uniforms = new Uniforms (openGLContext, *shader);
|
|
|
|
#if ! JUCE_OPENGL_ES
|
|
statusText = "GLSL: v" + String (OpenGLShaderProgram::getLanguageVersion(), 2);
|
|
#else
|
|
statusText = "GLSL ES";
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
statusText = newShader->getLastError();
|
|
}
|
|
|
|
controlsOverlay->statusLabel.setText (statusText, dontSendNotification);
|
|
|
|
|
|
newVertexShader = String::empty;
|
|
newFragmentShader = String::empty;
|
|
}
|
|
}
|
|
|
|
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OpenGLDemo)
|
|
};
|
|
|
|
//==============================================================================
|
|
struct ShaderPreset
|
|
{
|
|
const char* name;
|
|
const char* vertexShader;
|
|
const char* fragmentShader;
|
|
};
|
|
|
|
static Array<ShaderPreset> getPresets()
|
|
{
|
|
#define SHADER_DEMO_HEADER \
|
|
"/* This is a live OpenGL Shader demo.\n" \
|
|
" Edit the shader program below and it will be \n" \
|
|
" compiled and applied to the model above!\n" \
|
|
"*/\n\n"
|
|
|
|
ShaderPreset presets[] =
|
|
{
|
|
{
|
|
"Texture + Lighting",
|
|
|
|
SHADER_DEMO_HEADER
|
|
"attribute vec4 position;\n"
|
|
"attribute vec4 normal;\n"
|
|
"attribute vec4 sourceColour;\n"
|
|
"attribute vec2 texureCoordIn;\n"
|
|
"\n"
|
|
"uniform mat4 projectionMatrix;\n"
|
|
"uniform mat4 viewMatrix;\n"
|
|
"uniform vec4 lightPosition;\n"
|
|
"\n"
|
|
"varying vec4 destinationColour;\n"
|
|
"varying vec2 textureCoordOut;\n"
|
|
"varying float lightIntensity;\n"
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" destinationColour = sourceColour;\n"
|
|
" textureCoordOut = texureCoordIn;\n"
|
|
"\n"
|
|
" vec4 light = viewMatrix * lightPosition;\n"
|
|
" lightIntensity = dot (light, normal);\n"
|
|
"\n"
|
|
" gl_Position = projectionMatrix * viewMatrix * position;\n"
|
|
"}\n",
|
|
|
|
SHADER_DEMO_HEADER
|
|
#if JUCE_OPENGL_ES
|
|
"varying lowp vec4 destinationColour;\n"
|
|
"varying lowp vec2 textureCoordOut;\n"
|
|
"varying highp float lightIntensity;\n"
|
|
#else
|
|
"varying vec4 destinationColour;\n"
|
|
"varying vec2 textureCoordOut;\n"
|
|
"varying float lightIntensity;\n"
|
|
#endif
|
|
"\n"
|
|
"uniform sampler2D texture;\n"
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
#if JUCE_OPENGL_ES
|
|
" highp float l = max (0.3, lightIntensity * 0.3);\n"
|
|
" highp vec4 colour = vec4 (l, l, l, 1.0);\n"
|
|
#else
|
|
" float l = max (0.3, lightIntensity * 0.3);\n"
|
|
" vec4 colour = vec4 (l, l, l, 1.0);\n"
|
|
#endif
|
|
" gl_FragColor = colour * texture2D (texture, textureCoordOut);\n"
|
|
"}\n"
|
|
},
|
|
|
|
{
|
|
"Textured",
|
|
|
|
SHADER_DEMO_HEADER
|
|
"attribute vec4 position;\n"
|
|
"attribute vec4 sourceColour;\n"
|
|
"attribute vec2 texureCoordIn;\n"
|
|
"\n"
|
|
"uniform mat4 projectionMatrix;\n"
|
|
"uniform mat4 viewMatrix;\n"
|
|
"\n"
|
|
"varying vec4 destinationColour;\n"
|
|
"varying vec2 textureCoordOut;\n"
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" destinationColour = sourceColour;\n"
|
|
" textureCoordOut = texureCoordIn;\n"
|
|
" gl_Position = projectionMatrix * viewMatrix * position;\n"
|
|
"}\n",
|
|
|
|
SHADER_DEMO_HEADER
|
|
#if JUCE_OPENGL_ES
|
|
"varying lowp vec4 destinationColour;\n"
|
|
"varying lowp vec2 textureCoordOut;\n"
|
|
#else
|
|
"varying vec4 destinationColour;\n"
|
|
"varying vec2 textureCoordOut;\n"
|
|
#endif
|
|
"\n"
|
|
"uniform sampler2D texture;\n"
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" gl_FragColor = texture2D (texture, textureCoordOut);\n"
|
|
"}\n"
|
|
},
|
|
|
|
{
|
|
"Flat Colour",
|
|
|
|
SHADER_DEMO_HEADER
|
|
"attribute vec4 position;\n"
|
|
"attribute vec4 sourceColour;\n"
|
|
"attribute vec2 texureCoordIn;\n"
|
|
"\n"
|
|
"uniform mat4 projectionMatrix;\n"
|
|
"uniform mat4 viewMatrix;\n"
|
|
"\n"
|
|
"varying vec4 destinationColour;\n"
|
|
"varying vec2 textureCoordOut;\n"
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" destinationColour = sourceColour;\n"
|
|
" textureCoordOut = texureCoordIn;\n"
|
|
" gl_Position = projectionMatrix * viewMatrix * position;\n"
|
|
"}\n",
|
|
|
|
SHADER_DEMO_HEADER
|
|
#if JUCE_OPENGL_ES
|
|
"varying lowp vec4 destinationColour;\n"
|
|
"varying lowp vec2 textureCoordOut;\n"
|
|
#else
|
|
"varying vec4 destinationColour;\n"
|
|
"varying vec2 textureCoordOut;\n"
|
|
#endif
|
|
"\n"
|
|
"uniform sampler2D texture;\n"
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" gl_FragColor = destinationColour;\n"
|
|
"}\n"
|
|
},
|
|
|
|
{
|
|
"Rainbow",
|
|
|
|
SHADER_DEMO_HEADER
|
|
"attribute vec4 position;\n"
|
|
"attribute vec4 sourceColour;\n"
|
|
"attribute vec2 texureCoordIn;\n"
|
|
"\n"
|
|
"uniform mat4 projectionMatrix;\n"
|
|
"uniform mat4 viewMatrix;\n"
|
|
"\n"
|
|
"varying vec4 destinationColour;\n"
|
|
"varying vec2 textureCoordOut;\n"
|
|
"\n"
|
|
"varying float xPos;\n"
|
|
"varying float yPos;\n"
|
|
"varying float zPos;\n"
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" vec4 v = vec4 (position);\n"
|
|
" xPos = clamp (v.x, 0.0, 1.0);\n"
|
|
" yPos = clamp (v.y, 0.0, 1.0);\n"
|
|
" zPos = clamp (v.z, 0.0, 1.0);\n"
|
|
" gl_Position = projectionMatrix * viewMatrix * position;\n"
|
|
"}",
|
|
|
|
SHADER_DEMO_HEADER
|
|
#if JUCE_OPENGL_ES
|
|
"varying lowp vec4 destinationColour;\n"
|
|
"varying lowp vec2 textureCoordOut;\n"
|
|
"varying lowp float xPos;\n"
|
|
"varying lowp float yPos;\n"
|
|
"varying lowp float zPos;\n"
|
|
#else
|
|
"varying vec4 destinationColour;\n"
|
|
"varying vec2 textureCoordOut;\n"
|
|
"varying float xPos;\n"
|
|
"varying float yPos;\n"
|
|
"varying float zPos;\n"
|
|
#endif
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" gl_FragColor = vec4 (xPos, yPos, zPos, 1.0);\n"
|
|
"}"
|
|
},
|
|
|
|
{
|
|
"Changing Colour",
|
|
|
|
SHADER_DEMO_HEADER
|
|
"attribute vec4 position;\n"
|
|
"attribute vec2 texureCoordIn;\n"
|
|
"\n"
|
|
"uniform mat4 projectionMatrix;\n"
|
|
"uniform mat4 viewMatrix;\n"
|
|
"\n"
|
|
"varying vec2 textureCoordOut;\n"
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" textureCoordOut = texureCoordIn;\n"
|
|
" gl_Position = projectionMatrix * viewMatrix * position;\n"
|
|
"}\n",
|
|
|
|
SHADER_DEMO_HEADER
|
|
"#define PI 3.1415926535897932384626433832795\n"
|
|
"\n"
|
|
#if JUCE_OPENGL_ES
|
|
"precision mediump float;\n"
|
|
"varying lowp vec2 textureCoordOut;\n"
|
|
#else
|
|
"varying vec2 textureCoordOut;\n"
|
|
#endif
|
|
"uniform float bouncingNumber;\n"
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" float b = bouncingNumber;\n"
|
|
" float n = b * PI * 2.0;\n"
|
|
" float sn = (sin (n * textureCoordOut.x) * 0.5) + 0.5;\n"
|
|
" float cn = (sin (n * textureCoordOut.y) * 0.5) + 0.5;\n"
|
|
"\n"
|
|
" vec4 col = vec4 (b, sn, cn, 1.0);\n"
|
|
" gl_FragColor = col;\n"
|
|
"}\n"
|
|
},
|
|
|
|
{
|
|
"Simple Light",
|
|
|
|
SHADER_DEMO_HEADER
|
|
"attribute vec4 position;\n"
|
|
"attribute vec4 normal;\n"
|
|
"\n"
|
|
"uniform mat4 projectionMatrix;\n"
|
|
"uniform mat4 viewMatrix;\n"
|
|
"uniform vec4 lightPosition;\n"
|
|
"\n"
|
|
"varying float lightIntensity;\n"
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" vec4 light = viewMatrix * lightPosition;\n"
|
|
" lightIntensity = dot (light, normal);\n"
|
|
"\n"
|
|
" gl_Position = projectionMatrix * viewMatrix * position;\n"
|
|
"}\n",
|
|
|
|
SHADER_DEMO_HEADER
|
|
#if JUCE_OPENGL_ES
|
|
"varying highp float lightIntensity;\n"
|
|
#else
|
|
"varying float lightIntensity;\n"
|
|
#endif
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
#if JUCE_OPENGL_ES
|
|
" highp float l = lightIntensity * 0.25;\n"
|
|
" highp vec4 colour = vec4 (l, l, l, 1.0);\n"
|
|
#else
|
|
" float l = lightIntensity * 0.25;\n"
|
|
" vec4 colour = vec4 (l, l, l, 1.0);\n"
|
|
#endif
|
|
"\n"
|
|
" gl_FragColor = colour;\n"
|
|
"}\n"
|
|
},
|
|
|
|
{
|
|
"Flattened",
|
|
|
|
SHADER_DEMO_HEADER
|
|
"attribute vec4 position;\n"
|
|
"attribute vec4 normal;\n"
|
|
"\n"
|
|
"uniform mat4 projectionMatrix;\n"
|
|
"uniform mat4 viewMatrix;\n"
|
|
"uniform vec4 lightPosition;\n"
|
|
"\n"
|
|
"varying float lightIntensity;\n"
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" vec4 light = viewMatrix * lightPosition;\n"
|
|
" lightIntensity = dot (light, normal);\n"
|
|
"\n"
|
|
" vec4 v = vec4 (position);\n"
|
|
" v.z = v.z * 0.1;\n"
|
|
"\n"
|
|
" gl_Position = projectionMatrix * viewMatrix * v;\n"
|
|
"}\n",
|
|
|
|
SHADER_DEMO_HEADER
|
|
#if JUCE_OPENGL_ES
|
|
"varying highp float lightIntensity;\n"
|
|
#else
|
|
"varying float lightIntensity;\n"
|
|
#endif
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
#if JUCE_OPENGL_ES
|
|
" highp float l = lightIntensity * 0.25;\n"
|
|
" highp vec4 colour = vec4 (l, l, l, 1.0);\n"
|
|
#else
|
|
" float l = lightIntensity * 0.25;\n"
|
|
" vec4 colour = vec4 (l, l, l, 1.0);\n"
|
|
#endif
|
|
"\n"
|
|
" gl_FragColor = colour;\n"
|
|
"}\n"
|
|
},
|
|
|
|
{
|
|
"Toon Shader",
|
|
|
|
SHADER_DEMO_HEADER
|
|
"attribute vec4 position;\n"
|
|
"attribute vec4 normal;\n"
|
|
"\n"
|
|
"uniform mat4 projectionMatrix;\n"
|
|
"uniform mat4 viewMatrix;\n"
|
|
"uniform vec4 lightPosition;\n"
|
|
"\n"
|
|
"varying float lightIntensity;\n"
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
" vec4 light = viewMatrix * lightPosition;\n"
|
|
" lightIntensity = dot (light, normal);\n"
|
|
"\n"
|
|
" gl_Position = projectionMatrix * viewMatrix * position;\n"
|
|
"}\n",
|
|
|
|
SHADER_DEMO_HEADER
|
|
#if JUCE_OPENGL_ES
|
|
"varying highp float lightIntensity;\n"
|
|
#else
|
|
"varying float lightIntensity;\n"
|
|
#endif
|
|
"\n"
|
|
"void main()\n"
|
|
"{\n"
|
|
#if JUCE_OPENGL_ES
|
|
" highp float intensity = lightIntensity * 0.5;\n"
|
|
" highp vec4 colour;\n"
|
|
#else
|
|
" float intensity = lightIntensity * 0.5;\n"
|
|
" vec4 colour;\n"
|
|
#endif
|
|
"\n"
|
|
" if (intensity > 0.95)\n"
|
|
" colour = vec4 (1.0, 0.5, 0.5, 1.0);\n"
|
|
" else if (intensity > 0.5)\n"
|
|
" colour = vec4 (0.6, 0.3, 0.3, 1.0);\n"
|
|
" else if (intensity > 0.25)\n"
|
|
" colour = vec4 (0.4, 0.2, 0.2, 1.0);\n"
|
|
" else\n"
|
|
" colour = vec4 (0.2, 0.1, 0.1, 1.0);\n"
|
|
"\n"
|
|
" gl_FragColor = colour;\n"
|
|
"}\n"
|
|
}
|
|
};
|
|
|
|
return Array<ShaderPreset> (presets, numElementsInArray (presets));
|
|
}
|
|
};
|
|
|
|
// This static object will register this demo type in a global list of demos..
|
|
static JuceDemoType<OpenGLDemoClasses::OpenGLDemo> demo ("20 Graphics: OpenGL");
|
|
|
|
#endif
|