example program for using a c++ program to stream a webcam in browser
This commit is contained in:
parent
52ef535eac
commit
f63cb2bfb2
|
@ -71,6 +71,9 @@ Test collision detection between a 2D sprite and other 2D sprites and boxes. Per
|
||||||
|
|
||||||
Map an image from a rectangle to a circle or from a circle to a rectangle using a shader program. Based on the elliptical grid mapping equations at http://squircular.blogspot.com/2015/09/mapping-circle-to-square.html
|
Map an image from a rectangle to a circle or from a circle to a rectangle using a shader program. Based on the elliptical grid mapping equations at http://squircular.blogspot.com/2015/09/mapping-circle-to-square.html
|
||||||
|
|
||||||
|
### browser webcam
|
||||||
|
|
||||||
|
An example for using a C++ program to display a webcam stream in the browser using Emscripten to translate the code from C++ to WebAssembly. Get the frame pixel data from a canvas element, read it into a SPACEBOX object, write the pixel data to an OpenGL texture, and use Emscripten to display the video.
|
||||||
|
|
||||||
Other libraries
|
Other libraries
|
||||||
---------------
|
---------------
|
||||||
|
@ -126,7 +129,7 @@ To build a WASM library that can be used to build an Emscripten version of a SPA
|
||||||
$ find . -iname *.a
|
$ find . -iname *.a
|
||||||
./zbar/.libs/libzbar.a
|
./zbar/.libs/libzbar.a
|
||||||
|
|
||||||
There is a detailed tutorial on using Zbar with Web Assembly at https://barkeywolf.consulting/posts/barcode-scanner-webassembly/
|
There is a detailed tutorial on using Zbar with WebAssembly at https://barkeywolf.consulting/posts/barcode-scanner-webassembly/
|
||||||
|
|
||||||
Font
|
Font
|
||||||
----
|
----
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
# Browser Webcam Test
|
||||||
|
|
||||||
|
#######################
|
||||||
|
# Location parameters #
|
||||||
|
#######################
|
||||||
|
|
||||||
|
# Location of project specific source files
|
||||||
|
SRC_DIR := ./
|
||||||
|
|
||||||
|
# Locations of [SPACEBOX] source and dependencies required to be compiled from source. These locations are configured to match the
|
||||||
|
# structure of the [SPACEBOX] repository but can be modified as necessary.
|
||||||
|
SB_DIR := ../../
|
||||||
|
SB_SRC_DIR := $(SB_DIR)src/
|
||||||
|
SB_LIB_DIR := $(SB_DIR)lib/
|
||||||
|
SDLGFX2_DIR := $(SB_LIB_DIR)sdl2-gfx/
|
||||||
|
GLEW_DIR := $(SB_LIB_DIR)glew/
|
||||||
|
|
||||||
|
# C and C++ compiler commands
|
||||||
|
CC := clang
|
||||||
|
CXX := clang++
|
||||||
|
|
||||||
|
# Location of SDL config program
|
||||||
|
SDLCONFIG := $(HOME)/local/sdl/bin/sdl2-config
|
||||||
|
|
||||||
|
# Edit to point to the location of BPmono.ttf
|
||||||
|
CREATE_FONT_SYMLINK := ln -nsf $(SB_DIR)"BPmono.ttf" .
|
||||||
|
|
||||||
|
#############################
|
||||||
|
# Based on above parameters #
|
||||||
|
#############################
|
||||||
|
|
||||||
|
SDL_CFLAGS = $(shell $(SDLCONFIG) --cflags)
|
||||||
|
SDL_LFLAGS := $(shell $(SDLCONFIG) --libs)
|
||||||
|
SB_H_FILES := $(wildcard $(addprefix $(SB_SRC_DIR),*.hpp))
|
||||||
|
SB_O_FILES := $(filter-out $(addprefix $(SB_SRC_DIR),filesystem.o),$(SB_H_FILES:.hpp=.o))
|
||||||
|
SRC_H_FILES := $(wildcard $(addprefix $(SRC_DIR),*.hpp))
|
||||||
|
SRC_O_FILES := browser_webcam_test.o $(SRC_H_FILES:.hpp=.o)
|
||||||
|
|
||||||
|
#####################################################################
|
||||||
|
# Targets for building [SPACE BOX], dependencies and project source #
|
||||||
|
#####################################################################
|
||||||
|
|
||||||
|
$(SDLGFX2_DIR)%.o: $(SDLGFX2_DIR)%.c $(SDLGFX2_DIR)%.h
|
||||||
|
$(GLEW_DIR)%.o: $(GLEW_DIR)%.c $(GLEW_DIR)%.h
|
||||||
|
$(CC) $< $(CFLAGS) -c -o $@
|
||||||
|
|
||||||
|
$(SB_SRC_DIR)extension.o : $(addprefix $(SB_SRC_DIR),Box.hpp Segment.hpp Color.hpp filesystem.hpp Pixels.hpp Log.hpp)
|
||||||
|
$(SB_SRC_DIR)Node.o : $(addprefix $(SB_SRC_DIR),Game.hpp Configuration.hpp Delegate.hpp Display.hpp Input.hpp Box.hpp Audio.hpp Log.hpp)
|
||||||
|
$(SB_SRC_DIR)Sprite.o : $(addprefix $(SB_SRC_DIR),Node.hpp Game.hpp Box.hpp Animation.hpp Color.hpp extension.hpp Pixels.hpp Log.hpp)
|
||||||
|
$(SB_SRC_DIR)Game.o : $(addprefix $(SB_SRC_DIR),extension.hpp Node.hpp Sprite.hpp Recorder.hpp Input.hpp Configuration.hpp \
|
||||||
|
Delegate.hpp Audio.hpp Log.hpp)
|
||||||
|
$(SB_SRC_DIR)Animation.o : $(addprefix $(SB_SRC_DIR),Node.hpp Timer.hpp)
|
||||||
|
$(SB_SRC_DIR)Recorder.o : $(addprefix $(SB_SRC_DIR),Node.hpp Game.hpp Configuration.hpp Delegate.hpp Animation.hpp extension.hpp)
|
||||||
|
$(SB_SRC_DIR)Input.o : $(addprefix $(SB_SRC_DIR),Node.hpp Animation.hpp Configuration.hpp Delegate.hpp)
|
||||||
|
$(SB_SRC_DIR)Configuration.o : $(addprefix $(SB_SRC_DIR),Node.hpp Animation.hpp Log.hpp)
|
||||||
|
$(SB_SRC_DIR)Delegate.o : $(addprefix $(SB_SRC_DIR),Node.hpp Game.hpp Input.hpp)
|
||||||
|
$(SB_SRC_DIR)Display.o : $(addprefix $(SB_SRC_DIR),Node.hpp Game.hpp Box.hpp Configuration.hpp Delegate.hpp Log.hpp)
|
||||||
|
$(SB_SRC_DIR)Box.o : $(addprefix $(SB_SRC_DIR),extension.hpp Segment.hpp)
|
||||||
|
$(SB_SRC_DIR)Segment.o : $(addprefix $(SB_SRC_DIR),extension.hpp Box.hpp)
|
||||||
|
$(SB_SRC_DIR)Pixels.o : $(addprefix $(SB_SRC_DIR),Box.hpp extension.hpp Log.hpp utility.hpp)
|
||||||
|
$(SB_SRC_DIR)Audio.o : $(addprefix $(SB_SRC_DIR),Node.hpp Display.hpp Configuration.hpp Box.hpp filesystem.hpp extension.hpp)
|
||||||
|
$(SB_SRC_DIR)GLObject.o : $(addprefix $(SB_SRC_DIR),Log.hpp)
|
||||||
|
$(SB_SRC_DIR)Texture.o : $(addprefix $(SB_SRC_DIR),GLObject.hpp filesystem.hpp Log.hpp)
|
||||||
|
$(SB_SRC_DIR)VBO.o : $(addprefix $(SB_SRC_DIR),Log.hpp GLObject.hpp Attributes.hpp extension.hpp)
|
||||||
|
$(SB_SRC_DIR)Attributes.o : $(addprefix $(SB_SRC_DIR),Log.hpp extension.hpp)
|
||||||
|
$(SRC_DIR)Model.o : $(addprefix $(SB_SRC_DIR),extension.hpp Attributes.hpp Texture.hpp utility.hpp)
|
||||||
|
$(SRC_DIR)*.o : $(SRC_H_FILES) $(SB_H_FILES)
|
||||||
|
%.o : %.cpp %.hpp
|
||||||
|
$(CXX) $(CXXFLAGS) $< -c -o $@
|
||||||
|
|
||||||
|
#############
|
||||||
|
# Web build #
|
||||||
|
#############
|
||||||
|
|
||||||
|
# Use Emscripten to output JavaScript and an HTML index page for running in the browser
|
||||||
|
|
||||||
|
EMSCRIPTENHOME = $(HOME)/ext/software/emsdk/upstream/emscripten
|
||||||
|
EMSCRIPTEN_CFLAGS = -O1 -Wall -s USE_SDL=2 -s USE_SDL_IMAGE=2 -s SDL2_IMAGE_FORMATS="['png', 'jpg']" -s USE_SDL_TTF=2 -s USE_SDL_MIXER=2 \
|
||||||
|
-I $(SB_LIB_DIR) -I $(SB_SRC_DIR)
|
||||||
|
EMSCRIPTEN_LFLAGS = -s MIN_WEBGL_VERSION=2 -s EXPORTED_FUNCTIONS="['_main']" -s ALLOW_MEMORY_GROWTH=1 -s FULL_ES3=1 \
|
||||||
|
-s LLD_REPORT_UNDEFINED $(wildcard $(addprefix $(HOME)/ext/software/opencv-4.6.0/build_wasm/lib/,*.a)) $(HOME)/ext/software/ZBar/zbar/.libs/libzbar.a \
|
||||||
|
--bind
|
||||||
|
EMSCRIPTEN_PRELOADS = --preload-file "BPmono.ttf"@/ --preload-file "shaders/"@/"shaders/" --preload-file "config.json"@/
|
||||||
|
|
||||||
|
emscripten : CC = $(EMSCRIPTENHOME)/emcc
|
||||||
|
emscripten : CXX = $(EMSCRIPTENHOME)/em++
|
||||||
|
emscripten : CFLAGS = $(EMSCRIPTEN_CFLAGS)
|
||||||
|
emscripten : CXXFLAGS = $(CFLAGS) --std=c++17
|
||||||
|
emscripten : $(addprefix $(SDLGFX2_DIR),SDL2_rotozoom.o SDL2_gfxPrimitives.o) $(SB_O_FILES) $(SRC_O_FILES)
|
||||||
|
$(CREATE_FONT_SYMLINK)
|
||||||
|
$(CXX) $^ $(CXXFLAGS) $(EMSCRIPTEN_LFLAGS) $(EMSCRIPTEN_PRELOADS) -o browser_webcam_test.js
|
||||||
|
|
||||||
|
#########################
|
||||||
|
# Clean up object files #
|
||||||
|
#########################
|
||||||
|
|
||||||
|
clean :
|
||||||
|
-find $(SRC_DIR) -iname "*.o" -delete
|
||||||
|
rm -f BPmono.ttf browser_webcam_test.data browser_webcam_test.js browser_webcam_test.wasm
|
||||||
|
|
||||||
|
clean-all : clean
|
||||||
|
-find $(SB_SRC_DIR) -iname "*.o" -delete
|
||||||
|
-find $(SB_LIB_DIR) -iname "*.o" -delete
|
|
@ -0,0 +1,166 @@
|
||||||
|
/* Browser Webcam Test - "Model.cpp"
|
||||||
|
*
|
||||||
|
* This is a class for creating an OpenGL model (basically a collection of vertices, optional texture, and optional transformation).
|
||||||
|
* It contains convenience functions for loading models into OpenGL.
|
||||||
|
*
|
||||||
|
* It is copied from the in-development game Gunkiss. It will eventually be merged into [SPACE BOX].
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "Model.hpp"
|
||||||
|
|
||||||
|
/* Default constructor for Model */
|
||||||
|
Model::Model() {};
|
||||||
|
|
||||||
|
/* Construct a Model, adding Attributes each already wrapped in a shared pointer. The attributes should
|
||||||
|
* be passed as a map with each key being a name and each value being a shared pointer to attributes. */
|
||||||
|
Model::Model(const std::map<std::string, std::shared_ptr<sb::Attributes>>& attributes_pack)
|
||||||
|
{
|
||||||
|
for (auto attributes : attributes_pack)
|
||||||
|
{
|
||||||
|
this->attributes(attributes.second, attributes.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Construct a Model, adding Attributes, which will each be wrapped in a shared pointer and stored in the
|
||||||
|
* created object. The attributes should be passed as a map with each key being a name and each value being
|
||||||
|
* an attributes object. */
|
||||||
|
Model::Model(const std::map<std::string, sb::Attributes>& attributes_pack)
|
||||||
|
{
|
||||||
|
for (auto attributes : attributes_pack)
|
||||||
|
{
|
||||||
|
this->attributes(attributes.second, attributes.first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Construct a new model object by passing a list of names which will be used to initialize
|
||||||
|
* empty attributes objects with the given names */
|
||||||
|
Model::Model(const std::initializer_list<std::string>& names)
|
||||||
|
{
|
||||||
|
for (const std::string& name : names)
|
||||||
|
{
|
||||||
|
this->attributes(sb::Attributes(), name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get the entire map of attributes, each wrapped in its shared pointer held by this object.
|
||||||
|
* Can be used to iterate through the attributes. */
|
||||||
|
std::map<std::string, std::shared_ptr<sb::Attributes>>& Model::attributes()
|
||||||
|
{
|
||||||
|
return model_attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get the attributes under name, wrapped in the shared pointer held by this object. This
|
||||||
|
* function uses the at method of std::map, so name must refer to attributes already
|
||||||
|
* stored in this model. Use this function to share ownership of the attributes or to gain
|
||||||
|
* access to the public interface of the attributes. */
|
||||||
|
std::shared_ptr<sb::Attributes>& Model::attributes(const std::string& name)
|
||||||
|
{
|
||||||
|
return attributes().at(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get the attributes under name, wrapped in the shared pointer held by this object. This
|
||||||
|
* function uses operator[] or std::map, so this can be used to add new attributes to the
|
||||||
|
* object if they are wrapped in a shared pointer. */
|
||||||
|
std::shared_ptr<sb::Attributes>& Model::operator[](const std::string& name)
|
||||||
|
{
|
||||||
|
auto element = attributes().find(name);
|
||||||
|
/* add an empty Attributes at name if it doesn't exist yet */
|
||||||
|
if (element == attributes().end())
|
||||||
|
{
|
||||||
|
attributes(sb::Attributes{}, name);
|
||||||
|
}
|
||||||
|
return attributes()[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assign name to attributes, copy and wrap in a shared pointer. The model can share
|
||||||
|
* ownership of the created attribute memory with callers that request it. */
|
||||||
|
void Model::attributes(const sb::Attributes& attributes, const std::string& name)
|
||||||
|
{
|
||||||
|
this->attributes(std::make_shared<sb::Attributes>(attributes), name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assign name to attributes and share ownership. */
|
||||||
|
void Model::attributes(const std::shared_ptr<sb::Attributes>& attributes, const std::string& name)
|
||||||
|
{
|
||||||
|
this->attributes()[name] = attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enable all attributes. */
|
||||||
|
void Model::enable()
|
||||||
|
{
|
||||||
|
for (const auto& attributes : this->attributes())
|
||||||
|
{
|
||||||
|
attributes.second->enable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disable all attributes. */
|
||||||
|
void Model::disable()
|
||||||
|
{
|
||||||
|
for (const auto& attributes : this->attributes())
|
||||||
|
{
|
||||||
|
attributes.second->disable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return a reference to the texture container. */
|
||||||
|
std::map<std::string, sb::Texture>& Model::textures()
|
||||||
|
{
|
||||||
|
return model_textures;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get the texture at name. This can be used to read the texture memory, share ownership of it, or
|
||||||
|
* anything else a Texture object can be used for with direct calls to GL functions. */
|
||||||
|
sb::Texture& Model::texture(const std::string& name)
|
||||||
|
{
|
||||||
|
return textures().at(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get the default texture. The default texture must have previously been set with the default key as
|
||||||
|
* the name, which can be done using Model::texture(sb::Texture). */
|
||||||
|
sb::Texture& Model::texture()
|
||||||
|
{
|
||||||
|
return texture(DEFAULT_TEXTURE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Assign name to texture and share ownership. */
|
||||||
|
void Model::texture(const sb::Texture& texture, const std::string& name)
|
||||||
|
{
|
||||||
|
textures()[name] = texture;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If no name is specified, use the default texture. This can be used to conveniently setup a model
|
||||||
|
* with only one texture. */
|
||||||
|
void Model::texture(const sb::Texture& texture)
|
||||||
|
{
|
||||||
|
this->texture(texture, DEFAULT_TEXTURE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get the model's transformation matrix. */
|
||||||
|
const glm::mat4& Model::transformation() const
|
||||||
|
{
|
||||||
|
return model_transformation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set the model's transformation matrix. */
|
||||||
|
void Model::transformation(const glm::mat4& transformation)
|
||||||
|
{
|
||||||
|
model_transformation = transformation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return the size in bytes of the sum of the attributes. */
|
||||||
|
std::size_t Model::size()
|
||||||
|
{
|
||||||
|
std::size_t sum = 0;
|
||||||
|
for (const auto& attributes : this->attributes())
|
||||||
|
{
|
||||||
|
sum += attributes.second->size();
|
||||||
|
}
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Return the transformation matrix. */
|
||||||
|
Model::operator glm::mat4() const
|
||||||
|
{
|
||||||
|
return model_transformation;
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
/* Browser Webcam Test - "Model.hpp"
|
||||||
|
*
|
||||||
|
* This is a class for creating an OpenGL model (basically a collection of vertices, optional texture, and optional transformation).
|
||||||
|
* It contains convenience functions for loading models into OpenGL.
|
||||||
|
*
|
||||||
|
* It is copied from the in-development game Gunkiss. It will eventually be merged into [SPACE BOX].
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef MODEL_H_
|
||||||
|
#define MODEL_H_
|
||||||
|
|
||||||
|
/* GL functions */
|
||||||
|
#if defined(__EMSCRIPTEN__)
|
||||||
|
#include <GL/glew.h>
|
||||||
|
#else
|
||||||
|
#include "glew/glew.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <iterator>
|
||||||
|
#include "glm/glm.hpp"
|
||||||
|
#include "Attributes.hpp"
|
||||||
|
#include "Texture.hpp"
|
||||||
|
|
||||||
|
class Model
|
||||||
|
{
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
inline static const std::string DEFAULT_TEXTURE_NAME = "default";
|
||||||
|
std::map<std::string, sb::Texture> model_textures;
|
||||||
|
std::map<std::string, std::shared_ptr<sb::Attributes>> model_attributes;
|
||||||
|
glm::mat4 model_transformation {1.0f};
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
Model();
|
||||||
|
Model(const std::map<std::string, std::shared_ptr<sb::Attributes>>&);
|
||||||
|
Model(const std::map<std::string, sb::Attributes>&);
|
||||||
|
Model(const std::initializer_list<std::string>&);
|
||||||
|
std::map<std::string, std::shared_ptr<sb::Attributes>>& attributes();
|
||||||
|
std::shared_ptr<sb::Attributes>& attributes(const std::string&);
|
||||||
|
void attributes(const sb::Attributes&, const std::string&);
|
||||||
|
void attributes(const std::shared_ptr<sb::Attributes>&, const std::string&);
|
||||||
|
std::shared_ptr<sb::Attributes>& operator[](const std::string&);
|
||||||
|
void enable();
|
||||||
|
void disable();
|
||||||
|
std::map<std::string, sb::Texture>& textures();
|
||||||
|
sb::Texture& texture(const std::string&);
|
||||||
|
sb::Texture& texture();
|
||||||
|
void texture(const sb::Texture&, const std::string&);
|
||||||
|
void texture(const sb::Texture&);
|
||||||
|
const glm::mat4& transformation() const;
|
||||||
|
void transformation(const glm::mat4&);
|
||||||
|
std::size_t size();
|
||||||
|
operator glm::mat4() const;
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
class Plane : public Model
|
||||||
|
{
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
inline const static std::shared_ptr<sb::Attributes> position = std::make_shared<sb::Attributes>(sb::Attributes{
|
||||||
|
{-1.0f, 1.0f}, {1.0f, 1.0f}, {-1.0f, -1.0f},
|
||||||
|
{1.0f, 1.0f}, {1.0f, -1.0f}, {-1.0f, -1.0f}
|
||||||
|
});
|
||||||
|
inline const static std::shared_ptr<sb::Attributes> uv = std::make_shared<sb::Attributes>(sb::Attributes{
|
||||||
|
{0.0f, 1.0f}, {1.0f, 1.0f}, {0.0f, 0.0f},
|
||||||
|
{1.0f, 1.0f}, {1.0f, 0.0f}, {0.0f, 0.0f}
|
||||||
|
});
|
||||||
|
|
||||||
|
Plane() : Model(std::map<std::string, std::shared_ptr<sb::Attributes>>({{"position", position}, {"uv", uv}})) {}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
|
@ -0,0 +1,35 @@
|
||||||
|
Emscripten webcam pixel data test
|
||||||
|
=================================
|
||||||
|
|
||||||
|
This is a demo program for passing image data from an HTML5 canvas object in JavaScript to an OpenGL context in a C++ program. It can be useful, for example, for developing cross-platform applications that use the same codebase to export both desktop and web versions, or for using C++ libraries and code to edit and display images on a web page.
|
||||||
|
|
||||||
|
It uses the [SPACEBOX][] engine to set up an SDL + GL environment and create a model conveniently, but it can be ported to just Emscripten.
|
||||||
|
|
||||||
|
Setup
|
||||||
|
-----
|
||||||
|
|
||||||
|
### Emscripten
|
||||||
|
|
||||||
|
Install the latest Emscripten version to a directory and specify the directory in the Makefile
|
||||||
|
|
||||||
|
### SPACE BOX
|
||||||
|
|
||||||
|
The [SPACEBOX][] game and interactive application framework is required for setting up SDL + OpenGL. It is being used for convenience in setting up the rendering, but this technique can be used without it. Get it from https://git.nugget.fun/nugget/spacebox and specify the path to it in the Makefile.
|
||||||
|
|
||||||
|
Compiling
|
||||||
|
---------
|
||||||
|
|
||||||
|
Run at the root of the directory after setting up Emscripten and SPACEBOX
|
||||||
|
|
||||||
|
make emscripten
|
||||||
|
|
||||||
|
Running
|
||||||
|
-------
|
||||||
|
|
||||||
|
Run at the root of the directory to create an HTTP server
|
||||||
|
|
||||||
|
python -m http.server
|
||||||
|
|
||||||
|
Browse to http://localhost:8000 to view the demo
|
||||||
|
|
||||||
|
[SPACEBOX]: https://git.nugget.fun/nugget/spacebox
|
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
* Browser Webcam Test by frank at shampoo.ooo
|
||||||
|
*
|
||||||
|
* Program for testing passing image data from an HTML5 canvas object in JavaScript to an OpenGL context in this C++ program.
|
||||||
|
* This can be useful, for example, for developing cross-platform applications that use the same codebase to export both desktop
|
||||||
|
* and web versions, or for using C++ libraries and code to edit and display images on a web page.
|
||||||
|
*
|
||||||
|
* It uses the [SPACE BOX] engine (https://git.shampoo.ooo/nugget/spacebox) to set up an SDL + GL environment and create a model
|
||||||
|
* conveniently, but it can be ported to just Emscripten.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <emscripten/bind.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include "Game.hpp"
|
||||||
|
#include "GLObject.hpp"
|
||||||
|
#include "VBO.hpp"
|
||||||
|
#include "Model.hpp"
|
||||||
|
|
||||||
|
using namespace emscripten;
|
||||||
|
|
||||||
|
/* These variables will be bound to JS. They are placed in the global scope, so they can be read and written by both
|
||||||
|
* C++ and JS. The following functions are bound to JS so they can be used to write values to the variables.
|
||||||
|
*/
|
||||||
|
unsigned int emscripten_heap_offset = 0;
|
||||||
|
bool new_frame_flag = false;
|
||||||
|
|
||||||
|
void flag_frame()
|
||||||
|
{
|
||||||
|
new_frame_flag = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_heap_offset(int offset)
|
||||||
|
{
|
||||||
|
emscripten_heap_offset = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Browser_Webcam_Test : public Game
|
||||||
|
{
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
sb::VAO vao;
|
||||||
|
sb::VBO vbo;
|
||||||
|
GLuint flat_program, texture_uniform;
|
||||||
|
Plane camera_frame_model;
|
||||||
|
|
||||||
|
Browser_Webcam_Test()
|
||||||
|
{
|
||||||
|
/* The parent constructor initializes SDL and SDL extensions, sets GL attributes and creates a window. */
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Load pixel data from the Emscripten heap into an OpenGL texture. This will be called whenever new frame data is added to the Emscripten heap */
|
||||||
|
void refresh()
|
||||||
|
{
|
||||||
|
/* Address of frame RGBA pixel data on the Emscripten heap (received as an unsigned int and cast to an unsigned 8-bit pointer) */
|
||||||
|
unsigned char* pos = reinterpret_cast<std::uint8_t*>(emscripten_heap_offset);
|
||||||
|
|
||||||
|
/* Print the first four 8-bit values, which should be an RGBA color */
|
||||||
|
std::cout << "heap address " << emscripten_heap_offset << " first 4 bytes: ";
|
||||||
|
for (std::size_t ii = 0; ii < 4; ii++)
|
||||||
|
{
|
||||||
|
std::cout << static_cast<int>(pos[ii]) << " ";
|
||||||
|
}
|
||||||
|
std::cout << std::endl;
|
||||||
|
|
||||||
|
// If OpenCV were being used, a cv::Mat could be created:
|
||||||
|
// frame = cv::Mat(FW, FH, CV_8UC4, pos);;
|
||||||
|
|
||||||
|
/* Add a texture to the camera Plane for storing frame image data */
|
||||||
|
camera_frame_model.texture().load(pos, configuration()["display"]["dimensions"]);
|
||||||
|
|
||||||
|
/* Indicate pixel data has finished loading */
|
||||||
|
new_frame_flag = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Set up GL buffers for attributes. Set up shaders and uniforms. Create the texture that will hold the camera pixel data. */
|
||||||
|
void load_gl_context()
|
||||||
|
{
|
||||||
|
/* [SPACE BOX] creates an SDL GL context and initializes GLEW. */
|
||||||
|
Game::load_gl_context();
|
||||||
|
|
||||||
|
/* Generate a vertex array object ID, bind it as current (requirement of OpenGL) */
|
||||||
|
vao.generate();
|
||||||
|
vao.bind();
|
||||||
|
|
||||||
|
/* Generate ID for the vertex buffer object that will hold all vertex data. Using one buffer for all attributes, data
|
||||||
|
* will be copied in one after the other. */
|
||||||
|
vbo.generate();
|
||||||
|
vbo.bind();
|
||||||
|
|
||||||
|
/* Load, configure, and set GL to use the shader program */
|
||||||
|
GLuint vertex_shader = load_shader("shaders/flat.vert", GL_VERTEX_SHADER);
|
||||||
|
GLuint fragment_shader = load_shader("shaders/flat.frag", GL_FRAGMENT_SHADER);
|
||||||
|
flat_program = glCreateProgram();
|
||||||
|
glAttachShader(flat_program, vertex_shader);
|
||||||
|
glAttachShader(flat_program, fragment_shader);
|
||||||
|
Plane::position->bind(0, flat_program, "in_position");
|
||||||
|
Plane::uv->bind(1, flat_program, "vertex_uv");
|
||||||
|
link_shader(flat_program);
|
||||||
|
glUseProgram(flat_program);
|
||||||
|
|
||||||
|
/* Fill VBO with attribute data */
|
||||||
|
vbo.allocate(camera_frame_model.size(), GL_STATIC_DRAW);
|
||||||
|
vbo.add(*Plane::position);
|
||||||
|
vbo.add(*Plane::uv);
|
||||||
|
|
||||||
|
/* Set the active texture unit to #0, Get the texture uniform from the shader and set it use texture #0. See
|
||||||
|
* https://www.khronos.org/opengl/wiki/Sampler_(GLSL)#Binding_textures_to_samplers */
|
||||||
|
glActiveTexture(GL_TEXTURE0);
|
||||||
|
texture_uniform = glGetUniformLocation(flat_program, "base_texture");
|
||||||
|
glUniform1i(texture_uniform, 0);
|
||||||
|
|
||||||
|
/* Create a texture the size of the video resolution (defined as 320, 240 in config.json and index.html) */
|
||||||
|
camera_frame_model.texture(sb::Texture());
|
||||||
|
camera_frame_model.texture().generate(configuration()["display"]["dimensions"]);
|
||||||
|
camera_frame_model.texture().bind();
|
||||||
|
camera_frame_model.enable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This gets called every frame by the parent class. Refresh the texture pixel data if a new frame is available.
|
||||||
|
* Clear the screen, then draw the camera model, which will render the texture. */
|
||||||
|
void update()
|
||||||
|
{
|
||||||
|
/* This flag is set in JS whenever a camera frame is read and stored */
|
||||||
|
if (new_frame_flag)
|
||||||
|
{
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
glClearColor(0, 0, 0, 1);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, camera_frame_model.attributes("position")->count());
|
||||||
|
SDL_GL_SwapWindow(window());
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Emscripten will call this function. Create a game object, load its GL context, and run the game. */
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
Browser_Webcam_Test browser_webcam_test = Browser_Webcam_Test();
|
||||||
|
browser_webcam_test.load_gl_context();
|
||||||
|
browser_webcam_test.run();
|
||||||
|
browser_webcam_test.quit();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* This will bind the global functions at the beginning of the file to Emscripten so those values can be set and read by this program */
|
||||||
|
EMSCRIPTEN_BINDINGS(my_module)
|
||||||
|
{
|
||||||
|
function("flag_frame", &flag_frame);
|
||||||
|
function("set_heap_offset", &set_heap_offset);
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"display": {
|
||||||
|
"render driver": "opengles3",
|
||||||
|
"dimensions": [320, 240]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,98 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<!-- WebGL output will be drawn here through Emscripten -->
|
||||||
|
<canvas id="canvas"></canvas>
|
||||||
|
|
||||||
|
<!-- navigator.mediaDevices.getUserMedia will stream the webcam video directly here for testing -->
|
||||||
|
<video id="webcam"></video>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const FPS = 15;
|
||||||
|
const BPP = 4;
|
||||||
|
|
||||||
|
// Direct output of webcam
|
||||||
|
var video = document.getElementById("webcam");
|
||||||
|
video.width = 320;
|
||||||
|
video.height = 240;
|
||||||
|
|
||||||
|
// Undisplayed canvas which is used to draw the video frame and then read the pixel data directly
|
||||||
|
var intermediate = document.createElement("canvas");
|
||||||
|
intermediate.width = video.width;
|
||||||
|
intermediate.height = video.height;
|
||||||
|
var context = intermediate.getContext("2d");
|
||||||
|
|
||||||
|
// Indicates whether webcam is opened or not
|
||||||
|
var streaming = false;
|
||||||
|
|
||||||
|
// Address of the webcam frame pixel data on the Emscripten heap
|
||||||
|
var image_heap_address;
|
||||||
|
|
||||||
|
var Module = {
|
||||||
|
onRuntimeInitialized: function()
|
||||||
|
{
|
||||||
|
|
||||||
|
// Open the webcam and start displaying frames if successfully opened. Allocate space for 32-bit RGBA frame pixel data
|
||||||
|
// on the Emscripten heap.
|
||||||
|
navigator.mediaDevices.getUserMedia({video: {width: video.width, height: video.height}, audio: false})
|
||||||
|
.then(function(stream) {
|
||||||
|
video.srcObject = stream;
|
||||||
|
video.play();
|
||||||
|
streaming = true;
|
||||||
|
|
||||||
|
// Get the memory address of the pixel data
|
||||||
|
image_heap_address = Module._malloc(video.width * video.height * BPP);
|
||||||
|
|
||||||
|
// Pass the address to the C++ program
|
||||||
|
Module.set_heap_offset(image_heap_address);
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
console.log('Camera Error: ' + err.name + ' ' + err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
// This function will run continuously, drawing the webcam frame to the intermediate canvas, reading the pixel data,
|
||||||
|
// storing the data on the heap, and setting the new frame available flag.
|
||||||
|
function processVideo()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (streaming)
|
||||||
|
{
|
||||||
|
// Draw the webcam frame on a hidden canvas
|
||||||
|
context.drawImage(video, 0, 0, video.width, video.height);
|
||||||
|
|
||||||
|
// Read the pixel data
|
||||||
|
image_data = context.getImageData(0, 0, video.width, video.height).data;
|
||||||
|
|
||||||
|
// Get a memory view object that provides access to the heap at the previously allocated address
|
||||||
|
image_heap_data = new Uint8Array(Module.HEAPU8.buffer, image_heap_address, video.width * video.height * BPP);
|
||||||
|
|
||||||
|
// Write the pixel data to the heap
|
||||||
|
image_heap_data.set(image_data);
|
||||||
|
|
||||||
|
// Flag the C++ that new data is available
|
||||||
|
Module.flag_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop processVideo at roughly the FPS
|
||||||
|
let begin = Date.now();
|
||||||
|
let delay = 1000/FPS - (Date.now() - begin);
|
||||||
|
setTimeout(processVideo, delay);
|
||||||
|
}
|
||||||
|
catch (err)
|
||||||
|
{
|
||||||
|
console.log(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
processVideo();
|
||||||
|
},
|
||||||
|
|
||||||
|
// Tell Emscripten to use this canvas for display
|
||||||
|
canvas: document.getElementById("canvas")
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- This file is built by Emscripten when compiling the program -->
|
||||||
|
<script src="browser_webcam_test.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,21 @@
|
||||||
|
#version 300 es
|
||||||
|
|
||||||
|
/* Browser Webcam Test */
|
||||||
|
|
||||||
|
/* The precision declaration is required by OpenGL ES */
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
/* Forwarded from the vertex shader */
|
||||||
|
in vec2 uv;
|
||||||
|
|
||||||
|
/* The texture is provided by the program when it sets the uniform value. */
|
||||||
|
uniform sampler2D base_texture;
|
||||||
|
|
||||||
|
/* Setting this to a color value will color the fragment in the output display */
|
||||||
|
out vec4 myOutputColor;
|
||||||
|
|
||||||
|
void main(void)
|
||||||
|
{
|
||||||
|
/* Get the color from the texture at the UV coordinates */
|
||||||
|
myOutputColor = texture(base_texture, uv);
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
#version 300 es
|
||||||
|
|
||||||
|
/* Browser Webcam Test */
|
||||||
|
|
||||||
|
/* The precision declaration is required by OpenGL ES */
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
/* Values which are bound the VBO. */
|
||||||
|
in vec2 in_position;
|
||||||
|
in vec2 vertex_uv;
|
||||||
|
|
||||||
|
/* Will be forwarded to the fragment shader */
|
||||||
|
out vec2 uv;
|
||||||
|
|
||||||
|
/* Value is provided by the program when it sets the uniform value. */
|
||||||
|
uniform mat4 transformation;
|
||||||
|
|
||||||
|
void main(void)
|
||||||
|
{
|
||||||
|
/* Reflect 2D coordinates over the X-axis to flip the canvas coordinates into GL coordinates and return as a vec4 */
|
||||||
|
gl_Position = vec4(vec2(1, -1) * in_position, 0, 1);
|
||||||
|
|
||||||
|
/* Forward the UV coordinates to the fragment shader. */
|
||||||
|
uv = vertex_uv;
|
||||||
|
}
|
Loading…
Reference in New Issue