test(3d): screenshot-baseline TDD suite for the 3D viewer OpenGL->WebGL port — 47 native goldens + red-state WebGL harness
tests/3d-regression mirrors the gal-regression pattern at renderer scale: shared C++ scenarios call real KiCad 3D-viewer code (opengl_utils, display lists + DrawCulled stencil subtraction, MODEL_3D VBOs, private generators via a rob-template accessor, and full reload()+Redraw() composites over a synthetic BOARD_ADAPTER). A native macOS harness renders them on real OpenGL into 47 committed goldens (bit-deterministic, FBO capture); the wasm harness compiles the same TUs against wasm/stubs/gl_ffp_stub.c no-ops so every scenario renders blank — the TDD red state (parity meter: 47/47 changed). Comparisons use the CI pixelmatch engine via the new generic compare-dirs.ts (floors.json levels; manifest.json cmp-guards registry drift). Documents an upstream bug: appendPostMachiningGeometry's countersink path adds middle quads without normals, silently erasing the walls of any display list it is batched into (3d-post-machining.png keeps the lists separate to record it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
1a24c2894e
commit
ce1473c9ab
85 changed files with 6797 additions and 1 deletions
209
tests/3d-regression/scenarios/scene3d_test_ctx.cpp
Normal file
209
tests/3d-regression/scenarios/scene3d_test_ctx.cpp
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
#include "scene3d_test_ctx.h"
|
||||
|
||||
#include "3d_math.h" // SphericalToCartesian (inline, 3d-viewer/3d_math.h)
|
||||
#include "3d_rendering/image.h" // IMAGE for the circle texture
|
||||
#include "common_ogl/ogl_utils.h" // OglResetTextureState, OglDrawBackground, OglLoadTexture
|
||||
|
||||
#include <glm/ext.hpp> // value_ptr
|
||||
|
||||
// SIZE_OF_CIRCLE_TEXTURE lives in render_3d_opengl.h (too heavy for Stage 1);
|
||||
// keep the value in sync (render_3d_opengl.h:53).
|
||||
static constexpr int CIRCLE_TEXTURE_SIZE = 1024;
|
||||
|
||||
// Same premultiply the renderer applies before OglDrawBackground
|
||||
// (render_3d_opengl.cpp:505-508).
|
||||
static inline SFVEC4F premultiplyAlpha( const SFVEC4F& aInput )
|
||||
{
|
||||
return SFVEC4F( aInput.r * aInput.a, aInput.g * aInput.a, aInput.b * aInput.a, aInput.a );
|
||||
}
|
||||
|
||||
|
||||
SCENE3D_CTX::SCENE3D_CTX( int aWidth, int aHeight ) :
|
||||
m_width( aWidth ),
|
||||
m_height( aHeight ),
|
||||
m_camera( 2.0f * SCENE3D_RANGE_SCALE_3D )
|
||||
{
|
||||
ResetCamera();
|
||||
}
|
||||
|
||||
|
||||
void SCENE3D_CTX::ResetCamera()
|
||||
{
|
||||
m_camera.SetProjection( PROJECTION_TYPE::PERSPECTIVE );
|
||||
m_camera.SetCurWindowSize( wxSize( m_width, m_height ) );
|
||||
m_camera.Reset();
|
||||
}
|
||||
|
||||
|
||||
void SCENE3D_CTX::SetView( VIEW3D_TYPE aView )
|
||||
{
|
||||
// The settled end state of EDA_3D_CANVAS::SetView3D's animation
|
||||
// (eda_3d_canvas.cpp:1455-1481 + the Interpolate(1.0f) at :1354).
|
||||
m_camera.SetT0_and_T1_current_T();
|
||||
m_camera.ViewCommand_T1( aView );
|
||||
m_camera.SetInterpolateMode( CAMERA_INTERPOLATION::LINEAR );
|
||||
m_camera.Interpolate( 1.0f );
|
||||
}
|
||||
|
||||
|
||||
void SCENE3D_CTX::SetIsoView()
|
||||
{
|
||||
ResetCamera();
|
||||
// Tilt board toward the viewer, then spin — a deterministic 3/4 view that
|
||||
// shows top faces, side walls and lighting gradients at once.
|
||||
m_camera.RotateX( -glm::pi<float>() / 3.0f ); // -60°
|
||||
m_camera.RotateZ( glm::pi<float>() / 6.0f ); // +30°
|
||||
}
|
||||
|
||||
|
||||
void SCENE3D_CTX::SetOrtho( bool aOrtho )
|
||||
{
|
||||
m_camera.SetProjection( aOrtho ? PROJECTION_TYPE::ORTHO : PROJECTION_TYPE::PERSPECTIVE );
|
||||
// Force a projection rebuild for the (possibly unchanged) window size:
|
||||
// SetCurWindowSize only rebuilds on size change, so nudge through Reset-safe API.
|
||||
m_camera.SetCurWindowSize( wxSize( m_width, m_height - 1 ) );
|
||||
m_camera.SetCurWindowSize( wxSize( m_width, m_height ) );
|
||||
}
|
||||
|
||||
|
||||
void SCENE3D_CTX::BeginFrame()
|
||||
{
|
||||
// Default viewer background (BOARD_ADAPTER ctor, board_adapter.cpp:132-133).
|
||||
BeginFrame( SFVEC4F( 0.8f, 0.8f, 0.9f, 1.0f ), SFVEC4F( 0.4f, 0.4f, 0.5f, 1.0f ) );
|
||||
}
|
||||
|
||||
|
||||
void SCENE3D_CTX::BeginFrame( const SFVEC4F& aBgTop, const SFVEC4F& aBgBot )
|
||||
{
|
||||
// Per-frame state block replicated verbatim from RENDER_3D_OPENGL::Redraw()
|
||||
// (render_3d_opengl.cpp:553-586). Kept in the same order so the state the
|
||||
// scenarios render under is auditable against the real renderer.
|
||||
glDepthFunc( GL_LESS );
|
||||
glEnable( GL_CULL_FACE );
|
||||
glFrontFace( GL_CCW );
|
||||
glEnable( GL_NORMALIZE );
|
||||
glViewport( 0, 0, m_width, m_height );
|
||||
glEnable( GL_MULTISAMPLE );
|
||||
|
||||
glClearColor( 0.0f, 0.0f, 0.0f, 0.0f );
|
||||
glClearDepth( 1.0f );
|
||||
glClearStencil( 0x00 );
|
||||
glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT );
|
||||
|
||||
OglResetTextureState();
|
||||
|
||||
OglDrawBackground( premultiplyAlpha( aBgTop ), premultiplyAlpha( aBgBot ) );
|
||||
|
||||
glEnable( GL_DEPTH_TEST );
|
||||
|
||||
glMatrixMode( GL_PROJECTION );
|
||||
glLoadMatrixf( glm::value_ptr( m_camera.GetProjectionMatrix() ) );
|
||||
glMatrixMode( GL_MODELVIEW );
|
||||
glLoadIdentity();
|
||||
glLoadMatrixf( glm::value_ptr( m_camera.GetViewMatrix() ) );
|
||||
}
|
||||
|
||||
|
||||
void SCENE3D_CTX::SetupLights()
|
||||
{
|
||||
// Per-frame part only: Redraw() enables the lights and repositions the
|
||||
// headlight after loading the camera matrices (render_3d_opengl.cpp:589-611).
|
||||
// The light PARAMETERS were set once by initLights() at init time — GL bakes
|
||||
// directional-light positions in eye space using the modelview current at
|
||||
// the glLightfv call, and the real renderer sets them under the identity
|
||||
// matrix of a fresh context (initializeOpenGL -> init_lights).
|
||||
EnableLights( true, true, true );
|
||||
glEnable( GL_LIGHTING );
|
||||
|
||||
PositionHeadlight();
|
||||
}
|
||||
|
||||
|
||||
// The real light-rig initializer from render_3d_opengl.cpp:401 (free function
|
||||
// with external linkage; linked since Stage 2).
|
||||
void init_lights();
|
||||
|
||||
void SCENE3D_CTX::initLights()
|
||||
{
|
||||
::init_lights();
|
||||
}
|
||||
|
||||
|
||||
void SCENE3D_CTX::EnableLights( bool aFront, bool aTop, bool aBottom )
|
||||
{
|
||||
// Same GL_LIGHTx mapping as RENDER_3D_OPENGL::setLightFront/Top/Bottom
|
||||
// (render_3d_opengl.cpp:128-148).
|
||||
if( aFront )
|
||||
glEnable( GL_LIGHT0 );
|
||||
else
|
||||
glDisable( GL_LIGHT0 );
|
||||
|
||||
if( aTop )
|
||||
glEnable( GL_LIGHT1 );
|
||||
else
|
||||
glDisable( GL_LIGHT1 );
|
||||
|
||||
if( aBottom )
|
||||
glEnable( GL_LIGHT2 );
|
||||
else
|
||||
glDisable( GL_LIGHT2 );
|
||||
}
|
||||
|
||||
|
||||
void SCENE3D_CTX::PositionHeadlight()
|
||||
{
|
||||
// Exact headlight placement from Redraw() (render_3d_opengl.cpp:595-611).
|
||||
const SFVEC3F& cameraPos = m_camera.GetPos();
|
||||
|
||||
float zpos;
|
||||
|
||||
if( cameraPos.z > 0.0f )
|
||||
zpos = glm::max( cameraPos.z, 0.5f ) + cameraPos.z * cameraPos.z;
|
||||
else
|
||||
zpos = glm::min( cameraPos.z, -0.5f ) - cameraPos.z * cameraPos.z;
|
||||
|
||||
const GLfloat headlight_pos[] = { cameraPos.x, cameraPos.y, zpos, 1.0f };
|
||||
|
||||
glLightfv( GL_LIGHT0, GL_POSITION, headlight_pos );
|
||||
}
|
||||
|
||||
|
||||
void SCENE3D_CTX::InitOnce()
|
||||
{
|
||||
if( m_circleTexture )
|
||||
return;
|
||||
|
||||
// Replicates RENDER_3D_OPENGL::initializeOpenGL() (render_3d_opengl.cpp:858-896)
|
||||
// minus init_lights() (SetupLights) and m_canvasInitialized bookkeeping.
|
||||
glEnable( GL_LINE_SMOOTH );
|
||||
glShadeModel( GL_SMOOTH );
|
||||
glPixelStorei( GL_UNPACK_ALIGNMENT, 4 );
|
||||
|
||||
IMAGE circleImage( CIRCLE_TEXTURE_SIZE, CIRCLE_TEXTURE_SIZE );
|
||||
|
||||
const unsigned int circleRadius = ( CIRCLE_TEXTURE_SIZE / 2 ) - 4;
|
||||
|
||||
circleImage.CircleFilled( ( CIRCLE_TEXTURE_SIZE / 2 ) - 0, ( CIRCLE_TEXTURE_SIZE / 2 ) - 0,
|
||||
circleRadius, 0xFF );
|
||||
|
||||
IMAGE circleImageBlured( circleImage.GetWidth(), circleImage.GetHeight() );
|
||||
|
||||
circleImageBlured.EfxFilter_SkipCenter( &circleImage, IMAGE_FILTER::GAUSSIAN_BLUR,
|
||||
circleRadius - 8 );
|
||||
|
||||
m_circleTexture = OglLoadTexture( circleImageBlured );
|
||||
|
||||
// initializeOpenGL() ends with init_lights(): the directional lights are
|
||||
// baked in EYE space under the fresh context's identity modelview — that is
|
||||
// why the viewer's scene lighting follows the camera. Keep that semantic.
|
||||
glMatrixMode( GL_MODELVIEW );
|
||||
glLoadIdentity();
|
||||
initLights();
|
||||
}
|
||||
|
||||
|
||||
OPENGL_RENDER_LIST* SCENE3D_CTX::MakeRenderList( const TRIANGLE_DISPLAY_LIST& aTdl, float aZBot,
|
||||
float aZTop ) const
|
||||
{
|
||||
return new OPENGL_RENDER_LIST( aTdl, m_circleTexture, aZBot, aZTop );
|
||||
}
|
||||
Loading…
Reference in a new issue