feat: render spectrogram sphere skin
Context: Task 5 of oscope-sphere - add the SphereViz layer A skin and full ofApp wiring so the app produces a visual output. Approach: Build an icosphere (5 subdivisions) skinned by a scrolling 2-channel spectrogram texture uploaded each frame via ofFloatPixels. GLSL 150 shaders compute spherical UV from vertex direction so the texture wraps seamlessly; scroll is handled by a fract() offset in the fragment shader to avoid re-uploading geometry. Changes: - bin/data/shaders/sphere.vert: spherical UV projection from position - bin/data/shaders/sphere.frag: magma/viridis colormaps + scroll - src/SphereViz.h/.cpp: icosphere mesh, shader, texture management, pushSpectrogramColumn() wraps SpectrogramBuffer for dual-channel - src/ofApp.h: full member declarations (camera, analyzers, flags) - src/ofApp.cpp: setup/update/draw/HUD/keyPressed wired end-to-end; HantekDevice probed at startup, falls back to DemoSignal on failure Impact: app renders a rotating spectrogram globe in demo mode with orbit camera, colormap toggle, freeze, and per-layer visibility keys.
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
#version 150
|
||||
|
||||
uniform sampler2D spectroTex; // width = time, height = freq, R32F [0,1]
|
||||
uniform float scrollOffset;
|
||||
uniform int colormapId;
|
||||
|
||||
in vec2 vSphereUV;
|
||||
out vec4 fragColor;
|
||||
|
||||
// Polynomial colormap fits (public domain, Matt Zucker).
|
||||
vec3 magma(float t) {
|
||||
const vec3 c0 = vec3(-0.002136485053939,-0.000749655052795,-0.005386127855323);
|
||||
const vec3 c1 = vec3( 0.251660540737164, 0.677523243683767, 2.494026599312351);
|
||||
const vec3 c2 = vec3( 8.353717279216625,-3.577719514958484, 0.314467903013257);
|
||||
const vec3 c3 = vec3(-27.66873308576866, 14.26473078096533,-13.64921318813922);
|
||||
const vec3 c4 = vec3( 52.17613981234068,-27.94360607168351, 12.94416944238394);
|
||||
const vec3 c5 = vec3(-50.76852536473588, 29.04658282127291, 4.234152993845980);
|
||||
const vec3 c6 = vec3( 18.65570506591883,-11.48977351997711,-5.601961508734096);
|
||||
return c0+t*(c1+t*(c2+t*(c3+t*(c4+t*(c5+t*c6)))));
|
||||
}
|
||||
vec3 viridis(float t) {
|
||||
const vec3 c0 = vec3( 0.277727327223418, 0.005407344544967, 0.334099805335306);
|
||||
const vec3 c1 = vec3( 0.105093043108577, 1.404613529898575, 1.384590162594685);
|
||||
const vec3 c2 = vec3(-0.330861828725556, 0.214847559468213, 0.095095163028237);
|
||||
const vec3 c3 = vec3(-4.634230498983486,-5.799100973351585,-19.33244095627987);
|
||||
const vec3 c4 = vec3( 6.228269936347081,14.17993336680509, 56.69055260068105);
|
||||
const vec3 c5 = vec3( 4.776384997670288,-13.74514537774601,-65.35303263337234);
|
||||
const vec3 c6 = vec3(-5.435455855934631, 4.645852612178535, 26.3124352495832);
|
||||
return c0+t*(c1+t*(c2+t*(c3+t*(c4+t*(c5+t*c6)))));
|
||||
}
|
||||
|
||||
void main() {
|
||||
float u = fract(vSphereUV.x - scrollOffset);
|
||||
float mag = clamp(texture(spectroTex, vec2(u, vSphereUV.y)).r, 0.0, 1.0);
|
||||
vec3 col = (colormapId == 0) ? magma(mag) : viridis(mag);
|
||||
fragColor = vec4(col, 1.0);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#version 150
|
||||
|
||||
uniform mat4 modelViewProjectionMatrix;
|
||||
in vec4 position;
|
||||
|
||||
out vec2 vSphereUV; // x = longitude [0,1], y = latitude [0,1]
|
||||
|
||||
const float PI = 3.14159265359;
|
||||
|
||||
void main() {
|
||||
vec3 dir = normalize(position.xyz);
|
||||
float lon = atan(dir.z, dir.x) / (2.0 * PI) + 0.5;
|
||||
float lat = asin(clamp(dir.y, -1.0, 1.0)) / PI + 0.5;
|
||||
gl_Position = modelViewProjectionMatrix * position;
|
||||
vSphereUV = vec2(lon, lat);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#include "SphereViz.h"
|
||||
#include <algorithm>
|
||||
|
||||
void SphereViz::setup(int icoIterations, int spectroWidth, int spectroHeight) {
|
||||
spectro_ = std::make_unique<oscope::SpectrogramBuffer>(spectroWidth,
|
||||
spectroHeight);
|
||||
|
||||
ofIcoSpherePrimitive ico(baseRadius_, icoIterations);
|
||||
ofMesh src = ico.getMesh();
|
||||
mesh_.clear();
|
||||
mesh_.addVertices(src.getVertices());
|
||||
mesh_.addNormals(src.getNormals());
|
||||
mesh_.addIndices(src.getIndices());
|
||||
|
||||
ofDisableArbTex();
|
||||
spectroPix_.allocate(spectroWidth, spectroHeight, OF_PIXELS_GRAY);
|
||||
spectroPix_.set(0.0f);
|
||||
spectroTex_.allocate(spectroPix_);
|
||||
spectroTex_.setTextureWrap(GL_REPEAT, GL_CLAMP_TO_EDGE);
|
||||
spectroTex_.setTextureMinMagFilter(GL_LINEAR, GL_LINEAR);
|
||||
|
||||
shader_.load("shaders/sphere");
|
||||
}
|
||||
|
||||
void SphereViz::pushSpectrogramColumn(const std::vector<float>& magCh1,
|
||||
const std::vector<float>& magCh2) {
|
||||
spectro_->pushColumn(magCh1, magCh2);
|
||||
const std::vector<float>& d = spectro_->data();
|
||||
std::copy(d.begin(), d.end(), spectroPix_.getData());
|
||||
spectroTex_.loadData(spectroPix_);
|
||||
scrollOffset_ = static_cast<float>(spectro_->writeIndex()) /
|
||||
static_cast<float>(spectro_->width());
|
||||
}
|
||||
|
||||
void SphereViz::drawSkin() {
|
||||
shader_.begin();
|
||||
shader_.setUniformTexture("spectroTex", spectroTex_, 0);
|
||||
shader_.setUniform1f("scrollOffset", scrollOffset_);
|
||||
shader_.setUniform1i("colormapId", colormapId_);
|
||||
mesh_.draw();
|
||||
shader_.end();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include "ofMain.h"
|
||||
#include "SpectrogramBuffer.h"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
// GL visual unit: an icosphere whose skin is the scrolling spectrogram.
|
||||
// The northern hemisphere shows CH1, the southern shows CH2.
|
||||
class SphereViz {
|
||||
public:
|
||||
void setup(int icoIterations, int spectroWidth, int spectroHeight);
|
||||
|
||||
// Advances the spectrogram by one column and uploads it.
|
||||
void pushSpectrogramColumn(const std::vector<float>& magCh1,
|
||||
const std::vector<float>& magCh2);
|
||||
|
||||
void drawSkin();
|
||||
void setColormap(int id) { colormapId_ = id; }
|
||||
|
||||
private:
|
||||
std::unique_ptr<oscope::SpectrogramBuffer> spectro_;
|
||||
ofVboMesh mesh_;
|
||||
ofShader shader_;
|
||||
ofTexture spectroTex_;
|
||||
ofFloatPixels spectroPix_;
|
||||
float baseRadius_ = 200.0f;
|
||||
float scrollOffset_ = 0.0f;
|
||||
int colormapId_ = 0;
|
||||
};
|
||||
@@ -2,12 +2,82 @@
|
||||
|
||||
void ofApp::setup() {
|
||||
ofSetFrameRate(60);
|
||||
ofSetVerticalSync(true);
|
||||
ofBackground(6, 6, 10);
|
||||
ofEnableDepthTest();
|
||||
glEnable(GL_PROGRAM_POINT_SIZE);
|
||||
|
||||
cam_.setDistance(750.0f);
|
||||
cam_.setNearClip(1.0f);
|
||||
cam_.setFarClip(5000.0f);
|
||||
|
||||
sphere_.setup(5, 512, 256);
|
||||
|
||||
hantek_.setSampleRate(16000000u);
|
||||
const oscope::HantekStatus st = hantek_.start();
|
||||
if (st == oscope::HantekStatus::Ok) {
|
||||
demoMode_ = false;
|
||||
statusText_ = "SCOPE OK";
|
||||
} else {
|
||||
demoMode_ = true;
|
||||
statusText_ = (st == oscope::HantekStatus::FirmwareNeeded)
|
||||
? "DEMO - firmware needed (see docs/HANTEK_SETUP.md)"
|
||||
: "DEMO - scope not found";
|
||||
}
|
||||
}
|
||||
|
||||
void ofApp::update() {}
|
||||
void ofApp::update() {
|
||||
if (frozen_) return;
|
||||
|
||||
if (demoMode_) {
|
||||
demo_.next(buf1_, buf2_, 8192);
|
||||
} else {
|
||||
hantek_.ring().readLatest(buf1_, buf2_, 8192);
|
||||
if (hantek_.status() != oscope::HantekStatus::Ok) {
|
||||
demoMode_ = true;
|
||||
statusText_ = "DEMO - scope lost";
|
||||
}
|
||||
}
|
||||
|
||||
const float sr = demoMode_ ? 48000.0f : scopeSr_;
|
||||
analyzerCh1_.update(buf1_, buf1_, sr);
|
||||
analyzerCh2_.update(buf2_, buf2_, sr);
|
||||
|
||||
sphere_.setColormap(colormap_);
|
||||
sphere_.pushSpectrogramColumn(analyzerCh1_.magDown(),
|
||||
analyzerCh2_.magDown());
|
||||
}
|
||||
|
||||
void ofApp::draw() {
|
||||
ofSetColor(230);
|
||||
ofDrawBitmapString("oscope-sphere skeleton", 16, 24);
|
||||
cam_.begin();
|
||||
ofPushMatrix();
|
||||
ofRotateYDeg(ofGetElapsedTimef() * 6.0f);
|
||||
if (layerA_) sphere_.drawSkin();
|
||||
ofPopMatrix();
|
||||
cam_.end();
|
||||
drawHud();
|
||||
}
|
||||
|
||||
void ofApp::drawHud() {
|
||||
ofDisableDepthTest();
|
||||
ofSetColor(230);
|
||||
std::string hud = statusText_ + "\n";
|
||||
hud += std::string("[1] skin ") + (layerA_ ? "on" : "off") + "\n";
|
||||
hud += std::string("[2] rings ") + (layerB_ ? "on" : "off") + "\n";
|
||||
hud += std::string("[3] points ") + (layerC_ ? "on" : "off") + "\n";
|
||||
hud += std::string("[c] colormap [space] ") +
|
||||
(frozen_ ? "frozen" : "live");
|
||||
ofDrawBitmapString(hud, 16, 24);
|
||||
ofEnableDepthTest();
|
||||
}
|
||||
|
||||
void ofApp::keyPressed(int key) {
|
||||
switch (key) {
|
||||
case '1': layerA_ = !layerA_; break;
|
||||
case '2': layerB_ = !layerB_; break;
|
||||
case '3': layerC_ = !layerC_; break;
|
||||
case 'c':
|
||||
case 'C': colormap_ = (colormap_ + 1) % 2; break;
|
||||
case ' ': frozen_ = !frozen_; break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,39 @@
|
||||
#pragma once
|
||||
#include "ofMain.h"
|
||||
#include "HantekDevice.h"
|
||||
#include "AudioAnalyzer.h"
|
||||
#include "DemoSignal.h"
|
||||
#include "SphereViz.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class ofApp : public ofBaseApp {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void draw() override;
|
||||
void keyPressed(int key) override;
|
||||
|
||||
private:
|
||||
void drawHud();
|
||||
|
||||
oscope::HantekDevice hantek_;
|
||||
oscope::AudioAnalyzer analyzerCh1_;
|
||||
oscope::AudioAnalyzer analyzerCh2_;
|
||||
oscope::DemoSignal demo_{48000.0f};
|
||||
|
||||
SphereViz sphere_;
|
||||
ofEasyCam cam_;
|
||||
|
||||
std::vector<float> buf1_;
|
||||
std::vector<float> buf2_;
|
||||
|
||||
bool demoMode_ = false;
|
||||
bool frozen_ = false;
|
||||
bool layerA_ = true;
|
||||
bool layerB_ = true;
|
||||
bool layerC_ = true;
|
||||
int colormap_ = 0;
|
||||
float scopeSr_ = 16.0e6f;
|
||||
std::string statusText_;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user