3D viewport, milestone 1
This commit is contained in:
parent
38156dd570
commit
e277663ba0
27 changed files with 7389 additions and 8 deletions
|
|
@ -49,6 +49,9 @@ endif()
|
||||||
# Add include directories
|
# Add include directories
|
||||||
include_directories(${CMAKE_SOURCE_DIR}/deps)
|
include_directories(${CMAKE_SOURCE_DIR}/deps)
|
||||||
include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/deps/libtcod)
|
include_directories(SYSTEM ${CMAKE_SOURCE_DIR}/deps/libtcod)
|
||||||
|
include_directories(${CMAKE_SOURCE_DIR}/src)
|
||||||
|
include_directories(${CMAKE_SOURCE_DIR}/src/3d)
|
||||||
|
include_directories(${CMAKE_SOURCE_DIR}/src/platform)
|
||||||
|
|
||||||
# Python includes: use different paths for Windows vs Linux vs Emscripten
|
# Python includes: use different paths for Windows vs Linux vs Emscripten
|
||||||
if(EMSCRIPTEN)
|
if(EMSCRIPTEN)
|
||||||
|
|
@ -102,6 +105,8 @@ file(GLOB_RECURSE SOURCES "src/*.cpp")
|
||||||
# Add ImGui sources to the build (only if using SFML)
|
# Add ImGui sources to the build (only if using SFML)
|
||||||
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
|
if(NOT MCRF_HEADLESS AND NOT MCRF_SDL2)
|
||||||
list(APPEND SOURCES ${IMGUI_SOURCES})
|
list(APPEND SOURCES ${IMGUI_SOURCES})
|
||||||
|
# Add GLAD for OpenGL function loading (needed for 3D rendering on SFML)
|
||||||
|
list(APPEND SOURCES "${CMAKE_SOURCE_DIR}/src/3d/glad.c")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Find OpenGL (required by ImGui-SFML) - not needed in headless mode
|
# Find OpenGL (required by ImGui-SFML) - not needed in headless mode
|
||||||
|
|
|
||||||
109
src/3d/Camera3D.cpp
Normal file
109
src/3d/Camera3D.cpp
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
// Camera3D.cpp - 3D camera implementation
|
||||||
|
|
||||||
|
#include "Camera3D.h"
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
Camera3D::Camera3D()
|
||||||
|
: position_(0.0f, 0.0f, 5.0f)
|
||||||
|
, target_(0.0f, 0.0f, 0.0f)
|
||||||
|
, up_(0.0f, 1.0f, 0.0f)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Camera3D::Camera3D(const vec3& position, const vec3& target)
|
||||||
|
: position_(position)
|
||||||
|
, target_(target)
|
||||||
|
, up_(0.0f, 1.0f, 0.0f)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::setPosition(const vec3& pos) {
|
||||||
|
position_ = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::setTarget(const vec3& target) {
|
||||||
|
target_ = target;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::setUp(const vec3& up) {
|
||||||
|
up_ = up.normalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 Camera3D::getForward() const {
|
||||||
|
return (target_ - position_).normalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 Camera3D::getRight() const {
|
||||||
|
return getForward().cross(up_).normalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::setFOV(float fovDegrees) {
|
||||||
|
fov_ = fovDegrees;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::setAspect(float aspect) {
|
||||||
|
aspect_ = aspect;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::setClipPlanes(float near, float far) {
|
||||||
|
nearClip_ = near;
|
||||||
|
farClip_ = far;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 Camera3D::getViewMatrix() const {
|
||||||
|
return mat4::lookAt(position_, target_, up_);
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 Camera3D::getProjectionMatrix() const {
|
||||||
|
return mat4::perspective(radians(fov_), aspect_, nearClip_, farClip_);
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 Camera3D::getViewProjectionMatrix() const {
|
||||||
|
return getProjectionMatrix() * getViewMatrix();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::moveForward(float distance) {
|
||||||
|
vec3 forward = getForward();
|
||||||
|
position_ += forward * distance;
|
||||||
|
target_ += forward * distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::moveRight(float distance) {
|
||||||
|
vec3 right = getRight();
|
||||||
|
position_ += right * distance;
|
||||||
|
target_ += right * distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::moveUp(float distance) {
|
||||||
|
position_ += up_ * distance;
|
||||||
|
target_ += up_ * distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::orbit(float yawDelta, float pitchDelta) {
|
||||||
|
// Get current offset from target
|
||||||
|
vec3 offset = position_ - target_;
|
||||||
|
float distance = offset.length();
|
||||||
|
|
||||||
|
// Convert to spherical coordinates
|
||||||
|
float yaw = std::atan2(offset.x, offset.z);
|
||||||
|
float pitch = std::asin(clamp(offset.y / distance, -1.0f, 1.0f));
|
||||||
|
|
||||||
|
// Apply deltas (in radians)
|
||||||
|
yaw += yawDelta;
|
||||||
|
pitch += pitchDelta;
|
||||||
|
|
||||||
|
// Clamp pitch to avoid gimbal lock
|
||||||
|
pitch = clamp(pitch, -HALF_PI + 0.01f, HALF_PI - 0.01f);
|
||||||
|
|
||||||
|
// Convert back to Cartesian
|
||||||
|
position_.x = target_.x + distance * std::cos(pitch) * std::sin(yaw);
|
||||||
|
position_.y = target_.y + distance * std::sin(pitch);
|
||||||
|
position_.z = target_.z + distance * std::cos(pitch) * std::cos(yaw);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Camera3D::lookAt(const vec3& point) {
|
||||||
|
target_ = point;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
69
src/3d/Camera3D.h
Normal file
69
src/3d/Camera3D.h
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
// Camera3D.h - 3D camera for McRogueFace
|
||||||
|
// Provides view and projection matrices for 3D rendering
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Math3D.h"
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Camera3D - First-person style camera with position, target, up vector
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class Camera3D {
|
||||||
|
public:
|
||||||
|
Camera3D();
|
||||||
|
Camera3D(const vec3& position, const vec3& target);
|
||||||
|
|
||||||
|
// Position and orientation
|
||||||
|
void setPosition(const vec3& pos);
|
||||||
|
void setTarget(const vec3& target);
|
||||||
|
void setUp(const vec3& up);
|
||||||
|
|
||||||
|
vec3 getPosition() const { return position_; }
|
||||||
|
vec3 getTarget() const { return target_; }
|
||||||
|
vec3 getUp() const { return up_; }
|
||||||
|
|
||||||
|
// Direction vectors
|
||||||
|
vec3 getForward() const;
|
||||||
|
vec3 getRight() const;
|
||||||
|
|
||||||
|
// Projection settings
|
||||||
|
void setFOV(float fovDegrees);
|
||||||
|
void setAspect(float aspect);
|
||||||
|
void setClipPlanes(float near, float far);
|
||||||
|
|
||||||
|
float getFOV() const { return fov_; }
|
||||||
|
float getAspect() const { return aspect_; }
|
||||||
|
float getNearClip() const { return nearClip_; }
|
||||||
|
float getFarClip() const { return farClip_; }
|
||||||
|
|
||||||
|
// Matrix computation
|
||||||
|
mat4 getViewMatrix() const;
|
||||||
|
mat4 getProjectionMatrix() const;
|
||||||
|
mat4 getViewProjectionMatrix() const;
|
||||||
|
|
||||||
|
// Convenience methods for camera movement
|
||||||
|
void moveForward(float distance);
|
||||||
|
void moveRight(float distance);
|
||||||
|
void moveUp(float distance);
|
||||||
|
|
||||||
|
// Orbit around target
|
||||||
|
void orbit(float yawDelta, float pitchDelta);
|
||||||
|
|
||||||
|
// Look at a specific point (updates target)
|
||||||
|
void lookAt(const vec3& point);
|
||||||
|
|
||||||
|
private:
|
||||||
|
vec3 position_;
|
||||||
|
vec3 target_;
|
||||||
|
vec3 up_;
|
||||||
|
|
||||||
|
float fov_ = 60.0f; // Vertical FOV in degrees
|
||||||
|
float aspect_ = 1.0f; // Width / height
|
||||||
|
float nearClip_ = 0.1f;
|
||||||
|
float farClip_ = 100.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
311
src/3d/KHR/khrplatform.h
Normal file
311
src/3d/KHR/khrplatform.h
Normal file
|
|
@ -0,0 +1,311 @@
|
||||||
|
#ifndef __khrplatform_h_
|
||||||
|
#define __khrplatform_h_
|
||||||
|
|
||||||
|
/*
|
||||||
|
** Copyright (c) 2008-2018 The Khronos Group Inc.
|
||||||
|
**
|
||||||
|
** Permission is hereby granted, free of charge, to any person obtaining a
|
||||||
|
** copy of this software and/or associated documentation files (the
|
||||||
|
** "Materials"), to deal in the Materials without restriction, including
|
||||||
|
** without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
** distribute, sublicense, and/or sell copies of the Materials, and to
|
||||||
|
** permit persons to whom the Materials are furnished to do so, subject to
|
||||||
|
** the following conditions:
|
||||||
|
**
|
||||||
|
** The above copyright notice and this permission notice shall be included
|
||||||
|
** in all copies or substantial portions of the Materials.
|
||||||
|
**
|
||||||
|
** THE MATERIALS ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
** EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
** MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
** IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||||
|
** CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
** TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
|
** MATERIALS OR THE USE OR OTHER DEALINGS IN THE MATERIALS.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Khronos platform-specific types and definitions.
|
||||||
|
*
|
||||||
|
* The master copy of khrplatform.h is maintained in the Khronos EGL
|
||||||
|
* Registry repository at https://github.com/KhronosGroup/EGL-Registry
|
||||||
|
* The last semantic modification to khrplatform.h was at commit ID:
|
||||||
|
* 67a3e0864c2d75ea5287b9f3d2eb74a745936692
|
||||||
|
*
|
||||||
|
* Adopters may modify this file to suit their platform. Adopters are
|
||||||
|
* encouraged to submit platform specific modifications to the Khronos
|
||||||
|
* group so that they can be included in future versions of this file.
|
||||||
|
* Please submit changes by filing pull requests or issues on
|
||||||
|
* the EGL Registry repository linked above.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* See the Implementer's Guidelines for information about where this file
|
||||||
|
* should be located on your system and for more details of its use:
|
||||||
|
* http://www.khronos.org/registry/implementers_guide.pdf
|
||||||
|
*
|
||||||
|
* This file should be included as
|
||||||
|
* #include <KHR/khrplatform.h>
|
||||||
|
* by Khronos client API header files that use its types and defines.
|
||||||
|
*
|
||||||
|
* The types in khrplatform.h should only be used to define API-specific types.
|
||||||
|
*
|
||||||
|
* Types defined in khrplatform.h:
|
||||||
|
* khronos_int8_t signed 8 bit
|
||||||
|
* khronos_uint8_t unsigned 8 bit
|
||||||
|
* khronos_int16_t signed 16 bit
|
||||||
|
* khronos_uint16_t unsigned 16 bit
|
||||||
|
* khronos_int32_t signed 32 bit
|
||||||
|
* khronos_uint32_t unsigned 32 bit
|
||||||
|
* khronos_int64_t signed 64 bit
|
||||||
|
* khronos_uint64_t unsigned 64 bit
|
||||||
|
* khronos_intptr_t signed same number of bits as a pointer
|
||||||
|
* khronos_uintptr_t unsigned same number of bits as a pointer
|
||||||
|
* khronos_ssize_t signed size
|
||||||
|
* khronos_usize_t unsigned size
|
||||||
|
* khronos_float_t signed 32 bit floating point
|
||||||
|
* khronos_time_ns_t unsigned 64 bit time in nanoseconds
|
||||||
|
* khronos_utime_nanoseconds_t unsigned time interval or absolute time in
|
||||||
|
* nanoseconds
|
||||||
|
* khronos_stime_nanoseconds_t signed time interval in nanoseconds
|
||||||
|
* khronos_boolean_enum_t enumerated boolean type. This should
|
||||||
|
* only be used as a base type when a client API's boolean type is
|
||||||
|
* an enum. Client APIs which use an integer or other type for
|
||||||
|
* booleans cannot use this as the base type for their boolean.
|
||||||
|
*
|
||||||
|
* Tokens defined in khrplatform.h:
|
||||||
|
*
|
||||||
|
* KHRONOS_FALSE, KHRONOS_TRUE Enumerated boolean false/true values.
|
||||||
|
*
|
||||||
|
* KHRONOS_SUPPORT_INT64 is 1 if 64 bit integers are supported; otherwise 0.
|
||||||
|
* KHRONOS_SUPPORT_FLOAT is 1 if floats are supported; otherwise 0.
|
||||||
|
*
|
||||||
|
* Calling convention macros defined in this file:
|
||||||
|
* KHRONOS_APICALL
|
||||||
|
* KHRONOS_APIENTRY
|
||||||
|
* KHRONOS_APIATTRIBUTES
|
||||||
|
*
|
||||||
|
* These may be used in function prototypes as:
|
||||||
|
*
|
||||||
|
* KHRONOS_APICALL void KHRONOS_APIENTRY funcname(
|
||||||
|
* int arg1,
|
||||||
|
* int arg2) KHRONOS_APIATTRIBUTES;
|
||||||
|
*/
|
||||||
|
|
||||||
|
#if defined(__SCITECH_SNAP__) && !defined(KHRONOS_STATIC)
|
||||||
|
# define KHRONOS_STATIC 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*-------------------------------------------------------------------------
|
||||||
|
* Definition of KHRONOS_APICALL
|
||||||
|
*-------------------------------------------------------------------------
|
||||||
|
* This precedes the return type of the function in the function prototype.
|
||||||
|
*/
|
||||||
|
#if defined(KHRONOS_STATIC)
|
||||||
|
/* If the preprocessor constant KHRONOS_STATIC is defined, make the
|
||||||
|
* header compatible with static linking. */
|
||||||
|
# define KHRONOS_APICALL
|
||||||
|
#elif defined(_WIN32)
|
||||||
|
# define KHRONOS_APICALL __declspec(dllimport)
|
||||||
|
#elif defined (__SYMBIAN32__)
|
||||||
|
# define KHRONOS_APICALL IMPORT_C
|
||||||
|
#elif defined(__ANDROID__)
|
||||||
|
# define KHRONOS_APICALL __attribute__((visibility("default")))
|
||||||
|
#else
|
||||||
|
# define KHRONOS_APICALL
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*-------------------------------------------------------------------------
|
||||||
|
* Definition of KHRONOS_APIENTRY
|
||||||
|
*-------------------------------------------------------------------------
|
||||||
|
* This follows the return type of the function and precedes the function
|
||||||
|
* name in the function prototype.
|
||||||
|
*/
|
||||||
|
#if defined(_WIN32) && !defined(_WIN32_WCE) && !defined(__SCITECH_SNAP__)
|
||||||
|
/* Win32 but not WinCE */
|
||||||
|
# define KHRONOS_APIENTRY __stdcall
|
||||||
|
#else
|
||||||
|
# define KHRONOS_APIENTRY
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*-------------------------------------------------------------------------
|
||||||
|
* Definition of KHRONOS_APIATTRIBUTES
|
||||||
|
*-------------------------------------------------------------------------
|
||||||
|
* This follows the closing parenthesis of the function prototype arguments.
|
||||||
|
*/
|
||||||
|
#if defined (__ARMCC_2__)
|
||||||
|
#define KHRONOS_APIATTRIBUTES __softfp
|
||||||
|
#else
|
||||||
|
#define KHRONOS_APIATTRIBUTES
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*-------------------------------------------------------------------------
|
||||||
|
* basic type definitions
|
||||||
|
*-----------------------------------------------------------------------*/
|
||||||
|
#if (defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L) || defined(__GNUC__) || defined(__SCO__) || defined(__USLC__)
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Using <stdint.h>
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
typedef int32_t khronos_int32_t;
|
||||||
|
typedef uint32_t khronos_uint32_t;
|
||||||
|
typedef int64_t khronos_int64_t;
|
||||||
|
typedef uint64_t khronos_uint64_t;
|
||||||
|
#define KHRONOS_SUPPORT_INT64 1
|
||||||
|
#define KHRONOS_SUPPORT_FLOAT 1
|
||||||
|
/*
|
||||||
|
* To support platform where unsigned long cannot be used interchangeably with
|
||||||
|
* inptr_t (e.g. CHERI-extended ISAs), we can use the stdint.h intptr_t.
|
||||||
|
* Ideally, we could just use (u)intptr_t everywhere, but this could result in
|
||||||
|
* ABI breakage if khronos_uintptr_t is changed from unsigned long to
|
||||||
|
* unsigned long long or similar (this results in different C++ name mangling).
|
||||||
|
* To avoid changes for existing platforms, we restrict usage of intptr_t to
|
||||||
|
* platforms where the size of a pointer is larger than the size of long.
|
||||||
|
*/
|
||||||
|
#if defined(__SIZEOF_LONG__) && defined(__SIZEOF_POINTER__)
|
||||||
|
#if __SIZEOF_POINTER__ > __SIZEOF_LONG__
|
||||||
|
#define KHRONOS_USE_INTPTR_T
|
||||||
|
#endif
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#elif defined(__VMS ) || defined(__sgi)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Using <inttypes.h>
|
||||||
|
*/
|
||||||
|
#include <inttypes.h>
|
||||||
|
typedef int32_t khronos_int32_t;
|
||||||
|
typedef uint32_t khronos_uint32_t;
|
||||||
|
typedef int64_t khronos_int64_t;
|
||||||
|
typedef uint64_t khronos_uint64_t;
|
||||||
|
#define KHRONOS_SUPPORT_INT64 1
|
||||||
|
#define KHRONOS_SUPPORT_FLOAT 1
|
||||||
|
|
||||||
|
#elif defined(_WIN32) && !defined(__SCITECH_SNAP__)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Win32
|
||||||
|
*/
|
||||||
|
typedef __int32 khronos_int32_t;
|
||||||
|
typedef unsigned __int32 khronos_uint32_t;
|
||||||
|
typedef __int64 khronos_int64_t;
|
||||||
|
typedef unsigned __int64 khronos_uint64_t;
|
||||||
|
#define KHRONOS_SUPPORT_INT64 1
|
||||||
|
#define KHRONOS_SUPPORT_FLOAT 1
|
||||||
|
|
||||||
|
#elif defined(__sun__) || defined(__digital__)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Sun or Digital
|
||||||
|
*/
|
||||||
|
typedef int khronos_int32_t;
|
||||||
|
typedef unsigned int khronos_uint32_t;
|
||||||
|
#if defined(__arch64__) || defined(_LP64)
|
||||||
|
typedef long int khronos_int64_t;
|
||||||
|
typedef unsigned long int khronos_uint64_t;
|
||||||
|
#else
|
||||||
|
typedef long long int khronos_int64_t;
|
||||||
|
typedef unsigned long long int khronos_uint64_t;
|
||||||
|
#endif /* __arch64__ */
|
||||||
|
#define KHRONOS_SUPPORT_INT64 1
|
||||||
|
#define KHRONOS_SUPPORT_FLOAT 1
|
||||||
|
|
||||||
|
#elif 0
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Hypothetical platform with no float or int64 support
|
||||||
|
*/
|
||||||
|
typedef int khronos_int32_t;
|
||||||
|
typedef unsigned int khronos_uint32_t;
|
||||||
|
#define KHRONOS_SUPPORT_INT64 0
|
||||||
|
#define KHRONOS_SUPPORT_FLOAT 0
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Generic fallback
|
||||||
|
*/
|
||||||
|
#include <stdint.h>
|
||||||
|
typedef int32_t khronos_int32_t;
|
||||||
|
typedef uint32_t khronos_uint32_t;
|
||||||
|
typedef int64_t khronos_int64_t;
|
||||||
|
typedef uint64_t khronos_uint64_t;
|
||||||
|
#define KHRONOS_SUPPORT_INT64 1
|
||||||
|
#define KHRONOS_SUPPORT_FLOAT 1
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Types that are (so far) the same on all platforms
|
||||||
|
*/
|
||||||
|
typedef signed char khronos_int8_t;
|
||||||
|
typedef unsigned char khronos_uint8_t;
|
||||||
|
typedef signed short int khronos_int16_t;
|
||||||
|
typedef unsigned short int khronos_uint16_t;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Types that differ between LLP64 and LP64 architectures - in LLP64,
|
||||||
|
* pointers are 64 bits, but 'long' is still 32 bits. Win64 appears
|
||||||
|
* to be the only LLP64 architecture in current use.
|
||||||
|
*/
|
||||||
|
#ifdef KHRONOS_USE_INTPTR_T
|
||||||
|
typedef intptr_t khronos_intptr_t;
|
||||||
|
typedef uintptr_t khronos_uintptr_t;
|
||||||
|
#elif defined(_WIN64)
|
||||||
|
typedef signed long long int khronos_intptr_t;
|
||||||
|
typedef unsigned long long int khronos_uintptr_t;
|
||||||
|
#else
|
||||||
|
typedef signed long int khronos_intptr_t;
|
||||||
|
typedef unsigned long int khronos_uintptr_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if defined(_WIN64)
|
||||||
|
typedef signed long long int khronos_ssize_t;
|
||||||
|
typedef unsigned long long int khronos_usize_t;
|
||||||
|
#else
|
||||||
|
typedef signed long int khronos_ssize_t;
|
||||||
|
typedef unsigned long int khronos_usize_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if KHRONOS_SUPPORT_FLOAT
|
||||||
|
/*
|
||||||
|
* Float type
|
||||||
|
*/
|
||||||
|
typedef float khronos_float_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if KHRONOS_SUPPORT_INT64
|
||||||
|
/* Time types
|
||||||
|
*
|
||||||
|
* These types can be used to represent a time interval in nanoseconds or
|
||||||
|
* an absolute Unadjusted System Time. Unadjusted System Time is the number
|
||||||
|
* of nanoseconds since some arbitrary system event (e.g. since the last
|
||||||
|
* time the system booted). The Unadjusted System Time is an unsigned
|
||||||
|
* 64 bit value that wraps back to 0 every 584 years. Time intervals
|
||||||
|
* may be either signed or unsigned.
|
||||||
|
*/
|
||||||
|
typedef khronos_uint64_t khronos_utime_nanoseconds_t;
|
||||||
|
typedef khronos_int64_t khronos_stime_nanoseconds_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Dummy value used to pad enum types to 32 bits.
|
||||||
|
*/
|
||||||
|
#ifndef KHRONOS_MAX_ENUM
|
||||||
|
#define KHRONOS_MAX_ENUM 0x7FFFFFFF
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Enumerated boolean type
|
||||||
|
*
|
||||||
|
* Values other than zero should be considered to be true. Therefore
|
||||||
|
* comparisons should not be made against KHRONOS_TRUE.
|
||||||
|
*/
|
||||||
|
typedef enum {
|
||||||
|
KHRONOS_FALSE = 0,
|
||||||
|
KHRONOS_TRUE = 1,
|
||||||
|
KHRONOS_BOOLEAN_ENUM_FORCE_SIZE = KHRONOS_MAX_ENUM
|
||||||
|
} khronos_boolean_enum_t;
|
||||||
|
|
||||||
|
#endif /* __khrplatform_h_ */
|
||||||
630
src/3d/Math3D.h
Normal file
630
src/3d/Math3D.h
Normal file
|
|
@ -0,0 +1,630 @@
|
||||||
|
// Math3D.h - Minimal 3D math library for McRogueFace
|
||||||
|
// Header-only implementation of vec3, mat4, and quat
|
||||||
|
// Column-major matrices for OpenGL compatibility
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// vec2 - 2D vector
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct vec2 {
|
||||||
|
float x, y;
|
||||||
|
|
||||||
|
vec2() : x(0), y(0) {}
|
||||||
|
vec2(float x_, float y_) : x(x_), y(y_) {}
|
||||||
|
explicit vec2(float v) : x(v), y(v) {}
|
||||||
|
|
||||||
|
vec2 operator+(const vec2& other) const { return vec2(x + other.x, y + other.y); }
|
||||||
|
vec2 operator-(const vec2& other) const { return vec2(x - other.x, y - other.y); }
|
||||||
|
vec2 operator*(float s) const { return vec2(x * s, y * s); }
|
||||||
|
vec2 operator/(float s) const { return vec2(x / s, y / s); }
|
||||||
|
|
||||||
|
float dot(const vec2& other) const { return x * other.x + y * other.y; }
|
||||||
|
float length() const { return std::sqrt(x * x + y * y); }
|
||||||
|
float lengthSquared() const { return x * x + y * y; }
|
||||||
|
|
||||||
|
vec2 normalized() const {
|
||||||
|
float len = length();
|
||||||
|
if (len > 0.0001f) return vec2(x / len, y / len);
|
||||||
|
return vec2(0, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// vec3 - 3D vector
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct vec3 {
|
||||||
|
float x, y, z;
|
||||||
|
|
||||||
|
vec3() : x(0), y(0), z(0) {}
|
||||||
|
vec3(float x_, float y_, float z_) : x(x_), y(y_), z(z_) {}
|
||||||
|
explicit vec3(float v) : x(v), y(v), z(v) {}
|
||||||
|
|
||||||
|
vec3 operator+(const vec3& other) const {
|
||||||
|
return vec3(x + other.x, y + other.y, z + other.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 operator-(const vec3& other) const {
|
||||||
|
return vec3(x - other.x, y - other.y, z - other.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 operator*(float s) const {
|
||||||
|
return vec3(x * s, y * s, z * s);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 operator/(float s) const {
|
||||||
|
return vec3(x / s, y / s, z / s);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 operator-() const {
|
||||||
|
return vec3(-x, -y, -z);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3& operator+=(const vec3& other) {
|
||||||
|
x += other.x; y += other.y; z += other.z;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3& operator-=(const vec3& other) {
|
||||||
|
x -= other.x; y -= other.y; z -= other.z;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3& operator*=(float s) {
|
||||||
|
x *= s; y *= s; z *= s;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
float dot(const vec3& other) const {
|
||||||
|
return x * other.x + y * other.y + z * other.z;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 cross(const vec3& other) const {
|
||||||
|
return vec3(
|
||||||
|
y * other.z - z * other.y,
|
||||||
|
z * other.x - x * other.z,
|
||||||
|
x * other.y - y * other.x
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
float lengthSquared() const {
|
||||||
|
return x * x + y * y + z * z;
|
||||||
|
}
|
||||||
|
|
||||||
|
float length() const {
|
||||||
|
return std::sqrt(lengthSquared());
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 normalized() const {
|
||||||
|
float len = length();
|
||||||
|
if (len > 0.0001f) {
|
||||||
|
return *this / len;
|
||||||
|
}
|
||||||
|
return vec3(0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component-wise operations
|
||||||
|
vec3 hadamard(const vec3& other) const {
|
||||||
|
return vec3(x * other.x, y * other.y, z * other.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linear interpolation
|
||||||
|
static vec3 lerp(const vec3& a, const vec3& b, float t) {
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Left-hand scalar multiplication
|
||||||
|
inline vec3 operator*(float s, const vec3& v) {
|
||||||
|
return v * s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// vec4 - 4D vector (for homogeneous coordinates)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct vec4 {
|
||||||
|
float x, y, z, w;
|
||||||
|
|
||||||
|
vec4() : x(0), y(0), z(0), w(0) {}
|
||||||
|
vec4(float x_, float y_, float z_, float w_) : x(x_), y(y_), z(z_), w(w_) {}
|
||||||
|
vec4(const vec3& v, float w_) : x(v.x), y(v.y), z(v.z), w(w_) {}
|
||||||
|
|
||||||
|
vec3 xyz() const { return vec3(x, y, z); }
|
||||||
|
|
||||||
|
// Perspective divide
|
||||||
|
vec3 perspectiveDivide() const {
|
||||||
|
if (std::abs(w) > 0.0001f) {
|
||||||
|
return vec3(x / w, y / w, z / w);
|
||||||
|
}
|
||||||
|
return vec3(x, y, z);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// mat4 - 4x4 matrix (column-major for OpenGL)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct mat4 {
|
||||||
|
// Column-major storage: m[col][row] but stored as m[col*4 + row]
|
||||||
|
// This matches OpenGL's expected layout
|
||||||
|
float m[16];
|
||||||
|
|
||||||
|
mat4() {
|
||||||
|
for (int i = 0; i < 16; i++) m[i] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Access element at column c, row r
|
||||||
|
float& at(int c, int r) { return m[c * 4 + r]; }
|
||||||
|
const float& at(int c, int r) const { return m[c * 4 + r]; }
|
||||||
|
|
||||||
|
// Get column as vec4
|
||||||
|
vec4 col(int c) const {
|
||||||
|
return vec4(m[c*4], m[c*4+1], m[c*4+2], m[c*4+3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get raw data pointer (for OpenGL uniforms)
|
||||||
|
const float* data() const { return m; }
|
||||||
|
float* data() { return m; }
|
||||||
|
|
||||||
|
static mat4 identity() {
|
||||||
|
mat4 result;
|
||||||
|
result.at(0, 0) = 1.0f;
|
||||||
|
result.at(1, 1) = 1.0f;
|
||||||
|
result.at(2, 2) = 1.0f;
|
||||||
|
result.at(3, 3) = 1.0f;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static mat4 translate(const vec3& v) {
|
||||||
|
mat4 result = identity();
|
||||||
|
result.at(3, 0) = v.x;
|
||||||
|
result.at(3, 1) = v.y;
|
||||||
|
result.at(3, 2) = v.z;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static mat4 translate(float x, float y, float z) {
|
||||||
|
return translate(vec3(x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
static mat4 scale(const vec3& v) {
|
||||||
|
mat4 result = identity();
|
||||||
|
result.at(0, 0) = v.x;
|
||||||
|
result.at(1, 1) = v.y;
|
||||||
|
result.at(2, 2) = v.z;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static mat4 scale(float x, float y, float z) {
|
||||||
|
return scale(vec3(x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
|
static mat4 scale(float s) {
|
||||||
|
return scale(vec3(s, s, s));
|
||||||
|
}
|
||||||
|
|
||||||
|
static mat4 rotateX(float radians) {
|
||||||
|
mat4 result = identity();
|
||||||
|
float c = std::cos(radians);
|
||||||
|
float s = std::sin(radians);
|
||||||
|
result.at(1, 1) = c;
|
||||||
|
result.at(2, 1) = -s;
|
||||||
|
result.at(1, 2) = s;
|
||||||
|
result.at(2, 2) = c;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static mat4 rotateY(float radians) {
|
||||||
|
mat4 result = identity();
|
||||||
|
float c = std::cos(radians);
|
||||||
|
float s = std::sin(radians);
|
||||||
|
result.at(0, 0) = c;
|
||||||
|
result.at(2, 0) = s;
|
||||||
|
result.at(0, 2) = -s;
|
||||||
|
result.at(2, 2) = c;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static mat4 rotateZ(float radians) {
|
||||||
|
mat4 result = identity();
|
||||||
|
float c = std::cos(radians);
|
||||||
|
float s = std::sin(radians);
|
||||||
|
result.at(0, 0) = c;
|
||||||
|
result.at(1, 0) = -s;
|
||||||
|
result.at(0, 1) = s;
|
||||||
|
result.at(1, 1) = c;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perspective projection matrix
|
||||||
|
// fov: vertical field of view in radians
|
||||||
|
// aspect: width / height
|
||||||
|
// near, far: clipping planes
|
||||||
|
static mat4 perspective(float fov, float aspect, float near, float far) {
|
||||||
|
mat4 result;
|
||||||
|
float tanHalfFov = std::tan(fov / 2.0f);
|
||||||
|
|
||||||
|
result.at(0, 0) = 1.0f / (aspect * tanHalfFov);
|
||||||
|
result.at(1, 1) = 1.0f / tanHalfFov;
|
||||||
|
result.at(2, 2) = -(far + near) / (far - near);
|
||||||
|
result.at(2, 3) = -1.0f;
|
||||||
|
result.at(3, 2) = -(2.0f * far * near) / (far - near);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Orthographic projection matrix
|
||||||
|
static mat4 ortho(float left, float right, float bottom, float top, float near, float far) {
|
||||||
|
mat4 result = identity();
|
||||||
|
|
||||||
|
result.at(0, 0) = 2.0f / (right - left);
|
||||||
|
result.at(1, 1) = 2.0f / (top - bottom);
|
||||||
|
result.at(2, 2) = -2.0f / (far - near);
|
||||||
|
result.at(3, 0) = -(right + left) / (right - left);
|
||||||
|
result.at(3, 1) = -(top + bottom) / (top - bottom);
|
||||||
|
result.at(3, 2) = -(far + near) / (far - near);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// View matrix (camera transformation)
|
||||||
|
static mat4 lookAt(const vec3& eye, const vec3& target, const vec3& up) {
|
||||||
|
vec3 zaxis = (eye - target).normalized(); // Forward (camera looks down -Z)
|
||||||
|
vec3 xaxis = up.cross(zaxis).normalized(); // Right
|
||||||
|
vec3 yaxis = zaxis.cross(xaxis); // Up
|
||||||
|
|
||||||
|
mat4 result;
|
||||||
|
|
||||||
|
// Rotation part (transposed because we need the inverse)
|
||||||
|
result.at(0, 0) = xaxis.x;
|
||||||
|
result.at(1, 0) = xaxis.y;
|
||||||
|
result.at(2, 0) = xaxis.z;
|
||||||
|
|
||||||
|
result.at(0, 1) = yaxis.x;
|
||||||
|
result.at(1, 1) = yaxis.y;
|
||||||
|
result.at(2, 1) = yaxis.z;
|
||||||
|
|
||||||
|
result.at(0, 2) = zaxis.x;
|
||||||
|
result.at(1, 2) = zaxis.y;
|
||||||
|
result.at(2, 2) = zaxis.z;
|
||||||
|
|
||||||
|
// Translation part
|
||||||
|
result.at(3, 0) = -xaxis.dot(eye);
|
||||||
|
result.at(3, 1) = -yaxis.dot(eye);
|
||||||
|
result.at(3, 2) = -zaxis.dot(eye);
|
||||||
|
result.at(3, 3) = 1.0f;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Matrix multiplication
|
||||||
|
mat4 operator*(const mat4& other) const {
|
||||||
|
mat4 result;
|
||||||
|
for (int c = 0; c < 4; c++) {
|
||||||
|
for (int r = 0; r < 4; r++) {
|
||||||
|
float sum = 0.0f;
|
||||||
|
for (int k = 0; k < 4; k++) {
|
||||||
|
sum += at(k, r) * other.at(c, k);
|
||||||
|
}
|
||||||
|
result.at(c, r) = sum;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform a point (assumes w=1, returns xyz)
|
||||||
|
vec3 transformPoint(const vec3& p) const {
|
||||||
|
vec4 v(p, 1.0f);
|
||||||
|
vec4 result(
|
||||||
|
at(0, 0) * v.x + at(1, 0) * v.y + at(2, 0) * v.z + at(3, 0) * v.w,
|
||||||
|
at(0, 1) * v.x + at(1, 1) * v.y + at(2, 1) * v.z + at(3, 1) * v.w,
|
||||||
|
at(0, 2) * v.x + at(1, 2) * v.y + at(2, 2) * v.z + at(3, 2) * v.w,
|
||||||
|
at(0, 3) * v.x + at(1, 3) * v.y + at(2, 3) * v.z + at(3, 3) * v.w
|
||||||
|
);
|
||||||
|
return result.perspectiveDivide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform a direction (assumes w=0)
|
||||||
|
vec3 transformDirection(const vec3& d) const {
|
||||||
|
return vec3(
|
||||||
|
at(0, 0) * d.x + at(1, 0) * d.y + at(2, 0) * d.z,
|
||||||
|
at(0, 1) * d.x + at(1, 1) * d.y + at(2, 1) * d.z,
|
||||||
|
at(0, 2) * d.x + at(1, 2) * d.y + at(2, 2) * d.z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform a vec4
|
||||||
|
vec4 operator*(const vec4& v) const {
|
||||||
|
return vec4(
|
||||||
|
at(0, 0) * v.x + at(1, 0) * v.y + at(2, 0) * v.z + at(3, 0) * v.w,
|
||||||
|
at(0, 1) * v.x + at(1, 1) * v.y + at(2, 1) * v.z + at(3, 1) * v.w,
|
||||||
|
at(0, 2) * v.x + at(1, 2) * v.y + at(2, 2) * v.z + at(3, 2) * v.w,
|
||||||
|
at(0, 3) * v.x + at(1, 3) * v.y + at(2, 3) * v.z + at(3, 3) * v.w
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transpose
|
||||||
|
mat4 transposed() const {
|
||||||
|
mat4 result;
|
||||||
|
for (int c = 0; c < 4; c++) {
|
||||||
|
for (int r = 0; r < 4; r++) {
|
||||||
|
result.at(r, c) = at(c, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inverse (for general 4x4 matrix - used for camera)
|
||||||
|
// Returns identity if matrix is singular
|
||||||
|
mat4 inverse() const {
|
||||||
|
mat4 inv;
|
||||||
|
const float* m = this->m;
|
||||||
|
float* out = inv.m;
|
||||||
|
|
||||||
|
out[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] -
|
||||||
|
m[9] * m[6] * m[15] + m[9] * m[7] * m[14] +
|
||||||
|
m[13] * m[6] * m[11] - m[13] * m[7] * m[10];
|
||||||
|
out[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] +
|
||||||
|
m[8] * m[6] * m[15] - m[8] * m[7] * m[14] -
|
||||||
|
m[12] * m[6] * m[11] + m[12] * m[7] * m[10];
|
||||||
|
out[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] -
|
||||||
|
m[8] * m[5] * m[15] + m[8] * m[7] * m[13] +
|
||||||
|
m[12] * m[5] * m[11] - m[12] * m[7] * m[9];
|
||||||
|
out[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] +
|
||||||
|
m[8] * m[5] * m[14] - m[8] * m[6] * m[13] -
|
||||||
|
m[12] * m[5] * m[10] + m[12] * m[6] * m[9];
|
||||||
|
out[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] +
|
||||||
|
m[9] * m[2] * m[15] - m[9] * m[3] * m[14] -
|
||||||
|
m[13] * m[2] * m[11] + m[13] * m[3] * m[10];
|
||||||
|
out[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] -
|
||||||
|
m[8] * m[2] * m[15] + m[8] * m[3] * m[14] +
|
||||||
|
m[12] * m[2] * m[11] - m[12] * m[3] * m[10];
|
||||||
|
out[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] +
|
||||||
|
m[8] * m[1] * m[15] - m[8] * m[3] * m[13] -
|
||||||
|
m[12] * m[1] * m[11] + m[12] * m[3] * m[9];
|
||||||
|
out[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] -
|
||||||
|
m[8] * m[1] * m[14] + m[8] * m[2] * m[13] +
|
||||||
|
m[12] * m[1] * m[10] - m[12] * m[2] * m[9];
|
||||||
|
out[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] -
|
||||||
|
m[5] * m[2] * m[15] + m[5] * m[3] * m[14] +
|
||||||
|
m[13] * m[2] * m[7] - m[13] * m[3] * m[6];
|
||||||
|
out[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] +
|
||||||
|
m[4] * m[2] * m[15] - m[4] * m[3] * m[14] -
|
||||||
|
m[12] * m[2] * m[7] + m[12] * m[3] * m[6];
|
||||||
|
out[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] -
|
||||||
|
m[4] * m[1] * m[15] + m[4] * m[3] * m[13] +
|
||||||
|
m[12] * m[1] * m[7] - m[12] * m[3] * m[5];
|
||||||
|
out[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] +
|
||||||
|
m[4] * m[1] * m[14] - m[4] * m[2] * m[13] -
|
||||||
|
m[12] * m[1] * m[6] + m[12] * m[2] * m[5];
|
||||||
|
out[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] +
|
||||||
|
m[5] * m[2] * m[11] - m[5] * m[3] * m[10] -
|
||||||
|
m[9] * m[2] * m[7] + m[9] * m[3] * m[6];
|
||||||
|
out[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] -
|
||||||
|
m[4] * m[2] * m[11] + m[4] * m[3] * m[10] +
|
||||||
|
m[8] * m[2] * m[7] - m[8] * m[3] * m[6];
|
||||||
|
out[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] +
|
||||||
|
m[4] * m[1] * m[11] - m[4] * m[3] * m[9] -
|
||||||
|
m[8] * m[1] * m[7] + m[8] * m[3] * m[5];
|
||||||
|
out[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] -
|
||||||
|
m[4] * m[1] * m[10] + m[4] * m[2] * m[9] +
|
||||||
|
m[8] * m[1] * m[6] - m[8] * m[2] * m[5];
|
||||||
|
|
||||||
|
float det = m[0] * out[0] + m[1] * out[4] + m[2] * out[8] + m[3] * out[12];
|
||||||
|
|
||||||
|
if (std::abs(det) < 0.0001f) {
|
||||||
|
return identity();
|
||||||
|
}
|
||||||
|
|
||||||
|
det = 1.0f / det;
|
||||||
|
for (int i = 0; i < 16; i++) {
|
||||||
|
out[i] *= det;
|
||||||
|
}
|
||||||
|
|
||||||
|
return inv;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// quat - Quaternion for rotations
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct quat {
|
||||||
|
float x, y, z, w; // w is the scalar part
|
||||||
|
|
||||||
|
quat() : x(0), y(0), z(0), w(1) {} // Identity quaternion
|
||||||
|
quat(float x_, float y_, float z_, float w_) : x(x_), y(y_), z(z_), w(w_) {}
|
||||||
|
|
||||||
|
// Create from axis and angle (angle in radians)
|
||||||
|
static quat fromAxisAngle(const vec3& axis, float angle) {
|
||||||
|
float halfAngle = angle * 0.5f;
|
||||||
|
float s = std::sin(halfAngle);
|
||||||
|
vec3 n = axis.normalized();
|
||||||
|
return quat(n.x * s, n.y * s, n.z * s, std::cos(halfAngle));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create from Euler angles (in radians, applied as yaw-pitch-roll / Y-X-Z)
|
||||||
|
static quat fromEuler(float pitch, float yaw, float roll) {
|
||||||
|
float cy = std::cos(yaw * 0.5f);
|
||||||
|
float sy = std::sin(yaw * 0.5f);
|
||||||
|
float cp = std::cos(pitch * 0.5f);
|
||||||
|
float sp = std::sin(pitch * 0.5f);
|
||||||
|
float cr = std::cos(roll * 0.5f);
|
||||||
|
float sr = std::sin(roll * 0.5f);
|
||||||
|
|
||||||
|
return quat(
|
||||||
|
sr * cp * cy - cr * sp * sy,
|
||||||
|
cr * sp * cy + sr * cp * sy,
|
||||||
|
cr * cp * sy - sr * sp * cy,
|
||||||
|
cr * cp * cy + sr * sp * sy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
float lengthSquared() const {
|
||||||
|
return x * x + y * y + z * z + w * w;
|
||||||
|
}
|
||||||
|
|
||||||
|
float length() const {
|
||||||
|
return std::sqrt(lengthSquared());
|
||||||
|
}
|
||||||
|
|
||||||
|
quat normalized() const {
|
||||||
|
float len = length();
|
||||||
|
if (len > 0.0001f) {
|
||||||
|
float invLen = 1.0f / len;
|
||||||
|
return quat(x * invLen, y * invLen, z * invLen, w * invLen);
|
||||||
|
}
|
||||||
|
return quat();
|
||||||
|
}
|
||||||
|
|
||||||
|
quat conjugate() const {
|
||||||
|
return quat(-x, -y, -z, w);
|
||||||
|
}
|
||||||
|
|
||||||
|
quat inverse() const {
|
||||||
|
float lenSq = lengthSquared();
|
||||||
|
if (lenSq > 0.0001f) {
|
||||||
|
float invLenSq = 1.0f / lenSq;
|
||||||
|
return quat(-x * invLenSq, -y * invLenSq, -z * invLenSq, w * invLenSq);
|
||||||
|
}
|
||||||
|
return quat();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quaternion multiplication
|
||||||
|
quat operator*(const quat& other) const {
|
||||||
|
return quat(
|
||||||
|
w * other.x + x * other.w + y * other.z - z * other.y,
|
||||||
|
w * other.y - x * other.z + y * other.w + z * other.x,
|
||||||
|
w * other.z + x * other.y - y * other.x + z * other.w,
|
||||||
|
w * other.w - x * other.x - y * other.y - z * other.z
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate a vector by this quaternion
|
||||||
|
vec3 rotate(const vec3& v) const {
|
||||||
|
// q * v * q^-1
|
||||||
|
quat vq(v.x, v.y, v.z, 0);
|
||||||
|
quat result = (*this) * vq * conjugate();
|
||||||
|
return vec3(result.x, result.y, result.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to rotation matrix
|
||||||
|
mat4 toMatrix() const {
|
||||||
|
mat4 result = mat4::identity();
|
||||||
|
|
||||||
|
float xx = x * x;
|
||||||
|
float yy = y * y;
|
||||||
|
float zz = z * z;
|
||||||
|
float xy = x * y;
|
||||||
|
float xz = x * z;
|
||||||
|
float yz = y * z;
|
||||||
|
float wx = w * x;
|
||||||
|
float wy = w * y;
|
||||||
|
float wz = w * z;
|
||||||
|
|
||||||
|
result.at(0, 0) = 1.0f - 2.0f * (yy + zz);
|
||||||
|
result.at(0, 1) = 2.0f * (xy + wz);
|
||||||
|
result.at(0, 2) = 2.0f * (xz - wy);
|
||||||
|
|
||||||
|
result.at(1, 0) = 2.0f * (xy - wz);
|
||||||
|
result.at(1, 1) = 1.0f - 2.0f * (xx + zz);
|
||||||
|
result.at(1, 2) = 2.0f * (yz + wx);
|
||||||
|
|
||||||
|
result.at(2, 0) = 2.0f * (xz + wy);
|
||||||
|
result.at(2, 1) = 2.0f * (yz - wx);
|
||||||
|
result.at(2, 2) = 1.0f - 2.0f * (xx + yy);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spherical linear interpolation
|
||||||
|
static quat slerp(const quat& a, const quat& b, float t) {
|
||||||
|
float dot = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
|
||||||
|
|
||||||
|
quat b2 = b;
|
||||||
|
if (dot < 0.0f) {
|
||||||
|
// Take the shorter path
|
||||||
|
b2.x = -b.x;
|
||||||
|
b2.y = -b.y;
|
||||||
|
b2.z = -b.z;
|
||||||
|
b2.w = -b.w;
|
||||||
|
dot = -dot;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float DOT_THRESHOLD = 0.9995f;
|
||||||
|
if (dot > DOT_THRESHOLD) {
|
||||||
|
// Linear interpolation for very similar quaternions
|
||||||
|
return quat(
|
||||||
|
a.x + (b2.x - a.x) * t,
|
||||||
|
a.y + (b2.y - a.y) * t,
|
||||||
|
a.z + (b2.z - a.z) * t,
|
||||||
|
a.w + (b2.w - a.w) * t
|
||||||
|
).normalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
float theta_0 = std::acos(dot);
|
||||||
|
float theta = theta_0 * t;
|
||||||
|
float sin_theta = std::sin(theta);
|
||||||
|
float sin_theta_0 = std::sin(theta_0);
|
||||||
|
|
||||||
|
float s0 = std::cos(theta) - dot * sin_theta / sin_theta_0;
|
||||||
|
float s1 = sin_theta / sin_theta_0;
|
||||||
|
|
||||||
|
return quat(
|
||||||
|
a.x * s0 + b2.x * s1,
|
||||||
|
a.y * s0 + b2.y * s1,
|
||||||
|
a.z * s0 + b2.z * s1,
|
||||||
|
a.w * s0 + b2.w * s1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Linear interpolation (faster but less accurate for large angles)
|
||||||
|
static quat lerp(const quat& a, const quat& b, float t) {
|
||||||
|
float dot = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
|
||||||
|
|
||||||
|
quat result;
|
||||||
|
if (dot < 0.0f) {
|
||||||
|
result = quat(
|
||||||
|
a.x - (b.x + a.x) * t,
|
||||||
|
a.y - (b.y + a.y) * t,
|
||||||
|
a.z - (b.z + a.z) * t,
|
||||||
|
a.w - (b.w + a.w) * t
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = quat(
|
||||||
|
a.x + (b.x - a.x) * t,
|
||||||
|
a.y + (b.y - a.y) * t,
|
||||||
|
a.z + (b.z - a.z) * t,
|
||||||
|
a.w + (b.w - a.w) * t
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return result.normalized();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Utility constants and functions
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
constexpr float PI = 3.14159265358979323846f;
|
||||||
|
constexpr float TWO_PI = PI * 2.0f;
|
||||||
|
constexpr float HALF_PI = PI * 0.5f;
|
||||||
|
constexpr float DEG_TO_RAD = PI / 180.0f;
|
||||||
|
constexpr float RAD_TO_DEG = 180.0f / PI;
|
||||||
|
|
||||||
|
inline float radians(float degrees) { return degrees * DEG_TO_RAD; }
|
||||||
|
inline float degrees(float radians) { return radians * RAD_TO_DEG; }
|
||||||
|
|
||||||
|
inline float clamp(float v, float min, float max) {
|
||||||
|
return std::min(std::max(v, min), max);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
430
src/3d/Shader3D.cpp
Normal file
430
src/3d/Shader3D.cpp
Normal file
|
|
@ -0,0 +1,430 @@
|
||||||
|
// Shader3D.cpp - Shader management implementation
|
||||||
|
|
||||||
|
#include "Shader3D.h"
|
||||||
|
#include "../platform/GLContext.h"
|
||||||
|
|
||||||
|
// Include appropriate GL headers based on backend
|
||||||
|
#if defined(MCRF_SDL2)
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#include <GLES2/gl2.h>
|
||||||
|
#else
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glext.h>
|
||||||
|
#endif
|
||||||
|
#define MCRF_HAS_GL 1
|
||||||
|
#elif !defined(MCRF_HEADLESS)
|
||||||
|
// SFML backend - use GLAD
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#define MCRF_HAS_GL 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Embedded Shader Sources
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
namespace shaders {
|
||||||
|
|
||||||
|
const char* PS1_VERTEX_ES2 = R"(
|
||||||
|
// PS1-style vertex shader for OpenGL ES 2.0 / WebGL 1.0
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
uniform mat4 u_model;
|
||||||
|
uniform mat4 u_view;
|
||||||
|
uniform mat4 u_projection;
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform bool u_enable_snap;
|
||||||
|
uniform float u_fog_start;
|
||||||
|
uniform float u_fog_end;
|
||||||
|
uniform vec3 u_light_dir;
|
||||||
|
uniform vec3 u_ambient;
|
||||||
|
|
||||||
|
attribute vec3 a_position;
|
||||||
|
attribute vec2 a_texcoord;
|
||||||
|
attribute vec3 a_normal;
|
||||||
|
attribute vec4 a_color;
|
||||||
|
|
||||||
|
varying vec4 v_color;
|
||||||
|
varying vec2 v_texcoord;
|
||||||
|
varying float v_w;
|
||||||
|
varying float v_fog;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 worldPos = u_model * vec4(a_position, 1.0);
|
||||||
|
vec4 viewPos = u_view * worldPos;
|
||||||
|
vec4 clipPos = u_projection * viewPos;
|
||||||
|
|
||||||
|
if (u_enable_snap) {
|
||||||
|
vec4 ndc = clipPos;
|
||||||
|
ndc.xyz /= ndc.w;
|
||||||
|
vec2 grid = u_resolution * 0.5;
|
||||||
|
ndc.xy = floor(ndc.xy * grid + 0.5) / grid;
|
||||||
|
ndc.xyz *= clipPos.w;
|
||||||
|
clipPos = ndc;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_Position = clipPos;
|
||||||
|
|
||||||
|
vec3 worldNormal = normalize(mat3(u_model) * a_normal);
|
||||||
|
float diffuse = max(dot(worldNormal, -u_light_dir), 0.0);
|
||||||
|
vec3 lighting = u_ambient + vec3(diffuse);
|
||||||
|
v_color = vec4(a_color.rgb * lighting, a_color.a);
|
||||||
|
|
||||||
|
v_texcoord = a_texcoord * clipPos.w;
|
||||||
|
v_w = clipPos.w;
|
||||||
|
|
||||||
|
float depth = -viewPos.z;
|
||||||
|
v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* PS1_FRAGMENT_ES2 = R"(
|
||||||
|
// PS1-style fragment shader for OpenGL ES 2.0 / WebGL 1.0
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
uniform bool u_has_texture;
|
||||||
|
uniform bool u_enable_dither;
|
||||||
|
uniform vec3 u_fog_color;
|
||||||
|
|
||||||
|
varying vec4 v_color;
|
||||||
|
varying vec2 v_texcoord;
|
||||||
|
varying float v_w;
|
||||||
|
varying float v_fog;
|
||||||
|
|
||||||
|
float getBayerValue(vec2 fragCoord) {
|
||||||
|
int x = int(mod(fragCoord.x, 4.0));
|
||||||
|
int y = int(mod(fragCoord.y, 4.0));
|
||||||
|
if (y == 0) {
|
||||||
|
if (x == 0) return 0.0/16.0;
|
||||||
|
if (x == 1) return 8.0/16.0;
|
||||||
|
if (x == 2) return 2.0/16.0;
|
||||||
|
return 10.0/16.0;
|
||||||
|
}
|
||||||
|
if (y == 1) {
|
||||||
|
if (x == 0) return 12.0/16.0;
|
||||||
|
if (x == 1) return 4.0/16.0;
|
||||||
|
if (x == 2) return 14.0/16.0;
|
||||||
|
return 6.0/16.0;
|
||||||
|
}
|
||||||
|
if (y == 2) {
|
||||||
|
if (x == 0) return 3.0/16.0;
|
||||||
|
if (x == 1) return 11.0/16.0;
|
||||||
|
if (x == 2) return 1.0/16.0;
|
||||||
|
return 9.0/16.0;
|
||||||
|
}
|
||||||
|
if (x == 0) return 15.0/16.0;
|
||||||
|
if (x == 1) return 7.0/16.0;
|
||||||
|
if (x == 2) return 13.0/16.0;
|
||||||
|
return 5.0/16.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 quantize15bit(vec3 color) {
|
||||||
|
return floor(color * 31.0 + 0.5) / 31.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec2 uv = v_texcoord / v_w;
|
||||||
|
|
||||||
|
vec4 color;
|
||||||
|
if (u_has_texture) {
|
||||||
|
vec4 texColor = texture2D(u_texture, uv);
|
||||||
|
if (texColor.a < 0.5) discard;
|
||||||
|
color = texColor * v_color;
|
||||||
|
} else {
|
||||||
|
color = v_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (u_enable_dither) {
|
||||||
|
float threshold = getBayerValue(gl_FragCoord.xy);
|
||||||
|
vec3 dithered = color.rgb + (threshold - 0.5) / 31.0;
|
||||||
|
color.rgb = quantize15bit(dithered);
|
||||||
|
} else {
|
||||||
|
color.rgb = quantize15bit(color.rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
color.rgb = mix(color.rgb, u_fog_color, v_fog);
|
||||||
|
gl_FragColor = color;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* PS1_VERTEX = R"(
|
||||||
|
#version 150 core
|
||||||
|
uniform mat4 u_model;
|
||||||
|
uniform mat4 u_view;
|
||||||
|
uniform mat4 u_projection;
|
||||||
|
uniform vec2 u_resolution;
|
||||||
|
uniform bool u_enable_snap;
|
||||||
|
uniform float u_fog_start;
|
||||||
|
uniform float u_fog_end;
|
||||||
|
uniform vec3 u_light_dir;
|
||||||
|
uniform vec3 u_ambient;
|
||||||
|
|
||||||
|
in vec3 a_position;
|
||||||
|
in vec2 a_texcoord;
|
||||||
|
in vec3 a_normal;
|
||||||
|
in vec4 a_color;
|
||||||
|
|
||||||
|
out vec4 v_color;
|
||||||
|
noperspective out vec2 v_texcoord;
|
||||||
|
out float v_fog;
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 worldPos = u_model * vec4(a_position, 1.0);
|
||||||
|
vec4 viewPos = u_view * worldPos;
|
||||||
|
vec4 clipPos = u_projection * viewPos;
|
||||||
|
|
||||||
|
if (u_enable_snap) {
|
||||||
|
vec4 ndc = clipPos;
|
||||||
|
ndc.xyz /= ndc.w;
|
||||||
|
vec2 grid = u_resolution * 0.5;
|
||||||
|
ndc.xy = floor(ndc.xy * grid + 0.5) / grid;
|
||||||
|
ndc.xyz *= clipPos.w;
|
||||||
|
clipPos = ndc;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_Position = clipPos;
|
||||||
|
|
||||||
|
vec3 worldNormal = normalize(mat3(u_model) * a_normal);
|
||||||
|
float diffuse = max(dot(worldNormal, -u_light_dir), 0.0);
|
||||||
|
vec3 lighting = u_ambient + vec3(diffuse);
|
||||||
|
v_color = vec4(a_color.rgb * lighting, a_color.a);
|
||||||
|
v_texcoord = a_texcoord;
|
||||||
|
|
||||||
|
float depth = -viewPos.z;
|
||||||
|
v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
const char* PS1_FRAGMENT = R"(
|
||||||
|
#version 150 core
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
uniform bool u_has_texture;
|
||||||
|
uniform bool u_enable_dither;
|
||||||
|
uniform vec3 u_fog_color;
|
||||||
|
|
||||||
|
in vec4 v_color;
|
||||||
|
noperspective in vec2 v_texcoord;
|
||||||
|
in float v_fog;
|
||||||
|
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
const int bayerMatrix[16] = int[16](0,8,2,10,12,4,14,6,3,11,1,9,15,7,13,5);
|
||||||
|
|
||||||
|
float getBayerValue(vec2 fragCoord) {
|
||||||
|
int x = int(mod(fragCoord.x, 4.0));
|
||||||
|
int y = int(mod(fragCoord.y, 4.0));
|
||||||
|
return float(bayerMatrix[y * 4 + x]) / 16.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 quantize15bit(vec3 color) {
|
||||||
|
return floor(color * 31.0 + 0.5) / 31.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
vec4 color;
|
||||||
|
if (u_has_texture) {
|
||||||
|
vec4 texColor = texture(u_texture, v_texcoord);
|
||||||
|
if (texColor.a < 0.5) discard;
|
||||||
|
color = texColor * v_color;
|
||||||
|
} else {
|
||||||
|
color = v_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (u_enable_dither) {
|
||||||
|
float threshold = getBayerValue(gl_FragCoord.xy);
|
||||||
|
vec3 dithered = color.rgb + (threshold - 0.5) / 31.0;
|
||||||
|
color.rgb = quantize15bit(dithered);
|
||||||
|
} else {
|
||||||
|
color.rgb = quantize15bit(color.rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
color.rgb = mix(color.rgb, u_fog_color, v_fog);
|
||||||
|
fragColor = color;
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
} // namespace shaders
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Shader3D Implementation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
Shader3D::Shader3D() = default;
|
||||||
|
|
||||||
|
Shader3D::~Shader3D() {
|
||||||
|
if (program_ != 0) {
|
||||||
|
gl::deleteProgram(program_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Shader3D::loadPS1Shaders() {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
// Use GLES2 shaders for Emscripten/WebGL
|
||||||
|
return load(shaders::PS1_VERTEX_ES2, shaders::PS1_FRAGMENT_ES2);
|
||||||
|
#else
|
||||||
|
// Use desktop GL 3.2+ shaders
|
||||||
|
return load(shaders::PS1_VERTEX, shaders::PS1_FRAGMENT);
|
||||||
|
#endif
|
||||||
|
#else
|
||||||
|
// SFML backend - requires GLAD (not yet implemented)
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Shader3D::load(const char* vertexSource, const char* fragmentSource) {
|
||||||
|
if (!gl::isGLReady()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile vertex shader
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
unsigned int vertShader = gl::compileShader(GL_VERTEX_SHADER, vertexSource);
|
||||||
|
#else
|
||||||
|
unsigned int vertShader = gl::compileShader(0x8B31, vertexSource); // GL_VERTEX_SHADER
|
||||||
|
#endif
|
||||||
|
if (vertShader == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile fragment shader
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
unsigned int fragShader = gl::compileShader(GL_FRAGMENT_SHADER, fragmentSource);
|
||||||
|
#else
|
||||||
|
unsigned int fragShader = gl::compileShader(0x8B30, fragmentSource); // GL_FRAGMENT_SHADER
|
||||||
|
#endif
|
||||||
|
if (fragShader == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Link program
|
||||||
|
program_ = gl::linkProgram(vertShader, fragShader);
|
||||||
|
|
||||||
|
// Clean up individual shaders (they're now part of the program)
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
glDeleteShader(vertShader);
|
||||||
|
glDeleteShader(fragShader);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (program_ == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind standard attribute locations
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
glBindAttribLocation(program_, ATTRIB_POSITION, "a_position");
|
||||||
|
glBindAttribLocation(program_, ATTRIB_TEXCOORD, "a_texcoord");
|
||||||
|
glBindAttribLocation(program_, ATTRIB_NORMAL, "a_normal");
|
||||||
|
glBindAttribLocation(program_, ATTRIB_COLOR, "a_color");
|
||||||
|
|
||||||
|
// Re-link after binding attributes
|
||||||
|
glLinkProgram(program_);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
uniformCache_.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader3D::bind() {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
if (program_ != 0) {
|
||||||
|
glUseProgram(program_);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader3D::unbind() {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
glUseProgram(0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int Shader3D::getUniformLocation(const std::string& name) {
|
||||||
|
auto it = uniformCache_.find(name);
|
||||||
|
if (it != uniformCache_.end()) {
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
int location = glGetUniformLocation(program_, name.c_str());
|
||||||
|
uniformCache_[name] = location;
|
||||||
|
return location;
|
||||||
|
#else
|
||||||
|
return -1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int Shader3D::getAttribLocation(const std::string& name) {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
return glGetAttribLocation(program_, name.c_str());
|
||||||
|
#else
|
||||||
|
return -1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader3D::setUniform(const std::string& name, float value) {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
int loc = getUniformLocation(name);
|
||||||
|
if (loc >= 0) {
|
||||||
|
glUniform1f(loc, value);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader3D::setUniform(const std::string& name, int value) {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
int loc = getUniformLocation(name);
|
||||||
|
if (loc >= 0) {
|
||||||
|
glUniform1i(loc, value);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader3D::setUniform(const std::string& name, bool value) {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
int loc = getUniformLocation(name);
|
||||||
|
if (loc >= 0) {
|
||||||
|
glUniform1i(loc, value ? 1 : 0);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader3D::setUniform(const std::string& name, const vec2& value) {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
int loc = getUniformLocation(name);
|
||||||
|
if (loc >= 0) {
|
||||||
|
glUniform2f(loc, value.x, value.y);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader3D::setUniform(const std::string& name, const vec3& value) {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
int loc = getUniformLocation(name);
|
||||||
|
if (loc >= 0) {
|
||||||
|
glUniform3f(loc, value.x, value.y, value.z);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader3D::setUniform(const std::string& name, const vec4& value) {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
int loc = getUniformLocation(name);
|
||||||
|
if (loc >= 0) {
|
||||||
|
glUniform4f(loc, value.x, value.y, value.z, value.w);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Shader3D::setUniform(const std::string& name, const mat4& value) {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
int loc = getUniformLocation(name);
|
||||||
|
if (loc >= 0) {
|
||||||
|
glUniformMatrix4fv(loc, 1, GL_FALSE, value.m);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
72
src/3d/Shader3D.h
Normal file
72
src/3d/Shader3D.h
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
// Shader3D.h - Shader management for McRogueFace 3D
|
||||||
|
// Handles loading, compiling, and uniform management for PS1-style shaders
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Math3D.h"
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
class Shader3D {
|
||||||
|
public:
|
||||||
|
Shader3D();
|
||||||
|
~Shader3D();
|
||||||
|
|
||||||
|
// Load and compile shaders from embedded source strings
|
||||||
|
// Automatically selects desktop vs ES2 shaders based on platform
|
||||||
|
bool loadPS1Shaders();
|
||||||
|
|
||||||
|
// Load from custom source strings
|
||||||
|
bool load(const char* vertexSource, const char* fragmentSource);
|
||||||
|
|
||||||
|
// Bind/unbind shader for rendering
|
||||||
|
void bind();
|
||||||
|
void unbind();
|
||||||
|
|
||||||
|
// Check if shader is valid
|
||||||
|
bool isValid() const { return program_ != 0; }
|
||||||
|
|
||||||
|
// Uniform setters (cached location lookup)
|
||||||
|
void setUniform(const std::string& name, float value);
|
||||||
|
void setUniform(const std::string& name, int value);
|
||||||
|
void setUniform(const std::string& name, bool value);
|
||||||
|
void setUniform(const std::string& name, const vec2& value);
|
||||||
|
void setUniform(const std::string& name, const vec3& value);
|
||||||
|
void setUniform(const std::string& name, const vec4& value);
|
||||||
|
void setUniform(const std::string& name, const mat4& value);
|
||||||
|
|
||||||
|
// Get attribute location for VBO setup
|
||||||
|
int getAttribLocation(const std::string& name);
|
||||||
|
|
||||||
|
// Standard attribute locations for PS1 shaders
|
||||||
|
static constexpr int ATTRIB_POSITION = 0;
|
||||||
|
static constexpr int ATTRIB_TEXCOORD = 1;
|
||||||
|
static constexpr int ATTRIB_NORMAL = 2;
|
||||||
|
static constexpr int ATTRIB_COLOR = 3;
|
||||||
|
|
||||||
|
private:
|
||||||
|
unsigned int program_ = 0;
|
||||||
|
std::unordered_map<std::string, int> uniformCache_;
|
||||||
|
|
||||||
|
int getUniformLocation(const std::string& name);
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Embedded PS1 Shader Sources
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
namespace shaders {
|
||||||
|
|
||||||
|
// OpenGL ES 2.0 / WebGL 1.0 shaders
|
||||||
|
extern const char* PS1_VERTEX_ES2;
|
||||||
|
extern const char* PS1_FRAGMENT_ES2;
|
||||||
|
|
||||||
|
// OpenGL 3.2+ desktop shaders
|
||||||
|
extern const char* PS1_VERTEX;
|
||||||
|
extern const char* PS1_FRAGMENT;
|
||||||
|
|
||||||
|
} // namespace shaders
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
939
src/3d/Viewport3D.cpp
Normal file
939
src/3d/Viewport3D.cpp
Normal file
|
|
@ -0,0 +1,939 @@
|
||||||
|
// Viewport3D.cpp - 3D rendering viewport implementation
|
||||||
|
|
||||||
|
#include "Viewport3D.h"
|
||||||
|
#include "Shader3D.h"
|
||||||
|
#include "../platform/GLContext.h"
|
||||||
|
#include "PyVector.h"
|
||||||
|
#include "PyColor.h"
|
||||||
|
#include "PyPositionHelper.h"
|
||||||
|
#include "McRFPy_Doc.h"
|
||||||
|
#include <set>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
// Include appropriate GL headers based on backend
|
||||||
|
#if defined(MCRF_SDL2)
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#include <GLES2/gl2.h>
|
||||||
|
#else
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glext.h>
|
||||||
|
#endif
|
||||||
|
#define MCRF_HAS_GL 1
|
||||||
|
#elif !defined(MCRF_HEADLESS)
|
||||||
|
// SFML backend - use GLAD
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#define MCRF_HAS_GL 1
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Construction / Destruction
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
Viewport3D::Viewport3D()
|
||||||
|
: size_(320.0f, 240.0f)
|
||||||
|
{
|
||||||
|
position = sf::Vector2f(0, 0);
|
||||||
|
camera_.setAspect(size_.x / size_.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Viewport3D::Viewport3D(float x, float y, float width, float height)
|
||||||
|
: size_(width, height)
|
||||||
|
{
|
||||||
|
position = sf::Vector2f(x, y);
|
||||||
|
camera_.setAspect(size_.x / size_.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Viewport3D::~Viewport3D() {
|
||||||
|
cleanupTestGeometry();
|
||||||
|
cleanupFBO();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// UIDrawable Interface
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void Viewport3D::render(sf::Vector2f offset, sf::RenderTarget& target) {
|
||||||
|
if (!visible) return;
|
||||||
|
|
||||||
|
// Initialize resources if needed (only on GL-ready backends)
|
||||||
|
if (gl::isGLReady()) {
|
||||||
|
if (fbo_ == 0) {
|
||||||
|
initFBO();
|
||||||
|
}
|
||||||
|
if (!shader_) {
|
||||||
|
initShader();
|
||||||
|
}
|
||||||
|
if (testVBO_ == 0) {
|
||||||
|
initTestGeometry();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save SFML's GL state before raw GL rendering
|
||||||
|
// This is REQUIRED when mixing SFML 2D and raw OpenGL
|
||||||
|
target.pushGLStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render 3D content to FBO
|
||||||
|
render3DContent();
|
||||||
|
|
||||||
|
// Restore SFML's GL state after our GL calls
|
||||||
|
if (gl::isGLReady()) {
|
||||||
|
target.popGLStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blit FBO to screen (using SFML's drawing, so after state restore)
|
||||||
|
blitToScreen(offset, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
PyObjectsEnum Viewport3D::derived_type() {
|
||||||
|
return PyObjectsEnum::UIVIEWPORT3D;
|
||||||
|
}
|
||||||
|
|
||||||
|
UIDrawable* Viewport3D::click_at(sf::Vector2f point) {
|
||||||
|
sf::FloatRect bounds = get_bounds();
|
||||||
|
if (bounds.contains(point)) {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
sf::FloatRect Viewport3D::get_bounds() const {
|
||||||
|
return sf::FloatRect(position.x, position.y, size_.x, size_.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewport3D::move(float dx, float dy) {
|
||||||
|
position.x += dx;
|
||||||
|
position.y += dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewport3D::resize(float w, float h) {
|
||||||
|
size_.x = w;
|
||||||
|
size_.y = h;
|
||||||
|
camera_.setAspect(size_.x / size_.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Size and Resolution
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void Viewport3D::setSize(float width, float height) {
|
||||||
|
size_.x = width;
|
||||||
|
size_.y = height;
|
||||||
|
camera_.setAspect(size_.x / size_.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewport3D::setInternalResolution(int width, int height) {
|
||||||
|
if (width != internalWidth_ || height != internalHeight_) {
|
||||||
|
internalWidth_ = width;
|
||||||
|
internalHeight_ = height;
|
||||||
|
cleanupFBO(); // Force recreation on next render
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Fog Settings
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void Viewport3D::setFogColor(const sf::Color& color) {
|
||||||
|
fogColor_ = vec3(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
sf::Color Viewport3D::getFogColor() const {
|
||||||
|
return sf::Color(
|
||||||
|
static_cast<sf::Uint8>(fogColor_.x * 255),
|
||||||
|
static_cast<sf::Uint8>(fogColor_.y * 255),
|
||||||
|
static_cast<sf::Uint8>(fogColor_.z * 255)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewport3D::setFogRange(float nearDist, float farDist) {
|
||||||
|
fogNear_ = nearDist;
|
||||||
|
fogFar_ = farDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// FBO Management
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void Viewport3D::initFBO() {
|
||||||
|
if (fbo_ != 0) return; // Already initialized
|
||||||
|
|
||||||
|
fbo_ = gl::createFramebuffer(internalWidth_, internalHeight_,
|
||||||
|
&colorTexture_, &depthRenderbuffer_);
|
||||||
|
|
||||||
|
// Create SFML texture wrapper for blitting
|
||||||
|
// Note: We can't directly use the GL texture with SFML, so we'll
|
||||||
|
// read pixels back for now. This is inefficient but works across backends.
|
||||||
|
blitTexture_ = std::make_unique<sf::Texture>();
|
||||||
|
blitTexture_->create(internalWidth_, internalHeight_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewport3D::cleanupFBO() {
|
||||||
|
blitTexture_.reset();
|
||||||
|
if (fbo_ != 0) {
|
||||||
|
gl::deleteFramebuffer(fbo_, colorTexture_, depthRenderbuffer_);
|
||||||
|
fbo_ = 0;
|
||||||
|
colorTexture_ = 0;
|
||||||
|
depthRenderbuffer_ = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Shader and Geometry Initialization
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void Viewport3D::initShader() {
|
||||||
|
shader_ = std::make_unique<Shader3D>();
|
||||||
|
if (!shader_->loadPS1Shaders()) {
|
||||||
|
shader_.reset(); // Shader loading failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewport3D::initTestGeometry() {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
// Create a colored cube (no texture for now)
|
||||||
|
// Each vertex: position (3) + texcoord (2) + normal (3) + color (4) = 12 floats
|
||||||
|
// Cube has 6 faces * 2 triangles * 3 vertices = 36 vertices
|
||||||
|
|
||||||
|
float cubeVertices[] = {
|
||||||
|
// Front face (red) - normal (0, 0, 1)
|
||||||
|
-1, -1, 1, 0, 0, 0, 0, 1, 1, 0.2f, 0.2f, 1,
|
||||||
|
1, -1, 1, 1, 0, 0, 0, 1, 1, 0.2f, 0.2f, 1,
|
||||||
|
1, 1, 1, 1, 1, 0, 0, 1, 1, 0.2f, 0.2f, 1,
|
||||||
|
-1, -1, 1, 0, 0, 0, 0, 1, 1, 0.2f, 0.2f, 1,
|
||||||
|
1, 1, 1, 1, 1, 0, 0, 1, 1, 0.2f, 0.2f, 1,
|
||||||
|
-1, 1, 1, 0, 1, 0, 0, 1, 1, 0.2f, 0.2f, 1,
|
||||||
|
|
||||||
|
// Back face (cyan) - normal (0, 0, -1)
|
||||||
|
1, -1, -1, 0, 0, 0, 0,-1, 0.2f, 1, 1, 1,
|
||||||
|
-1, -1, -1, 1, 0, 0, 0,-1, 0.2f, 1, 1, 1,
|
||||||
|
-1, 1, -1, 1, 1, 0, 0,-1, 0.2f, 1, 1, 1,
|
||||||
|
1, -1, -1, 0, 0, 0, 0,-1, 0.2f, 1, 1, 1,
|
||||||
|
-1, 1, -1, 1, 1, 0, 0,-1, 0.2f, 1, 1, 1,
|
||||||
|
1, 1, -1, 0, 1, 0, 0,-1, 0.2f, 1, 1, 1,
|
||||||
|
|
||||||
|
// Top face (green) - normal (0, 1, 0)
|
||||||
|
-1, 1, 1, 0, 0, 0, 1, 0, 0.2f, 1, 0.2f, 1,
|
||||||
|
1, 1, 1, 1, 0, 0, 1, 0, 0.2f, 1, 0.2f, 1,
|
||||||
|
1, 1, -1, 1, 1, 0, 1, 0, 0.2f, 1, 0.2f, 1,
|
||||||
|
-1, 1, 1, 0, 0, 0, 1, 0, 0.2f, 1, 0.2f, 1,
|
||||||
|
1, 1, -1, 1, 1, 0, 1, 0, 0.2f, 1, 0.2f, 1,
|
||||||
|
-1, 1, -1, 0, 1, 0, 1, 0, 0.2f, 1, 0.2f, 1,
|
||||||
|
|
||||||
|
// Bottom face (magenta) - normal (0, -1, 0)
|
||||||
|
-1, -1, -1, 0, 0, 0,-1, 0, 1, 0.2f, 1, 1,
|
||||||
|
1, -1, -1, 1, 0, 0,-1, 0, 1, 0.2f, 1, 1,
|
||||||
|
1, -1, 1, 1, 1, 0,-1, 0, 1, 0.2f, 1, 1,
|
||||||
|
-1, -1, -1, 0, 0, 0,-1, 0, 1, 0.2f, 1, 1,
|
||||||
|
1, -1, 1, 1, 1, 0,-1, 0, 1, 0.2f, 1, 1,
|
||||||
|
-1, -1, 1, 0, 1, 0,-1, 0, 1, 0.2f, 1, 1,
|
||||||
|
|
||||||
|
// Right face (blue) - normal (1, 0, 0)
|
||||||
|
1, -1, 1, 0, 0, 1, 0, 0, 0.2f, 0.2f, 1, 1,
|
||||||
|
1, -1, -1, 1, 0, 1, 0, 0, 0.2f, 0.2f, 1, 1,
|
||||||
|
1, 1, -1, 1, 1, 1, 0, 0, 0.2f, 0.2f, 1, 1,
|
||||||
|
1, -1, 1, 0, 0, 1, 0, 0, 0.2f, 0.2f, 1, 1,
|
||||||
|
1, 1, -1, 1, 1, 1, 0, 0, 0.2f, 0.2f, 1, 1,
|
||||||
|
1, 1, 1, 0, 1, 1, 0, 0, 0.2f, 0.2f, 1, 1,
|
||||||
|
|
||||||
|
// Left face (yellow) - normal (-1, 0, 0)
|
||||||
|
-1, -1, -1, 0, 0, -1, 0, 0, 1, 1, 0.2f, 1,
|
||||||
|
-1, -1, 1, 1, 0, -1, 0, 0, 1, 1, 0.2f, 1,
|
||||||
|
-1, 1, 1, 1, 1, -1, 0, 0, 1, 1, 0.2f, 1,
|
||||||
|
-1, -1, -1, 0, 0, -1, 0, 0, 1, 1, 0.2f, 1,
|
||||||
|
-1, 1, 1, 1, 1, -1, 0, 0, 1, 1, 0.2f, 1,
|
||||||
|
-1, 1, -1, 0, 1, -1, 0, 0, 1, 1, 0.2f, 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
testVertexCount_ = 36;
|
||||||
|
|
||||||
|
glGenBuffers(1, &testVBO_);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, testVBO_);
|
||||||
|
glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), cubeVertices, GL_STATIC_DRAW);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewport3D::cleanupTestGeometry() {
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
if (testVBO_ != 0) {
|
||||||
|
glDeleteBuffers(1, &testVBO_);
|
||||||
|
testVBO_ = 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 3D Rendering
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void Viewport3D::render3DContent() {
|
||||||
|
// GL not available in current backend - skip 3D rendering
|
||||||
|
if (!gl::isGLReady() || fbo_ == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
// Save GL state
|
||||||
|
gl::pushState();
|
||||||
|
|
||||||
|
// Bind FBO
|
||||||
|
gl::bindFramebuffer(fbo_);
|
||||||
|
|
||||||
|
// Set viewport to internal resolution
|
||||||
|
glViewport(0, 0, internalWidth_, internalHeight_);
|
||||||
|
|
||||||
|
// Clear with background color
|
||||||
|
glClearColor(bgColor_.r / 255.0f, bgColor_.g / 255.0f,
|
||||||
|
bgColor_.b / 255.0f, bgColor_.a / 255.0f);
|
||||||
|
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
|
||||||
|
|
||||||
|
// Set up 3D state
|
||||||
|
gl::setup3DState();
|
||||||
|
|
||||||
|
// Update test rotation for spinning geometry
|
||||||
|
testRotation_ += 0.02f;
|
||||||
|
|
||||||
|
// Render test cube if shader and geometry are ready
|
||||||
|
if (shader_ && shader_->isValid() && testVBO_ != 0) {
|
||||||
|
shader_->bind();
|
||||||
|
|
||||||
|
// Set up matrices
|
||||||
|
mat4 model = mat4::rotateY(testRotation_) * mat4::rotateX(testRotation_ * 0.7f);
|
||||||
|
mat4 view = camera_.getViewMatrix();
|
||||||
|
mat4 projection = camera_.getProjectionMatrix();
|
||||||
|
|
||||||
|
shader_->setUniform("u_model", model);
|
||||||
|
shader_->setUniform("u_view", view);
|
||||||
|
shader_->setUniform("u_projection", projection);
|
||||||
|
|
||||||
|
// PS1 effect uniforms
|
||||||
|
shader_->setUniform("u_resolution", vec2(static_cast<float>(internalWidth_),
|
||||||
|
static_cast<float>(internalHeight_)));
|
||||||
|
shader_->setUniform("u_enable_snap", vertexSnapEnabled_);
|
||||||
|
shader_->setUniform("u_enable_dither", ditheringEnabled_);
|
||||||
|
|
||||||
|
// Lighting
|
||||||
|
vec3 lightDir = vec3(0.5f, -0.7f, 0.5f).normalized();
|
||||||
|
shader_->setUniform("u_light_dir", lightDir);
|
||||||
|
shader_->setUniform("u_ambient", vec3(0.3f, 0.3f, 0.3f));
|
||||||
|
|
||||||
|
// Fog
|
||||||
|
shader_->setUniform("u_fog_start", fogNear_);
|
||||||
|
shader_->setUniform("u_fog_end", fogFar_);
|
||||||
|
shader_->setUniform("u_fog_color", fogColor_);
|
||||||
|
|
||||||
|
// Texture (none for test geometry)
|
||||||
|
shader_->setUniform("u_has_texture", false);
|
||||||
|
|
||||||
|
// Bind VBO and set up attributes
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, testVBO_);
|
||||||
|
|
||||||
|
// Vertex format: pos(3) + texcoord(2) + normal(3) + color(4) = 12 floats
|
||||||
|
int stride = 12 * sizeof(float);
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_POSITION);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_POSITION, 3, GL_FLOAT, GL_FALSE, stride, (void*)0);
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, stride, (void*)(3 * sizeof(float)));
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_NORMAL, 3, GL_FLOAT, GL_FALSE, stride, (void*)(5 * sizeof(float)));
|
||||||
|
|
||||||
|
glEnableVertexAttribArray(Shader3D::ATTRIB_COLOR);
|
||||||
|
glVertexAttribPointer(Shader3D::ATTRIB_COLOR, 4, GL_FLOAT, GL_FALSE, stride, (void*)(8 * sizeof(float)));
|
||||||
|
|
||||||
|
// Draw cube
|
||||||
|
glDrawArrays(GL_TRIANGLES, 0, testVertexCount_);
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_POSITION);
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_TEXCOORD);
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_NORMAL);
|
||||||
|
glDisableVertexAttribArray(Shader3D::ATTRIB_COLOR);
|
||||||
|
glBindBuffer(GL_ARRAY_BUFFER, 0);
|
||||||
|
|
||||||
|
shader_->unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore 2D state
|
||||||
|
gl::restore2DState();
|
||||||
|
|
||||||
|
// Unbind FBO
|
||||||
|
gl::bindDefaultFramebuffer();
|
||||||
|
|
||||||
|
// Restore GL state
|
||||||
|
gl::popState();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewport3D::blitToScreen(sf::Vector2f offset, sf::RenderTarget& target) {
|
||||||
|
sf::Vector2f screenPos = position + offset;
|
||||||
|
|
||||||
|
// If GL is not ready, just draw a placeholder rectangle
|
||||||
|
if (!gl::isGLReady() || fbo_ == 0 || !blitTexture_) {
|
||||||
|
sf::RectangleShape placeholder(size_);
|
||||||
|
placeholder.setPosition(screenPos);
|
||||||
|
placeholder.setFillColor(bgColor_);
|
||||||
|
placeholder.setOutlineColor(sf::Color::White);
|
||||||
|
placeholder.setOutlineThickness(1.0f);
|
||||||
|
target.draw(placeholder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef MCRF_HAS_GL
|
||||||
|
// Read pixels from FBO and update SFML texture
|
||||||
|
// Note: This is inefficient but portable. Future optimization: use GL texture directly.
|
||||||
|
std::vector<sf::Uint8> pixels(internalWidth_ * internalHeight_ * 4);
|
||||||
|
|
||||||
|
gl::bindFramebuffer(fbo_);
|
||||||
|
glReadPixels(0, 0, internalWidth_, internalHeight_, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
|
||||||
|
gl::bindDefaultFramebuffer();
|
||||||
|
|
||||||
|
// Flip vertically (OpenGL vs SFML coordinate system)
|
||||||
|
std::vector<sf::Uint8> flipped(pixels.size());
|
||||||
|
for (int y = 0; y < internalHeight_; ++y) {
|
||||||
|
int srcRow = (internalHeight_ - 1 - y) * internalWidth_ * 4;
|
||||||
|
int dstRow = y * internalWidth_ * 4;
|
||||||
|
memcpy(&flipped[dstRow], &pixels[srcRow], internalWidth_ * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
blitTexture_->update(flipped.data());
|
||||||
|
|
||||||
|
// Draw to screen with nearest-neighbor scaling (PS1 style)
|
||||||
|
sf::Sprite sprite(*blitTexture_);
|
||||||
|
sprite.setPosition(screenPos);
|
||||||
|
sprite.setScale(size_.x / internalWidth_, size_.y / internalHeight_);
|
||||||
|
|
||||||
|
// Set nearest-neighbor filtering for that crispy PS1 look
|
||||||
|
// Note: SFML 2.x doesn't have per-draw texture filtering, so this
|
||||||
|
// affects the texture globally. In practice this is fine for our use.
|
||||||
|
const_cast<sf::Texture*>(sprite.getTexture())->setSmooth(false);
|
||||||
|
|
||||||
|
target.draw(sprite);
|
||||||
|
#else
|
||||||
|
// Non-SDL2 fallback (SFML desktop without GL)
|
||||||
|
sf::RectangleShape placeholder(size_);
|
||||||
|
placeholder.setPosition(screenPos);
|
||||||
|
placeholder.setFillColor(bgColor_);
|
||||||
|
target.draw(placeholder);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Animation Property System
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
bool Viewport3D::setProperty(const std::string& name, float value) {
|
||||||
|
if (name == "x") { position.x = value; return true; }
|
||||||
|
if (name == "y") { position.y = value; return true; }
|
||||||
|
if (name == "w") { size_.x = value; camera_.setAspect(size_.x / size_.y); return true; }
|
||||||
|
if (name == "h") { size_.y = value; camera_.setAspect(size_.x / size_.y); return true; }
|
||||||
|
if (name == "fov") { camera_.setFOV(value); return true; }
|
||||||
|
if (name == "fog_near") { fogNear_ = value; return true; }
|
||||||
|
if (name == "fog_far") { fogFar_ = value; return true; }
|
||||||
|
if (name == "opacity") { opacity = value; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Viewport3D::setProperty(const std::string& name, const sf::Color& value) {
|
||||||
|
if (name == "bg_color") { bgColor_ = value; return true; }
|
||||||
|
if (name == "fog_color") { setFogColor(value); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Viewport3D::setProperty(const std::string& name, const sf::Vector2f& value) {
|
||||||
|
if (name == "pos") { position = value; return true; }
|
||||||
|
if (name == "size") { size_ = value; camera_.setAspect(size_.x / size_.y); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Viewport3D::getProperty(const std::string& name, float& value) const {
|
||||||
|
if (name == "x") { value = position.x; return true; }
|
||||||
|
if (name == "y") { value = position.y; return true; }
|
||||||
|
if (name == "w") { value = size_.x; return true; }
|
||||||
|
if (name == "h") { value = size_.y; return true; }
|
||||||
|
if (name == "fov") { value = camera_.getFOV(); return true; }
|
||||||
|
if (name == "fog_near") { value = fogNear_; return true; }
|
||||||
|
if (name == "fog_far") { value = fogFar_; return true; }
|
||||||
|
if (name == "opacity") { value = opacity; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Viewport3D::getProperty(const std::string& name, sf::Color& value) const {
|
||||||
|
if (name == "bg_color") { value = bgColor_; return true; }
|
||||||
|
if (name == "fog_color") { value = getFogColor(); return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Viewport3D::getProperty(const std::string& name, sf::Vector2f& value) const {
|
||||||
|
if (name == "pos") { value = position; return true; }
|
||||||
|
if (name == "size") { value = size_; return true; }
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Viewport3D::hasProperty(const std::string& name) const {
|
||||||
|
static const std::set<std::string> props = {
|
||||||
|
"x", "y", "w", "h", "pos", "size",
|
||||||
|
"fov", "fog_near", "fog_far", "opacity",
|
||||||
|
"bg_color", "fog_color"
|
||||||
|
};
|
||||||
|
return props.count(name) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Python API
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Use PyObjectType for UIBase.h macros
|
||||||
|
#define PyObjectType PyViewport3DObject
|
||||||
|
|
||||||
|
// Helper to get vec3 from Python tuple
|
||||||
|
static bool PyTuple_GetVec3(PyObject* tuple, mcrf::vec3& out) {
|
||||||
|
if (!tuple || tuple == Py_None) return false;
|
||||||
|
if (!PyTuple_Check(tuple) && !PyList_Check(tuple)) return false;
|
||||||
|
|
||||||
|
Py_ssize_t size = PySequence_Size(tuple);
|
||||||
|
if (size != 3) return false;
|
||||||
|
|
||||||
|
PyObject* x = PySequence_GetItem(tuple, 0);
|
||||||
|
PyObject* y = PySequence_GetItem(tuple, 1);
|
||||||
|
PyObject* z = PySequence_GetItem(tuple, 2);
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
if (PyNumber_Check(x) && PyNumber_Check(y) && PyNumber_Check(z)) {
|
||||||
|
out.x = static_cast<float>(PyFloat_AsDouble(PyNumber_Float(x)));
|
||||||
|
out.y = static_cast<float>(PyFloat_AsDouble(PyNumber_Float(y)));
|
||||||
|
out.z = static_cast<float>(PyFloat_AsDouble(PyNumber_Float(z)));
|
||||||
|
} else {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Py_DECREF(x);
|
||||||
|
Py_DECREF(y);
|
||||||
|
Py_DECREF(z);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create Python tuple from vec3
|
||||||
|
static PyObject* PyTuple_FromVec3(const mcrf::vec3& v) {
|
||||||
|
return Py_BuildValue("(fff)", v.x, v.y, v.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position getters/setters
|
||||||
|
static PyObject* Viewport3D_get_pos(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyVector(self->data->position).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_pos(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
PyVectorObject* vec = PyVector::from_arg(value);
|
||||||
|
if (!vec) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "pos must be a Vector or (x, y) tuple");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->position = vec->data;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject* Viewport3D_get_x(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyFloat_FromDouble(self->data->position.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_x(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
if (!PyNumber_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "x must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->position.x = static_cast<float>(PyFloat_AsDouble(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject* Viewport3D_get_y(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyFloat_FromDouble(self->data->position.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_y(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
if (!PyNumber_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "y must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->position.y = static_cast<float>(PyFloat_AsDouble(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size getters/setters
|
||||||
|
static PyObject* Viewport3D_get_w(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyFloat_FromDouble(self->data->getWidth());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_w(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
if (!PyNumber_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "w must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setSize(static_cast<float>(PyFloat_AsDouble(value)), self->data->getHeight());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject* Viewport3D_get_h(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyFloat_FromDouble(self->data->getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_h(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
if (!PyNumber_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "h must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setSize(self->data->getWidth(), static_cast<float>(PyFloat_AsDouble(value)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render resolution
|
||||||
|
static PyObject* Viewport3D_get_render_resolution(PyViewport3DObject* self, void* closure) {
|
||||||
|
return Py_BuildValue("(ii)", self->data->getInternalWidth(), self->data->getInternalHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_render_resolution(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
int w, h;
|
||||||
|
if (!PyArg_ParseTuple(value, "ii", &w, &h)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "render_resolution must be (width, height)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setInternalResolution(w, h);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camera position
|
||||||
|
static PyObject* Viewport3D_get_camera_pos(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyTuple_FromVec3(self->data->getCameraPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_camera_pos(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
mcrf::vec3 pos;
|
||||||
|
if (!PyTuple_GetVec3(value, pos)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "camera_pos must be (x, y, z)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setCameraPosition(pos);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camera target
|
||||||
|
static PyObject* Viewport3D_get_camera_target(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyTuple_FromVec3(self->data->getCameraTarget());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_camera_target(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
mcrf::vec3 target;
|
||||||
|
if (!PyTuple_GetVec3(value, target)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "camera_target must be (x, y, z)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setCameraTarget(target);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FOV
|
||||||
|
static PyObject* Viewport3D_get_fov(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyFloat_FromDouble(self->data->getCamera().getFOV());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_fov(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
if (!PyNumber_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "fov must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->getCamera().setFOV(static_cast<float>(PyFloat_AsDouble(value)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background color
|
||||||
|
static PyObject* Viewport3D_get_bg_color(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyColor(self->data->getBackgroundColor()).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_bg_color(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
sf::Color color = PyColor::fromPy(value);
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setBackgroundColor(color);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PS1 effect toggles
|
||||||
|
static PyObject* Viewport3D_get_enable_vertex_snap(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyBool_FromLong(self->data->isVertexSnapEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_enable_vertex_snap(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
self->data->setVertexSnapEnabled(PyObject_IsTrue(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject* Viewport3D_get_enable_affine(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyBool_FromLong(self->data->isAffineMappingEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_enable_affine(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
self->data->setAffineMappingEnabled(PyObject_IsTrue(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject* Viewport3D_get_enable_dither(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyBool_FromLong(self->data->isDitheringEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_enable_dither(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
self->data->setDitheringEnabled(PyObject_IsTrue(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject* Viewport3D_get_enable_fog(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyBool_FromLong(self->data->isFogEnabled());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_enable_fog(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
self->data->setFogEnabled(PyObject_IsTrue(value));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fog color
|
||||||
|
static PyObject* Viewport3D_get_fog_color(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyColor(self->data->getFogColor()).pyObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_fog_color(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
sf::Color color = PyColor::fromPy(value);
|
||||||
|
if (PyErr_Occurred()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setFogColor(color);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fog range
|
||||||
|
static PyObject* Viewport3D_get_fog_near(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyFloat_FromDouble(self->data->getFogNear());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_fog_near(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
if (!PyNumber_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "fog_near must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setFogRange(static_cast<float>(PyFloat_AsDouble(value)), self->data->getFogFar());
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static PyObject* Viewport3D_get_fog_far(PyViewport3DObject* self, void* closure) {
|
||||||
|
return PyFloat_FromDouble(self->data->getFogFar());
|
||||||
|
}
|
||||||
|
|
||||||
|
static int Viewport3D_set_fog_far(PyViewport3DObject* self, PyObject* value, void* closure) {
|
||||||
|
if (!PyNumber_Check(value)) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "fog_far must be a number");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->setFogRange(self->data->getFogNear(), static_cast<float>(PyFloat_AsDouble(value)));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
PyGetSetDef Viewport3D::getsetters[] = {
|
||||||
|
// Position and size
|
||||||
|
{"x", (getter)Viewport3D_get_x, (setter)Viewport3D_set_x,
|
||||||
|
MCRF_PROPERTY(x, "X position in pixels."), NULL},
|
||||||
|
{"y", (getter)Viewport3D_get_y, (setter)Viewport3D_set_y,
|
||||||
|
MCRF_PROPERTY(y, "Y position in pixels."), NULL},
|
||||||
|
{"pos", (getter)Viewport3D_get_pos, (setter)Viewport3D_set_pos,
|
||||||
|
MCRF_PROPERTY(pos, "Position as Vector (x, y)."), NULL},
|
||||||
|
{"w", (getter)Viewport3D_get_w, (setter)Viewport3D_set_w,
|
||||||
|
MCRF_PROPERTY(w, "Display width in pixels."), NULL},
|
||||||
|
{"h", (getter)Viewport3D_get_h, (setter)Viewport3D_set_h,
|
||||||
|
MCRF_PROPERTY(h, "Display height in pixels."), NULL},
|
||||||
|
|
||||||
|
// Render resolution
|
||||||
|
{"render_resolution", (getter)Viewport3D_get_render_resolution, (setter)Viewport3D_set_render_resolution,
|
||||||
|
MCRF_PROPERTY(render_resolution, "Internal render resolution (width, height). Lower values for PS1 effect."), NULL},
|
||||||
|
|
||||||
|
// Camera
|
||||||
|
{"camera_pos", (getter)Viewport3D_get_camera_pos, (setter)Viewport3D_set_camera_pos,
|
||||||
|
MCRF_PROPERTY(camera_pos, "Camera position as (x, y, z) tuple."), NULL},
|
||||||
|
{"camera_target", (getter)Viewport3D_get_camera_target, (setter)Viewport3D_set_camera_target,
|
||||||
|
MCRF_PROPERTY(camera_target, "Camera look-at target as (x, y, z) tuple."), NULL},
|
||||||
|
{"fov", (getter)Viewport3D_get_fov, (setter)Viewport3D_set_fov,
|
||||||
|
MCRF_PROPERTY(fov, "Camera field of view in degrees."), NULL},
|
||||||
|
|
||||||
|
// Background
|
||||||
|
{"bg_color", (getter)Viewport3D_get_bg_color, (setter)Viewport3D_set_bg_color,
|
||||||
|
MCRF_PROPERTY(bg_color, "Background clear color."), NULL},
|
||||||
|
|
||||||
|
// PS1 effects
|
||||||
|
{"enable_vertex_snap", (getter)Viewport3D_get_enable_vertex_snap, (setter)Viewport3D_set_enable_vertex_snap,
|
||||||
|
MCRF_PROPERTY(enable_vertex_snap, "Enable PS1-style vertex snapping (jittery vertices)."), NULL},
|
||||||
|
{"enable_affine", (getter)Viewport3D_get_enable_affine, (setter)Viewport3D_set_enable_affine,
|
||||||
|
MCRF_PROPERTY(enable_affine, "Enable PS1-style affine texture mapping (warped textures)."), NULL},
|
||||||
|
{"enable_dither", (getter)Viewport3D_get_enable_dither, (setter)Viewport3D_set_enable_dither,
|
||||||
|
MCRF_PROPERTY(enable_dither, "Enable PS1-style color dithering."), NULL},
|
||||||
|
{"enable_fog", (getter)Viewport3D_get_enable_fog, (setter)Viewport3D_set_enable_fog,
|
||||||
|
MCRF_PROPERTY(enable_fog, "Enable distance fog."), NULL},
|
||||||
|
|
||||||
|
// Fog settings
|
||||||
|
{"fog_color", (getter)Viewport3D_get_fog_color, (setter)Viewport3D_set_fog_color,
|
||||||
|
MCRF_PROPERTY(fog_color, "Fog color."), NULL},
|
||||||
|
{"fog_near", (getter)Viewport3D_get_fog_near, (setter)Viewport3D_set_fog_near,
|
||||||
|
MCRF_PROPERTY(fog_near, "Fog start distance."), NULL},
|
||||||
|
{"fog_far", (getter)Viewport3D_get_fog_far, (setter)Viewport3D_set_fog_far,
|
||||||
|
MCRF_PROPERTY(fog_far, "Fog end distance."), NULL},
|
||||||
|
|
||||||
|
// Common UIDrawable properties
|
||||||
|
UIDRAWABLE_GETSETTERS,
|
||||||
|
UIDRAWABLE_PARENT_GETSETTERS(PyObjectsEnum::UIVIEWPORT3D),
|
||||||
|
|
||||||
|
{NULL} // Sentinel
|
||||||
|
};
|
||||||
|
|
||||||
|
PyObject* Viewport3D::repr(PyViewport3DObject* self) {
|
||||||
|
char buffer[256];
|
||||||
|
snprintf(buffer, sizeof(buffer), "<Viewport3D at (%.1f, %.1f) size (%.1f, %.1f) render %dx%d>",
|
||||||
|
self->data->position.x, self->data->position.y,
|
||||||
|
self->data->getWidth(), self->data->getHeight(),
|
||||||
|
self->data->getInternalWidth(), self->data->getInternalHeight());
|
||||||
|
return PyUnicode_FromString(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Viewport3D::init(PyViewport3DObject* self, PyObject* args, PyObject* kwds) {
|
||||||
|
static const char* kwlist[] = {
|
||||||
|
"pos", "size", "render_resolution", "fov",
|
||||||
|
"camera_pos", "camera_target", "bg_color",
|
||||||
|
"enable_vertex_snap", "enable_affine", "enable_dither", "enable_fog",
|
||||||
|
"fog_color", "fog_near", "fog_far",
|
||||||
|
"visible", "z_index", "name",
|
||||||
|
NULL
|
||||||
|
};
|
||||||
|
|
||||||
|
PyObject* pos_obj = nullptr;
|
||||||
|
PyObject* size_obj = nullptr;
|
||||||
|
PyObject* render_res_obj = nullptr;
|
||||||
|
float fov = 60.0f;
|
||||||
|
PyObject* camera_pos_obj = nullptr;
|
||||||
|
PyObject* camera_target_obj = nullptr;
|
||||||
|
PyObject* bg_color_obj = nullptr;
|
||||||
|
int enable_vertex_snap = 1;
|
||||||
|
int enable_affine = 1;
|
||||||
|
int enable_dither = 1;
|
||||||
|
int enable_fog = 1;
|
||||||
|
PyObject* fog_color_obj = nullptr;
|
||||||
|
float fog_near = 10.0f;
|
||||||
|
float fog_far = 100.0f;
|
||||||
|
int visible = 1;
|
||||||
|
int z_index = 0;
|
||||||
|
const char* name = nullptr;
|
||||||
|
|
||||||
|
if (!PyArg_ParseTupleAndKeywords(args, kwds, "|OOOfOOOppppOffpis", const_cast<char**>(kwlist),
|
||||||
|
&pos_obj, &size_obj, &render_res_obj, &fov,
|
||||||
|
&camera_pos_obj, &camera_target_obj, &bg_color_obj,
|
||||||
|
&enable_vertex_snap, &enable_affine, &enable_dither, &enable_fog,
|
||||||
|
&fog_color_obj, &fog_near, &fog_far,
|
||||||
|
&visible, &z_index, &name)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position
|
||||||
|
if (pos_obj && pos_obj != Py_None) {
|
||||||
|
PyVectorObject* vec = PyVector::from_arg(pos_obj);
|
||||||
|
if (!vec) {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "pos must be a tuple (x, y)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
self->data->position = vec->data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size
|
||||||
|
if (size_obj && size_obj != Py_None) {
|
||||||
|
float w, h;
|
||||||
|
if (PyTuple_Check(size_obj) && PyTuple_Size(size_obj) == 2) {
|
||||||
|
w = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(size_obj, 0)));
|
||||||
|
h = static_cast<float>(PyFloat_AsDouble(PyTuple_GetItem(size_obj, 1)));
|
||||||
|
self->data->setSize(w, h);
|
||||||
|
} else {
|
||||||
|
PyErr_SetString(PyExc_TypeError, "size must be a tuple (width, height)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render resolution
|
||||||
|
if (render_res_obj && render_res_obj != Py_None) {
|
||||||
|
int rw, rh;
|
||||||
|
if (PyTuple_Check(render_res_obj) && PyTuple_Size(render_res_obj) == 2) {
|
||||||
|
rw = static_cast<int>(PyLong_AsLong(PyTuple_GetItem(render_res_obj, 0)));
|
||||||
|
rh = static_cast<int>(PyLong_AsLong(PyTuple_GetItem(render_res_obj, 1)));
|
||||||
|
self->data->setInternalResolution(rw, rh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FOV
|
||||||
|
self->data->getCamera().setFOV(fov);
|
||||||
|
|
||||||
|
// Camera position
|
||||||
|
if (camera_pos_obj && camera_pos_obj != Py_None) {
|
||||||
|
mcrf::vec3 cam_pos;
|
||||||
|
if (PyTuple_GetVec3(camera_pos_obj, cam_pos)) {
|
||||||
|
self->data->setCameraPosition(cam_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Camera target
|
||||||
|
if (camera_target_obj && camera_target_obj != Py_None) {
|
||||||
|
mcrf::vec3 cam_target;
|
||||||
|
if (PyTuple_GetVec3(camera_target_obj, cam_target)) {
|
||||||
|
self->data->setCameraTarget(cam_target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Background color
|
||||||
|
if (bg_color_obj && bg_color_obj != Py_None) {
|
||||||
|
sf::Color bg = PyColor::fromPy(bg_color_obj);
|
||||||
|
if (!PyErr_Occurred()) {
|
||||||
|
self->data->setBackgroundColor(bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PS1 effects
|
||||||
|
self->data->setVertexSnapEnabled(enable_vertex_snap);
|
||||||
|
self->data->setAffineMappingEnabled(enable_affine);
|
||||||
|
self->data->setDitheringEnabled(enable_dither);
|
||||||
|
self->data->setFogEnabled(enable_fog);
|
||||||
|
|
||||||
|
// Fog color
|
||||||
|
if (fog_color_obj && fog_color_obj != Py_None) {
|
||||||
|
sf::Color fc = PyColor::fromPy(fog_color_obj);
|
||||||
|
if (!PyErr_Occurred()) {
|
||||||
|
self->data->setFogColor(fc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fog range
|
||||||
|
self->data->setFogRange(fog_near, fog_far);
|
||||||
|
|
||||||
|
// Common properties
|
||||||
|
self->data->visible = visible;
|
||||||
|
self->data->z_index = z_index;
|
||||||
|
if (name) {
|
||||||
|
self->data->name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undef PyObjectType
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
|
|
||||||
|
// Methods array (outside namespace)
|
||||||
|
PyMethodDef Viewport3D_methods[] = {
|
||||||
|
// Add UIDRAWABLE_METHODS when ready
|
||||||
|
{NULL} // Sentinel
|
||||||
|
};
|
||||||
272
src/3d/Viewport3D.h
Normal file
272
src/3d/Viewport3D.h
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
// Viewport3D.h - 3D rendering viewport for McRogueFace
|
||||||
|
// A UIDrawable that renders a 3D scene to an FBO and displays it
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Common.h"
|
||||||
|
#include "Python.h"
|
||||||
|
#include "structmember.h"
|
||||||
|
#include "UIDrawable.h"
|
||||||
|
#include "UIBase.h"
|
||||||
|
#include "PyDrawable.h"
|
||||||
|
#include "Math3D.h"
|
||||||
|
#include "Camera3D.h"
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
// Forward declarations
|
||||||
|
class Viewport3D;
|
||||||
|
class Shader3D;
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
|
|
||||||
|
// Python object struct
|
||||||
|
typedef struct {
|
||||||
|
PyObject_HEAD
|
||||||
|
std::shared_ptr<mcrf::Viewport3D> data;
|
||||||
|
PyObject* weakreflist;
|
||||||
|
} PyViewport3DObject;
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Viewport3D - 3D rendering viewport as a UIDrawable
|
||||||
|
// Renders 3D content to an FBO, then blits to screen at display size
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
class Viewport3D : public UIDrawable {
|
||||||
|
public:
|
||||||
|
Viewport3D();
|
||||||
|
Viewport3D(float x, float y, float width, float height);
|
||||||
|
~Viewport3D();
|
||||||
|
|
||||||
|
// UIDrawable interface
|
||||||
|
void render(sf::Vector2f offset, sf::RenderTarget& target) override final;
|
||||||
|
PyObjectsEnum derived_type() override final;
|
||||||
|
UIDrawable* click_at(sf::Vector2f point) override final;
|
||||||
|
sf::FloatRect get_bounds() const override;
|
||||||
|
void move(float dx, float dy) override;
|
||||||
|
void resize(float w, float h) override;
|
||||||
|
|
||||||
|
// Size (screen display size)
|
||||||
|
void setSize(float width, float height);
|
||||||
|
float getWidth() const { return size_.x; }
|
||||||
|
float getHeight() const { return size_.y; }
|
||||||
|
|
||||||
|
// Internal resolution (PS1 style: render at low res, upscale)
|
||||||
|
void setInternalResolution(int width, int height);
|
||||||
|
int getInternalWidth() const { return internalWidth_; }
|
||||||
|
int getInternalHeight() const { return internalHeight_; }
|
||||||
|
|
||||||
|
// Camera access
|
||||||
|
Camera3D& getCamera() { return camera_; }
|
||||||
|
const Camera3D& getCamera() const { return camera_; }
|
||||||
|
|
||||||
|
// Camera convenience methods (exposed to Python)
|
||||||
|
void setCameraPosition(const vec3& pos) { camera_.setPosition(pos); }
|
||||||
|
void setCameraTarget(const vec3& target) { camera_.setTarget(target); }
|
||||||
|
vec3 getCameraPosition() const { return camera_.getPosition(); }
|
||||||
|
vec3 getCameraTarget() const { return camera_.getTarget(); }
|
||||||
|
|
||||||
|
// Background color
|
||||||
|
void setBackgroundColor(const sf::Color& color) { bgColor_ = color; }
|
||||||
|
sf::Color getBackgroundColor() const { return bgColor_; }
|
||||||
|
|
||||||
|
// PS1 effect settings
|
||||||
|
void setVertexSnapEnabled(bool enable) { vertexSnapEnabled_ = enable; }
|
||||||
|
bool isVertexSnapEnabled() const { return vertexSnapEnabled_; }
|
||||||
|
|
||||||
|
void setAffineMappingEnabled(bool enable) { affineMappingEnabled_ = enable; }
|
||||||
|
bool isAffineMappingEnabled() const { return affineMappingEnabled_; }
|
||||||
|
|
||||||
|
void setDitheringEnabled(bool enable) { ditheringEnabled_ = enable; }
|
||||||
|
bool isDitheringEnabled() const { return ditheringEnabled_; }
|
||||||
|
|
||||||
|
void setFogEnabled(bool enable) { fogEnabled_ = enable; }
|
||||||
|
bool isFogEnabled() const { return fogEnabled_; }
|
||||||
|
void setFogColor(const sf::Color& color);
|
||||||
|
sf::Color getFogColor() const;
|
||||||
|
void setFogRange(float nearDist, float farDist);
|
||||||
|
float getFogNear() const { return fogNear_; }
|
||||||
|
float getFogFar() const { return fogFar_; }
|
||||||
|
|
||||||
|
// Animation property system
|
||||||
|
bool setProperty(const std::string& name, float value) override;
|
||||||
|
bool setProperty(const std::string& name, const sf::Color& value) override;
|
||||||
|
bool setProperty(const std::string& name, const sf::Vector2f& value) override;
|
||||||
|
|
||||||
|
bool getProperty(const std::string& name, float& value) const override;
|
||||||
|
bool getProperty(const std::string& name, sf::Color& value) const override;
|
||||||
|
bool getProperty(const std::string& name, sf::Vector2f& value) const override;
|
||||||
|
|
||||||
|
bool hasProperty(const std::string& name) const override;
|
||||||
|
|
||||||
|
// Python API
|
||||||
|
static PyGetSetDef getsetters[];
|
||||||
|
static PyObject* repr(PyViewport3DObject* self);
|
||||||
|
static int init(PyViewport3DObject* self, PyObject* args, PyObject* kwds);
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Display size (screen coordinates)
|
||||||
|
sf::Vector2f size_;
|
||||||
|
|
||||||
|
// Internal render target dimensions (PS1 was 320x240)
|
||||||
|
int internalWidth_ = 320;
|
||||||
|
int internalHeight_ = 240;
|
||||||
|
|
||||||
|
// FBO for render-to-texture
|
||||||
|
unsigned int fbo_ = 0;
|
||||||
|
unsigned int colorTexture_ = 0;
|
||||||
|
unsigned int depthRenderbuffer_ = 0;
|
||||||
|
|
||||||
|
// Camera
|
||||||
|
Camera3D camera_;
|
||||||
|
|
||||||
|
// Background color
|
||||||
|
sf::Color bgColor_ = sf::Color(25, 25, 50);
|
||||||
|
|
||||||
|
// PS1 effect flags
|
||||||
|
bool vertexSnapEnabled_ = true;
|
||||||
|
bool affineMappingEnabled_ = true;
|
||||||
|
bool ditheringEnabled_ = true;
|
||||||
|
bool fogEnabled_ = true;
|
||||||
|
|
||||||
|
// Fog parameters
|
||||||
|
vec3 fogColor_ = vec3(0.5f, 0.5f, 0.6f);
|
||||||
|
float fogNear_ = 10.0f;
|
||||||
|
float fogFar_ = 100.0f;
|
||||||
|
|
||||||
|
// Render test geometry (temporary until Entity3D/MeshLayer added)
|
||||||
|
float testRotation_ = 0.0f;
|
||||||
|
|
||||||
|
// Shader for PS1-style rendering
|
||||||
|
std::unique_ptr<Shader3D> shader_;
|
||||||
|
|
||||||
|
// Test geometry VBO (cube)
|
||||||
|
unsigned int testVBO_ = 0;
|
||||||
|
unsigned int testVertexCount_ = 0;
|
||||||
|
|
||||||
|
// SFML texture for blitting (wraps GL texture)
|
||||||
|
std::unique_ptr<sf::Texture> blitTexture_;
|
||||||
|
|
||||||
|
// Initialize/cleanup FBO
|
||||||
|
void initFBO();
|
||||||
|
void cleanupFBO();
|
||||||
|
|
||||||
|
// Initialize shader and test geometry
|
||||||
|
void initShader();
|
||||||
|
void initTestGeometry();
|
||||||
|
void cleanupTestGeometry();
|
||||||
|
|
||||||
|
// Render 3D content to FBO
|
||||||
|
void render3DContent();
|
||||||
|
|
||||||
|
// Blit FBO to screen
|
||||||
|
void blitToScreen(sf::Vector2f offset, sf::RenderTarget& target);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mcrf
|
||||||
|
|
||||||
|
// Forward declaration of methods array
|
||||||
|
extern PyMethodDef Viewport3D_methods[];
|
||||||
|
|
||||||
|
namespace mcrfpydef {
|
||||||
|
|
||||||
|
static PyTypeObject PyViewport3DType = {
|
||||||
|
.ob_base = {.ob_base = {.ob_refcnt = 1, .ob_type = NULL}, .ob_size = 0},
|
||||||
|
.tp_name = "mcrfpy.Viewport3D",
|
||||||
|
.tp_basicsize = sizeof(PyViewport3DObject),
|
||||||
|
.tp_itemsize = 0,
|
||||||
|
.tp_dealloc = (destructor)[](PyObject* self)
|
||||||
|
{
|
||||||
|
PyViewport3DObject* obj = (PyViewport3DObject*)self;
|
||||||
|
PyObject_GC_UnTrack(self);
|
||||||
|
if (obj->weakreflist != NULL) {
|
||||||
|
PyObject_ClearWeakRefs(self);
|
||||||
|
}
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
|
obj->data.reset();
|
||||||
|
Py_TYPE(self)->tp_free(self);
|
||||||
|
},
|
||||||
|
.tp_repr = (reprfunc)mcrf::Viewport3D::repr,
|
||||||
|
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
|
||||||
|
.tp_doc = PyDoc_STR("Viewport3D(pos=None, size=None, **kwargs)\n\n"
|
||||||
|
"A 3D rendering viewport that displays a 3D scene as a UI element.\n\n"
|
||||||
|
"Args:\n"
|
||||||
|
" pos (tuple, optional): Position as (x, y) tuple. Default: (0, 0)\n"
|
||||||
|
" size (tuple, optional): Display size as (width, height). Default: (320, 240)\n\n"
|
||||||
|
"Keyword Args:\n"
|
||||||
|
" render_resolution (tuple): Internal render resolution (width, height). Default: (320, 240)\n"
|
||||||
|
" fov (float): Camera field of view in degrees. Default: 60\n"
|
||||||
|
" camera_pos (tuple): Camera position (x, y, z). Default: (0, 0, 5)\n"
|
||||||
|
" camera_target (tuple): Camera look-at point (x, y, z). Default: (0, 0, 0)\n"
|
||||||
|
" bg_color (Color): Background clear color. Default: (25, 25, 50)\n"
|
||||||
|
" enable_vertex_snap (bool): PS1-style vertex snapping. Default: True\n"
|
||||||
|
" enable_affine (bool): PS1-style affine texture mapping. Default: True\n"
|
||||||
|
" enable_dither (bool): PS1-style color dithering. Default: True\n"
|
||||||
|
" enable_fog (bool): Distance fog. Default: True\n"
|
||||||
|
" fog_color (Color): Fog color. Default: (128, 128, 153)\n"
|
||||||
|
" fog_near (float): Fog start distance. Default: 10\n"
|
||||||
|
" fog_far (float): Fog end distance. Default: 100\n"),
|
||||||
|
.tp_traverse = [](PyObject* self, visitproc visit, void* arg) -> int {
|
||||||
|
PyViewport3DObject* obj = (PyViewport3DObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
if (obj->data->click_callable) {
|
||||||
|
PyObject* callback = obj->data->click_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) {
|
||||||
|
Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj->data->on_enter_callable) {
|
||||||
|
PyObject* callback = obj->data->on_enter_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) {
|
||||||
|
Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj->data->on_exit_callable) {
|
||||||
|
PyObject* callback = obj->data->on_exit_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) {
|
||||||
|
Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj->data->on_move_callable) {
|
||||||
|
PyObject* callback = obj->data->on_move_callable->borrow();
|
||||||
|
if (callback && callback != Py_None) {
|
||||||
|
Py_VISIT(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
.tp_clear = [](PyObject* self) -> int {
|
||||||
|
PyViewport3DObject* obj = (PyViewport3DObject*)self;
|
||||||
|
if (obj->data) {
|
||||||
|
obj->data->click_unregister();
|
||||||
|
obj->data->on_enter_unregister();
|
||||||
|
obj->data->on_exit_unregister();
|
||||||
|
obj->data->on_move_unregister();
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
.tp_methods = Viewport3D_methods,
|
||||||
|
.tp_getset = mcrf::Viewport3D::getsetters,
|
||||||
|
.tp_base = &mcrfpydef::PyDrawableType,
|
||||||
|
.tp_init = (initproc)mcrf::Viewport3D::init,
|
||||||
|
.tp_new = [](PyTypeObject* type, PyObject* args, PyObject* kwds) -> PyObject*
|
||||||
|
{
|
||||||
|
PyViewport3DObject* self = (PyViewport3DObject*)type->tp_alloc(type, 0);
|
||||||
|
if (self) {
|
||||||
|
self->data = std::make_shared<mcrf::Viewport3D>();
|
||||||
|
self->weakreflist = nullptr;
|
||||||
|
}
|
||||||
|
return (PyObject*)self;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mcrfpydef
|
||||||
1018
src/3d/glad.c
Normal file
1018
src/3d/glad.c
Normal file
File diff suppressed because it is too large
Load diff
1935
src/3d/glad/glad.h
Normal file
1935
src/3d/glad/glad.h
Normal file
File diff suppressed because it is too large
Load diff
2
src/3d/shaders/.gitkeep
Normal file
2
src/3d/shaders/.gitkeep
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Placeholder for shader files
|
||||||
|
# PS1-style shaders will be added in Milestone 1
|
||||||
90
src/3d/shaders/ps1_fragment.glsl
Normal file
90
src/3d/shaders/ps1_fragment.glsl
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
// PS1-style fragment shader for OpenGL 3.2+
|
||||||
|
// Implements affine texture mapping, fog, color quantization, and dithering
|
||||||
|
|
||||||
|
#version 150 core
|
||||||
|
|
||||||
|
// Uniforms - texturing
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
uniform bool u_has_texture; // Whether to use texture or just vertex color
|
||||||
|
|
||||||
|
// Uniforms - PS1 effects
|
||||||
|
uniform bool u_enable_dither; // Enable ordered dithering
|
||||||
|
uniform vec3 u_fog_color; // Fog color (usually matches background)
|
||||||
|
|
||||||
|
// Varyings from vertex shader
|
||||||
|
in vec4 v_color; // Gouraud-shaded vertex color
|
||||||
|
noperspective in vec2 v_texcoord; // Texture coordinates (affine interpolation!)
|
||||||
|
in float v_fog; // Fog factor
|
||||||
|
|
||||||
|
// Output
|
||||||
|
out vec4 fragColor;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 4x4 Bayer Dithering Matrix
|
||||||
|
// Used to add ordered noise for color quantization, reducing banding
|
||||||
|
// =========================================================================
|
||||||
|
const int bayerMatrix[16] = int[16](
|
||||||
|
0, 8, 2, 10,
|
||||||
|
12, 4, 14, 6,
|
||||||
|
3, 11, 1, 9,
|
||||||
|
15, 7, 13, 5
|
||||||
|
);
|
||||||
|
|
||||||
|
float getBayerValue(vec2 fragCoord) {
|
||||||
|
int x = int(mod(fragCoord.x, 4.0));
|
||||||
|
int y = int(mod(fragCoord.y, 4.0));
|
||||||
|
return float(bayerMatrix[y * 4 + x]) / 16.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 15-bit Color Quantization
|
||||||
|
// PS1 had 15-bit color (5 bits per channel), creating visible color banding
|
||||||
|
// =========================================================================
|
||||||
|
vec3 quantize15bit(vec3 color) {
|
||||||
|
// Quantize to 5 bits per channel (32 levels)
|
||||||
|
return floor(color * 31.0 + 0.5) / 31.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Sample texture or use vertex color
|
||||||
|
vec4 color;
|
||||||
|
if (u_has_texture) {
|
||||||
|
vec4 texColor = texture(u_texture, v_texcoord);
|
||||||
|
|
||||||
|
// Binary alpha test (PS1 style - no alpha blending)
|
||||||
|
if (texColor.a < 0.5) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
color = texColor * v_color;
|
||||||
|
} else {
|
||||||
|
color = v_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Color Quantization with Dithering
|
||||||
|
// Reduce color depth to 15-bit, using dithering to reduce banding
|
||||||
|
// =========================================================================
|
||||||
|
if (u_enable_dither) {
|
||||||
|
// Get Bayer dither threshold for this pixel
|
||||||
|
float threshold = getBayerValue(gl_FragCoord.xy);
|
||||||
|
|
||||||
|
// Apply dither before quantization
|
||||||
|
// Threshold is in range [0, 1), we scale it to affect quantization
|
||||||
|
vec3 dithered = color.rgb + (threshold - 0.5) / 31.0;
|
||||||
|
|
||||||
|
// Quantize to 15-bit
|
||||||
|
color.rgb = quantize15bit(dithered);
|
||||||
|
} else {
|
||||||
|
// Just quantize without dithering
|
||||||
|
color.rgb = quantize15bit(color.rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Fog Application
|
||||||
|
// Linear fog blending based on depth
|
||||||
|
// =========================================================================
|
||||||
|
color.rgb = mix(color.rgb, u_fog_color, v_fog);
|
||||||
|
|
||||||
|
fragColor = color;
|
||||||
|
}
|
||||||
120
src/3d/shaders/ps1_fragment_es2.glsl
Normal file
120
src/3d/shaders/ps1_fragment_es2.glsl
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
// PS1-style fragment shader for OpenGL ES 2.0 / WebGL 1.0
|
||||||
|
// Implements affine texture mapping, fog, color quantization, and dithering
|
||||||
|
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
// Uniforms - texturing
|
||||||
|
uniform sampler2D u_texture;
|
||||||
|
uniform bool u_has_texture; // Whether to use texture or just vertex color
|
||||||
|
|
||||||
|
// Uniforms - PS1 effects
|
||||||
|
uniform bool u_enable_dither; // Enable ordered dithering
|
||||||
|
uniform vec3 u_fog_color; // Fog color (usually matches background)
|
||||||
|
|
||||||
|
// Varyings from vertex shader
|
||||||
|
varying vec4 v_color; // Gouraud-shaded vertex color
|
||||||
|
varying vec2 v_texcoord; // Texture coordinates (pre-multiplied by w)
|
||||||
|
varying float v_w; // Clip space w for affine restoration
|
||||||
|
varying float v_fog; // Fog factor
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 4x4 Bayer Dithering Matrix
|
||||||
|
// Used to add ordered noise for color quantization, reducing banding
|
||||||
|
// =========================================================================
|
||||||
|
const mat4 bayerMatrix = mat4(
|
||||||
|
0.0/16.0, 8.0/16.0, 2.0/16.0, 10.0/16.0,
|
||||||
|
12.0/16.0, 4.0/16.0, 14.0/16.0, 6.0/16.0,
|
||||||
|
3.0/16.0, 11.0/16.0, 1.0/16.0, 9.0/16.0,
|
||||||
|
15.0/16.0, 7.0/16.0, 13.0/16.0, 5.0/16.0
|
||||||
|
);
|
||||||
|
|
||||||
|
float getBayerValue(vec2 fragCoord) {
|
||||||
|
int x = int(mod(fragCoord.x, 4.0));
|
||||||
|
int y = int(mod(fragCoord.y, 4.0));
|
||||||
|
|
||||||
|
// Manual matrix lookup (GLES2 doesn't support integer indexing of mat4)
|
||||||
|
if (y == 0) {
|
||||||
|
if (x == 0) return 0.0/16.0;
|
||||||
|
if (x == 1) return 8.0/16.0;
|
||||||
|
if (x == 2) return 2.0/16.0;
|
||||||
|
return 10.0/16.0;
|
||||||
|
}
|
||||||
|
if (y == 1) {
|
||||||
|
if (x == 0) return 12.0/16.0;
|
||||||
|
if (x == 1) return 4.0/16.0;
|
||||||
|
if (x == 2) return 14.0/16.0;
|
||||||
|
return 6.0/16.0;
|
||||||
|
}
|
||||||
|
if (y == 2) {
|
||||||
|
if (x == 0) return 3.0/16.0;
|
||||||
|
if (x == 1) return 11.0/16.0;
|
||||||
|
if (x == 2) return 1.0/16.0;
|
||||||
|
return 9.0/16.0;
|
||||||
|
}
|
||||||
|
// y == 3
|
||||||
|
if (x == 0) return 15.0/16.0;
|
||||||
|
if (x == 1) return 7.0/16.0;
|
||||||
|
if (x == 2) return 13.0/16.0;
|
||||||
|
return 5.0/16.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 15-bit Color Quantization
|
||||||
|
// PS1 had 15-bit color (5 bits per channel), creating visible color banding
|
||||||
|
// =========================================================================
|
||||||
|
vec3 quantize15bit(vec3 color) {
|
||||||
|
// Quantize to 5 bits per channel (32 levels)
|
||||||
|
return floor(color * 31.0 + 0.5) / 31.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Affine Texture Mapping
|
||||||
|
// Divide by interpolated w to restore texture coordinates.
|
||||||
|
// Because w was interpolated linearly (not perspectively), this creates
|
||||||
|
// the characteristic texture warping on PS1.
|
||||||
|
// =========================================================================
|
||||||
|
vec2 uv = v_texcoord / v_w;
|
||||||
|
|
||||||
|
// Sample texture or use vertex color
|
||||||
|
vec4 color;
|
||||||
|
if (u_has_texture) {
|
||||||
|
vec4 texColor = texture2D(u_texture, uv);
|
||||||
|
|
||||||
|
// Binary alpha test (PS1 style - no alpha blending)
|
||||||
|
if (texColor.a < 0.5) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
color = texColor * v_color;
|
||||||
|
} else {
|
||||||
|
color = v_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Color Quantization with Dithering
|
||||||
|
// Reduce color depth to 15-bit, using dithering to reduce banding
|
||||||
|
// =========================================================================
|
||||||
|
if (u_enable_dither) {
|
||||||
|
// Get Bayer dither threshold for this pixel
|
||||||
|
float threshold = getBayerValue(gl_FragCoord.xy);
|
||||||
|
|
||||||
|
// Apply dither before quantization
|
||||||
|
// Threshold is in range [0, 1), we scale it to affect quantization
|
||||||
|
vec3 dithered = color.rgb + (threshold - 0.5) / 31.0;
|
||||||
|
|
||||||
|
// Quantize to 15-bit
|
||||||
|
color.rgb = quantize15bit(dithered);
|
||||||
|
} else {
|
||||||
|
// Just quantize without dithering
|
||||||
|
color.rgb = quantize15bit(color.rgb);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Fog Application
|
||||||
|
// Linear fog blending based on depth
|
||||||
|
// =========================================================================
|
||||||
|
color.rgb = mix(color.rgb, u_fog_color, v_fog);
|
||||||
|
|
||||||
|
gl_FragColor = color;
|
||||||
|
}
|
||||||
87
src/3d/shaders/ps1_vertex.glsl
Normal file
87
src/3d/shaders/ps1_vertex.glsl
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
// PS1-style vertex shader for OpenGL 3.2+
|
||||||
|
// Implements vertex snapping, Gouraud shading, and fog distance calculation
|
||||||
|
|
||||||
|
#version 150 core
|
||||||
|
|
||||||
|
// Uniforms - transform matrices
|
||||||
|
uniform mat4 u_model;
|
||||||
|
uniform mat4 u_view;
|
||||||
|
uniform mat4 u_projection;
|
||||||
|
|
||||||
|
// Uniforms - PS1 effects
|
||||||
|
uniform vec2 u_resolution; // Internal render resolution for vertex snapping
|
||||||
|
uniform bool u_enable_snap; // Enable vertex snapping to pixel grid
|
||||||
|
uniform float u_fog_start; // Fog start distance
|
||||||
|
uniform float u_fog_end; // Fog end distance
|
||||||
|
|
||||||
|
// Uniforms - lighting
|
||||||
|
uniform vec3 u_light_dir; // Directional light direction (normalized)
|
||||||
|
uniform vec3 u_ambient; // Ambient light color
|
||||||
|
|
||||||
|
// Attributes
|
||||||
|
in vec3 a_position;
|
||||||
|
in vec2 a_texcoord;
|
||||||
|
in vec3 a_normal;
|
||||||
|
in vec4 a_color;
|
||||||
|
|
||||||
|
// Varyings - passed to fragment shader
|
||||||
|
out vec4 v_color; // Gouraud-shaded vertex color
|
||||||
|
noperspective out vec2 v_texcoord; // Texture coordinates (affine interpolation!)
|
||||||
|
out float v_fog; // Fog factor (0 = no fog, 1 = full fog)
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Transform vertex to clip space
|
||||||
|
vec4 worldPos = u_model * vec4(a_position, 1.0);
|
||||||
|
vec4 viewPos = u_view * worldPos;
|
||||||
|
vec4 clipPos = u_projection * viewPos;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Vertex Snapping
|
||||||
|
// The PS1 had limited precision for vertex positions, causing vertices
|
||||||
|
// to "snap" to a grid, creating the characteristic jittery look.
|
||||||
|
// =========================================================================
|
||||||
|
if (u_enable_snap) {
|
||||||
|
// Convert to NDC
|
||||||
|
vec4 ndc = clipPos;
|
||||||
|
ndc.xyz /= ndc.w;
|
||||||
|
|
||||||
|
// Snap to pixel grid based on render resolution
|
||||||
|
vec2 grid = u_resolution * 0.5;
|
||||||
|
ndc.xy = floor(ndc.xy * grid + 0.5) / grid;
|
||||||
|
|
||||||
|
// Convert back to clip space
|
||||||
|
ndc.xyz *= clipPos.w;
|
||||||
|
clipPos = ndc;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_Position = clipPos;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Gouraud Shading
|
||||||
|
// Per-vertex lighting was used on PS1 due to hardware limitations.
|
||||||
|
// This creates characteristic flat-shaded polygons.
|
||||||
|
// =========================================================================
|
||||||
|
vec3 worldNormal = mat3(u_model) * a_normal;
|
||||||
|
worldNormal = normalize(worldNormal);
|
||||||
|
|
||||||
|
// Simple directional light + ambient
|
||||||
|
float diffuse = max(dot(worldNormal, -u_light_dir), 0.0);
|
||||||
|
vec3 lighting = u_ambient + vec3(diffuse);
|
||||||
|
|
||||||
|
// Apply lighting to vertex color
|
||||||
|
v_color = vec4(a_color.rgb * lighting, a_color.a);
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Affine Texture Mapping
|
||||||
|
// Using 'noperspective' qualifier disables perspective-correct interpolation
|
||||||
|
// This creates the characteristic texture warping on large polygons
|
||||||
|
// =========================================================================
|
||||||
|
v_texcoord = a_texcoord;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Fog Distance Calculation
|
||||||
|
// Calculate linear fog factor based on view-space depth
|
||||||
|
// =========================================================================
|
||||||
|
float depth = -viewPos.z; // View space depth (positive)
|
||||||
|
v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0);
|
||||||
|
}
|
||||||
90
src/3d/shaders/ps1_vertex_es2.glsl
Normal file
90
src/3d/shaders/ps1_vertex_es2.glsl
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
// PS1-style vertex shader for OpenGL ES 2.0 / WebGL 1.0
|
||||||
|
// Implements vertex snapping, Gouraud shading, and fog distance calculation
|
||||||
|
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
|
// Uniforms - transform matrices
|
||||||
|
uniform mat4 u_model;
|
||||||
|
uniform mat4 u_view;
|
||||||
|
uniform mat4 u_projection;
|
||||||
|
|
||||||
|
// Uniforms - PS1 effects
|
||||||
|
uniform vec2 u_resolution; // Internal render resolution for vertex snapping
|
||||||
|
uniform bool u_enable_snap; // Enable vertex snapping to pixel grid
|
||||||
|
uniform float u_fog_start; // Fog start distance
|
||||||
|
uniform float u_fog_end; // Fog end distance
|
||||||
|
|
||||||
|
// Uniforms - lighting
|
||||||
|
uniform vec3 u_light_dir; // Directional light direction (normalized)
|
||||||
|
uniform vec3 u_ambient; // Ambient light color
|
||||||
|
|
||||||
|
// Attributes
|
||||||
|
attribute vec3 a_position;
|
||||||
|
attribute vec2 a_texcoord;
|
||||||
|
attribute vec3 a_normal;
|
||||||
|
attribute vec4 a_color;
|
||||||
|
|
||||||
|
// Varyings - passed to fragment shader
|
||||||
|
varying vec4 v_color; // Gouraud-shaded vertex color
|
||||||
|
varying vec2 v_texcoord; // Texture coordinates (multiplied by w for affine trick)
|
||||||
|
varying float v_w; // Clip space w for affine mapping restoration
|
||||||
|
varying float v_fog; // Fog factor (0 = no fog, 1 = full fog)
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
// Transform vertex to clip space
|
||||||
|
vec4 worldPos = u_model * vec4(a_position, 1.0);
|
||||||
|
vec4 viewPos = u_view * worldPos;
|
||||||
|
vec4 clipPos = u_projection * viewPos;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Vertex Snapping
|
||||||
|
// The PS1 had limited precision for vertex positions, causing vertices
|
||||||
|
// to "snap" to a grid, creating the characteristic jittery look.
|
||||||
|
// =========================================================================
|
||||||
|
if (u_enable_snap) {
|
||||||
|
// Convert to NDC
|
||||||
|
vec4 ndc = clipPos;
|
||||||
|
ndc.xyz /= ndc.w;
|
||||||
|
|
||||||
|
// Snap to pixel grid based on render resolution
|
||||||
|
vec2 grid = u_resolution * 0.5;
|
||||||
|
ndc.xy = floor(ndc.xy * grid + 0.5) / grid;
|
||||||
|
|
||||||
|
// Convert back to clip space
|
||||||
|
ndc.xyz *= clipPos.w;
|
||||||
|
clipPos = ndc;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_Position = clipPos;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Gouraud Shading
|
||||||
|
// Per-vertex lighting was used on PS1 due to hardware limitations.
|
||||||
|
// This creates characteristic flat-shaded polygons.
|
||||||
|
// =========================================================================
|
||||||
|
vec3 worldNormal = mat3(u_model) * a_normal;
|
||||||
|
worldNormal = normalize(worldNormal);
|
||||||
|
|
||||||
|
// Simple directional light + ambient
|
||||||
|
float diffuse = max(dot(worldNormal, -u_light_dir), 0.0);
|
||||||
|
vec3 lighting = u_ambient + vec3(diffuse);
|
||||||
|
|
||||||
|
// Apply lighting to vertex color
|
||||||
|
v_color = vec4(a_color.rgb * lighting, a_color.a);
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// PS1 Effect: Affine Texture Mapping Trick
|
||||||
|
// GLES2 doesn't have 'noperspective' interpolation, so we manually
|
||||||
|
// multiply texcoords by w here and divide by w in fragment shader.
|
||||||
|
// This creates the characteristic texture warping on large polygons.
|
||||||
|
// =========================================================================
|
||||||
|
v_texcoord = a_texcoord * clipPos.w;
|
||||||
|
v_w = clipPos.w;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Fog Distance Calculation
|
||||||
|
// Calculate linear fog factor based on view-space depth
|
||||||
|
// =========================================================================
|
||||||
|
float depth = -viewPos.z; // View space depth (positive)
|
||||||
|
v_fog = clamp((depth - u_fog_start) / (u_fog_end - u_fog_start), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include "Animation.h"
|
#include "Animation.h"
|
||||||
#include "Timer.h"
|
#include "Timer.h"
|
||||||
#include "BenchmarkLogger.h"
|
#include "BenchmarkLogger.h"
|
||||||
|
#include "platform/GLContext.h"
|
||||||
// ImGui is only available for SFML builds (not headless, not SDL2)
|
// ImGui is only available for SFML builds (not headless, not SDL2)
|
||||||
#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2)
|
#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2)
|
||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
|
@ -87,6 +88,12 @@ GameEngine::GameEngine(const McRogueFaceConfig& cfg)
|
||||||
window->setFramerateLimit(60);
|
window->setFramerateLimit(60);
|
||||||
render_target = window.get();
|
render_target = window.get();
|
||||||
|
|
||||||
|
// Initialize OpenGL function pointers via GLAD for 3D rendering
|
||||||
|
if (!mcrf::gl::initGL()) {
|
||||||
|
// Non-fatal: 3D features will be unavailable but 2D still works
|
||||||
|
fprintf(stderr, "Warning: OpenGL initialization failed. 3D features disabled.\n");
|
||||||
|
}
|
||||||
|
|
||||||
#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2)
|
#if !defined(MCRF_HEADLESS) && !defined(MCRF_SDL2)
|
||||||
// Initialize ImGui for the window (SFML builds only)
|
// Initialize ImGui for the window (SFML builds only)
|
||||||
if (ImGui::SFML::Init(*window)) {
|
if (ImGui::SFML::Init(*window)) {
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
#include "PyShader.h" // Shader support (#106)
|
#include "PyShader.h" // Shader support (#106)
|
||||||
#include "PyUniformBinding.h" // Shader uniform bindings (#106)
|
#include "PyUniformBinding.h" // Shader uniform bindings (#106)
|
||||||
#include "PyUniformCollection.h" // Shader uniform collection (#106)
|
#include "PyUniformCollection.h" // Shader uniform collection (#106)
|
||||||
|
#include "3d/Viewport3D.h" // 3D rendering viewport
|
||||||
#include "McRogueFaceVersion.h"
|
#include "McRogueFaceVersion.h"
|
||||||
#include "GameEngine.h"
|
#include "GameEngine.h"
|
||||||
// ImGui is only available for SFML builds
|
// ImGui is only available for SFML builds
|
||||||
|
|
@ -415,7 +416,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
// This must be done BEFORE PyType_Ready is called on these types
|
// This must be done BEFORE PyType_Ready is called on these types
|
||||||
PyTypeObject* ui_types_with_callbacks[] = {
|
PyTypeObject* ui_types_with_callbacks[] = {
|
||||||
&PyUIFrameType, &PyUICaptionType, &PyUISpriteType, &PyUIGridType,
|
&PyUIFrameType, &PyUICaptionType, &PyUISpriteType, &PyUIGridType,
|
||||||
&PyUILineType, &PyUICircleType, &PyUIArcType,
|
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
|
||||||
nullptr
|
nullptr
|
||||||
};
|
};
|
||||||
for (int i = 0; ui_types_with_callbacks[i] != nullptr; i++) {
|
for (int i = 0; ui_types_with_callbacks[i] != nullptr; i++) {
|
||||||
|
|
@ -432,7 +433,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
|
|
||||||
/*UI widgets*/
|
/*UI widgets*/
|
||||||
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
|
&PyUICaptionType, &PyUISpriteType, &PyUIFrameType, &PyUIEntityType, &PyUIGridType,
|
||||||
&PyUILineType, &PyUICircleType, &PyUIArcType,
|
&PyUILineType, &PyUICircleType, &PyUIArcType, &PyViewport3DType,
|
||||||
|
|
||||||
/*grid layers (#147)*/
|
/*grid layers (#147)*/
|
||||||
&PyColorLayerType, &PyTileLayerType,
|
&PyColorLayerType, &PyTileLayerType,
|
||||||
|
|
@ -547,6 +548,7 @@ PyObject* PyInit_mcrfpy()
|
||||||
PyUILineType.tp_weaklistoffset = offsetof(PyUILineObject, weakreflist);
|
PyUILineType.tp_weaklistoffset = offsetof(PyUILineObject, weakreflist);
|
||||||
PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist);
|
PyUICircleType.tp_weaklistoffset = offsetof(PyUICircleObject, weakreflist);
|
||||||
PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist);
|
PyUIArcType.tp_weaklistoffset = offsetof(PyUIArcObject, weakreflist);
|
||||||
|
PyViewport3DType.tp_weaklistoffset = offsetof(PyViewport3DObject, weakreflist);
|
||||||
|
|
||||||
// #219 - Initialize PyLock context manager type
|
// #219 - Initialize PyLock context manager type
|
||||||
if (PyLock::init() < 0) {
|
if (PyLock::init() < 0) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
#include "UILine.h"
|
#include "UILine.h"
|
||||||
#include "UICircle.h"
|
#include "UICircle.h"
|
||||||
#include "UIArc.h"
|
#include "UIArc.h"
|
||||||
|
#include "3d/Viewport3D.h"
|
||||||
#include "McRFPy_API.h"
|
#include "McRFPy_API.h"
|
||||||
#include "PyObjectUtils.h"
|
#include "PyObjectUtils.h"
|
||||||
#include "PythonObjectCache.h"
|
#include "PythonObjectCache.h"
|
||||||
|
|
@ -118,6 +119,18 @@ static PyObject* convertDrawableToPython(std::shared_ptr<UIDrawable> drawable) {
|
||||||
obj = (PyObject*)pyObj;
|
obj = (PyObject*)pyObj;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case PyObjectsEnum::UIVIEWPORT3D:
|
||||||
|
{
|
||||||
|
type = (PyTypeObject*)PyObject_GetAttrString(McRFPy_API::mcrf_module, "Viewport3D");
|
||||||
|
if (!type) return nullptr;
|
||||||
|
auto pyObj = (PyViewport3DObject*)type->tp_alloc(type, 0);
|
||||||
|
if (pyObj) {
|
||||||
|
pyObj->data = std::static_pointer_cast<mcrf::Viewport3D>(drawable);
|
||||||
|
pyObj->weakreflist = NULL;
|
||||||
|
}
|
||||||
|
obj = (PyObject*)pyObj;
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
PyErr_SetString(PyExc_TypeError, "Unknown UIDrawable derived type");
|
PyErr_SetString(PyExc_TypeError, "Unknown UIDrawable derived type");
|
||||||
return nullptr;
|
return nullptr;
|
||||||
|
|
@ -644,10 +657,11 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
|
||||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")) &&
|
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")) &&
|
||||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")) &&
|
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")) &&
|
||||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")) &&
|
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")) &&
|
||||||
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))
|
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc")) &&
|
||||||
|
!PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Viewport3D"))
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
PyErr_SetString(PyExc_TypeError, "Only Frame, Caption, Sprite, Grid, Line, Circle, and Arc objects can be added to UICollection");
|
PyErr_SetString(PyExc_TypeError, "Only Frame, Caption, Sprite, Grid, Line, Circle, Arc, and Viewport3D objects can be added to UICollection");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -713,6 +727,10 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o)
|
||||||
{
|
{
|
||||||
addDrawable(((PyUIArcObject*)o)->data);
|
addDrawable(((PyUIArcObject*)o)->data);
|
||||||
}
|
}
|
||||||
|
if (PyObject_IsInstance(o, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Viewport3D")))
|
||||||
|
{
|
||||||
|
addDrawable(((PyViewport3DObject*)o)->data);
|
||||||
|
}
|
||||||
|
|
||||||
// Mark scene as needing resort after adding element
|
// Mark scene as needing resort after adding element
|
||||||
McRFPy_API::markSceneNeedsSort();
|
McRFPy_API::markSceneNeedsSort();
|
||||||
|
|
@ -752,11 +770,12 @@ PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
|
||||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")) &&
|
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid")) &&
|
||||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")) &&
|
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Line")) &&
|
||||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")) &&
|
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Circle")) &&
|
||||||
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc")))
|
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc")) &&
|
||||||
|
!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Viewport3D")))
|
||||||
{
|
{
|
||||||
Py_DECREF(item);
|
Py_DECREF(item);
|
||||||
Py_DECREF(iterator);
|
Py_DECREF(iterator);
|
||||||
PyErr_SetString(PyExc_TypeError, "All items must be Frame, Caption, Sprite, Grid, Line, Circle, or Arc objects");
|
PyErr_SetString(PyExc_TypeError, "All items must be Frame, Caption, Sprite, Grid, Line, Circle, Arc, or Viewport3D objects");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -803,6 +822,9 @@ PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable)
|
||||||
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))) {
|
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Arc"))) {
|
||||||
addDrawable(((PyUIArcObject*)item)->data);
|
addDrawable(((PyUIArcObject*)item)->data);
|
||||||
}
|
}
|
||||||
|
else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Viewport3D"))) {
|
||||||
|
addDrawable(((PyViewport3DObject*)item)->data);
|
||||||
|
}
|
||||||
|
|
||||||
Py_DECREF(item);
|
Py_DECREF(item);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ enum PyObjectsEnum : int
|
||||||
UIGRID,
|
UIGRID,
|
||||||
UILINE,
|
UILINE,
|
||||||
UICIRCLE,
|
UICIRCLE,
|
||||||
UIARC
|
UIARC,
|
||||||
|
UIVIEWPORT3D
|
||||||
};
|
};
|
||||||
|
|
||||||
class UIDrawable
|
class UIDrawable
|
||||||
|
|
|
||||||
113
src/platform/GLContext.h
Normal file
113
src/platform/GLContext.h
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
// GLContext.h - OpenGL context abstraction for McRogueFace 3D
|
||||||
|
// Provides uniform GL access across SFML and SDL2 backends
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../3d/Math3D.h"
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
namespace gl {
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Initialization
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Initialize OpenGL function pointers if needed (GLAD for desktop SFML)
|
||||||
|
// Returns true if GL is ready to use
|
||||||
|
bool initGL();
|
||||||
|
|
||||||
|
// Check if GL is initialized and ready
|
||||||
|
bool isGLReady();
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Framebuffer Object (FBO) Management
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Create a framebuffer with color texture and optional depth renderbuffer
|
||||||
|
// Returns FBO id, sets colorTex to the color attachment texture
|
||||||
|
// depthRB is optional - pass nullptr if depth buffer not needed
|
||||||
|
unsigned int createFramebuffer(int width, int height, unsigned int* colorTex, unsigned int* depthRB = nullptr);
|
||||||
|
|
||||||
|
// Bind a framebuffer for rendering
|
||||||
|
void bindFramebuffer(unsigned int fbo);
|
||||||
|
|
||||||
|
// Bind the default framebuffer (screen)
|
||||||
|
void bindDefaultFramebuffer();
|
||||||
|
|
||||||
|
// Delete a framebuffer and its attachments
|
||||||
|
void deleteFramebuffer(unsigned int fbo, unsigned int colorTex, unsigned int depthRB);
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Shader Compilation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Compile a vertex or fragment shader from source
|
||||||
|
// type should be GL_VERTEX_SHADER or GL_FRAGMENT_SHADER
|
||||||
|
unsigned int compileShader(unsigned int type, const char* source);
|
||||||
|
|
||||||
|
// Link vertex and fragment shaders into a program
|
||||||
|
// Returns program id, or 0 on failure
|
||||||
|
unsigned int linkProgram(unsigned int vertShader, unsigned int fragShader);
|
||||||
|
|
||||||
|
// Delete a shader program
|
||||||
|
void deleteProgram(unsigned int program);
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// GL State Management (for mixing with SFML rendering)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Save current OpenGL state before custom 3D rendering
|
||||||
|
// This includes blend mode, depth test, culling, bound textures, etc.
|
||||||
|
void pushState();
|
||||||
|
|
||||||
|
// Restore OpenGL state after custom 3D rendering
|
||||||
|
void popState();
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 3D Rendering State Setup
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Set up GL state for 3D rendering (depth test, culling, etc.)
|
||||||
|
void setup3DState();
|
||||||
|
|
||||||
|
// Restore GL state for 2D rendering (disable depth, etc.)
|
||||||
|
void restore2DState();
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Depth Buffer Operations
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Enable/disable depth testing
|
||||||
|
void setDepthTest(bool enable);
|
||||||
|
|
||||||
|
// Enable/disable depth writing
|
||||||
|
void setDepthWrite(bool enable);
|
||||||
|
|
||||||
|
// Set depth test function (GL_LESS, GL_LEQUAL, etc.)
|
||||||
|
void setDepthFunc(unsigned int func);
|
||||||
|
|
||||||
|
// Clear the depth buffer
|
||||||
|
void clearDepth();
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Face Culling
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Enable/disable face culling
|
||||||
|
void setCulling(bool enable);
|
||||||
|
|
||||||
|
// Set which face to cull (GL_BACK, GL_FRONT, GL_FRONT_AND_BACK)
|
||||||
|
void setCullFace(unsigned int face);
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Utility
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
// Get last GL error as string (for debugging)
|
||||||
|
const char* getErrorString();
|
||||||
|
|
||||||
|
// Check for GL errors and log them
|
||||||
|
bool checkError(const char* operation);
|
||||||
|
|
||||||
|
} // namespace gl
|
||||||
|
} // namespace mcrf
|
||||||
60
src/platform/GLContext_Headless.cpp
Normal file
60
src/platform/GLContext_Headless.cpp
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
// GLContext_Headless.cpp - Headless backend for OpenGL context abstraction
|
||||||
|
// Returns failure for all operations since there's no GPU
|
||||||
|
|
||||||
|
#ifdef MCRF_HEADLESS
|
||||||
|
|
||||||
|
#include "GLContext.h"
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
namespace gl {
|
||||||
|
|
||||||
|
bool initGL() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isGLReady() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int createFramebuffer(int width, int height, unsigned int* colorTex, unsigned int* depthRB) {
|
||||||
|
if (colorTex) *colorTex = 0;
|
||||||
|
if (depthRB) *depthRB = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bindFramebuffer(unsigned int fbo) {}
|
||||||
|
void bindDefaultFramebuffer() {}
|
||||||
|
void deleteFramebuffer(unsigned int fbo, unsigned int colorTex, unsigned int depthRB) {}
|
||||||
|
|
||||||
|
unsigned int compileShader(unsigned int type, const char* source) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int linkProgram(unsigned int vertShader, unsigned int fragShader) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteProgram(unsigned int program) {}
|
||||||
|
void pushState() {}
|
||||||
|
void popState() {}
|
||||||
|
void setup3DState() {}
|
||||||
|
void restore2DState() {}
|
||||||
|
void setDepthTest(bool enable) {}
|
||||||
|
void setDepthWrite(bool enable) {}
|
||||||
|
void setDepthFunc(unsigned int func) {}
|
||||||
|
void clearDepth() {}
|
||||||
|
void setCulling(bool enable) {}
|
||||||
|
void setCullFace(unsigned int face) {}
|
||||||
|
|
||||||
|
const char* getErrorString() {
|
||||||
|
return "Headless mode - no GL context";
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkError(const char* operation) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace gl
|
||||||
|
} // namespace mcrf
|
||||||
|
|
||||||
|
#endif // MCRF_HEADLESS
|
||||||
305
src/platform/GLContext_SDL2.cpp
Normal file
305
src/platform/GLContext_SDL2.cpp
Normal file
|
|
@ -0,0 +1,305 @@
|
||||||
|
// GLContext_SDL2.cpp - SDL2 backend for OpenGL context abstraction
|
||||||
|
// Leverages existing SDL2Renderer infrastructure
|
||||||
|
|
||||||
|
#ifdef MCRF_SDL2
|
||||||
|
|
||||||
|
#include "GLContext.h"
|
||||||
|
#include "SDL2Renderer.h"
|
||||||
|
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
#include <GLES2/gl2.h>
|
||||||
|
#else
|
||||||
|
#include <GL/gl.h>
|
||||||
|
#include <GL/glext.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
namespace gl {
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// State tracking structures (same as SFML version)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
struct GLState {
|
||||||
|
GLboolean depthTest;
|
||||||
|
GLboolean depthWrite;
|
||||||
|
GLenum depthFunc;
|
||||||
|
GLboolean cullFace;
|
||||||
|
GLenum cullMode;
|
||||||
|
GLboolean blend;
|
||||||
|
GLenum blendSrc;
|
||||||
|
GLenum blendDst;
|
||||||
|
GLint viewport[4];
|
||||||
|
GLint boundFBO;
|
||||||
|
GLint boundProgram;
|
||||||
|
GLint boundTexture;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::vector<GLState> stateStack;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Initialization
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
bool initGL() {
|
||||||
|
// SDL2Renderer handles GL initialization
|
||||||
|
auto& renderer = sf::SDL2Renderer::getInstance();
|
||||||
|
return renderer.isGLInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isGLReady() {
|
||||||
|
auto& renderer = sf::SDL2Renderer::getInstance();
|
||||||
|
return renderer.isGLInitialized();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// FBO Management
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
unsigned int createFramebuffer(int width, int height, unsigned int* colorTex, unsigned int* depthRB) {
|
||||||
|
GLuint fbo, tex, depth = 0;
|
||||||
|
|
||||||
|
// Create FBO
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
|
||||||
|
// Create color texture
|
||||||
|
glGenTextures(1, &tex);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, tex);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
|
||||||
|
|
||||||
|
// Create depth renderbuffer if requested
|
||||||
|
// Note: GLES2 uses GL_DEPTH_COMPONENT16 instead of GL_DEPTH_COMPONENT24
|
||||||
|
if (depthRB) {
|
||||||
|
glGenRenderbuffers(1, &depth);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, depth);
|
||||||
|
#ifdef __EMSCRIPTEN__
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height);
|
||||||
|
#else
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height);
|
||||||
|
#endif
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth);
|
||||||
|
*depthRB = depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check completeness
|
||||||
|
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
if (depth) glDeleteRenderbuffers(1, &depth);
|
||||||
|
glDeleteTextures(1, &tex);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
*colorTex = tex;
|
||||||
|
return fbo;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bindFramebuffer(unsigned int fbo) {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bindDefaultFramebuffer() {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteFramebuffer(unsigned int fbo, unsigned int colorTex, unsigned int depthRB) {
|
||||||
|
if (depthRB) {
|
||||||
|
GLuint rb = depthRB;
|
||||||
|
glDeleteRenderbuffers(1, &rb);
|
||||||
|
}
|
||||||
|
if (colorTex) {
|
||||||
|
GLuint tex = colorTex;
|
||||||
|
glDeleteTextures(1, &tex);
|
||||||
|
}
|
||||||
|
if (fbo) {
|
||||||
|
GLuint f = fbo;
|
||||||
|
glDeleteFramebuffers(1, &f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Shader Compilation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
unsigned int compileShader(unsigned int type, const char* source) {
|
||||||
|
GLuint shader = glCreateShader(type);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
|
||||||
|
GLint success;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
|
||||||
|
if (!success) {
|
||||||
|
GLchar infoLog[512];
|
||||||
|
glGetShaderInfoLog(shader, 512, nullptr, infoLog);
|
||||||
|
// TODO: Log error
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int linkProgram(unsigned int vertShader, unsigned int fragShader) {
|
||||||
|
GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vertShader);
|
||||||
|
glAttachShader(program, fragShader);
|
||||||
|
glLinkProgram(program);
|
||||||
|
|
||||||
|
GLint success;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &success);
|
||||||
|
if (!success) {
|
||||||
|
GLchar infoLog[512];
|
||||||
|
glGetProgramInfoLog(program, 512, nullptr, infoLog);
|
||||||
|
// TODO: Log error
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteProgram(unsigned int program) {
|
||||||
|
glDeleteProgram(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// State Management
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void pushState() {
|
||||||
|
GLState state;
|
||||||
|
|
||||||
|
state.depthTest = glIsEnabled(GL_DEPTH_TEST);
|
||||||
|
glGetBooleanv(GL_DEPTH_WRITEMASK, &state.depthWrite);
|
||||||
|
glGetIntegerv(GL_DEPTH_FUNC, (GLint*)&state.depthFunc);
|
||||||
|
|
||||||
|
state.cullFace = glIsEnabled(GL_CULL_FACE);
|
||||||
|
glGetIntegerv(GL_CULL_FACE_MODE, (GLint*)&state.cullMode);
|
||||||
|
|
||||||
|
state.blend = glIsEnabled(GL_BLEND);
|
||||||
|
glGetIntegerv(GL_BLEND_SRC_ALPHA, (GLint*)&state.blendSrc);
|
||||||
|
glGetIntegerv(GL_BLEND_DST_ALPHA, (GLint*)&state.blendDst);
|
||||||
|
|
||||||
|
glGetIntegerv(GL_VIEWPORT, state.viewport);
|
||||||
|
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &state.boundFBO);
|
||||||
|
glGetIntegerv(GL_CURRENT_PROGRAM, &state.boundProgram);
|
||||||
|
glGetIntegerv(GL_TEXTURE_BINDING_2D, &state.boundTexture);
|
||||||
|
|
||||||
|
stateStack.push_back(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void popState() {
|
||||||
|
if (stateStack.empty()) return;
|
||||||
|
|
||||||
|
GLState& state = stateStack.back();
|
||||||
|
|
||||||
|
if (state.depthTest) glEnable(GL_DEPTH_TEST);
|
||||||
|
else glDisable(GL_DEPTH_TEST);
|
||||||
|
glDepthMask(state.depthWrite);
|
||||||
|
glDepthFunc(state.depthFunc);
|
||||||
|
|
||||||
|
if (state.cullFace) glEnable(GL_CULL_FACE);
|
||||||
|
else glDisable(GL_CULL_FACE);
|
||||||
|
glCullFace(state.cullMode);
|
||||||
|
|
||||||
|
if (state.blend) glEnable(GL_BLEND);
|
||||||
|
else glDisable(GL_BLEND);
|
||||||
|
glBlendFunc(state.blendSrc, state.blendDst);
|
||||||
|
|
||||||
|
glViewport(state.viewport[0], state.viewport[1], state.viewport[2], state.viewport[3]);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, state.boundFBO);
|
||||||
|
glUseProgram(state.boundProgram);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, state.boundTexture);
|
||||||
|
|
||||||
|
stateStack.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 3D State Setup
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void setup3DState() {
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glDepthFunc(GL_LESS);
|
||||||
|
glEnable(GL_CULL_FACE);
|
||||||
|
glCullFace(GL_BACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
void restore2DState() {
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_CULL_FACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Depth Operations
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void setDepthTest(bool enable) {
|
||||||
|
if (enable) glEnable(GL_DEPTH_TEST);
|
||||||
|
else glDisable(GL_DEPTH_TEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDepthWrite(bool enable) {
|
||||||
|
glDepthMask(enable ? GL_TRUE : GL_FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDepthFunc(unsigned int func) {
|
||||||
|
glDepthFunc(func);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearDepth() {
|
||||||
|
glClear(GL_DEPTH_BUFFER_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Culling
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void setCulling(bool enable) {
|
||||||
|
if (enable) glEnable(GL_CULL_FACE);
|
||||||
|
else glDisable(GL_CULL_FACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCullFace(unsigned int face) {
|
||||||
|
glCullFace(face);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Error Handling
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const char* getErrorString() {
|
||||||
|
GLenum err = glGetError();
|
||||||
|
switch (err) {
|
||||||
|
case GL_NO_ERROR: return nullptr;
|
||||||
|
case GL_INVALID_ENUM: return "GL_INVALID_ENUM";
|
||||||
|
case GL_INVALID_VALUE: return "GL_INVALID_VALUE";
|
||||||
|
case GL_INVALID_OPERATION: return "GL_INVALID_OPERATION";
|
||||||
|
case GL_OUT_OF_MEMORY: return "GL_OUT_OF_MEMORY";
|
||||||
|
default: return "Unknown GL error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkError(const char* operation) {
|
||||||
|
const char* err = getErrorString();
|
||||||
|
if (err) {
|
||||||
|
// TODO: Log error with operation name
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace gl
|
||||||
|
} // namespace mcrf
|
||||||
|
|
||||||
|
#endif // MCRF_SDL2
|
||||||
339
src/platform/GLContext_SFML.cpp
Normal file
339
src/platform/GLContext_SFML.cpp
Normal file
|
|
@ -0,0 +1,339 @@
|
||||||
|
// GLContext_SFML.cpp - SFML backend for OpenGL context abstraction
|
||||||
|
// Uses GLAD for GL function loading
|
||||||
|
|
||||||
|
#ifndef MCRF_SDL2
|
||||||
|
#ifndef MCRF_HEADLESS
|
||||||
|
|
||||||
|
#include "GLContext.h"
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#include <SFML/OpenGL.hpp>
|
||||||
|
#include <vector>
|
||||||
|
#include <cstdio>
|
||||||
|
|
||||||
|
namespace mcrf {
|
||||||
|
namespace gl {
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// State tracking
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
static bool s_gladInitialized = false;
|
||||||
|
|
||||||
|
struct GLState {
|
||||||
|
GLboolean depthTest;
|
||||||
|
GLboolean depthWrite;
|
||||||
|
GLenum depthFunc;
|
||||||
|
GLboolean cullFace;
|
||||||
|
GLenum cullMode;
|
||||||
|
GLboolean blend;
|
||||||
|
GLenum blendSrc;
|
||||||
|
GLenum blendDst;
|
||||||
|
GLint viewport[4];
|
||||||
|
GLint boundFBO;
|
||||||
|
GLint boundProgram;
|
||||||
|
GLint boundTexture;
|
||||||
|
};
|
||||||
|
|
||||||
|
static std::vector<GLState> stateStack;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Initialization
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
bool initGL() {
|
||||||
|
if (s_gladInitialized) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load GL function pointers via GLAD
|
||||||
|
// Note: SFML must have created an OpenGL context before this is called
|
||||||
|
if (!gladLoadGL()) {
|
||||||
|
fprintf(stderr, "GLContext_SFML: Failed to initialize GLAD\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_gladInitialized = true;
|
||||||
|
printf("GLContext_SFML: GLAD initialized - OpenGL %d.%d\n", GLVersion.major, GLVersion.minor);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isGLReady() {
|
||||||
|
return s_gladInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// FBO Management
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
unsigned int createFramebuffer(int width, int height, unsigned int* colorTex, unsigned int* depthRB) {
|
||||||
|
if (!s_gladInitialized) return 0;
|
||||||
|
|
||||||
|
GLuint fbo, tex, depth = 0;
|
||||||
|
|
||||||
|
// Create FBO
|
||||||
|
glGenFramebuffers(1, &fbo);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
|
||||||
|
// Create color texture
|
||||||
|
glGenTextures(1, &tex);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, tex);
|
||||||
|
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0);
|
||||||
|
|
||||||
|
// Create depth renderbuffer if requested
|
||||||
|
if (depthRB) {
|
||||||
|
glGenRenderbuffers(1, &depth);
|
||||||
|
glBindRenderbuffer(GL_RENDERBUFFER, depth);
|
||||||
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height);
|
||||||
|
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth);
|
||||||
|
*depthRB = depth;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check completeness
|
||||||
|
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER);
|
||||||
|
if (status != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
fprintf(stderr, "GLContext_SFML: Framebuffer incomplete: 0x%x\n", status);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
if (depth) glDeleteRenderbuffers(1, &depth);
|
||||||
|
glDeleteTextures(1, &tex);
|
||||||
|
glDeleteFramebuffers(1, &fbo);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
*colorTex = tex;
|
||||||
|
return fbo;
|
||||||
|
}
|
||||||
|
|
||||||
|
void bindFramebuffer(unsigned int fbo) {
|
||||||
|
if (s_gladInitialized) {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void bindDefaultFramebuffer() {
|
||||||
|
if (s_gladInitialized) {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteFramebuffer(unsigned int fbo, unsigned int colorTex, unsigned int depthRB) {
|
||||||
|
if (!s_gladInitialized) return;
|
||||||
|
|
||||||
|
if (depthRB) {
|
||||||
|
GLuint rb = depthRB;
|
||||||
|
glDeleteRenderbuffers(1, &rb);
|
||||||
|
}
|
||||||
|
if (colorTex) {
|
||||||
|
GLuint tex = colorTex;
|
||||||
|
glDeleteTextures(1, &tex);
|
||||||
|
}
|
||||||
|
if (fbo) {
|
||||||
|
GLuint f = fbo;
|
||||||
|
glDeleteFramebuffers(1, &f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Shader Compilation
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
unsigned int compileShader(unsigned int type, const char* source) {
|
||||||
|
if (!s_gladInitialized) return 0;
|
||||||
|
|
||||||
|
GLuint shader = glCreateShader(type);
|
||||||
|
glShaderSource(shader, 1, &source, nullptr);
|
||||||
|
glCompileShader(shader);
|
||||||
|
|
||||||
|
GLint success;
|
||||||
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
|
||||||
|
if (!success) {
|
||||||
|
GLchar infoLog[512];
|
||||||
|
glGetShaderInfoLog(shader, 512, nullptr, infoLog);
|
||||||
|
fprintf(stderr, "GLContext_SFML: Shader compilation failed:\n%s\n", infoLog);
|
||||||
|
glDeleteShader(shader);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return shader;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int linkProgram(unsigned int vertShader, unsigned int fragShader) {
|
||||||
|
if (!s_gladInitialized) return 0;
|
||||||
|
|
||||||
|
GLuint program = glCreateProgram();
|
||||||
|
glAttachShader(program, vertShader);
|
||||||
|
glAttachShader(program, fragShader);
|
||||||
|
glLinkProgram(program);
|
||||||
|
|
||||||
|
GLint success;
|
||||||
|
glGetProgramiv(program, GL_LINK_STATUS, &success);
|
||||||
|
if (!success) {
|
||||||
|
GLchar infoLog[512];
|
||||||
|
glGetProgramInfoLog(program, 512, nullptr, infoLog);
|
||||||
|
fprintf(stderr, "GLContext_SFML: Program linking failed:\n%s\n", infoLog);
|
||||||
|
glDeleteProgram(program);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return program;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deleteProgram(unsigned int program) {
|
||||||
|
if (s_gladInitialized && program) {
|
||||||
|
glDeleteProgram(program);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// State Management
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void pushState() {
|
||||||
|
if (!s_gladInitialized) return;
|
||||||
|
|
||||||
|
GLState state;
|
||||||
|
|
||||||
|
state.depthTest = glIsEnabled(GL_DEPTH_TEST);
|
||||||
|
glGetBooleanv(GL_DEPTH_WRITEMASK, &state.depthWrite);
|
||||||
|
glGetIntegerv(GL_DEPTH_FUNC, (GLint*)&state.depthFunc);
|
||||||
|
|
||||||
|
state.cullFace = glIsEnabled(GL_CULL_FACE);
|
||||||
|
glGetIntegerv(GL_CULL_FACE_MODE, (GLint*)&state.cullMode);
|
||||||
|
|
||||||
|
state.blend = glIsEnabled(GL_BLEND);
|
||||||
|
glGetIntegerv(GL_BLEND_SRC_ALPHA, (GLint*)&state.blendSrc);
|
||||||
|
glGetIntegerv(GL_BLEND_DST_ALPHA, (GLint*)&state.blendDst);
|
||||||
|
|
||||||
|
glGetIntegerv(GL_VIEWPORT, state.viewport);
|
||||||
|
glGetIntegerv(GL_FRAMEBUFFER_BINDING, &state.boundFBO);
|
||||||
|
glGetIntegerv(GL_CURRENT_PROGRAM, &state.boundProgram);
|
||||||
|
glGetIntegerv(GL_TEXTURE_BINDING_2D, &state.boundTexture);
|
||||||
|
|
||||||
|
stateStack.push_back(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void popState() {
|
||||||
|
if (!s_gladInitialized || stateStack.empty()) return;
|
||||||
|
|
||||||
|
GLState& state = stateStack.back();
|
||||||
|
|
||||||
|
if (state.depthTest) glEnable(GL_DEPTH_TEST);
|
||||||
|
else glDisable(GL_DEPTH_TEST);
|
||||||
|
glDepthMask(state.depthWrite);
|
||||||
|
glDepthFunc(state.depthFunc);
|
||||||
|
|
||||||
|
if (state.cullFace) glEnable(GL_CULL_FACE);
|
||||||
|
else glDisable(GL_CULL_FACE);
|
||||||
|
glCullFace(state.cullMode);
|
||||||
|
|
||||||
|
if (state.blend) glEnable(GL_BLEND);
|
||||||
|
else glDisable(GL_BLEND);
|
||||||
|
glBlendFunc(state.blendSrc, state.blendDst);
|
||||||
|
|
||||||
|
glViewport(state.viewport[0], state.viewport[1], state.viewport[2], state.viewport[3]);
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, state.boundFBO);
|
||||||
|
glUseProgram(state.boundProgram);
|
||||||
|
glBindTexture(GL_TEXTURE_2D, state.boundTexture);
|
||||||
|
|
||||||
|
stateStack.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// 3D State Setup
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void setup3DState() {
|
||||||
|
if (!s_gladInitialized) return;
|
||||||
|
|
||||||
|
glEnable(GL_DEPTH_TEST);
|
||||||
|
glDepthMask(GL_TRUE);
|
||||||
|
glDepthFunc(GL_LESS);
|
||||||
|
glEnable(GL_CULL_FACE);
|
||||||
|
glCullFace(GL_BACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
void restore2DState() {
|
||||||
|
if (!s_gladInitialized) return;
|
||||||
|
|
||||||
|
glDisable(GL_DEPTH_TEST);
|
||||||
|
glDisable(GL_CULL_FACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Depth Operations
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void setDepthTest(bool enable) {
|
||||||
|
if (!s_gladInitialized) return;
|
||||||
|
if (enable) glEnable(GL_DEPTH_TEST);
|
||||||
|
else glDisable(GL_DEPTH_TEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDepthWrite(bool enable) {
|
||||||
|
if (!s_gladInitialized) return;
|
||||||
|
glDepthMask(enable ? GL_TRUE : GL_FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setDepthFunc(unsigned int func) {
|
||||||
|
if (!s_gladInitialized) return;
|
||||||
|
glDepthFunc(func);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearDepth() {
|
||||||
|
if (!s_gladInitialized) return;
|
||||||
|
glClear(GL_DEPTH_BUFFER_BIT);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Culling
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
void setCulling(bool enable) {
|
||||||
|
if (!s_gladInitialized) return;
|
||||||
|
if (enable) glEnable(GL_CULL_FACE);
|
||||||
|
else glDisable(GL_CULL_FACE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCullFace(unsigned int face) {
|
||||||
|
if (!s_gladInitialized) return;
|
||||||
|
glCullFace(face);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Error Handling
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
const char* getErrorString() {
|
||||||
|
if (!s_gladInitialized) return "GLAD not initialized";
|
||||||
|
|
||||||
|
GLenum err = glGetError();
|
||||||
|
switch (err) {
|
||||||
|
case GL_NO_ERROR: return nullptr;
|
||||||
|
case GL_INVALID_ENUM: return "GL_INVALID_ENUM";
|
||||||
|
case GL_INVALID_VALUE: return "GL_INVALID_VALUE";
|
||||||
|
case GL_INVALID_OPERATION: return "GL_INVALID_OPERATION";
|
||||||
|
case GL_OUT_OF_MEMORY: return "GL_OUT_OF_MEMORY";
|
||||||
|
case GL_INVALID_FRAMEBUFFER_OPERATION: return "GL_INVALID_FRAMEBUFFER_OPERATION";
|
||||||
|
default: return "Unknown GL error";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkError(const char* operation) {
|
||||||
|
const char* err = getErrorString();
|
||||||
|
if (err) {
|
||||||
|
fprintf(stderr, "GLContext_SFML: GL error after %s: %s\n", operation, err);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace gl
|
||||||
|
} // namespace mcrf
|
||||||
|
|
||||||
|
#endif // MCRF_HEADLESS
|
||||||
|
#endif // MCRF_SDL2
|
||||||
BIN
tests/unit/math3d_test
Executable file
BIN
tests/unit/math3d_test
Executable file
Binary file not shown.
141
tests/unit/math3d_test.cpp
Normal file
141
tests/unit/math3d_test.cpp
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
// math3d_test.cpp - Quick verification of Math3D library
|
||||||
|
// Compile: g++ -std=c++17 -I../../src math3d_test.cpp -o math3d_test && ./math3d_test
|
||||||
|
|
||||||
|
#include "3d/Math3D.h"
|
||||||
|
#include <iostream>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
using namespace mcrf;
|
||||||
|
|
||||||
|
bool approx(float a, float b, float eps = 0.0001f) {
|
||||||
|
return std::abs(a - b) < eps;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int passed = 0, failed = 0;
|
||||||
|
|
||||||
|
// vec3 tests
|
||||||
|
{
|
||||||
|
vec3 a(1, 2, 3);
|
||||||
|
vec3 b(4, 5, 6);
|
||||||
|
|
||||||
|
vec3 sum = a + b;
|
||||||
|
if (approx(sum.x, 5) && approx(sum.y, 7) && approx(sum.z, 9)) {
|
||||||
|
std::cout << "[PASS] vec3 addition\n"; passed++;
|
||||||
|
} else {
|
||||||
|
std::cout << "[FAIL] vec3 addition\n"; failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
float dot = a.dot(b);
|
||||||
|
if (approx(dot, 32)) { // 1*4 + 2*5 + 3*6 = 32
|
||||||
|
std::cout << "[PASS] vec3 dot product\n"; passed++;
|
||||||
|
} else {
|
||||||
|
std::cout << "[FAIL] vec3 dot product: " << dot << "\n"; failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 c(1, 0, 0);
|
||||||
|
vec3 d(0, 1, 0);
|
||||||
|
vec3 cross = c.cross(d);
|
||||||
|
if (approx(cross.x, 0) && approx(cross.y, 0) && approx(cross.z, 1)) {
|
||||||
|
std::cout << "[PASS] vec3 cross product\n"; passed++;
|
||||||
|
} else {
|
||||||
|
std::cout << "[FAIL] vec3 cross product\n"; failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 n = vec3(3, 4, 0).normalized();
|
||||||
|
if (approx(n.length(), 1.0f)) {
|
||||||
|
std::cout << "[PASS] vec3 normalize\n"; passed++;
|
||||||
|
} else {
|
||||||
|
std::cout << "[FAIL] vec3 normalize\n"; failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mat4 tests
|
||||||
|
{
|
||||||
|
mat4 id = mat4::identity();
|
||||||
|
vec3 p(1, 2, 3);
|
||||||
|
vec3 transformed = id.transformPoint(p);
|
||||||
|
if (approx(transformed.x, 1) && approx(transformed.y, 2) && approx(transformed.z, 3)) {
|
||||||
|
std::cout << "[PASS] mat4 identity transform\n"; passed++;
|
||||||
|
} else {
|
||||||
|
std::cout << "[FAIL] mat4 identity transform\n"; failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 trans = mat4::translate(10, 20, 30);
|
||||||
|
vec3 moved = trans.transformPoint(p);
|
||||||
|
if (approx(moved.x, 11) && approx(moved.y, 22) && approx(moved.z, 33)) {
|
||||||
|
std::cout << "[PASS] mat4 translation\n"; passed++;
|
||||||
|
} else {
|
||||||
|
std::cout << "[FAIL] mat4 translation\n"; failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat4 scl = mat4::scale(2, 3, 4);
|
||||||
|
vec3 scaled = scl.transformPoint(p);
|
||||||
|
if (approx(scaled.x, 2) && approx(scaled.y, 6) && approx(scaled.z, 12)) {
|
||||||
|
std::cout << "[PASS] mat4 scale\n"; passed++;
|
||||||
|
} else {
|
||||||
|
std::cout << "[FAIL] mat4 scale\n"; failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test rotation: 90 degrees around Y should swap X and Z
|
||||||
|
mat4 rotY = mat4::rotateY(HALF_PI);
|
||||||
|
vec3 rotated = rotY.transformPoint(vec3(1, 0, 0));
|
||||||
|
if (approx(rotated.x, 0) && approx(rotated.z, -1)) {
|
||||||
|
std::cout << "[PASS] mat4 rotateY\n"; passed++;
|
||||||
|
} else {
|
||||||
|
std::cout << "[FAIL] mat4 rotateY: " << rotated.x << "," << rotated.y << "," << rotated.z << "\n"; failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Projection matrix test
|
||||||
|
{
|
||||||
|
mat4 proj = mat4::perspective(radians(90.0f), 1.0f, 0.1f, 100.0f);
|
||||||
|
// A point at z=-1 (in front of camera) should project to valid NDC
|
||||||
|
vec4 p(0, 0, -1, 1);
|
||||||
|
vec4 clip = proj * p;
|
||||||
|
vec3 ndc = clip.perspectiveDivide();
|
||||||
|
if (ndc.z > -1.0f && ndc.z < 1.0f) {
|
||||||
|
std::cout << "[PASS] mat4 perspective\n"; passed++;
|
||||||
|
} else {
|
||||||
|
std::cout << "[FAIL] mat4 perspective\n"; failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LookAt matrix test
|
||||||
|
{
|
||||||
|
mat4 view = mat4::lookAt(vec3(0, 0, 5), vec3(0, 0, 0), vec3(0, 1, 0));
|
||||||
|
vec3 origin = view.transformPoint(vec3(0, 0, 0));
|
||||||
|
// Origin should be at z=-5 in view space (5 units in front)
|
||||||
|
if (approx(origin.x, 0) && approx(origin.y, 0) && approx(origin.z, -5)) {
|
||||||
|
std::cout << "[PASS] mat4 lookAt\n"; passed++;
|
||||||
|
} else {
|
||||||
|
std::cout << "[FAIL] mat4 lookAt: " << origin.x << "," << origin.y << "," << origin.z << "\n"; failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quaternion tests
|
||||||
|
{
|
||||||
|
quat q = quat::fromAxisAngle(vec3(0, 1, 0), HALF_PI);
|
||||||
|
vec3 rotated = q.rotate(vec3(1, 0, 0));
|
||||||
|
if (approx(rotated.x, 0) && approx(rotated.z, -1)) {
|
||||||
|
std::cout << "[PASS] quat rotation\n"; passed++;
|
||||||
|
} else {
|
||||||
|
std::cout << "[FAIL] quat rotation: " << rotated.x << "," << rotated.y << "," << rotated.z << "\n"; failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
quat a = quat::fromAxisAngle(vec3(0, 1, 0), 0);
|
||||||
|
quat b = quat::fromAxisAngle(vec3(0, 1, 0), PI);
|
||||||
|
quat mid = quat::slerp(a, b, 0.5f);
|
||||||
|
vec3 half = mid.rotate(vec3(1, 0, 0));
|
||||||
|
// At t=0.5 between 0 and PI rotation, we should get 90 degrees
|
||||||
|
// Result should be perpendicular to input (x near 0, |z| near 1)
|
||||||
|
if (approx(half.x, 0, 0.01f) && approx(std::abs(half.z), 1, 0.01f)) {
|
||||||
|
std::cout << "[PASS] quat slerp\n"; passed++;
|
||||||
|
} else {
|
||||||
|
std::cout << "[FAIL] quat slerp: " << half.x << "," << half.y << "," << half.z << "\n"; failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "\n=== Results: " << passed << " passed, " << failed << " failed ===\n";
|
||||||
|
return failed > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
212
tests/unit/viewport3d_test.py
Normal file
212
tests/unit/viewport3d_test.py
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
# viewport3d_test.py - Unit test for Viewport3D 3D rendering viewport
|
||||||
|
|
||||||
|
import mcrfpy
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def test_viewport3d_creation():
|
||||||
|
"""Test basic Viewport3D creation and default properties"""
|
||||||
|
vp = mcrfpy.Viewport3D()
|
||||||
|
|
||||||
|
# Default position
|
||||||
|
assert vp.x == 0.0, f"Expected x=0, got {vp.x}"
|
||||||
|
assert vp.y == 0.0, f"Expected y=0, got {vp.y}"
|
||||||
|
|
||||||
|
# Default size (320x240 - PS1 resolution)
|
||||||
|
assert vp.w == 320.0, f"Expected w=320, got {vp.w}"
|
||||||
|
assert vp.h == 240.0, f"Expected h=240, got {vp.h}"
|
||||||
|
|
||||||
|
# Default render resolution
|
||||||
|
assert vp.render_resolution == (320, 240), f"Expected (320, 240), got {vp.render_resolution}"
|
||||||
|
|
||||||
|
# Default camera position
|
||||||
|
assert vp.camera_pos == (0.0, 0.0, 5.0), f"Expected (0, 0, 5), got {vp.camera_pos}"
|
||||||
|
|
||||||
|
# Default camera target
|
||||||
|
assert vp.camera_target == (0.0, 0.0, 0.0), f"Expected (0, 0, 0), got {vp.camera_target}"
|
||||||
|
|
||||||
|
# Default FOV
|
||||||
|
assert vp.fov == 60.0, f"Expected fov=60, got {vp.fov}"
|
||||||
|
|
||||||
|
# Default PS1 effect flags
|
||||||
|
assert vp.enable_vertex_snap == True, f"Expected vertex_snap=True, got {vp.enable_vertex_snap}"
|
||||||
|
assert vp.enable_affine == True, f"Expected affine=True, got {vp.enable_affine}"
|
||||||
|
assert vp.enable_dither == True, f"Expected dither=True, got {vp.enable_dither}"
|
||||||
|
assert vp.enable_fog == True, f"Expected fog=True, got {vp.enable_fog}"
|
||||||
|
|
||||||
|
# Default fog range
|
||||||
|
assert vp.fog_near == 10.0, f"Expected fog_near=10, got {vp.fog_near}"
|
||||||
|
assert vp.fog_far == 100.0, f"Expected fog_far=100, got {vp.fog_far}"
|
||||||
|
|
||||||
|
print("[PASS] test_viewport3d_creation")
|
||||||
|
|
||||||
|
def test_viewport3d_with_kwargs():
|
||||||
|
"""Test Viewport3D creation with keyword arguments"""
|
||||||
|
vp = mcrfpy.Viewport3D(
|
||||||
|
pos=(100, 200),
|
||||||
|
size=(640, 480),
|
||||||
|
render_resolution=(160, 120),
|
||||||
|
fov=90.0,
|
||||||
|
camera_pos=(10.0, 5.0, 10.0),
|
||||||
|
camera_target=(0.0, 2.0, 0.0),
|
||||||
|
enable_vertex_snap=False,
|
||||||
|
enable_affine=False,
|
||||||
|
enable_dither=False,
|
||||||
|
enable_fog=False,
|
||||||
|
fog_near=5.0,
|
||||||
|
fog_far=50.0
|
||||||
|
)
|
||||||
|
|
||||||
|
assert vp.x == 100.0, f"Expected x=100, got {vp.x}"
|
||||||
|
assert vp.y == 200.0, f"Expected y=200, got {vp.y}"
|
||||||
|
assert vp.w == 640.0, f"Expected w=640, got {vp.w}"
|
||||||
|
assert vp.h == 480.0, f"Expected h=480, got {vp.h}"
|
||||||
|
assert vp.render_resolution == (160, 120), f"Expected (160, 120), got {vp.render_resolution}"
|
||||||
|
assert vp.fov == 90.0, f"Expected fov=90, got {vp.fov}"
|
||||||
|
assert vp.camera_pos == (10.0, 5.0, 10.0), f"Expected (10, 5, 10), got {vp.camera_pos}"
|
||||||
|
assert vp.camera_target == (0.0, 2.0, 0.0), f"Expected (0, 2, 0), got {vp.camera_target}"
|
||||||
|
assert vp.enable_vertex_snap == False, f"Expected vertex_snap=False, got {vp.enable_vertex_snap}"
|
||||||
|
assert vp.enable_affine == False, f"Expected affine=False, got {vp.enable_affine}"
|
||||||
|
assert vp.enable_dither == False, f"Expected dither=False, got {vp.enable_dither}"
|
||||||
|
assert vp.enable_fog == False, f"Expected fog=False, got {vp.enable_fog}"
|
||||||
|
assert vp.fog_near == 5.0, f"Expected fog_near=5, got {vp.fog_near}"
|
||||||
|
assert vp.fog_far == 50.0, f"Expected fog_far=50, got {vp.fog_far}"
|
||||||
|
|
||||||
|
print("[PASS] test_viewport3d_with_kwargs")
|
||||||
|
|
||||||
|
def test_viewport3d_property_modification():
|
||||||
|
"""Test modifying Viewport3D properties after creation"""
|
||||||
|
vp = mcrfpy.Viewport3D()
|
||||||
|
|
||||||
|
# Modify position
|
||||||
|
vp.x = 50
|
||||||
|
vp.y = 75
|
||||||
|
assert vp.x == 50.0, f"Expected x=50, got {vp.x}"
|
||||||
|
assert vp.y == 75.0, f"Expected y=75, got {vp.y}"
|
||||||
|
|
||||||
|
# Modify size
|
||||||
|
vp.w = 800
|
||||||
|
vp.h = 600
|
||||||
|
assert vp.w == 800.0, f"Expected w=800, got {vp.w}"
|
||||||
|
assert vp.h == 600.0, f"Expected h=600, got {vp.h}"
|
||||||
|
|
||||||
|
# Modify render resolution
|
||||||
|
vp.render_resolution = (256, 192)
|
||||||
|
assert vp.render_resolution == (256, 192), f"Expected (256, 192), got {vp.render_resolution}"
|
||||||
|
|
||||||
|
# Modify camera
|
||||||
|
vp.camera_pos = (0.0, 10.0, 20.0)
|
||||||
|
vp.camera_target = (5.0, 0.0, 5.0)
|
||||||
|
vp.fov = 45.0
|
||||||
|
assert vp.camera_pos == (0.0, 10.0, 20.0), f"Expected (0, 10, 20), got {vp.camera_pos}"
|
||||||
|
assert vp.camera_target == (5.0, 0.0, 5.0), f"Expected (5, 0, 5), got {vp.camera_target}"
|
||||||
|
assert vp.fov == 45.0, f"Expected fov=45, got {vp.fov}"
|
||||||
|
|
||||||
|
# Modify PS1 effects
|
||||||
|
vp.enable_vertex_snap = False
|
||||||
|
vp.enable_affine = False
|
||||||
|
vp.enable_dither = True
|
||||||
|
vp.enable_fog = True
|
||||||
|
assert vp.enable_vertex_snap == False
|
||||||
|
assert vp.enable_affine == False
|
||||||
|
assert vp.enable_dither == True
|
||||||
|
assert vp.enable_fog == True
|
||||||
|
|
||||||
|
# Modify fog range
|
||||||
|
vp.fog_near = 1.0
|
||||||
|
vp.fog_far = 200.0
|
||||||
|
assert vp.fog_near == 1.0, f"Expected fog_near=1, got {vp.fog_near}"
|
||||||
|
assert vp.fog_far == 200.0, f"Expected fog_far=200, got {vp.fog_far}"
|
||||||
|
|
||||||
|
print("[PASS] test_viewport3d_property_modification")
|
||||||
|
|
||||||
|
def test_viewport3d_scene_integration():
|
||||||
|
"""Test adding Viewport3D to a scene"""
|
||||||
|
scene = mcrfpy.Scene("viewport3d_test_scene")
|
||||||
|
vp = mcrfpy.Viewport3D(pos=(10, 10), size=(400, 300))
|
||||||
|
|
||||||
|
# Add to scene
|
||||||
|
scene.children.append(vp)
|
||||||
|
|
||||||
|
# Verify it was added
|
||||||
|
assert len(scene.children) == 1, f"Expected 1 child, got {len(scene.children)}"
|
||||||
|
|
||||||
|
# Retrieve and verify type
|
||||||
|
child = scene.children[0]
|
||||||
|
assert type(child).__name__ == "Viewport3D", f"Expected Viewport3D, got {type(child).__name__}"
|
||||||
|
|
||||||
|
# Verify properties match
|
||||||
|
assert child.x == 10.0
|
||||||
|
assert child.y == 10.0
|
||||||
|
assert child.w == 400.0
|
||||||
|
assert child.h == 300.0
|
||||||
|
|
||||||
|
print("[PASS] test_viewport3d_scene_integration")
|
||||||
|
|
||||||
|
def test_viewport3d_visibility():
|
||||||
|
"""Test visibility and opacity properties"""
|
||||||
|
vp = mcrfpy.Viewport3D()
|
||||||
|
|
||||||
|
# Default visibility
|
||||||
|
assert vp.visible == True, f"Expected visible=True, got {vp.visible}"
|
||||||
|
assert vp.opacity == 1.0, f"Expected opacity=1.0, got {vp.opacity}"
|
||||||
|
|
||||||
|
# Modify visibility
|
||||||
|
vp.visible = False
|
||||||
|
assert vp.visible == False, f"Expected visible=False, got {vp.visible}"
|
||||||
|
|
||||||
|
# Modify opacity
|
||||||
|
vp.opacity = 0.5
|
||||||
|
assert vp.opacity == 0.5, f"Expected opacity=0.5, got {vp.opacity}"
|
||||||
|
|
||||||
|
# Opacity clamping
|
||||||
|
vp.opacity = 2.0 # Should clamp to 1.0
|
||||||
|
assert vp.opacity == 1.0, f"Expected opacity=1.0 after clamping, got {vp.opacity}"
|
||||||
|
|
||||||
|
vp.opacity = -0.5 # Should clamp to 0.0
|
||||||
|
assert vp.opacity == 0.0, f"Expected opacity=0.0 after clamping, got {vp.opacity}"
|
||||||
|
|
||||||
|
print("[PASS] test_viewport3d_visibility")
|
||||||
|
|
||||||
|
def test_viewport3d_repr():
|
||||||
|
"""Test Viewport3D string representation"""
|
||||||
|
vp = mcrfpy.Viewport3D(pos=(100, 200), size=(640, 480), render_resolution=(320, 240))
|
||||||
|
repr_str = repr(vp)
|
||||||
|
|
||||||
|
# Check that repr contains expected information
|
||||||
|
assert "Viewport3D" in repr_str, f"Expected 'Viewport3D' in repr, got {repr_str}"
|
||||||
|
assert "100" in repr_str, f"Expected x position in repr, got {repr_str}"
|
||||||
|
assert "200" in repr_str, f"Expected y position in repr, got {repr_str}"
|
||||||
|
|
||||||
|
print("[PASS] test_viewport3d_repr")
|
||||||
|
|
||||||
|
def run_all_tests():
|
||||||
|
"""Run all Viewport3D tests"""
|
||||||
|
tests = [
|
||||||
|
test_viewport3d_creation,
|
||||||
|
test_viewport3d_with_kwargs,
|
||||||
|
test_viewport3d_property_modification,
|
||||||
|
test_viewport3d_scene_integration,
|
||||||
|
test_viewport3d_visibility,
|
||||||
|
test_viewport3d_repr,
|
||||||
|
]
|
||||||
|
|
||||||
|
passed = 0
|
||||||
|
failed = 0
|
||||||
|
|
||||||
|
for test in tests:
|
||||||
|
try:
|
||||||
|
test()
|
||||||
|
passed += 1
|
||||||
|
except AssertionError as e:
|
||||||
|
print(f"[FAIL] {test.__name__}: {e}")
|
||||||
|
failed += 1
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERROR] {test.__name__}: {e}")
|
||||||
|
failed += 1
|
||||||
|
|
||||||
|
print(f"\n=== Results: {passed} passed, {failed} failed ===")
|
||||||
|
return failed == 0
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
success = run_all_tests()
|
||||||
|
sys.exit(0 if success else 1)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue