From b57310b38c1e97e4cd397c1f1fd88634b85489ae Mon Sep 17 00:00:00 2001 From: lhark Date: Sat, 25 Nov 2017 16:18:26 -0500 Subject: [PATCH] Initial commit OpenGL boilerplate by Benoit Ozell Packets code taken from https://github.com/jeschke/water-wave-packets --- Packets.cpp | 1264 +++++++++++++ Packets.h | 147 ++ constants.h | 44 + inf2705.h | 3070 ++++++++++++++++++++++++++++++++ makefile | 33 + nuanceurFragmentsSolution.glsl | 141 ++ nuanceurGeometrieSolution.glsl | 73 + nuanceurSommetsSolution.glsl | 130 ++ ripple.cpp | 954 ++++++++++ textures/TestIsland.bmp | Bin 0 -> 3145784 bytes 10 files changed, 5856 insertions(+) create mode 100644 Packets.cpp create mode 100644 Packets.h create mode 100644 constants.h create mode 100644 inf2705.h create mode 100644 makefile create mode 100644 nuanceurFragmentsSolution.glsl create mode 100644 nuanceurGeometrieSolution.glsl create mode 100644 nuanceurSommetsSolution.glsl create mode 100644 ripple.cpp create mode 100644 textures/TestIsland.bmp diff --git a/Packets.cpp b/Packets.cpp new file mode 100644 index 0000000..0cc5ba3 --- /dev/null +++ b/Packets.cpp @@ -0,0 +1,1264 @@ +// taken from https://github.com/jeschke/water-wave-packets +// Include the OS headers +//----------------------- +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "Packets.h" +#pragma warning( disable: 4996 ) + + +inline float rational_tanh(float x) +{ + if (x < -3.0f) + return -1.0f; + else if (x > 3.0f) + return 1.0f; + else + return x*(27.0f + x*x) / (27.0f+9.0f*x*x); +} + + +float Packets::GetIntersectionDistance(Vector2f pos1, Vector2f dir1, Vector2f pos2, Vector2f dir2) +{ + ParametrizedLine line1(pos1, dir1); + Hyperplane line2 = Hyperplane::Through(pos2, pos2+dir2); + float intPointDist = line1.intersectionParameter(line2); + if (abs(intPointDist) > 10000.0f) + intPointDist = 10000.0f; + return intPointDist; +} + + + +inline float Packets::GetGroundVal(Vector2f &p) +{ + Vector2f pTex = Vector2f(p.x()/SCENE_EXTENT+0.5f,p.y()/SCENE_EXTENT+0.5f); // convert from world space to texture space + float val1 = m_ground[(int)(max(0,min(m_groundSizeY-1,pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,pTex.x()*m_groundSizeX)))]; + float val2 = m_ground[(int)(max(0,min(m_groundSizeY-1,pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,1+pTex.x()*m_groundSizeX)))]; + float val3 = m_ground[(int)(max(0,min(m_groundSizeY-1,1+pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,pTex.x()*m_groundSizeX)))]; + float val4 = m_ground[(int)(max(0,min(m_groundSizeY-1,1+pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,1+pTex.x()*m_groundSizeX)))]; + float xOffs = (pTex.x()*m_groundSizeX) - (int)(pTex.x()*m_groundSizeX); + float yOffs = (pTex.y()*m_groundSizeY) - (int)(pTex.y()*m_groundSizeY); + float valH1 = (1.0f-xOffs)*val1 + xOffs*val2; + float valH2 = (1.0f-xOffs)*val3 + xOffs*val4; + return( (1.0f-yOffs)*valH1 + yOffs*valH2 ); +} + +inline Vector2f Packets::GetGroundNormal(Vector2f &p) +{ + Vector2f pTex = Vector2f(p.x()/SCENE_EXTENT+0.5f,p.y()/SCENE_EXTENT+0.5f); // convert from world space to texture space + Vector2f val1 = m_gndDeriv[(int)(max(0,min(m_groundSizeY-1,pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,pTex.x()*m_groundSizeX)))]; + Vector2f val2 = m_gndDeriv[(int)(max(0,min(m_groundSizeY-1,pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,1+pTex.x()*m_groundSizeX)))]; + Vector2f val3 = m_gndDeriv[(int)(max(0,min(m_groundSizeY-1,1+pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,pTex.x()*m_groundSizeX)))]; + Vector2f val4 = m_gndDeriv[(int)(max(0,min(m_groundSizeY-1,1+pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,1+pTex.x()*m_groundSizeX)))]; + float xOffs = (pTex.x()*m_groundSizeX) - (int)(pTex.x()*m_groundSizeX); + float yOffs = (pTex.y()*m_groundSizeY) - (int)(pTex.y()*m_groundSizeY); + Vector2f valH1 = (1.0f-xOffs)*val1 + xOffs*val2; + Vector2f valH2 = (1.0f-xOffs)*val3 + xOffs*val4; + Vector2f res = (1.0f-yOffs)*valH1 + yOffs*valH2; + Vector2f resN = Vector2f(0,1); + if (abs(res.x()) + abs(res.y()) > 0.0) + resN = res.normalized(); + return( resN ); +} + +inline float Packets::GetBoundaryDist(Vector2f &p) +{ + Vector2f pTex = Vector2f(p.x()/SCENE_EXTENT+0.5f,p.y()/SCENE_EXTENT+0.5f); // convert from world space to texture space + float val1 = m_distMap[(int)(max(0,min(m_groundSizeY-1,pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,pTex.x()*m_groundSizeX)))]; + float val2 = m_distMap[(int)(max(0,min(m_groundSizeY-1,pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,1+pTex.x()*m_groundSizeX)))]; + float val3 = m_distMap[(int)(max(0,min(m_groundSizeY-1,1+pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,pTex.x()*m_groundSizeX)))]; + float val4 = m_distMap[(int)(max(0,min(m_groundSizeY-1,1+pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,1+pTex.x()*m_groundSizeX)))]; + float xOffs = (pTex.x()*m_groundSizeX) - (int)(pTex.x()*m_groundSizeX); + float yOffs = (pTex.y()*m_groundSizeY) - (int)(pTex.y()*m_groundSizeY); + float valH1 = (1.0f-xOffs)*val1 + xOffs*val2; + float valH2 = (1.0f-xOffs)*val3 + xOffs*val4; + return( (1.0f-yOffs)*valH1 + yOffs*valH2 ); +} + +inline Vector2f Packets::GetBoundaryNormal(Vector2f &p) +{ + Vector2f pTex = Vector2f(p.x()/SCENE_EXTENT+0.5f,p.y()/SCENE_EXTENT+0.5f); // convert from world space to texture space + Vector2f val1 = m_bndDeriv[(int)(max(0,min(m_groundSizeY-1,pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,pTex.x()*m_groundSizeX)))]; + Vector2f val2 = m_bndDeriv[(int)(max(0,min(m_groundSizeY-1,pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,1+pTex.x()*m_groundSizeX)))]; + Vector2f val3 = m_bndDeriv[(int)(max(0,min(m_groundSizeY-1,1+pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,pTex.x()*m_groundSizeX)))]; + Vector2f val4 = m_bndDeriv[(int)(max(0,min(m_groundSizeY-1,1+pTex.y()*m_groundSizeY)))*m_groundSizeX + (int)(max(0,min(m_groundSizeX-1,1+pTex.x()*m_groundSizeX)))]; + float xOffs = (pTex.x()*m_groundSizeX) - (int)(pTex.x()*m_groundSizeX); + float yOffs = (pTex.y()*m_groundSizeY) - (int)(pTex.y()*m_groundSizeY); + Vector2f valH1 = (1.0f-xOffs)*val1 + xOffs*val2; + Vector2f valH2 = (1.0f-xOffs)*val3 + xOffs*val4; + Vector2f res = (1.0f-yOffs)*valH1 + yOffs*valH2; + Vector2f resN = Vector2f(0,1); + if (abs(res.x())+abs(res.y()) > 0.0) + resN = res.normalized(); + return( resN ); +} + + + + +#pragma warning( push ) +#pragma warning( disable : 4996) +Packets::Packets(int packetBudget) +{ + WCHAR wcInfo[512]; + + //load ground/boundary texture for CPU processing + LPCWSTR groundTexFile = WATER_TERRAIN_FILE; + tagBITMAPFILEHEADER bmpheader; + tagBITMAPINFOHEADER bmpinfo; + DWORD bytesread; + HANDLE file = CreateFile( groundTexFile , GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL ); + if (file == NULL) + { + throw std::exception("Media file not found"); + return; + } + if ( (ReadFile ( file, &bmpheader, sizeof ( BITMAPFILEHEADER ), &bytesread, NULL ) == false) + || (ReadFile(file, &bmpinfo, sizeof(BITMAPINFOHEADER), &bytesread, NULL) == false) + || (bmpheader.bfType != 'MB') + || (bmpinfo.biCompression != BI_RGB) + || (bmpinfo.biBitCount != 24) ) + { + CloseHandle ( file ); + throw std::exception("Error reading media file"); + return; + } + m_groundSizeX = abs(bmpinfo.biWidth); + m_groundSizeY = abs(bmpinfo.biHeight); + long size = bmpheader.bfSize-bmpheader.bfOffBits; + SetFilePointer ( file, bmpheader.bfOffBits, NULL, FILE_BEGIN ); + BYTE* Buffer = new BYTE[size]; + if ( ReadFile ( file, Buffer, size, &bytesread, NULL ) == false ) + { + delete[](Buffer); + CloseHandle(file); + throw std::exception("Media file not found"); + return; + } + CloseHandle(file); + // convert read buffer to our rgb datastructure + int padding = 0; + int scanlinebytes = m_groundSizeX*3; + while ( ( scanlinebytes + padding ) % 4 != 0 ) + padding++; + int psw = scanlinebytes + padding; + m_ground = new float[m_groundSizeX*m_groundSizeY]; + float *bound = new float[m_groundSizeX*m_groundSizeY]; + for (int y=0; y 11.1f/255.0f) + bound[y*m_groundSizeX+x] = 1.0f; + else + bound[y*m_groundSizeX+x] = 0.0f; + } + delete[](Buffer); + + // boundary texture distance transform + // init helper distance map (pMap) + StringCchPrintf(wcInfo, 512, L"Computing boundary distance transform.."); + OutputDebugString(wcInfo); + int *pMap = new int[m_groundSizeX*m_groundSizeY]; + #pragma omp parallel for + for (int y = 0; y < m_groundSizeY; y++) + for (int x = 0; x < m_groundSizeX; x++) + { + // if we are at the boundary, intialize the distance function with 0, otherwise with maximum value + if ((bound[y*m_groundSizeX + x] > 0.5f) && + ((bound[max(0, min(m_groundSizeY - 1, y + 1))*m_groundSizeX + max(0, min(m_groundSizeX - 1, x + 0))] <= 0.5f) + || (bound[max(0, min(m_groundSizeY - 1, y + 0))*m_groundSizeX + max(0, min(m_groundSizeX - 1, x + 1))] <= 0.5f) + || (bound[max(0, min(m_groundSizeY - 1, y - 1))*m_groundSizeX + max(0, min(m_groundSizeX - 1, x + 0))] <= 0.5f) + || (bound[max(0, min(m_groundSizeY - 1, y + 0))*m_groundSizeX + max(0, min(m_groundSizeX - 1, x - 1))] <= 0.5f))) + pMap[y*m_groundSizeX + x] = 0; // initialize with maximum x distance + else if ((bound[y*m_groundSizeX + x] <= 0.5f) && + ((bound[max(0, min(m_groundSizeY - 1, y + 1))*m_groundSizeX + max(0, min(m_groundSizeX - 1, x + 0))] > 0.5f) + || (bound[max(0, min(m_groundSizeY - 1, y + 0))*m_groundSizeX + max(0, min(m_groundSizeX - 1, x + 1))] > 0.5f) + || (bound[max(0, min(m_groundSizeY - 1, y - 1))*m_groundSizeX + max(0, min(m_groundSizeX - 1, x + 0))] > 0.5f) + || (bound[max(0, min(m_groundSizeY - 1, y + 0))*m_groundSizeX + max(0, min(m_groundSizeX - 1, x - 1))] > 0.5f))) + pMap[y*m_groundSizeX + x] = 0; // initialize with maximum x distance + else + pMap[y*m_groundSizeX + x] = m_groundSizeX*m_groundSizeX; // initialize with maximum x distance + } + m_distMap = new float[m_groundSizeX*m_groundSizeY]; + #pragma omp parallel for + for (int y=0; y=0; x--) + { + if (pMap[y*m_groundSizeX+x] == 0) + lastBoundX = x; + pMap[y*m_groundSizeX+x] = min(pMap[y*m_groundSizeX+x], (lastBoundX-x)*(lastBoundX-x)); + } + } + #pragma omp parallel for + for (int x=0; x=0; yd--) + { + minDist = min(minDist, yd*yd+pMap[(y+yd)*m_groundSizeX+x]); + if (minDist < yd*yd) + break; + } + m_distMap[y*m_groundSizeX+x] = (float)(minDist); + } + delete[](pMap); + // m_distMap now contains the _squared_ euklidean distance to closest label boundary, so take the sqroot. And sign the distance + #pragma omp parallel for + for (int y=0; y 0.5f) + m_distMap[y*m_groundSizeX+x] = -m_distMap[y*m_groundSizeX+x]; // negative distance INSIDE a boundary regions + m_distMap[y*m_groundSizeX+x] = m_distMap[y*m_groundSizeX+x]*SCENE_EXTENT / m_groundSizeX; + } + StringCchPrintf(wcInfo, 512, L"done!\n"); + OutputDebugString(wcInfo); + + // derivative (2D normal) of the boundary texture + StringCchPrintf( wcInfo, 512, L"Computing boundary derivatives.."); + OutputDebugString( wcInfo ); + m_bndDeriv = new Vector2f[m_groundSizeX*m_groundSizeY]; + #pragma omp parallel for + for (int y=0; y m_packetNum) + ExpandWavePacketMemory(max(m_usedPackets,m_usedGhosts) + PACKET_BUFFER_DELTA); + float speedDummy, kDummy; + int firstfree = GetFreePackedID(); + m_packet[firstfree].pos1 = Vector2f(pos1x,pos1y); + m_packet[firstfree].pOld1 = m_packet[firstfree].pos1; + m_packet[firstfree].pos2 = Vector2f(pos2x,pos2y); + m_packet[firstfree].pOld2 = m_packet[firstfree].pos2; + m_packet[firstfree].dir1 = Vector2f(dir1x,dir1y); + m_packet[firstfree].dOld1 = m_packet[firstfree].dir1; + m_packet[firstfree].dir2 = Vector2f(dir2x,dir2y); + m_packet[firstfree].dOld2 = m_packet[firstfree].dir2; + m_packet[firstfree].phase = 0.0; + m_packet[firstfree].phOld = 0.0; + m_packet[firstfree].E = E; + m_packet[firstfree].use3rd = false; + m_packet[firstfree].bounced1 = false; + m_packet[firstfree].bounced2 = false; + m_packet[firstfree].bounced3 = false; + m_packet[firstfree].sliding3 = false; + // set the wavelength/freq interval + m_packet[firstfree].k_L = k_L; + float wd = GetWaterDepth(m_packet[firstfree].pos1); + m_packet[firstfree].w0_L = sqrt((GRAVITY + k_L*k_L*SIGMA/DENSITY)*k_L*rational_tanh(k_L*wd)); // this take surface tension into account + m_packet[firstfree].k_H = k_H; + m_packet[firstfree].w0_H = sqrt((GRAVITY + k_H*k_H*SIGMA/DENSITY)*k_H*rational_tanh(k_H*wd)); // this take surface tension into account + m_packet[firstfree].d_L = 0.0; + m_packet[firstfree].d_H = 0.0; + // set the representative wave as average of interval boundaries + m_packet[firstfree].k = 0.5f*(m_packet[firstfree].k_L+m_packet[firstfree].k_H); + m_packet[firstfree].w0 = sqrt((GRAVITY + m_packet[firstfree].k*m_packet[firstfree].k*SIGMA/DENSITY)*m_packet[firstfree].k*rational_tanh(m_packet[firstfree].k*wd)); // this takes surface tension into account + GetWaveParameters(GetWaterDepth(m_packet[firstfree].pos1), m_packet[firstfree].w0, m_packet[firstfree].k, kDummy, m_packet[firstfree].speed1); + m_packet[firstfree].sOld1 = m_packet[firstfree].speed1; + GetWaveParameters(GetWaterDepth(m_packet[firstfree].pos2), m_packet[firstfree].w0, m_packet[firstfree].k, kDummy, m_packet[firstfree].speed2); + m_packet[firstfree].sOld2 = m_packet[firstfree].speed2; + m_packet[firstfree].envelope = min(PACKET_ENVELOPE_MAXSIZE, max(PACKET_ENVELOPE_MINSIZE, PACKET_ENVELOPE_SIZE_FACTOR*2.0f*M_PI/m_packet[firstfree].k)); // adjust envelope size to represented wavelength + m_packet[firstfree].ampOld = 0.0; + float a1 = min(MAX_SPEEDNESS*2.0f*M_PI / m_packet[firstfree].k, GetWaveAmplitude(m_packet[firstfree].envelope*(m_packet[firstfree].pos1 - m_packet[firstfree].pos2).norm(), m_packet[firstfree].E, m_packet[firstfree].k)); + m_packet[firstfree].dAmp = 0.5f*(m_packet[firstfree].speed1+m_packet[firstfree].speed2)*m_elapsedTime/(PACKET_BLEND_TRAVEL_FACTOR*m_packet[firstfree].envelope)*a1; + + // Test for wave number splitting -> if the packet interval crosses the slowest waves, divide so that each part has a monotonic speed function (assumed for travel spread/error calculation) + int i1=firstfree; + if ((m_packet[i1].w0_L>PACKET_SLOWAVE_W0) && (m_packet[i1].w0_H 45 degrees +void Packets::CreateSpreadingPacket(float xPos, float yPos, float dirx, float diry, float spreadFactor, float crestlength, float lambda_L, float lambda_H, float E) +{ + Vector2f dir = Vector2f(dirx,diry); + dir.normalize(); + Vector2f wfAlign = 0.5f*crestlength*Vector2f(dir.y(),-dir.x()); + Vector2f dirSpread1 = dir - spreadFactor*Vector2f(dir.y(),-dir.x()); + Vector2f dirSpread2 = dir + spreadFactor*Vector2f(dir.y(),-dir.x()); + CreatePacket( xPos-wfAlign.x(), yPos-wfAlign.y(), xPos+wfAlign.x(), yPos+wfAlign.y(), dirSpread1.x(), dirSpread1.y(), dirSpread2.x(), dirSpread2.y(), 2.0f*M_PI/lambda_L, 2.0f*M_PI/lambda_H, E); +} + + + +// adds a new circular wave at normalized position x,y with wavelength boundaries lambda_L and lambda_H (in meters) +void Packets::CreateCircularWavefront(float xPos, float yPos, float radius, float lambda_L, float lambda_H, float E) +{ + // adapt initial packet crestlength to impact radius and wavelength + float dAng = min(24.0f, 360.0f * ((0.5f*lambda_L + 0.5f*lambda_H)*3.0f) / (2.0f*M_PI*radius)); + for (float i = 0; i < 360.0f; i += dAng) + CreatePacket( + xPos + radius*sin(i*M_PI / 180.0f), yPos + radius*cos(i*M_PI / 180.0f), + xPos + radius*sin((i + dAng)*M_PI / 180.0f), yPos + radius*cos((i + dAng)*M_PI / 180.0f), + sin(i*M_PI / 180.0f), cos(i*M_PI / 180.0f), + sin((i + dAng)*M_PI / 180.0), cos((i + dAng)*M_PI / 180.0f), + 2.0f*M_PI / lambda_L, 2.0f*M_PI / lambda_H, E); +} + + + +// update the simulation time +void Packets::UpdateTime(float dTime) +{ + if (m_oldTime < 0) // if we are entering the first time, set the current time as time + m_oldTime = 0.0f; + else + m_oldTime = m_time; + m_time = m_oldTime + dTime; // time stepping + m_elapsedTime = abs(m_time - m_oldTime); +} + + + +// returns water depth at position p +float Packets::GetWaterDepth(Vector2f &p) +{ + float v = 1.0f-GetGroundVal(p); + return(MIN_WATER_DEPTH + (MAX_WATER_DEPTH-MIN_WATER_DEPTH)*v*v*v*v); +} + + + + +void Packets::GetWaveParameters(float waterDepth, float w_0, float kIn, float &k_out, float &speed_out) +{ + float k = kIn; + float dk = 1.0f; + float kOld; + int it = 0; + while ((dk > 1.0e-04) && (it<6)) + { + kOld = k; + k = w_0/sqrt((GRAVITY/k+k*SIGMA/DENSITY)*rational_tanh(k*waterDepth)); // this includes surface tension / capillary waves + dk = abs(k-kOld); + it++; + } + k_out = k; + float t = rational_tanh(k*waterDepth); + const float c = SIGMA/DENSITY; + speed_out = ((c*k*k + GRAVITY)*(t + waterDepth*k*(1.0f-t*t)) + 2.0f*c*k*k*t) / (2.0f*sqrt(k*(c*k*k + GRAVITY)*t)); // this is group speed as dw/dk + return; +} + + + +float Packets::GetPhaseSpeed(float w_0, float kIn) +{ + return( w_0/kIn ); +} + + + +// area = surface area of the wave packet, E = Energy, k = wavenumber +// computing the amplitude from energy flux for a wave packet +float Packets::GetWaveAmplitude(float area, float E, float k) +{ + return( sqrt(abs(E)/(abs(area)*0.5f*(DENSITY*GRAVITY+SIGMA*k*k))) ); +} + + + + + +// advects a single packet vertex with groupspeed, returns 1 if boundary reflection occured +bool Packets::AdvectPacketVertex(float elapsedTime, Vector2f &posIn, Vector2f &dirIn, float w0, float &kIn, float &speedIn, Vector2f &posOut, Vector2f &dirOut, float &speedOut) +{ + bool bounced = false; + // intialize the output with the input + posOut = posIn; + dirOut = dirIn; + speedOut = speedIn; + + // compute new direction and speed based on snells law (depending on water depth) + float speed1, k; + GetWaveParameters(GetWaterDepth( posIn ), w0, kIn, k, speed1); + speedOut = speed1; // the new speed is defined by the speed of this wave at this water depth, this is does not necessarily respect snells law! + Vector2f nDir = GetGroundNormal( posIn ); + if (abs(nDir.x())+abs(nDir.y()) > 0.1f) // if there is a change in water depth here, indicated by a non-zero ground normal + { + Vector2f pNext = posIn + elapsedTime*speed1*dirIn; + float speed2; + GetWaveParameters(GetWaterDepth(pNext), w0, kIn, k, speed2); + + float cos1 = nDir.dot(-dirIn); + float cos2 = sqrt( max(0.0f, 1.0f - (speed2*speed2)/(speed1*speed1)*(1.0f - cos1*cos1) )); + Vector2f nRefrac; + if (cos1 <= 0.0f) + nRefrac = speed2/speed1*dirIn + (speed2/speed1*cos1 + cos2)*nDir; + else + nRefrac = speed2/speed1*dirIn + (speed2/speed1*cos1 - cos2)*nDir; + if (nRefrac.norm() > 0.000001f) + dirOut = nRefrac.normalized(); + } + posOut = posIn + elapsedTime*speed1*dirOut; // advect wave vertex position + + // if we ran into a boundary -> step back and bounce off + if (GetBoundaryDist(posOut)<0.0f) + { + Vector2f nor = GetBoundaryNormal(posOut); + float a = nor.dot(dirOut); + if (a <= -0.08f) // a wave reflects if it travels with >4.5 degrees towards a surface. Otherwise, it gets diffracted + { + bounced = true; + // step back until we are outside the boundary + Vector2f pD = posIn; + Vector2f vD = elapsedTime*speedIn*dirOut; + for (int j = 0; j < 16; j++) + { + Vector2f pDD = pD + vD; + if (GetBoundaryDist(pDD) > 0.0f) + pD = pDD; + vD = 0.5f*vD; + } + Vector2f wayVec = pD - posIn; + float lGone = wayVec.norm(); + posOut = pD; + // compute the traveling direction after the bounce + dirOut = -dirOut; + Vector2f nor2 = GetBoundaryNormal(posOut); + float a2 = nor2.dot(dirOut); + Vector2f bFrac = a2*nor2 - dirOut; + Vector2f d0 = dirOut + 2.0f*bFrac; + dirOut = d0.normalized(); + posOut += (elapsedTime*speedOut - lGone)*dirOut; + } + } + + // if we got trapped in a boundary (again), just project onto the nearest surface (this approximates multiple bounces) + if (GetBoundaryDist(posOut) < 0.0) + for (int i2=0; i2<16; i2++) + posOut += -0.5f*GetBoundaryDist(posOut)*GetBoundaryNormal(posOut); + + return(bounced); +} + + + + + + +// updates the wavefield using the movin wavefronts and generated an output image from the wavefield +void Packets::AdvectWavePackets(float dTime) +{ + UpdateTime(dTime); + if (m_elapsedTime <= 0.0) // if there is no time advancement, do not update anything.. + return; + + // compute the new packet vertex positions, directions and speeds based on snells law + #pragma omp parallel for + for (int uP=0; uP find new "has to bounce" point if no other vertex bounced + { + float s = 0.0f; + float sD = 0.5f; + Vector2f posOld = m_packet[i1].pOld1; + Vector2f dirOld = m_packet[i1].dOld1; + float speedOld = m_packet[i1].sOld1; + Vector2f pos = m_packet[i1].pos1; + Vector2f dir = m_packet[i1].dir1; + float speed = m_packet[i1].speed1; + float wN = m_packet[i1].k; + float w0 = m_packet[i1].w0; + for (int j=0; j<16; j++) + { + Vector2f p = (1.0f-(s+sD))*m_packet[i1].pOld1 + (s+sD)*m_packet[i1].pOld3; + Vector2f d = (1.0f-(s+sD))*m_packet[i1].dOld1 + (s+sD)*m_packet[i1].dOld3; + float sp = (1.0f-(s+sD))*m_packet[i1].sOld1 + (s+sD)*m_packet[i1].sOld3; + Vector2f posD, dirD; + float speedD; + if (!AdvectPacketVertex(m_elapsedTime, p, d, w0, wN, sp, posD, dirD, speedD)) + { + s += sD; + posOld = p; + dirOld = d; + speedOld = sp; + pos = posD; + dir = dirD; + speed = speedD; + } + sD = 0.5f*sD; + } + m_packet[i1].pOld3 = posOld; + m_packet[i1].dOld3 = dirOld.normalized(); + m_packet[i1].sOld3 = speedOld; + m_packet[i1].pos3 = pos; + m_packet[i1].dir3 = dir; + m_packet[i1].speed3 = speed; + } + } + + + // first contact to a boundary -> sent a ghost packet, make packet invisible for now, add 3rd vertex + if (m_usedGhosts + m_usedPackets > m_packetNum) + ExpandWavePacketMemory(m_usedGhosts + m_usedPackets + PACKET_BUFFER_DELTA); + #pragma omp parallel for + for (int uP = m_usedPackets-1; uP>=0; uP--) + if ((!m_packet[m_usedPacket[uP]].use3rd) && (m_packet[m_usedPacket[uP]].bounced1 || m_packet[m_usedPacket[uP]].bounced2)) + { + int i1 = m_usedPacket[uP]; + int firstghost = GetFreeGhostID(); + m_ghostPacket[firstghost].pos = 0.5f*(m_packet[i1].pOld1+m_packet[i1].pOld2); + m_ghostPacket[firstghost].dir = (m_packet[i1].dOld1+m_packet[i1].dOld2).normalized(); // the new position is wrong after the reflection, so use the old direction instead + m_ghostPacket[firstghost].speed = 0.5f*(m_packet[i1].sOld1+m_packet[i1].sOld2); + m_ghostPacket[firstghost].envelope = m_packet[i1].envelope; + m_ghostPacket[firstghost].ampOld = m_packet[i1].ampOld; + m_ghostPacket[firstghost].dAmp = m_ghostPacket[firstghost].ampOld* m_ghostPacket[firstghost].speed*m_elapsedTime/(PACKET_BLEND_TRAVEL_FACTOR*m_ghostPacket[firstghost].envelope); + m_ghostPacket[firstghost].k = m_packet[i1].k; + m_ghostPacket[firstghost].phase = m_packet[i1].phOld; + m_ghostPacket[firstghost].dPhase = m_packet[i1].phase-m_packet[i1].phOld; + m_ghostPacket[firstghost].bending = GetIntersectionDistance(m_ghostPacket[firstghost].pos, m_ghostPacket[firstghost].dir, m_packet[i1].pOld1, m_packet[i1].dOld1); + // hide this packet from display + m_packet[i1].ampOld = 0.0; + m_packet[i1].dAmp = 0.0; + // emit all (higher-)frequency waves after a bounce + if ((PACKET_BOUNCE_FREQSPLIT) && (m_packet[i1].k_L < PACKET_BOUNCE_FREQSPLIT_K)) // split the frequency range if smallest wave is > 20cm + { + m_packet[i1].k_L = PACKET_BOUNCE_FREQSPLIT_K; + m_packet[i1].w0_L = sqrt(GRAVITY/m_packet[i1].k_L)*m_packet[i1].k_L; // initial guess for angular frequency + m_packet[i1].w0 = 0.5f*(m_packet[i1].w0_L+m_packet[i1].w0_H); + // distribute the error according to current speed difference + float dummySpeed; + Vector2f pos = 0.5f*(m_packet[i1].pos1+m_packet[i1].pos2); + float wd = GetWaterDepth(pos); + GetWaveParameters(wd, m_packet[i1].w0_L, m_packet[i1].k_L, m_packet[i1].k_L, dummySpeed); + GetWaveParameters(wd, m_packet[i1].w0_H, m_packet[i1].k_H, m_packet[i1].k_H, dummySpeed); + GetWaveParameters(wd, m_packet[i1].w0, 0.5f*(m_packet[i1].k_L+m_packet[i1].k_H), m_packet[i1].k, dummySpeed); + m_packet[i1].d_L = 0.0; m_packet[i1].d_H = 0.0; // reset the internally tracked error + m_packet[i1].envelope = min(PACKET_ENVELOPE_MAXSIZE, max(PACKET_ENVELOPE_MINSIZE, PACKET_ENVELOPE_SIZE_FACTOR*2.0f*M_PI/m_packet[i1].k)); + } + //if both vertices bounced, the reflected wave needs to smoothly reappear + if (m_packet[i1].bounced1==m_packet[i1].bounced2) + { + m_packet[i1].ampOld = 0.0; + m_packet[i1].dAmp = 0.5f*(m_packet[i1].speed1+m_packet[i1].speed2)*m_elapsedTime/(PACKET_BLEND_TRAVEL_FACTOR*m_packet[i1].envelope)*GetWaveAmplitude( m_packet[i1].envelope*(m_packet[i1].pos1-m_packet[i1].pos2).norm(), m_packet[i1].E, m_packet[i1].k); + } + if (m_packet[i1].bounced1 != m_packet[i1].bounced2) // only one vertex bounced -> insert 3rd "wait for bounce" vertex and reorder such that 1st is waiting for bounce + { + if (m_packet[i1].bounced1) // if the first bounced an the second did not -> exchange the two points, as we assume that the second bounced already and the 3rd will be "ahead" of the first.. + { + WAVE_PACKET *seg = &m_packet[i1]; // use the 3rd vertex as copy element + seg->pos3 = seg->pos2; seg->pos2 = seg->pos1; seg->pos1 = seg->pos3; + seg->pOld3 = seg->pOld2; seg->pOld2 = seg->pOld1; seg->pOld1 = seg->pOld3; + seg->dir3 = seg->dir2; seg->dir2 = seg->dir1; seg->dir1 = seg->dir3; + seg->dOld3 = seg->dOld2; seg->dOld2 = seg->dOld1; seg->dOld1 = seg->dOld3; + seg->speed3 = seg->speed2; seg->speed2 = seg->speed1; seg->speed1 = seg->speed3; + seg->sOld3 = seg->sOld2; seg->sOld2 = seg->sOld1; seg->sOld1 = seg->sOld3; + seg->bounced3 = seg->bounced2; seg->bounced2 = seg->bounced1; seg->bounced1 = seg->bounced3; + } + float s = 0.0; + float sD = 0.5f; + Vector2f posOld = m_packet[i1].pOld1; + Vector2f dirOld = m_packet[i1].dOld1; + float speedOld = m_packet[i1].sOld1; + Vector2f pos = m_packet[i1].pos1; + Vector2f dir = m_packet[i1].dir1; + float speed = m_packet[i1].speed1; + float wN = m_packet[i1].k; + float w0 = m_packet[i1].w0; + for (int j=0; j<16; j++) // find the last point before the boundary that does not bounce in this timestep, it becomes the 3rd point + { + Vector2f p = (1.0f-(s+sD))*m_packet[i1].pOld1 + (s+sD)*m_packet[i1].pOld2; + Vector2f d = (1.0f-(s+sD))*m_packet[i1].dOld1 + (s+sD)*m_packet[i1].dOld2; + float sp = (1.0f-(s+sD))*m_packet[i1].sOld1 + (s+sD)*m_packet[i1].sOld2; + Vector2f posD, dirD; + float speedD; + if (!AdvectPacketVertex(m_elapsedTime, p, d, w0, wN, sp, posD, dirD, speedD)) + { + s += sD; + posOld = p; + dirOld = d; + speedOld = sp; + pos = posD; + dir = dirD; + speed = speedD; + } + sD = 0.5f*sD; + } + // the new 3rd vertex has "has to bounce" state (not sliding yet) + m_packet[i1].pOld3 = posOld; + m_packet[i1].dOld3 = dirOld.normalized(); + m_packet[i1].sOld3 = speedOld; + m_packet[i1].pos3 = pos; + m_packet[i1].dir3 = dir; + m_packet[i1].speed3 = speed; + } + } + + + // define new state based on current state and bouncings + #pragma omp parallel for + for (int uP = 0; uP case "intiate new 3rd vertex" + { + m_packet[i1].use3rd = true; + m_packet[i1].bounced3 = false; + m_packet[i1].sliding3 = false; + } + } + else // 3rd vertex already present + { + if (!m_packet[i1].sliding3) // 3rd point was in "waiting for bounce" state + { + if (!m_packet[i1].bounced3) // case: 3rd "has to bounce" vertex did not bounce -> make it sliding in any case + m_packet[i1].sliding3 = true; + else if ((m_packet[i1].bounced1) || (m_packet[i1].bounced2)) // case: 3rd "has to bounce" and one other point bounced as well -> release 3rd vertex + m_packet[i1].use3rd = false; + } + else // 3rd point was already in "sliding" state + { + if (m_packet[i1].bounced3) // if sliding 3rd point bounced, release it + m_packet[i1].use3rd = false; + } + // if we released this wave from a boundary (3rd vertex released) -> blend it smoothly in again + if (!m_packet[i1].use3rd) + { + m_packet[i1].ampOld = 0.0; + m_packet[i1].dAmp = 0.5f*(m_packet[i1].speed1+m_packet[i1].speed2)*m_elapsedTime/(PACKET_BLEND_TRAVEL_FACTOR*m_packet[i1].envelope)*GetWaveAmplitude( m_packet[i1].envelope*(m_packet[i1].pos1-m_packet[i1].pos2).norm(), m_packet[i1].E, m_packet[i1].k); + } + } + } + + + // wavenumber interval subdivision if travel distance between fastest and slowest wave packets differ more than PACKET_SPLIT_DISPERSION x envelope size + if ( max(m_usedGhosts + m_usedPackets, 2*m_usedPackets) > m_packetNum) + ExpandWavePacketMemory(max(m_usedGhosts + m_usedPackets, 2 * m_usedPackets) + PACKET_BUFFER_DELTA); + #pragma omp parallel for + for (int uP = m_usedPackets-1; uP>=0; uP--) + if (!m_packet[m_usedPacket[uP]].use3rd) + { + int i1 = m_usedPacket[uP]; + float speedDummy,kDummy; + Vector2f pos = 0.5f*(m_packet[i1].pos1+m_packet[i1].pos2); + float wd = GetWaterDepth(pos); + GetWaveParameters(wd, m_packet[i1].w0, m_packet[i1].k, kDummy, speedDummy); + float dist_Ref = m_elapsedTime*speedDummy; + GetWaveParameters(wd, m_packet[i1].w0_L, m_packet[i1].k_L, kDummy, speedDummy); + m_packet[i1].k_L = kDummy; + m_packet[i1].d_L += fabs(m_elapsedTime*speedDummy-dist_Ref); // taking the abs augments any errors caused by waterdepth independent slowest wave assumption.. + GetWaveParameters(wd, m_packet[i1].w0_H, m_packet[i1].k_H, kDummy, speedDummy); + m_packet[i1].k_H = kDummy; + m_packet[i1].d_H += fabs(m_elapsedTime*speedDummy-dist_Ref); + if (m_packet[i1].d_L+m_packet[i1].d_H > PACKET_SPLIT_DISPERSION*m_packet[i1].envelope) // if fastest/slowest waves in this packet diverged too much -> subdivide + { + // first create a ghost for the old packet + int firstghost = GetFreeGhostID(); + m_ghostPacket[firstghost].pos = 0.5f*(m_packet[i1].pOld1+m_packet[i1].pOld2); + m_ghostPacket[firstghost].dir = (0.5f*(m_packet[i1].pos1+m_packet[i1].pos2)-0.5f*(m_packet[i1].pOld1+m_packet[i1].pOld2)).normalized(); + m_ghostPacket[firstghost].speed = 0.5f*(m_packet[i1].sOld1+m_packet[i1].sOld2); + m_ghostPacket[firstghost].envelope = m_packet[i1].envelope; + m_ghostPacket[firstghost].ampOld = m_packet[i1].ampOld; + m_ghostPacket[firstghost].dAmp = m_ghostPacket[firstghost].ampOld* m_ghostPacket[firstghost].speed*m_elapsedTime/(PACKET_BLEND_TRAVEL_FACTOR*m_ghostPacket[firstghost].envelope); + m_ghostPacket[firstghost].k = m_packet[i1].k; + m_ghostPacket[firstghost].phase = m_packet[i1].phOld; + m_ghostPacket[firstghost].dPhase = m_packet[i1].phase-m_packet[i1].phOld; + m_ghostPacket[firstghost].bending = GetIntersectionDistance(m_ghostPacket[firstghost].pos, m_ghostPacket[firstghost].dir, m_packet[i1].pOld1, m_packet[i1].dOld1); + // create new packet and copy ALL parameters + int firstfree = GetFreePackedID(); + m_packet[firstfree] = m_packet[i1]; + // split the frequency range + m_packet[firstfree].k_H = m_packet[i1].k; + m_packet[firstfree].w0_H = m_packet[i1].w0; + m_packet[firstfree].w0 = 0.5f*(m_packet[firstfree].w0_L+m_packet[firstfree].w0_H); + // distribute the error according to current speed difference + float speed_L,speed_M,speed_H; + GetWaveParameters( wd, m_packet[firstfree].w0_L, m_packet[firstfree].k_L, m_packet[firstfree].k_L, speed_L); + GetWaveParameters( wd, m_packet[firstfree].w0_H, m_packet[firstfree].k_H, m_packet[firstfree].k_H, speed_H); + GetWaveParameters( wd, m_packet[firstfree].w0, 0.5f*(m_packet[firstfree].k_L+m_packet[firstfree].k_H), m_packet[firstfree].k, speed_M); + float dSL = abs(abs(speed_L)-abs(speed_M)); + float dSH = abs(abs(speed_H)-abs(speed_M)); + float d_All = m_packet[i1].d_L; + m_packet[firstfree].d_L = dSL*d_All / (dSH + dSL); + m_packet[firstfree].d_H = d_All - m_packet[firstfree].d_L; + m_packet[firstfree].envelope = min(PACKET_ENVELOPE_MAXSIZE, max(PACKET_ENVELOPE_MINSIZE, PACKET_ENVELOPE_SIZE_FACTOR*2.0f*M_PI/m_packet[firstfree].k)); + // set the new upper freq. boundary and representative freq. + m_packet[i1].k_L = m_packet[i1].k; + m_packet[i1].w0_L = m_packet[i1].w0; + m_packet[i1].w0 = 0.5f*(m_packet[i1].w0_L+m_packet[i1].w0_H); + // distribute the error according to current speed difference + GetWaveParameters( wd, m_packet[i1].w0_L, m_packet[i1].k_L, m_packet[i1].k_L, speed_L); + GetWaveParameters( wd, m_packet[i1].w0_H, m_packet[i1].k_H, m_packet[i1].k_H, speed_H); + GetWaveParameters( wd, m_packet[i1].w0, 0.5f*(m_packet[i1].k_L+m_packet[i1].k_H), m_packet[i1].k, speed_M); + dSL = abs(abs(speed_L)-abs(speed_M)); + dSH = abs(abs(speed_H)-abs(speed_M)); + d_All = m_packet[i1].d_H; + m_packet[i1].d_L = dSL*d_All / (dSH + dSL); + m_packet[i1].d_H = d_All - m_packet[i1].d_L; + m_packet[i1].envelope = min(PACKET_ENVELOPE_MAXSIZE, max(PACKET_ENVELOPE_MINSIZE, PACKET_ENVELOPE_SIZE_FACTOR*2.0f*M_PI/m_packet[i1].k)); + // distribute the energy such that both max. wave gradients are equal -> both get the same wave shape + m_packet[firstfree].E = abs(m_packet[i1].E)/(1.0f + (m_packet[i1].envelope*m_packet[firstfree].k*m_packet[firstfree].k*(DENSITY*GRAVITY+SIGMA*m_packet[i1].k*m_packet[i1].k))/(m_packet[firstfree].envelope*m_packet[i1].k*m_packet[i1].k*(DENSITY*GRAVITY+SIGMA*m_packet[firstfree].k*m_packet[firstfree].k))); + m_packet[i1].E = abs(m_packet[i1].E)-m_packet[firstfree].E; + // smoothly ramp the new waves + m_packet[i1].phase=0; + m_packet[i1].ampOld = 0.0f; + m_packet[i1].dAmp = 0.5f*(m_packet[i1].speed1+m_packet[i1].speed2)*m_elapsedTime/(PACKET_BLEND_TRAVEL_FACTOR*m_packet[i1].envelope)*GetWaveAmplitude( m_packet[i1].envelope*(m_packet[i1].pos1-m_packet[i1].pos2).norm(), m_packet[i1].E, m_packet[i1].k); + m_packet[firstfree].phase = 0.0f; + m_packet[firstfree].ampOld = 0.0f; + m_packet[firstfree].dAmp = 0.5f*(m_packet[firstfree].speed1+m_packet[firstfree].speed2)*m_elapsedTime/(PACKET_BLEND_TRAVEL_FACTOR*m_packet[firstfree].envelope)*GetWaveAmplitude( m_packet[firstfree].envelope*(m_packet[firstfree].pos1-m_packet[firstfree].pos2).norm(), m_packet[firstfree].E, m_packet[firstfree].k); + } + } + + + // crest-refinement of packets of regular packet (not at any boundary, i.e. having no 3rd vertex) + if (max(m_usedGhosts + m_usedPackets, 2 * m_usedPackets) > m_packetNum) + ExpandWavePacketMemory(max(m_usedGhosts + m_usedPackets, 2 * m_usedPackets) + PACKET_BUFFER_DELTA); + #pragma omp parallel for + for (int uP = m_usedPackets-1; uP>=0; uP--) + if (!m_packet[m_usedPacket[uP]].use3rd) + { + int i1 = m_usedPacket[uP]; + float sDiff = (m_packet[i1].pos2-m_packet[i1].pos1).norm(); + float aDiff = m_packet[i1].dir1.dot(m_packet[i1].dir2); + if ((sDiff > m_packet[i1].envelope) || (aDiff <= PACKET_SPLIT_ANGLE)) // if the two vertices move towards each other, do not subdivide + { + int firstghost = GetFreeGhostID(); + m_ghostPacket[firstghost].pos = 0.5f*(m_packet[i1].pOld1+m_packet[i1].pOld2); + m_ghostPacket[firstghost].dir = (m_packet[i1].dOld1+m_packet[i1].dOld2).normalized(); + m_ghostPacket[firstghost].speed = 0.5f*(m_packet[i1].sOld1+m_packet[i1].sOld2); + m_ghostPacket[firstghost].envelope = m_packet[i1].envelope; + m_ghostPacket[firstghost].ampOld = m_packet[i1].ampOld; + m_ghostPacket[firstghost].dAmp = m_ghostPacket[firstghost].ampOld* m_ghostPacket[firstghost].speed*m_elapsedTime/(PACKET_BLEND_TRAVEL_FACTOR*m_ghostPacket[firstghost].envelope); + m_ghostPacket[firstghost].k = m_packet[i1].k; + m_ghostPacket[firstghost].phase = m_packet[i1].phOld; + m_ghostPacket[firstghost].dPhase = m_packet[i1].phase-m_packet[i1].phOld; + m_ghostPacket[firstghost].bending = GetIntersectionDistance(m_ghostPacket[firstghost].pos, m_ghostPacket[firstghost].dir, m_packet[i1].pOld1, m_packet[i1].dOld1); + // create new vertex between existing packet vertices + int firstfree = GetFreePackedID(); + m_packet[firstfree] = m_packet[i1]; // first copy all data + m_packet[firstfree].pOld1 = 0.5f*(m_packet[i1].pOld1 + m_packet[i1].pOld2); + m_packet[firstfree].dOld1 = (m_packet[i1].dOld1 + m_packet[i1].dOld2).normalized(); + m_packet[firstfree].sOld1 = 0.5f*(m_packet[i1].sOld1 + m_packet[i1].sOld2); + m_packet[firstfree].pos1 = 0.5f*(m_packet[i1].pos1 + m_packet[i1].pos2); + m_packet[firstfree].dir1 = (m_packet[i1].dir1 + m_packet[i1].dir2).normalized(); + m_packet[firstfree].speed1 = 0.5f*(m_packet[i1].speed1 + m_packet[i1].speed2); + m_packet[firstfree].E = 0.5f*m_packet[i1].E; + m_packet[firstfree].ampOld = 0.0; + m_packet[firstfree].dAmp = 0.5f*(m_packet[firstfree].speed1+m_packet[firstfree].speed2)*m_elapsedTime/(PACKET_BLEND_TRAVEL_FACTOR*m_packet[firstfree].envelope)*GetWaveAmplitude( m_packet[firstfree].envelope*(m_packet[firstfree].pos1-m_packet[firstfree].pos2).norm(), m_packet[firstfree].E, m_packet[firstfree].k); + // use the same new middle vertex here + m_packet[i1].pOld2 = m_packet[firstfree].pOld1; + m_packet[i1].dOld2 = m_packet[firstfree].dOld1; + m_packet[i1].sOld2 = m_packet[firstfree].sOld1; + m_packet[i1].pos2 = m_packet[firstfree].pos1; + m_packet[i1].dir2 = m_packet[firstfree].dir1; + m_packet[i1].speed2 = m_packet[firstfree].speed1; + m_packet[i1].E *= 0.5f; + m_packet[i1].ampOld = 0.0; + m_packet[i1].dAmp = 0.5f*(m_packet[i1].speed1+m_packet[i1].speed2)*m_elapsedTime/(PACKET_BLEND_TRAVEL_FACTOR*m_packet[i1].envelope)*GetWaveAmplitude( m_packet[i1].envelope*(m_packet[i1].pos1-m_packet[i1].pos2).norm(), m_packet[i1].E, m_packet[i1].k); + } + } + + + + // crest-refinement of packets with a sliding 3rd vertex + if ( 3 * m_usedPackets > m_packetNum) + ExpandWavePacketMemory(3 * m_usedPackets + PACKET_BUFFER_DELTA); + #pragma omp parallel for + for (int uP = m_usedPackets-1; uP>=0; uP--) + if ((m_packet[m_usedPacket[uP]].use3rd) && (m_packet[m_usedPacket[uP]].sliding3)) + { + int i1 = m_usedPacket[uP]; + float sDiff1 = (m_packet[i1].pos1-m_packet[i1].pos3).norm(); + float aDiff1 = m_packet[i1].dir1.dot(m_packet[i1].dir3); + if ((sDiff1 >= m_packet[i1].envelope))// || (aDiff1 <= PACKET_SPLIT_ANGLE)) // angle criterion is removed here because this would prevent diffraction + { + int firstfree = GetFreePackedID(); + // first vertex becomes first in new wave packet, third one becomes second + m_packet[firstfree] = m_packet[i1]; // first copy all data + m_packet[firstfree].pOld2 = 0.5f*(m_packet[i1].pOld1 + m_packet[i1].pOld3); + m_packet[firstfree].dOld2 = (m_packet[i1].dOld1 + m_packet[i1].dOld3).normalized(); + m_packet[firstfree].sOld2 = 0.5f*(m_packet[i1].sOld1 + m_packet[i1].sOld3); + m_packet[firstfree].pos2 = 0.5f*(m_packet[i1].pos1 + m_packet[i1].pos3); + m_packet[firstfree].dir2 = (m_packet[i1].dir1 + m_packet[i1].dir3).normalized(); + m_packet[firstfree].speed2 = 0.5f*(m_packet[i1].speed1 + m_packet[i1].speed3); + m_packet[firstfree].ampOld = 0.0; + m_packet[firstfree].dAmp = 0.5f*(m_packet[i1].speed1+m_packet[i1].speed2)*m_elapsedTime/(PACKET_BLEND_TRAVEL_FACTOR*m_packet[i1].envelope)*GetWaveAmplitude( m_packet[i1].envelope*(m_packet[i1].pos1-m_packet[i1].pos2).norm(), m_packet[i1].E, m_packet[i1].k); + m_packet[firstfree].bounced1 = false; + m_packet[firstfree].bounced2 = false; + m_packet[firstfree].bounced3 = false; + m_packet[firstfree].use3rd = false; + m_packet[firstfree].sliding3 = false; + // use the same new middle vertex here + m_packet[i1].pOld1 = m_packet[firstfree].pOld2; + m_packet[i1].dOld1 = m_packet[firstfree].dOld2; + m_packet[i1].sOld1 = m_packet[firstfree].sOld2; + m_packet[i1].pos1 = m_packet[firstfree].pos2; + m_packet[i1].dir1 = m_packet[firstfree].dir2; + m_packet[i1].speed1 = m_packet[firstfree].speed2; + // distribute the energy according to length of the two new packets + float s = (m_packet[firstfree].pos1-m_packet[firstfree].pos2).norm()/((m_packet[firstfree].pos1-m_packet[firstfree].pos2).norm() + (m_packet[i1].pos1-m_packet[i1].pos3).norm() + (m_packet[i1].pos2-m_packet[i1].pos3).norm()); + m_packet[firstfree].E = s*m_packet[i1].E; + m_packet[i1].E *= (1.0f-s); + } + // same procedure for the other end of sliding vertex.. + sDiff1 = (m_packet[i1].pos2-m_packet[i1].pos3).norm(); + aDiff1 = m_packet[i1].dir2.dot(m_packet[i1].dir3); + if ((sDiff1 >= m_packet[i1].envelope)/* || (aDiff1 <= PACKET_SPLIT_ANGLE)*/) // angle criterion is removed here because this would prevent diffraction + { + int firstfree = GetFreePackedID(); + // first vertex becomes first in new packet, third one becomes second + m_packet[firstfree] = m_packet[i1]; // first copy all data + m_packet[firstfree].pOld1 = 0.5f*(m_packet[i1].pOld2 + m_packet[i1].pOld3); + m_packet[firstfree].dOld1 = (m_packet[i1].dOld2 + m_packet[i1].dOld3).normalized(); + m_packet[firstfree].sOld1 = 0.5f*(m_packet[i1].sOld2 + m_packet[i1].sOld3); + m_packet[firstfree].pos1 = 0.5f*(m_packet[i1].pos2 + m_packet[i1].pos3); + m_packet[firstfree].dir1 = (m_packet[i1].dir2 + m_packet[i1].dir3).normalized(); + m_packet[firstfree].speed1 = 0.5f*(m_packet[i1].speed2 + m_packet[i1].speed3); + m_packet[firstfree].ampOld = 0.0; + m_packet[firstfree].dAmp = 0.5f*(m_packet[firstfree].speed1+m_packet[firstfree].speed2)*m_elapsedTime/(PACKET_BLEND_TRAVEL_FACTOR*m_packet[firstfree].envelope)*GetWaveAmplitude( m_packet[firstfree].envelope*(m_packet[firstfree].pos1-m_packet[firstfree].pos2).norm(), m_packet[firstfree].E, m_packet[firstfree].k); + m_packet[firstfree].bounced1 = false; + m_packet[firstfree].bounced2 = false; + m_packet[firstfree].bounced3 = false; + m_packet[firstfree].use3rd = false; + m_packet[firstfree].sliding3 = false; + // use the same new middle vertex + m_packet[i1].pOld2 = m_packet[firstfree].pOld1; + m_packet[i1].dOld2 = m_packet[firstfree].dOld1; + m_packet[i1].sOld2 = m_packet[firstfree].sOld1; + m_packet[i1].pos2 = m_packet[firstfree].pos1; + m_packet[i1].dir2 = m_packet[firstfree].dir1; + m_packet[i1].speed2 = m_packet[firstfree].speed1; + // distribute the energy according to length of the two new packets + float s = (m_packet[firstfree].pos1-m_packet[firstfree].pos2).norm()/((m_packet[firstfree].pos1-m_packet[firstfree].pos2).norm() + (m_packet[i1].pos1-m_packet[i1].pos3).norm() + (m_packet[i1].pos2-m_packet[i1].pos3).norm()); + m_packet[firstfree].E = s*m_packet[i1].E; + m_packet[i1].E *= (1.0f-s); + } + } + + + // delete packets traveling outside the scene + #pragma omp parallel for + for (int uP = 0; uP < m_usedPackets; uP++) + { + int i1 = m_usedPacket[uP]; + m_packet[i1].toDelete = false; + if (!m_packet[i1].use3rd) + { + Vector2f dir = m_packet[i1].pos1 - m_packet[i1].pOld1; + Vector2f dir2 = m_packet[i1].pos2 - m_packet[i1].pOld2; + if ((((m_packet[i1].pos1.x() < -0.5f*SCENE_EXTENT) && (dir.x() < 0.0)) + || ((m_packet[i1].pos1.x() > +0.5f*SCENE_EXTENT) && (dir.x() > 0.0)) + || ((m_packet[i1].pos1.y() < -0.5f*SCENE_EXTENT) && (dir.y() < 0.0)) + || ((m_packet[i1].pos1.y() > +0.5f*SCENE_EXTENT) && (dir.y() > 0.0))) + && + (((m_packet[i1].pos2.x() < -0.5f*SCENE_EXTENT) && (dir2.x() < 0.0)) + || ((m_packet[i1].pos2.x() > +0.5f*SCENE_EXTENT) && (dir2.x() > 0.0)) + || ((m_packet[i1].pos2.y() < -0.5f*SCENE_EXTENT) && (dir2.y() < 0.0)) + || ((m_packet[i1].pos2.y() > +0.5f*SCENE_EXTENT) && (dir2.y() > 0.0)))) + m_packet[i1].toDelete = true; + } + } + + // damping, insignificant packet removal (if too low amplitude), reduce energy of steep waves with too high gradient + m_softDampFactor = 1.0f + 100.0f*pow(max(0.0f, (float)(m_usedPackets)/(float)(m_packetBudget) - 1.0f), 2.0f); + #pragma omp parallel for + for (int uP = 0; uP < m_usedPackets; uP++) + if ((!m_packet[m_usedPacket[uP]].use3rd) && (!m_packet[m_usedPacket[uP]].toDelete)) + { + int i1 = m_usedPacket[uP]; + float dampViscosity = -2.0f*m_packet[i1].k*m_packet[i1].k*KINEMATIC_VISCOSITY; + float dampJunkfilm = -0.5f*m_packet[i1].k*sqrt(0.5f*KINEMATIC_VISCOSITY*m_packet[i1].w0); + m_packet[i1].E *= exp((dampViscosity + dampJunkfilm)*m_elapsedTime*m_softDampFactor); // wavelength-dependent damping + // amplitude computation: lower if too steep, delete if too low + float area = m_packet[i1].envelope*(m_packet[i1].pos2 - m_packet[i1].pos1).norm(); + float a1 = GetWaveAmplitude( area, m_packet[i1].E, m_packet[i1].k); + if (a1*m_packet[i1].k < PACKET_KILL_AMPLITUDE_DERIV) + m_packet[i1].toDelete = true; + else + { + // get the biggest wave as reference for energy reduction (conservative but important to not remove too much energy in case of large k intervals) + float a_L = GetWaveAmplitude(area, m_packet[i1].E, m_packet[i1].k_L); + float a_H = GetWaveAmplitude(area, m_packet[i1].E, m_packet[i1].k_H); + float k; + if (a_L*m_packet[i1].k_L < a_H*m_packet[i1].k_H) // take the smallest wave steepness (=amplitude derivative) + { + a1 = a_L; + k = m_packet[i1].k_L; + } + else + { + a1 = a_H; + k = m_packet[i1].k_H; + } + if (a1 > MAX_SPEEDNESS*2.0f*M_PI / k) + { + a1 = MAX_SPEEDNESS*2.0f*M_PI / k; + m_packet[i1].E = a1*a1*(area*0.5f*(DENSITY*GRAVITY + SIGMA*k*k)); + } + } + m_packet[i1].ampOld = min(a1, m_packet[i1].ampOld + m_packet[i1].dAmp); // smoothly increase amplitude from last timestep + // update variables needed for packet display + Vector2f posMidNew = 0.5f*(m_packet[i1].pos1 + m_packet[i1].pos2); + Vector2f posMidOld = 0.5f*(m_packet[i1].pOld1 + m_packet[i1].pOld2); + Vector2f dirN = (posMidNew - posMidOld).normalized(); // vector in traveling direction + m_packet[i1].midPos = posMidNew; + m_packet[i1].travelDir = dirN; + m_packet[i1].bending = GetIntersectionDistance(posMidNew, dirN, m_packet[i1].pos1, m_packet[i1].dir1); + } + // delete all packets identified to be deleted (important: NO parallelization here!) + for (int uP = 0; uP < m_usedPackets; uP++) + if (m_packet[m_usedPacket[uP]].toDelete) + DeletePacket(uP); + + // advect ghost packets + #pragma omp parallel for + for (int uG = 0; uG < m_usedGhosts; uG++) + { + int i1 = m_usedGhost[uG]; + m_ghostPacket[i1].pos += m_elapsedTime*m_ghostPacket[i1].speed*m_ghostPacket[i1].dir; + m_ghostPacket[i1].phase += m_ghostPacket[i1].dPhase; + m_ghostPacket[i1].ampOld = max(0.0f, m_ghostPacket[i1].ampOld - m_softDampFactor*m_ghostPacket[i1].dAmp); // decrease amplitude to let this wave disappear (take budget-based softdamping into account) + } + // delete all ghost packets if they traveled long enough (important: NO parallelization here!) + for (int uG = 0; uG < m_usedGhosts; uG++) + if (m_ghostPacket[m_usedGhost[uG]].ampOld <= 0.0) + DeleteGhost(uG); +} + + + diff --git a/Packets.h b/Packets.h new file mode 100644 index 0000000..92c62cf --- /dev/null +++ b/Packets.h @@ -0,0 +1,147 @@ +// Taken from https://github.com/jeschke/water-wave-packets +#pragma once + +#include "constants.h" + +#include +#include + +using namespace Eigen; +using namespace std; + + +// simulation parameters +#define PACKET_SPLIT_ANGLE 0.95105f // direction angle variation threshold: 0.95105=18 degree +#define PACKET_SPLIT_DISPERSION 0.3f // if the fastest wave in a packet traveled PACKET_SPLIT_DISPERSION*Envelopesize ahead, or the slowest by the same amount behind, subdivide this packet into two wavelength intervals +#define PACKET_KILL_AMPLITUDE_DERIV 0.0001f // waves below this maximum amplitude derivative gets killed +#define PACKET_BLEND_TRAVEL_FACTOR 1.0f // in order to be fully blended (appear or disappear), any wave must travel PACKET_BLEND_TRAVEL_FACTOR times "envelope size" in space (1.0 is standard) +#define PACKET_ENVELOPE_SIZE_FACTOR 3.0f // size of the envelope relative to wavelength (determines how many "bumps" appear) +#define PACKET_ENVELOPE_MINSIZE 0.02f // minimum envelope size in meters (smallest expected feature) +#define PACKET_ENVELOPE_MAXSIZE 10.0f // maximum envelope size in meters (largest expected feature) +#define PACKET_BOUNCE_FREQSPLIT true // (boolean) should a wave packet produce smaller waves at a bounce/reflection (->widen the wavelength interval of this packet)? +#define PACKET_BOUNCE_FREQSPLIT_K 31.4f // if k_L is smaller than this value (lambda = 20cm), the wave is (potentially) split after a bounce +#define MAX_SPEEDNESS 0.07f // all wave amplitudes a are limited to a <= MAX_SPEEDNESS*2.0*M_PI/k + +// physical parameters +#define SIGMA 0.074f // surface tension N/m at 20 grad celsius +#define GRAVITY 9.81f // GRAVITY m/s^2 +#define DENSITY 998.2071f // water density at 20 degree celsius +#define KINEMATIC_VISCOSITY 0.0000089f // kinematic viscosity +#define PACKET_SLOWAVE_K 143.1405792f // k of the slowest possible wave packet +#define PACKET_SLOWAVE_W0 40.2646141f // w_0 of the slowest possible wave packet + +// memory management +#define PACKET_BUFFER_DELTA 500000 // initial number of vertices, packet memory will be increased on demand by this stepsize + + + + +struct WAVE_PACKET +{ + // positions, directions, speed of the tracked vertices + Vector2f pos1,pos2,pos3; // 2D position + Vector2f dir1,dir2,dir3; // current movement direction + float speed1,speed2,speed3; // speed of the particle + Vector2f pOld1,pOld2,pOld3; // position in last timestep (needed to handle bouncing) + Vector2f dOld1,dOld2,dOld3; // direction in last timestep (needed to handle bouncing) + float sOld1,sOld2,sOld3; // speed in last timestep (needed to handle bouncing) + Vector2f midPos; // middle position (tracked each timestep, used for rendering) + Vector2f travelDir; // travel direction (tracked each timestep, used for rendering) + float bending; // point used for circular arc bending of the wave function inside envelope + + // bouncing and sliding + bool bounced1, bounced2, bounced3; // indicates if this vertex bounced in this timestep + bool sliding3; // indicates if the 3rd vertex is "sliding" (used for diffraction) + bool use3rd; // indicates if the third vertex is present (it marks a (potential) sliding point) + // wave function related + float phase; // phase of the representative wave inside the envelope, phase speed vs. group speed + float phOld; // old phase + float E; // wave energy flux for this packet (determines amplitude) + float envelope; // envelope size for this packet + float k,w0; // w0 = angular frequency, k = current wavenumber + float k_L,w0_L,k_H,w0_H; // w0 = angular frequency, k = current wavenumber, L/H are for lower/upper boundary + float d_L,d_H; // d = travel distance to reference wave (gets accumulated over time), L/H are for lower/upper boundary + float ampOld; // amplitude from last timestep, will be smoothly adjusted in each timestep to meet current desired amplitude + float dAmp; // amplitude change in each timestep (depends on desired waveheight so all waves (dis)appear with same speed) + // serial deletion step variable + bool toDelete; // used internally for parallel deletion criterion computation +public: +EIGEN_MAKE_ALIGNED_OPERATOR_NEW +}; + + + +struct GHOST_PACKET +{ + Vector2f pos; // 2D position + Vector2f dir; // current movement direction + float speed; // speed of the packet + float envelope; // envelope size for this packet + float bending; // point used for circular arc bending of the wave function inside envelope + float k; // k = current (representative) wavenumber(s) + float phase; // phase of the representative wave inside the envelope + float dPhase; // phase speed relative to group speed inside the envelope + float ampOld; // amplitude from last timestep, will be smoothly adjusted in each timestep to meet current desired amplitude + float dAmp; // change in amplitude in each timestep (waves travel PACKET_BLEND_TRAVEL_FACTOR*envelopesize in space until they disappear) +public: +EIGEN_MAKE_ALIGNED_OPERATOR_NEW +}; + + +class Packets +{ +public: + // scene + int m_groundSizeX, m_groundSizeY; // pixel size of the ground texture + float *m_ground; // texture containing the water depth and land (0.95) + float *m_distMap; // distance map of the boundary map + Vector2f *m_gndDeriv; + Vector2f *m_bndDeriv; + + // packet managing + WAVE_PACKET *m_packet; // wave packet data + GHOST_PACKET*m_ghostPacket; // ghost packet data + int m_packetBudget; // this can be changed any time (soft budget) + int m_packetNum; // current size of the buffer used for packets / ghosts + float m_softDampFactor; + int *m_usedPacket; + int m_usedPackets; + int *m_freePacket; + int m_freePackets; + int *m_usedGhost; + int m_usedGhosts; + int *m_freeGhost; + int m_freeGhosts; + + // simulation + float m_time; + float m_oldTime; + float m_elapsedTime; + + public: + EIGEN_MAKE_ALIGNED_OPERATOR_NEW + Packets(int packetBudget); + ~Packets(void); + void Reset(); + float GetBoundaryDist(Vector2f &p); + Vector2f GetBoundaryNormal(Vector2f &p); + float GetGroundVal(Vector2f &p); + Vector2f GetGroundNormal(Vector2f &p); + float GetWaterDepth(Vector2f &p); + void UpdateTime(float dTime); + void ExpandWavePacketMemory(int targetNum); + int GetFreePackedID(); + void DeletePacket(int id); + int GetFreeGhostID(); + void DeleteGhost(int id); + void CreatePacket(float pos1x, float pos1y, float pos2x, float pos2y, float dir1x, float dir1y, float dir2x, float dir2y, float k_L, float k_H, float E); + void CreateLinearWavefront(float xPos, float yPos, float dirx, float diry, float crestlength, float lambda_L, float lambda_H, float E); + void CreateSpreadingPacket(float xPos, float yPos, float dirx, float diry, float spreadFactor, float crestlength, float lambda_L, float lambda_H, float E); + void CreateCircularWavefront(float xPos, float yPos, float radius, float lambda_L, float lambda_H, float E); + void GetWaveParameters(float waterDepth, float w0, float kIn, float &k_out, float &speed_out); + float GetPhaseSpeed(float w_0, float kIn); + float GetWaveAmplitude(float area, float E, float k); + float GetIntersectionDistance(Vector2f pos1, Vector2f dir1, Vector2f pos2, Vector2f dir2); + bool AdvectPacketVertex(float elapsedTime, Vector2f &posIn, Vector2f &dirIn, float w0, float &kIn, float &speedIn, Vector2f &posOut, Vector2f &dirOut, float &speedOut); + void AdvectWavePackets(float dTime); +}; diff --git a/constants.h b/constants.h new file mode 100644 index 0000000..1fe4a38 --- /dev/null +++ b/constants.h @@ -0,0 +1,44 @@ +// Taken from https://github.com/jeschke/water-wave-packets +// Originally GlobalDefs.h + +// Global definitions needed for packet simulation and rendering + +// scene parameters +#define SCENE_EXTENT 100.0f // extent of the entire scene (packets traveling outside are removed) +#define MIN_WATER_DEPTH 0.1f // minimum water depth (meters) +#define MAX_WATER_DEPTH 5.0f // maximum water depth (meters) +#define WATER_TERRAIN_FILE "TestIsland.bmp"// Contains water depth and land height in different channels + + +// rendering parameters +#define PACKET_GPU_BUFFER_SIZE 1000000 // maximum number of wave packets to be displayed in one draw call + + +/* +// Fast rendering setup +#define WAVETEX_WIDTH_FACTOR 0.5 // the wavemesh texture compared to screen resolution +#define WAVETEX_HEIGHT_FACTOR 1 // the wavemesh texture compared to screen resolution +#define WAVEMESH_WIDTH_FACTOR 0.1 // the fine wave mesh compared to screen resolution +#define WAVEMESH_HEIGHT_FACTOR 0.25 // the fine wave mesh compared to screen resolution +#define AA_OVERSAMPLE_FACTOR 2 // anti aliasing applied in BOTH X and Y directions {1,2,4,8} +*/ + +/* +// Balanced rendering setup +#define WAVETEX_WIDTH_FACTOR 1 // the wavemesh texture compared to screen resolution +#define WAVETEX_HEIGHT_FACTOR 2 // the wavemesh texture compared to screen resolution +#define WAVEMESH_WIDTH_FACTOR 1 // the fine wave mesh compared to screen resolution +#define WAVEMESH_HEIGHT_FACTOR 2 // the fine wave mesh compared to screen resolution +#define AA_OVERSAMPLE_FACTOR 2 // anti aliasing applied in BOTH X and Y directions {1,2,4,8} +*/ + + +// High quality rendering setup +#define WAVETEX_WIDTH_FACTOR 2 // the wavemesh texture compared to screen resolution +#define WAVETEX_HEIGHT_FACTOR 4 // the wavemesh texture compared to screen resolution +#define WAVEMESH_WIDTH_FACTOR 2 // the fine wave mesh compared to screen resolution +#define WAVEMESH_HEIGHT_FACTOR 4 // the fine wave mesh compared to screen resolution +#define AA_OVERSAMPLE_FACTOR 4 // anti aliasing applied in BOTH X and Y directions {1,2,4,8} + + + diff --git a/inf2705.h b/inf2705.h new file mode 100644 index 0000000..55d5fb6 --- /dev/null +++ b/inf2705.h @@ -0,0 +1,3070 @@ +// Version: mar nov 21 12:54:32 EST 2017 +#ifndef INF2705_MATRICE_H +#define INF2705_MATRICE_H + +//////////////////////////////////////////////////////////////////////////// +// +// Classe pour les matrices du pipeline +// (INF2705, Benoît Ozell) +//////////////////////////////////////////////////////////////////////////// + +#include +#include + +#define GLM_SWIZZLE +// (à compter de GLM 9.8, c'est plutôt la variable ci-dessous qu'il faut définir) +#define GLM_FORCE_SWIZZLE +#define GLM_FORCE_RADIANS 1 +#include +#include +#include +#include +#include + +class MatricePipeline +{ +public: + MatricePipeline() + { matr_.push( glm::mat4(1.0) ); } + + operator glm::mat4() const { return matr_.top(); } + operator const GLfloat*() const { return glm::value_ptr(matr_.top()); } + + void LoadIdentity() + { matr_.top() = glm::mat4(1.0); } + // Note: la librairie glm s’occupe de convertir les glm::vec3 en glm::vec4 pour la multiplication par glm::mat4 matr_. + void Scale( GLfloat sx, GLfloat sy, GLfloat sz ) + { matr_.top() = glm::scale( matr_.top(), glm::vec3(sx,sy,sz) ); } + void Translate( GLfloat tx, GLfloat ty, GLfloat tz ) + { matr_.top() = glm::translate( matr_.top(), glm::vec3(tx,ty,tz) ); } + void Rotate( GLfloat angle, GLfloat x, GLfloat y, GLfloat z ) + { matr_.top() = glm::rotate( matr_.top(), (GLfloat)glm::radians(angle), glm::vec3(x,y,z) ); } + + void LookAt( GLdouble obsX, GLdouble obsY, GLdouble obsZ, GLdouble versX, GLdouble versY, GLdouble versZ, GLdouble upX, GLdouble upY, GLdouble upZ ) + { matr_.top() = glm::lookAt( glm::vec3( obsX, obsY, obsZ ), glm::vec3( versX, versY, versZ ), glm::vec3( upX, upY, upZ ) ); } + void Frustum( GLdouble gauche, GLdouble droite, GLdouble bas, GLdouble haut, GLdouble planAvant, GLdouble planArriere ) + { matr_.top() = glm::frustum( gauche, droite, bas, haut, planAvant, planArriere ); } + void Perspective( GLdouble fovy, GLdouble aspect, GLdouble planAvant, GLdouble planArriere ) + { matr_.top() = glm::perspective( glm::radians(fovy), aspect, planAvant, planArriere );} + void Ortho( GLdouble gauche, GLdouble droite, GLdouble bas, GLdouble haut, GLdouble planAvant, GLdouble planArriere ) + { matr_.top() = glm::ortho( gauche, droite, bas, haut, planAvant, planArriere ); } + void Ortho2D( GLdouble gauche, GLdouble droite, GLdouble bas, GLdouble haut ) + { matr_.top() = glm::ortho( gauche, droite, bas, haut ); } + + void PushMatrix() + { matr_.push( matr_.top() ); } + void PopMatrix() + { matr_.pop(); } + + glm::mat4 getMatr() + { return matr_.top(); } + glm::mat4 setMatr( glm::mat4 m ) + { return( matr_.top() = m ); } + + friend std::ostream& operator<<( std::ostream& o, const MatricePipeline& mp ) + { + //return o << glm::to_string(mp.matr_.top()); + glm::mat4 m = mp.matr_.top(); //o.precision(3); o.width(6); + return o << std::endl + << " " << m[0][0] << " " << m[1][0] << " " << m[2][0] << " " << m[3][0] << std::endl + << " " << m[0][1] << " " << m[1][1] << " " << m[2][1] << " " << m[3][1] << std::endl + << " " << m[0][2] << " " << m[1][2] << " " << m[2][2] << " " << m[3][2] << std::endl + << " " << m[0][3] << " " << m[1][3] << " " << m[2][3] << " " << m[3][3] << std::endl; + } + +private: + std::stack matr_; +}; + +#endif + +#ifndef INF2705_NUANCEUR_H +#define INF2705_NUANCEUR_H + +//////////////////////////////////////////////////////////////////////////// +// +// Classe pour charger les nuanceurs +// (INF2705, Benoît Ozell) +//////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include + +class ProgNuanceur +{ +public: + ProgNuanceur( ) : prog_(0), etiquette_("vide") { } + ~ProgNuanceur( ) { /* glDeleteShader(); */ glDeleteProgram( prog_ ); } + operator GLuint() const { return prog_; } + ProgNuanceur& operator=( GLuint prog ) { prog_ = prog; return *this; } + + // Demander à OpenGL de créer un programme de nuanceur + void creer( const std::string etiquette = "" ) + { + if ( prog_ ) glDeleteProgram( prog_ ); + prog_ = glCreateProgram(); + etiquette_ = etiquette; + if ( etiquette_ == "" ) { std::ostringstream oss; oss << prog_; etiquette_ += oss.str(); } + } + + // Charger en mémoire le contenu du fichier + static const GLchar *lireNuanceur( const GLchar *fich ) + { + // Ouvrir le fichier + std::ifstream fichier( fich ); + if ( fichier.fail() ) + { + std::cerr << "!!! " << fich << ": " << strerror(errno) << std::endl; + return NULL; + } + + // Lire le fichier + std::stringstream contenuFichier; + contenuFichier << fichier.rdbuf(); + fichier.close(); + + // Obtenir le contenu du fichier + std::string contenu = contenuFichier.str(); + const int taille = contenu.size(); + + // Retourner une chaîne pour le nuanceur + char *source = new char[taille+1]; + strcpy( source, contenu.c_str() ); + return source; + } + + // Afficher le log de la compilation + static bool afficherLogCompile( GLuint nuanceurObj ) + { + // afficher le message d'erreur, le cas échéant + int infologLength = 0; + glGetShaderiv( nuanceurObj, GL_INFO_LOG_LENGTH, &infologLength ); + if ( infologLength > 1 ) + { + char* infoLog = new char[infologLength+1]; + int charsWritten = 0; + glGetShaderInfoLog( nuanceurObj, infologLength, &charsWritten, infoLog ); + std::cout << std::endl << infoLog << std::endl; + delete[] infoLog; + return( false ); + } + return( true ); + } + + // Compiler et attacher le nuanceur + bool attacher( GLuint type, GLsizei nbChaine, const GLchar **chaines, const GLint *longueur = NULL ) + { + GLuint nuanceurObj = glCreateShader( type ); + glShaderSource( nuanceurObj, nbChaine, chaines, longueur ); + glCompileShader( nuanceurObj ); + glAttachShader( prog_, nuanceurObj ); + return ( afficherLogCompile(nuanceurObj) ); + } + bool attacher( GLuint type, const GLchar *fich ) + { + bool rc = false; + const GLchar *fichChaine = lireNuanceur( fich ); + if ( fichChaine != NULL ) + { + rc = attacher( type, 1, &fichChaine, NULL ); + delete [] fichChaine; + } + return( rc ); + } + bool attacher( GLuint type, const GLchar *preambule, const GLchar *fich ) + { + bool rc = false; + if ( fich == NULL ) // le nuanceur complet est dans le préambule + rc = attacher( type, 1, &preambule, NULL ); + else + { + const GLchar *chaines[2] = { preambule, lireNuanceur( fich ) }; + if ( chaines[1] != NULL ) + { + rc = attacher( type, 2, chaines, NULL ); + delete [] chaines[1]; + } + } + return( rc ); + } + bool attacher( GLuint type, const std::string preambule, const GLchar *fich ) + { return attacher( type, preambule.c_str(), fich ); } + + // Afficher le log de l'édition des liens + static bool afficherLogLink( GLuint progObj ) + { + // afficher le message d'erreur, le cas échéant + int infologLength = 0; + glGetProgramiv( progObj, GL_INFO_LOG_LENGTH, &infologLength ); + if ( infologLength > 1 ) + { + char* infoLog = new char[infologLength+1]; + int charsWritten = 0; + glGetProgramInfoLog( progObj, infologLength, &charsWritten, infoLog ); + std::cout << "progObj" << std::endl << infoLog << std::endl; + delete[] infoLog; + return( false ); + } + return( true ); + } + + // Faire l'édition des liens du programme + bool lier( ) + { + glLinkProgram( prog_ ); + return( afficherLogLink(prog_) ); + } + + // le nuanceur de sommets minimal + static const GLchar *chainesSommetsMinimal; + // le nuanceur de fragments minimal + static const GLchar *chainesFragmentsMinimal; + +private: + GLuint prog_; // LE programme + std::string etiquette_; // une étiquette pour identifier le programme +}; + +// le nuanceur de sommets minimal +const GLchar *ProgNuanceur::chainesSommetsMinimal = +{ + "#version 410\n" + "uniform mat4 matrModel;\n" + "uniform mat4 matrVisu;\n" + "uniform mat4 matrProj;\n" + "layout(location=0) in vec4 Vertex;\n" + "layout(location=3) in vec4 Color;\n" + "out vec4 couleur;\n" + "void main( void )\n" + "{\n" + " // transformation standard du sommet\n" + " gl_Position = matrProj * matrVisu * matrModel * Vertex;\n" + " // couleur du sommet\n" + " couleur = Color;\n" + "}\n" +}; +// le nuanceur de fragments minimal +const GLchar *ProgNuanceur::chainesFragmentsMinimal = +{ + "#version 410\n" + "in vec4 couleur;\n" + "out vec4 FragColor;\n" + "void main( void )\n" + "{\n" + " // la couleur du fragment est la couleur interpolée\n" + " FragColor = couleur;\n" + "}\n" +}; + +#endif + +#ifndef INF2705_FENETRE_H +#define INF2705_FENETRE_H + +//////////////////////////////////////////////////////////////////////////// +// +// Classe pour créer une fenêtre +// (INF2705, Benoît Ozell) +//////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include + +#if defined( __APPLE__ ) +#define FENETRE_LIB_GLFW 1 +#else +#define FENETRE_LIB_SDL 1 +#endif + +#if defined(FENETRE_LIB_GLFW) +# include +#else +# include +#endif +#include +#include + +typedef enum { +#if defined(FENETRE_LIB_GLFW) + + TP_ECHAP = GLFW_KEY_ESCAPE, + TP_BAS = GLFW_KEY_UP, + TP_HAUT = GLFW_KEY_DOWN, + TP_PAGEPREC = GLFW_KEY_PAGE_UP, + TP_PAGESUIV = GLFW_KEY_PAGE_DOWN, + TP_DEBUT = GLFW_KEY_HOME, + TP_FIN = GLFW_KEY_END, + TP_EGAL = '=', // GLFW_KEY_EQUALS, + TP_SUPERIEUR = '>', // GLFW_KEY_GREATER, + TP_INFERIEUR = '<', // GLFW_KEY_LESS, + TP_DROITE = GLFW_KEY_RIGHT, + TP_GAUCHE = GLFW_KEY_LEFT, + TP_PLUS = '+', // GLFW_KEY_PLUS, + TP_MOINS = GLFW_KEY_MINUS, + TP_CROCHETDROIT = GLFW_KEY_RIGHT_BRACKET, + TP_CROCHETGAUCHE = GLFW_KEY_LEFT_BRACKET, + TP_POINT = GLFW_KEY_PERIOD, + TP_VIRGULE = GLFW_KEY_COMMA, + TP_POINTVIRGULE = GLFW_KEY_SEMICOLON, + TP_BARREOBLIQUE = GLFW_KEY_SLASH, + TP_ESPACE = GLFW_KEY_SPACE, + TP_SOULIGNE = '_', // GLFW_KEY_UNDERSCORE, + + TP_0 = GLFW_KEY_0, + TP_1 = GLFW_KEY_1, + TP_2 = GLFW_KEY_2, + TP_3 = GLFW_KEY_3, + TP_4 = GLFW_KEY_4, + TP_5 = GLFW_KEY_5, + TP_6 = GLFW_KEY_6, + TP_7 = GLFW_KEY_7, + TP_8 = GLFW_KEY_8, + TP_9 = GLFW_KEY_9, + + TP_a = GLFW_KEY_A, + TP_b = GLFW_KEY_B, + TP_c = GLFW_KEY_C, + TP_d = GLFW_KEY_D, + TP_e = GLFW_KEY_E, + TP_f = GLFW_KEY_F, + TP_g = GLFW_KEY_G, + TP_h = GLFW_KEY_H, + TP_i = GLFW_KEY_I, + TP_j = GLFW_KEY_J, + TP_k = GLFW_KEY_K, + TP_l = GLFW_KEY_L, + TP_m = GLFW_KEY_M, + TP_n = GLFW_KEY_N, + TP_o = GLFW_KEY_O, + TP_p = GLFW_KEY_P, + TP_q = GLFW_KEY_Q, + TP_r = GLFW_KEY_R, + TP_s = GLFW_KEY_S, + TP_t = GLFW_KEY_T, + TP_u = GLFW_KEY_U, + TP_v = GLFW_KEY_V, + TP_w = GLFW_KEY_W, + TP_x = GLFW_KEY_X, + TP_y = GLFW_KEY_Y, + TP_z = GLFW_KEY_Z, + +#else + + TP_ECHAP = SDLK_ESCAPE, + TP_BAS = SDLK_UP, + TP_HAUT = SDLK_DOWN, + TP_PAGEPREC = SDLK_PAGEUP, + TP_PAGESUIV = SDLK_PAGEDOWN, + TP_DEBUT = SDLK_HOME, + TP_FIN = SDLK_END, + TP_EGAL = SDLK_EQUALS, + TP_SUPERIEUR = SDLK_GREATER, + TP_INFERIEUR = SDLK_LESS, + TP_DROITE = SDLK_RIGHT, + TP_GAUCHE = SDLK_LEFT, + TP_PLUS = SDLK_PLUS, + TP_MOINS = SDLK_MINUS, + TP_CROCHETDROIT = SDLK_RIGHTBRACKET, + TP_CROCHETGAUCHE = SDLK_LEFTBRACKET, + TP_POINT = SDLK_PERIOD, + TP_VIRGULE = SDLK_COMMA, + TP_POINTVIRGULE = SDLK_SEMICOLON, + TP_BARREOBLIQUE = SDLK_SLASH, + TP_ESPACE = SDLK_SPACE, + TP_SOULIGNE = SDLK_UNDERSCORE, + + TP_0 = SDLK_0, + TP_1 = SDLK_1, + TP_2 = SDLK_2, + TP_3 = SDLK_3, + TP_4 = SDLK_4, + TP_5 = SDLK_5, + TP_6 = SDLK_6, + TP_7 = SDLK_7, + TP_8 = SDLK_8, + TP_9 = SDLK_9, + + TP_a = SDLK_a, + TP_b = SDLK_b, + TP_c = SDLK_c, + TP_d = SDLK_d, + TP_e = SDLK_e, + TP_f = SDLK_f, + TP_g = SDLK_g, + TP_h = SDLK_h, + TP_i = SDLK_i, + TP_j = SDLK_j, + TP_k = SDLK_k, + TP_l = SDLK_l, + TP_m = SDLK_m, + TP_n = SDLK_n, + TP_o = SDLK_o, + TP_p = SDLK_p, + TP_q = SDLK_q, + TP_r = SDLK_r, + TP_s = SDLK_s, + TP_t = SDLK_t, + TP_u = SDLK_u, + TP_v = SDLK_v, + TP_w = SDLK_w, + TP_x = SDLK_x, + TP_y = SDLK_y, + TP_z = SDLK_z, + +#endif +} TP_touche; + +typedef enum { +#if defined(FENETRE_LIB_GLFW) + TP_BOUTON_GAUCHE = GLFW_MOUSE_BUTTON_1, + TP_BOUTON_MILIEU = GLFW_MOUSE_BUTTON_3, + TP_BOUTON_DROIT = GLFW_MOUSE_BUTTON_2, + TP_RELACHE = GLFW_RELEASE, + TP_PRESSE = GLFW_PRESS, +#else + TP_BOUTON_GAUCHE = SDL_BUTTON_LEFT, + TP_BOUTON_MILIEU = SDL_BUTTON_MIDDLE, + TP_BOUTON_DROIT = SDL_BUTTON_RIGHT, + TP_RELACHE = SDL_RELEASED, + TP_PRESSE = SDL_PRESSED, +#endif +} TP_bouton; + + +// la fenêtre graphique +class FenetreTP +{ +#if defined(FENETRE_LIB_GLFW) + static void key_callback( GLFWwindow* window, int key, int scancode, int action, int mods ) + { + FenetreTP *fen = (FenetreTP*) glfwGetWindowUserPointer( window ); + if ( action == GLFW_PRESS ) + fen->clavier( (TP_touche) key ); + } + static void mouse_button_callback( GLFWwindow* window, int button, int action, int mods ) + { + FenetreTP *fen = (FenetreTP*) glfwGetWindowUserPointer( window ); + double xpos, ypos; glfwGetCursorPos( window, &xpos, &ypos ); + fen->sourisClic( button, action, xpos, ypos ); + } + static void cursor_position_callback( GLFWwindow* window, double xpos, double ypos ) + { + FenetreTP *fen = (FenetreTP*) glfwGetWindowUserPointer( window ); + fen->sourisMouvement( xpos, ypos ); + } + static void scroll_callback( GLFWwindow* window, double xoffset, double yoffset ) + { + FenetreTP *fen = (FenetreTP*) glfwGetWindowUserPointer( window ); + fen->sourisWheel( xoffset, yoffset ); + } + static void window_refresh_callback( GLFWwindow* window ) + { + FenetreTP *fen = (FenetreTP*) glfwGetWindowUserPointer( window ); + // int left, top, right, bottom; + // glfwGetWindowFrameSize( window, &left, &top, &right, &bottom ); + // fen->redimensionner( right-left, top-bottom ); + int width, height; + glfwGetWindowSize( window, &width, &height ); + fen->redimensionner( width, height ); + fen->afficherScene(); + fen->swap(); + } +#else +#endif + +public: + FenetreTP( std::string nom = "INF2705 TP", + int largeur = 900, int hauteur = 600, + int xpos = 100, int ypos = 100 ) + : fenetre_(NULL), +#if defined(FENETRE_LIB_GLFW) +#else + contexte_(NULL), +#endif + largeur_(largeur), hauteur_(hauteur) + { +#if defined(FENETRE_LIB_GLFW) + // initialiser GLFW + if ( !glfwInit() ) + { + glfwdie( "ERREUR: Incapable d'initialiser GLFW3\n"); + } + + // demander certaines caractéristiques: + glfwWindowHint( GLFW_RED_BITS, 8 ); + glfwWindowHint( GLFW_GREEN_BITS, 8 ); + glfwWindowHint( GLFW_BLUE_BITS, 8 ); + glfwWindowHint( GLFW_ALPHA_BITS, 8 ); + //glfwWindowHint( GLFW_DOUBLEBUFFER, GL_TRUE ); + glfwWindowHint( GLFW_DEPTH_BITS, 24 ); + glfwWindowHint( GLFW_STENCIL_BITS, 8 ); +#if defined( __APPLE__ ) + glfwWindowHint( GLFW_CONTEXT_VERSION_MAJOR, 4 ); + glfwWindowHint( GLFW_CONTEXT_VERSION_MINOR, 1 ); + glfwWindowHint( GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE ); + glfwWindowHint( GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE ); +#endif + + // créer la fenêtre + fenetre_ = glfwCreateWindow( largeur, hauteur, nom.c_str(), NULL, NULL ); + if ( !fenetre_ ) + { + glfwdie( "ERROR: Incapable de créer la fenêtre GLFW3\n"); + } + glfwMakeContextCurrent( fenetre_ ); + glfwSwapInterval(1); + + glfwSetWindowUserPointer( fenetre_, this ); + glfwSetKeyCallback( fenetre_, key_callback ); + glfwSetMouseButtonCallback( fenetre_, mouse_button_callback ); + glfwSetCursorPosCallback( fenetre_, cursor_position_callback ); + glfwSetScrollCallback( fenetre_, scroll_callback ); + glfwSetWindowRefreshCallback( fenetre_, window_refresh_callback ); +#else + // initialiser SDL + const Uint32 flags = SDL_INIT_VIDEO | SDL_INIT_EVENTS; + if ( SDL_WasInit( flags ) == 0 ) + { + if ( SDL_Init( flags ) < 0 ) sdldie( "ERREUR: Incapable d'initialiser SDL" ); + atexit( SDL_Quit ); + } + + // demander certaines caractéristiques: https://wiki.libsdl.org/SDL_GL_SetAttribute + SDL_GL_SetAttribute( SDL_GL_RED_SIZE, 8 ); + SDL_GL_SetAttribute( SDL_GL_GREEN_SIZE, 8 ); + SDL_GL_SetAttribute( SDL_GL_BLUE_SIZE, 8 ); + SDL_GL_SetAttribute( SDL_GL_ALPHA_SIZE, 8 ); + //SDL_GL_SetAttribute( SDL_GL_DOUBLEBUFFER, 1 ); + SDL_GL_SetAttribute( SDL_GL_DEPTH_SIZE, 24 ); + SDL_GL_SetAttribute( SDL_GL_STENCIL_SIZE, 8 ); + //SDL_GL_SetAttribute( SDL_GL_MULTISAMPLESAMPLES, 4 ); + SDL_GL_SetAttribute( SDL_GL_ACCELERATED_VISUAL, 1 ); +#if 0 + SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE ); + //SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 4 ); + SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 1 ); +#endif + + // créer la fenêtre + fenetre_ = SDL_CreateWindow( nom.c_str(), xpos, ypos, largeur, hauteur, + SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE ); + if ( !fenetre_ ) sdldie( "ERREUR: Incapable de créer la fenêtre SDL" ); + verifierErreurSDL(__LINE__); + + // créer le contexte OpenGL + contexte_ = SDL_GL_CreateContext( fenetre_ ); + verifierErreurSDL(__LINE__); + + // s'assurer que les réaffichage seront synchronisés avec le rafraîchissement de l'écran + SDL_GL_SetSwapInterval( 1 ); + verifierErreurSDL(__LINE__); +#endif + + // initiliaser GLEW + initialiserGLEW( ); + + // imprimer un peu d'information OpenGL + imprimerInfosGL( ); + } + +#if defined(FENETRE_LIB_GLFW) + void quit( ) + { + glfwDestroyWindow( fenetre_ ); + glfwTerminate(); + exit(0); + } + ~FenetreTP( ) + { + quit(); + } +#else + void quit( ) + { + SDL_Event sdlevent; sdlevent.type = SDL_QUIT; + SDL_PushEvent( &sdlevent ); + } + ~FenetreTP( ) + { + SDL_GL_DeleteContext( contexte_ ); contexte_ = NULL; + SDL_DestroyWindow( fenetre_ ); fenetre_ = NULL; + } +#endif + + // mettre à jour la fenêtre OpenGL: le tampon arrière devient le tampon avant et vice-versa +#if defined(FENETRE_LIB_GLFW) + void swap( ) { glfwSwapBuffers( fenetre_ ); } +#else + void swap( ) { SDL_GL_SwapWindow( fenetre_ ); } +#endif + + // fonction appelée pour tracer la scène + void afficherScene( ); + // fonction appelée lors d'un événement de redimensionnement + void redimensionner( GLsizei w, GLsizei h ); + // fonction appelée lors d'un événement de clavier + void clavier( TP_touche touche ); + // fonctions appelées lors d'un événement de souris + void sourisClic( int button, int state, int x, int y ); + void sourisWheel( int x, int y ); + void sourisMouvement( int x, int y ); + + // fonction de gestion de la boucle des événements + bool gererEvenement( ) + { +#if defined(FENETRE_LIB_GLFW) + glfwPollEvents(); + return( !glfwWindowShouldClose( fenetre_ ) ); +#else + SDL_Event e; + while ( SDL_PollEvent( &e ) ) + { + switch ( e.type ) + { + case SDL_QUIT: // c'est la fin + return( false ); + break; + case SDL_WINDOWEVENT: + if ( e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED ) // redimensionnement + { + largeur_ = e.window.data1; + hauteur_ = e.window.data2; + redimensionner( largeur_, hauteur_ ); + } + else if ( e.window.event == SDL_WINDOWEVENT_SHOWN ) // affichage + { + SDL_GetWindowSize( fenetre_, &largeur_, &hauteur_ ); + redimensionner( largeur_, hauteur_ ); + } + //else + // std::cout << "//@FenetreTP,WINDOWEVENT;" << " e.window.event=" << e.window.event << std::endl; + break; + case SDL_KEYDOWN: // une touche est pressée + clavier( (TP_touche) e.key.keysym.sym ); + break; + case SDL_KEYUP: // une touche est relâchée + break; + case SDL_MOUSEBUTTONDOWN: // un bouton de la souris est pressé + case SDL_MOUSEBUTTONUP: // un bouton de la souris est relâché + sourisClic( e.button.button, e.button.state, e.button.x, e.button.y ); + break; + case SDL_MOUSEMOTION: // la souris est déplacée + sourisMouvement( e.motion.x, e.motion.y ); + break; + case SDL_MOUSEWHEEL: // la molette de la souris est tournée + sourisWheel( e.wheel.x, e.wheel.y ); + break; + default: + //std::cerr << "//@FenetreTP," << __LINE__ << ";" << " e.type=" << e.type << std::endl; + break; + } + } + return( true ); +#endif + } + + // + // Quelques fonctions utilitaires + // + + // Charger en mémoire le contenu du fichier + static void imprimerTouches( ) + { + // Ouvrir le fichier + std::ifstream fichier( "touches.txt" ); + if ( fichier.fail() ) return; + + // Lire le fichier + std::stringstream contenuFichier; + contenuFichier << fichier.rdbuf(); + fichier.close(); + + // Ecrire le contenu du fichier + std::cout << " touches possibles :" << std::endl + << contenuFichier.str() << std::endl; + } + + // afficher les versions des éléments du pipeline OpenGL + static void imprimerInfosGL( const int verbose = 1 ) + { +#define PBYTE(CHAINE) ( (CHAINE) != NULL ? (CHAINE) : (const GLubyte *) "????" ) +#define PCHAR(CHAINE) ( (CHAINE) != NULL ? (CHAINE) : (const char *) "????" ) + + if ( verbose >= 1 ) + { +#if defined(FENETRE_LIB_GLFW) + int major, minor, rev; glfwGetVersion( &major, &minor, &rev ); + std::cout << "// GLFW " << major << "." << minor << "." << rev << std::endl; +#else + SDL_version linked; SDL_GetVersion( &linked ); + std::cout << "// SDL " << (int) linked.major << "." << (int) linked.minor << "." << (int) linked.patch << std::endl; +#endif + + const GLubyte *glVersion = glGetString( GL_VERSION ); + const GLubyte *glVendor = glGetString( GL_VENDOR ); + const GLubyte *glRenderer = glGetString( GL_RENDERER ); + const GLubyte *glslVersion = glGetString( GL_SHADING_LANGUAGE_VERSION ); + std::cout << "// OpenGL " << PBYTE(glVersion) << PBYTE(glVendor) << std::endl; + std::cout << "// GPU " << PBYTE(glRenderer) << std::endl; + std::cout << "// GLSL " << PBYTE(glslVersion) << std::endl; + + if ( verbose >= 2 ) + { + const GLubyte *glExtensions = glGetString( GL_EXTENSIONS ); + std::cout << "// extensions = " << PBYTE(glExtensions) << std::endl; + } + } +#undef PBYTE +#undef PCHAR + return; + } + +#if defined(FENETRE_LIB_GLFW) + // donner une message et mourir... + static void glfwdie( const char *msg ) + { + //const char *sdlerror = SDL_GetError(); + //std::cout << "glfwdie " << msg << " " << sdlerror << std::endl; + std::cout << "glfwdie " << msg << " " << std::endl; + //glfwTerminate(); + exit(1); + } +#else + // donner une message et mourir... + static void sdldie( const char *msg ) + { + const char *sdlerror = SDL_GetError(); + std::cout << "sdldie " << msg << " " << sdlerror << std::endl; + SDL_Quit(); + exit(1); + } + // vérifier les erreurs + static void verifierErreurSDL( int line = -1 ) + { + const char *sdlerror = SDL_GetError(); + if ( *sdlerror != '\0' ) + { + std::cout << "SDL Error: " << sdlerror << std::endl; + if ( line != -1 ) + std::cout << "line: " << line; + std::cout << std::endl; + SDL_ClearError(); + } + } +#endif + // La fonction glGetError() permet de savoir si une erreur est survenue depuis le dernier appel à cette fonction. + static int VerifierErreurGL( const std::string message ) + { + int rc = 0; + GLenum err; + while ( ( err = glGetError() ) != GL_NO_ERROR ) + { + std::cerr << "Erreur OpenGL, " << message << " " << std::endl; + switch ( err ) + { + case GL_INVALID_ENUM: + std::cerr << "GL_INVALID_ENUM: Valeur d'une énumération hors limite."; + break; + case GL_INVALID_VALUE: + std::cerr << "GL_INVALID_VALUE: Valeur numérique hors limite."; + break; + case GL_INVALID_OPERATION: + std::cerr << "GL_INVALID_OPERATION: Opération non permise dans l'état courant."; + break; + case GL_INVALID_FRAMEBUFFER_OPERATION: + std::cerr << "GL_INVALID_FRAMEBUFFER_OPERATION: L'objet est incomplet."; + break; + case GL_OUT_OF_MEMORY: + std::cerr << "GL_OUT_OF_MEMORY: Pas assez de mémoire pour exécuter la commande."; + break; + case GL_STACK_UNDERFLOW: + std::cerr << "GL_STACK_UNDERFLOW: Une opération entraînerait un débordement de pile interne."; + break; + case GL_STACK_OVERFLOW: + std::cerr << "GL_STACK_OVERFLOW: Une opération entraînerait un débordement de pile interne."; + break; + default: + std::cerr << "err = " << err << ": Erreur inconnue!"; + break; + } + std::cerr << std::endl; + ++rc; + } + return( rc ); + } + + // La fonction afficherAxes affiche des axes qui représentent l'orientation courante du repère + // Les axes sont colorés ainsi: X = Rouge, Y = Vert, Z = Bleu + static void afficherAxes( const GLfloat longueurAxe = 1.0, const GLfloat largeurLigne = 2.0 ) + { +#if !defined( __APPLE__ ) + glPushAttrib( GL_ENABLE_BIT | GL_LINE_BIT ); + glLineWidth( largeurLigne ); + + const GLfloat coo[] = { 0., 0., 0., + longueurAxe, 0., 0., + 0., longueurAxe, 0., + 0., 0., longueurAxe }; + const GLfloat couleur[] = { 1., 1., 1., + 1., 0., 0., + 0., 1., 0., + 0., 0., 1. }; + +#if 0 + const GLuint connec[] = { 0, 1, 0, 2, 0, 3 }; + GLint locVertex = 0; + glVertexAttribPointer( locVertex, 3, GL_FLOAT, GL_FALSE, 0, coo ); + glEnableVertexAttribArray(locVertex); + GLint locColor = 3; + glVertexAttribPointer( locColor, 3, GL_FLOAT, GL_FALSE, 0, couleur ); + glEnableVertexAttribArray(locColor); + glDrawElements( GL_LINES, sizeof(connec)/sizeof(GLuint), GL_UNSIGNED_INT, connec ); +#else + glBegin( GL_LINES );{ + glColor3fv( &(couleur[3]) ); glVertex3fv( &(coo[0]) ); glVertex3fv( &(coo[3]) ); + glColor3fv( &(couleur[6]) ); glVertex3fv( &(coo[0]) ); glVertex3fv( &(coo[6]) ); + glColor3fv( &(couleur[9]) ); glVertex3fv( &(coo[0]) ); glVertex3fv( &(coo[9]) ); + }glEnd( ); +#endif + + glPopAttrib( ); +#endif + return; + } + +private: + void initialiserGLEW( ) + { + //#if defined(_GLEW_H__) + glewExperimental = GL_TRUE; + GLenum rev = glewInit(); + if ( rev != GLEW_OK ) + { + std::cout << "Error: " << glewGetErrorString(rev) << std::endl; + exit( 1 ); + } + glGetError(); // Afin d'ignorer l'erreur générée par GLEW. Voir https://www.opengl.org/wiki/OpenGL_Loading_Library#GLEW_.28OpenGL_Extension_Wrangler.29 + //#endif + } + +#if defined(FENETRE_LIB_GLFW) + GLFWwindow *fenetre_; +#else + SDL_Window *fenetre_; + SDL_GLContext contexte_; +#endif + GLsizei largeur_; // la largeur de la fenêtre + GLsizei hauteur_; // la hauteur de la fenêtre +}; + +#endif + +#ifndef INF2705_TEXTURE_H +#define INF2705_TEXTURE_H + +//////////////////////////////////////////////////////////////////////////// +// +// Fonctions pour charger une texture +// (INF2705, Benoît Ozell) +//////////////////////////////////////////////////////////////////////////// + +#include +#include +#include + +/* + * Windows Bitmap File Loader + * Version 1.2.5 (20120929) + * + * Supported Formats: 1, 4, 8, 16, 24, 32 Bit Images + * Alpha Bitmaps are also supported. + * Supported compression types: RLE 8, BITFIELDS + * + * Created by: Benjamin Kalytta, 2006 - 2012 + * Thanks for bug fixes goes to: Chris Campbell + * + * Licence: Free to use, URL to my source and my name is required in your source code. + * + * Source can be found at http://www.kalytta.com/bitmap.h + * + * Warning: This code should not be used in unmodified form in a production environment. + * It should only serve as a basis for your own development. + * There is only a minimal error handling in this code. (Notice added 20111211) + */ + +#include +#include + +#ifndef __LITTLE_ENDIAN__ +# ifndef __BIG_ENDIAN__ +# define __LITTLE_ENDIAN__ +# endif +#endif + +#ifdef __LITTLE_ENDIAN__ +# define BITMAP_SIGNATURE 0x4d42 +#else +# define BITMAP_SIGNATURE 0x424d +#endif + +#if defined(_MSC_VER) || defined(__INTEL_COMPILER) +typedef unsigned __int32 uint32_t; +typedef unsigned __int16 uint16_t; +typedef unsigned __int8 uint8_t; +typedef __int32 int32_t; +#elif defined(__GNUC__) || defined(__CYGWIN__) || defined(__MWERKS__) || defined(__WATCOMC__) || defined(__PGI) || defined(__LCC__) +# include +#else +typedef unsigned int uint32_t; +typedef unsigned short int uint16_t; +typedef unsigned char uint8_t; +typedef int int32_t; +#endif + +#pragma pack(push, 1) + +typedef struct _BITMAP_FILEHEADER { + uint16_t Signature; + uint32_t Size; + uint32_t Reserved; + uint32_t BitsOffset; +} BITMAP_FILEHEADER; + +#define BITMAP_FILEHEADER_SIZE 14 + +typedef struct _BITMAP_HEADER { + uint32_t HeaderSize; + int32_t Width; + int32_t Height; + uint16_t Planes; + uint16_t BitCount; + uint32_t Compression; + uint32_t SizeImage; + int32_t PelsPerMeterX; + int32_t PelsPerMeterY; + uint32_t ClrUsed; + uint32_t ClrImportant; + uint32_t RedMask; + uint32_t GreenMask; + uint32_t BlueMask; + uint32_t AlphaMask; + uint32_t CsType; + uint32_t Endpoints[9]; // see http://msdn2.microsoft.com/en-us/library/ms536569.aspx + uint32_t GammaRed; + uint32_t GammaGreen; + uint32_t GammaBlue; +} BITMAP_HEADER; + +typedef struct _RGBA { + uint8_t Red; + uint8_t Green; + uint8_t Blue; + uint8_t Alpha; +} RGBA; + +typedef struct _BGRA { + uint8_t Blue; + uint8_t Green; + uint8_t Red; + uint8_t Alpha; +} BGRA; + +#pragma pack(pop) + +class CBitmap { +private: + BITMAP_FILEHEADER m_BitmapFileHeader; + BITMAP_HEADER m_BitmapHeader; + RGBA *m_BitmapData; + unsigned int m_BitmapSize; + + // Masks and bit counts shouldn't exceed 32 Bits +public: + class CColor { + public: + static inline unsigned int BitCountByMask(unsigned int Mask) { + unsigned int BitCount = 0; + while (Mask) { + Mask &= Mask - 1; + BitCount++; + } + return BitCount; + } + + static inline unsigned int BitPositionByMask(unsigned int Mask) { + return BitCountByMask((Mask & (~Mask + 1)) - 1); + } + + static inline unsigned int ComponentByMask(unsigned int Color, unsigned int Mask) { + unsigned int Component = Color & Mask; + return Component >> BitPositionByMask(Mask); + } + + static inline unsigned int BitCountToMask(unsigned int BitCount) { + return (BitCount == 32) ? 0xFFFFFFFF : (1 << BitCount) - 1; + } + + static unsigned int Convert(unsigned int Color, unsigned int FromBitCount, unsigned int ToBitCount) { + if (ToBitCount < FromBitCount) { + Color >>= (FromBitCount - ToBitCount); + } else { + Color <<= (ToBitCount - FromBitCount); + if (Color > 0) { + Color |= BitCountToMask(ToBitCount - FromBitCount); + } + } + return Color; + } + }; + +public: + + CBitmap() : m_BitmapData(0), m_BitmapSize(0) { + Dispose(); + } + + CBitmap(const char* Filename) : m_BitmapData(0), m_BitmapSize(0) { + Load(Filename); + } + + ~CBitmap() { + Dispose(); + } + + void Dispose() { + if (m_BitmapData) { + delete[] m_BitmapData; + m_BitmapData = 0; + } + memset(&m_BitmapFileHeader, 0, sizeof(m_BitmapFileHeader)); + memset(&m_BitmapHeader, 0, sizeof(m_BitmapHeader)); + } + + /* Load specified Bitmap and stores it as RGBA in an internal buffer */ + + bool Load(const char *Filename) { + std::ifstream file(Filename, std::ios::binary | std::ios::in); + + if (file.bad()) { + return false; + } + + if (file.is_open() == false) { + return false; + } + + Dispose(); + + file.read((char*) &m_BitmapFileHeader, BITMAP_FILEHEADER_SIZE); + if (m_BitmapFileHeader.Signature != BITMAP_SIGNATURE) { + return false; + } + + file.read((char*) &m_BitmapHeader, sizeof(BITMAP_HEADER)); + + /* Load Color Table */ + + file.seekg(BITMAP_FILEHEADER_SIZE + m_BitmapHeader.HeaderSize, std::ios::beg); + + unsigned int ColorTableSize = 0; + + if (m_BitmapHeader.BitCount == 1) { + ColorTableSize = 2; + } else if (m_BitmapHeader.BitCount == 4) { + ColorTableSize = 16; + } else if (m_BitmapHeader.BitCount == 8) { + ColorTableSize = 256; + } + + // Always allocate full sized color table + + BGRA* ColorTable = new BGRA[ColorTableSize]; // std::bad_alloc exception should be thrown if memory is not available + + file.read((char*) ColorTable, sizeof(BGRA) * m_BitmapHeader.ClrUsed); + + /* ... Color Table for 16 bits images are not supported yet */ + + m_BitmapSize = GetWidth() * GetHeight(); + m_BitmapData = new RGBA[m_BitmapSize]; + + unsigned int LineWidth = ((GetWidth() * GetBitCount() / 8) + 3) & ~3; + uint8_t *Line = new uint8_t[LineWidth]; + + file.seekg(m_BitmapFileHeader.BitsOffset, std::ios::beg); + + int Index = 0; + bool Result = true; + + if (m_BitmapHeader.Compression == 0) { + for (unsigned int i = 0; i < GetHeight(); i++) { + file.read((char*) Line, LineWidth); + + uint8_t *LinePtr = Line; + + for (unsigned int j = 0; j < GetWidth(); j++) { + if (m_BitmapHeader.BitCount == 1) { + uint32_t Color = *((uint8_t*) LinePtr); + for (int k = 0; k < 8; k++) { + m_BitmapData[Index].Red = ColorTable[Color & 0x80 ? 1 : 0].Red; + m_BitmapData[Index].Green = ColorTable[Color & 0x80 ? 1 : 0].Green; + m_BitmapData[Index].Blue = ColorTable[Color & 0x80 ? 1 : 0].Blue; + m_BitmapData[Index].Alpha = ColorTable[Color & 0x80 ? 1 : 0].Alpha; + Index++; + Color <<= 1; + } + LinePtr++; + j += 7; + } else if (m_BitmapHeader.BitCount == 4) { + uint32_t Color = *((uint8_t*) LinePtr); + m_BitmapData[Index].Red = ColorTable[(Color >> 4) & 0x0f].Red; + m_BitmapData[Index].Green = ColorTable[(Color >> 4) & 0x0f].Green; + m_BitmapData[Index].Blue = ColorTable[(Color >> 4) & 0x0f].Blue; + m_BitmapData[Index].Alpha = ColorTable[(Color >> 4) & 0x0f].Alpha; + Index++; + m_BitmapData[Index].Red = ColorTable[Color & 0x0f].Red; + m_BitmapData[Index].Green = ColorTable[Color & 0x0f].Green; + m_BitmapData[Index].Blue = ColorTable[Color & 0x0f].Blue; + m_BitmapData[Index].Alpha = ColorTable[Color & 0x0f].Alpha; + Index++; + LinePtr++; + j++; + } else if (m_BitmapHeader.BitCount == 8) { + uint32_t Color = *((uint8_t*) LinePtr); + m_BitmapData[Index].Red = ColorTable[Color].Red; + m_BitmapData[Index].Green = ColorTable[Color].Green; + m_BitmapData[Index].Blue = ColorTable[Color].Blue; + m_BitmapData[Index].Alpha = ColorTable[Color].Alpha; + Index++; + LinePtr++; + } else if (m_BitmapHeader.BitCount == 16) { + uint32_t Color = *((uint16_t*) LinePtr); + m_BitmapData[Index].Red = ((Color >> 10) & 0x1f) << 3; + m_BitmapData[Index].Green = ((Color >> 5) & 0x1f) << 3; + m_BitmapData[Index].Blue = (Color & 0x1f) << 3; + m_BitmapData[Index].Alpha = 255; + Index++; + LinePtr += 2; + } else if (m_BitmapHeader.BitCount == 24) { + uint32_t Color = *((uint32_t*) LinePtr); + m_BitmapData[Index].Blue = Color & 0xff; + m_BitmapData[Index].Green = (Color >> 8) & 0xff; + m_BitmapData[Index].Red = (Color >> 16) & 0xff; + m_BitmapData[Index].Alpha = 255; + Index++; + LinePtr += 3; + } else if (m_BitmapHeader.BitCount == 32) { + uint32_t Color = *((uint32_t*) LinePtr); + m_BitmapData[Index].Blue = Color & 0xff; + m_BitmapData[Index].Green = (Color >> 8) & 0xff; + m_BitmapData[Index].Red = (Color >> 16) & 0xff; + m_BitmapData[Index].Alpha = Color >> 24; + Index++; + LinePtr += 4; + } + } + } + } else if (m_BitmapHeader.Compression == 1) { // RLE 8 + uint8_t Count = 0; + uint8_t ColorIndex = 0; + int x = 0, y = 0; + + while (file.eof() == false) { + file.read((char*) &Count, sizeof(uint8_t)); + file.read((char*) &ColorIndex, sizeof(uint8_t)); + + if (Count > 0) { + Index = x + y * GetWidth(); + for (int k = 0; k < Count; k++) { + m_BitmapData[Index + k].Red = ColorTable[ColorIndex].Red; + m_BitmapData[Index + k].Green = ColorTable[ColorIndex].Green; + m_BitmapData[Index + k].Blue = ColorTable[ColorIndex].Blue; + m_BitmapData[Index + k].Alpha = ColorTable[ColorIndex].Alpha; + } + x += Count; + } else if (Count == 0) { + int Flag = ColorIndex; + if (Flag == 0) { + x = 0; + y++; + } else if (Flag == 1) { + break; + } else if (Flag == 2) { + char rx = 0; + char ry = 0; + file.read((char*) &rx, sizeof(char)); + file.read((char*) &ry, sizeof(char)); + x += rx; + y += ry; + } else { + Count = Flag; + Index = x + y * GetWidth(); + for (int k = 0; k < Count; k++) { + file.read((char*) &ColorIndex, sizeof(uint8_t)); + m_BitmapData[Index + k].Red = ColorTable[ColorIndex].Red; + m_BitmapData[Index + k].Green = ColorTable[ColorIndex].Green; + m_BitmapData[Index + k].Blue = ColorTable[ColorIndex].Blue; + m_BitmapData[Index + k].Alpha = ColorTable[ColorIndex].Alpha; + } + x += Count; + // Attention: Current Microsoft STL implementation seems to be buggy, tellg() always returns 0. + if (file.tellg() & 1) { + file.seekg(1, std::ios::cur); + } + } + } + } + } else if (m_BitmapHeader.Compression == 2) { // RLE 4 + /* RLE 4 is not supported */ + Result = false; + } else if (m_BitmapHeader.Compression == 3) { // BITFIELDS + + /* We assumes that mask of each color component can be in any order */ + + uint32_t BitCountRed = CColor::BitCountByMask(m_BitmapHeader.RedMask); + uint32_t BitCountGreen = CColor::BitCountByMask(m_BitmapHeader.GreenMask); + uint32_t BitCountBlue = CColor::BitCountByMask(m_BitmapHeader.BlueMask); + uint32_t BitCountAlpha = CColor::BitCountByMask(m_BitmapHeader.AlphaMask); + + for (unsigned int i = 0; i < GetHeight(); i++) { + file.read((char*) Line, LineWidth); + + uint8_t *LinePtr = Line; + + for (unsigned int j = 0; j < GetWidth(); j++) { + + uint32_t Color = 0; + + if (m_BitmapHeader.BitCount == 16) { + Color = *((uint16_t*) LinePtr); + LinePtr += 2; + } else if (m_BitmapHeader.BitCount == 32) { + Color = *((uint32_t*) LinePtr); + LinePtr += 4; + } else { + // Other formats are not valid + } + m_BitmapData[Index].Red = CColor::Convert(CColor::ComponentByMask(Color, m_BitmapHeader.RedMask), BitCountRed, 8); + m_BitmapData[Index].Green = CColor::Convert(CColor::ComponentByMask(Color, m_BitmapHeader.GreenMask), BitCountGreen, 8); + m_BitmapData[Index].Blue = CColor::Convert(CColor::ComponentByMask(Color, m_BitmapHeader.BlueMask), BitCountBlue, 8); + m_BitmapData[Index].Alpha = CColor::Convert(CColor::ComponentByMask(Color, m_BitmapHeader.AlphaMask), BitCountAlpha, 8); + + Index++; + } + } + } + + delete [] ColorTable; + delete [] Line; + + file.close(); + return Result; + } + + bool Save(const char* Filename, unsigned int BitCount = 32) { + bool Result = true; + + std::ofstream file(Filename, std::ios::out | std::ios::binary); + + if (file.is_open() == false) { + return false; + } + + BITMAP_FILEHEADER bfh; + BITMAP_HEADER bh; + memset(&bfh, 0, sizeof(bfh)); + memset(&bh, 0, sizeof(bh)); + + bfh.Signature = BITMAP_SIGNATURE; + bfh.BitsOffset = BITMAP_FILEHEADER_SIZE + sizeof(BITMAP_HEADER); + bfh.Size = (GetWidth() * GetHeight() * BitCount) / 8 + bfh.BitsOffset; + + bh.HeaderSize = sizeof(BITMAP_HEADER); + bh.BitCount = BitCount; + + if (BitCount == 32) { + bh.Compression = 3; // BITFIELD + bh.AlphaMask = 0xff000000; + bh.BlueMask = 0x00ff0000; + bh.GreenMask = 0x0000ff00; + bh.RedMask = 0x000000ff; + } else if (BitCount == 16) { + bh.Compression = 3; // BITFIELD + bh.AlphaMask = 0x00000000; + bh.BlueMask = 0x0000001f; + bh.GreenMask = 0x000007E0; + bh.RedMask = 0x0000F800; + } else { + bh.Compression = 0; // RGB + } + + unsigned int LineWidth = (GetWidth() + 3) & ~3; + + bh.Planes = 1; + bh.Height = GetHeight(); + bh.Width = GetWidth(); + bh.SizeImage = (LineWidth * BitCount * GetHeight()) / 8; + bh.PelsPerMeterX = 3780; + bh.PelsPerMeterY = 3780; + + if (BitCount == 32) { + file.write((char*) &bfh, sizeof(BITMAP_FILEHEADER)); + file.write((char*) &bh, sizeof(BITMAP_HEADER)); + file.write((char*) m_BitmapData, bh.SizeImage); + } else if (BitCount < 16) { + uint8_t* Bitmap = new uint8_t[bh.SizeImage]; + + BGRA *Palette = 0; + unsigned int PaletteSize = 0; + + if (GetBitsWithPalette(Bitmap, bh.SizeImage, BitCount, Palette, PaletteSize)) { + bfh.BitsOffset += PaletteSize * sizeof(BGRA); + + file.write((char*) &bfh, BITMAP_FILEHEADER_SIZE); + file.write((char*) &bh, sizeof(BITMAP_HEADER)); + file.write((char*) Palette, PaletteSize * sizeof(BGRA)); + file.write((char*) Bitmap, bh.SizeImage); + } + delete [] Bitmap; + delete [] Palette; + } else { + uint32_t RedMask = 0; + uint32_t GreenMask = 0; + uint32_t BlueMask = 0; + uint32_t AlphaMask = 0; + + if (BitCount == 16) { + RedMask = 0x0000F800; + GreenMask = 0x000007E0; + BlueMask = 0x0000001F; + AlphaMask = 0x00000000; + } else if (BitCount == 24) { + RedMask = 0x00FF0000; + GreenMask = 0x0000FF00; + BlueMask = 0x000000FF; + } else { + // Other color formats are not valid + Result = false; + } + + if (Result) { + if (GetBits(NULL, bh.SizeImage, RedMask, GreenMask, BlueMask, AlphaMask)) { + uint8_t* Bitmap = new uint8_t[bh.SizeImage]; + if (GetBits(Bitmap, bh.SizeImage, RedMask, GreenMask, BlueMask, AlphaMask)) { + file.write((char*) &bfh, sizeof(BITMAP_FILEHEADER)); + file.write((char*) &bh, sizeof(BITMAP_HEADER)); + file.write((char*) Bitmap, bh.SizeImage); + } + delete [] Bitmap; + } + } + } + + file.close(); + return Result; + } + + unsigned int GetWidth() { + /* Add plausibility test */ + // if (abs(m_BitmapHeader.Width) > 8192) { + // m_BitmapHeader.Width = 8192; + // } + return m_BitmapHeader.Width < 0 ? -m_BitmapHeader.Width : m_BitmapHeader.Width; + } + + unsigned int GetHeight() { + /* Add plausibility test */ + // if (abs(m_BitmapHeader.Height) > 8192) { + // m_BitmapHeader.Height = 8192; + // } + return m_BitmapHeader.Height < 0 ? -m_BitmapHeader.Height : m_BitmapHeader.Height; + } + + unsigned int GetBitCount() { + /* Add plausibility test */ + // if (m_BitmapHeader.BitCount > 32) { + // m_BitmapHeader.BitCount = 32; + // } + return m_BitmapHeader.BitCount; + } + + /* Copies internal RGBA buffer to user specified buffer */ + + bool GetBits(void* Buffer, unsigned int &Size) { + bool Result = false; + if (Size == 0 || Buffer == 0) { + Size = m_BitmapSize * sizeof(RGBA); + Result = m_BitmapSize != 0; + } else { + memcpy(Buffer, m_BitmapData, Size); + Result = true; + } + return Result; + } + + /* Returns internal RGBA buffer */ + + void* GetBits() { + return m_BitmapData; + } + + /* Copies internal RGBA buffer to user specified buffer and converts it into destination + * bit format specified by component masks. + * + * Typical Bitmap color formats (BGR/BGRA): + * + * Masks for 16 bit (5-5-5): ALPHA = 0x00000000, RED = 0x00007C00, GREEN = 0x000003E0, BLUE = 0x0000001F + * Masks for 16 bit (5-6-5): ALPHA = 0x00000000, RED = 0x0000F800, GREEN = 0x000007E0, BLUE = 0x0000001F + * Masks for 24 bit: ALPHA = 0x00000000, RED = 0x00FF0000, GREEN = 0x0000FF00, BLUE = 0x000000FF + * Masks for 32 bit: ALPHA = 0xFF000000, RED = 0x00FF0000, GREEN = 0x0000FF00, BLUE = 0x000000FF + * + * Other color formats (RGB/RGBA): + * + * Masks for 32 bit (RGBA): ALPHA = 0xFF000000, RED = 0x000000FF, GREEN = 0x0000FF00, BLUE = 0x00FF0000 + * + * Bit count will be rounded to next 8 bit boundary. If IncludePadding is true, it will be ensured + * that line width is a multiple of 4. padding bytes are included if necessary. + * + * NOTE: systems with big endian byte order may require masks in inversion order. + */ + + bool GetBits(void* Buffer, unsigned int &Size, unsigned int RedMask, unsigned int GreenMask, unsigned int BlueMask, unsigned int AlphaMask, bool IncludePadding = true) { + bool Result = false; + + uint32_t BitCountRed = CColor::BitCountByMask(RedMask); + uint32_t BitCountGreen = CColor::BitCountByMask(GreenMask); + uint32_t BitCountBlue = CColor::BitCountByMask(BlueMask); + uint32_t BitCountAlpha = CColor::BitCountByMask(AlphaMask); + + unsigned int BitCount = (BitCountRed + BitCountGreen + BitCountBlue + BitCountAlpha + 7) & ~7; + + if (BitCount > 32) { + return false; + } + + unsigned int w = GetWidth(); + //unsigned int LineWidth = (w + 3) & ~3; + unsigned int dataBytesPerLine = (w * BitCount + 7) / 8; + unsigned int LineWidth = (dataBytesPerLine + 3) & ~3; + + if (Size == 0 || Buffer == 0) { + //Size = (LineWidth * GetHeight() * BitCount) / 8 + sizeof(unsigned int); + Size = (GetWidth() * GetHeight() * BitCount) / 8 + sizeof(unsigned int); + return true; + } + + uint8_t* BufferPtr = (uint8_t*) Buffer; + + Result = true; + + uint32_t BitPosRed = CColor::BitPositionByMask(RedMask); + uint32_t BitPosGreen = CColor::BitPositionByMask(GreenMask); + uint32_t BitPosBlue = CColor::BitPositionByMask(BlueMask); + uint32_t BitPosAlpha = CColor::BitPositionByMask(AlphaMask); + + unsigned int j = 0; + + for (unsigned int i = 0; i < m_BitmapSize; i++) { + *(uint32_t*) BufferPtr = + (CColor::Convert(m_BitmapData[i].Blue, 8, BitCountBlue) << BitPosBlue) | + (CColor::Convert(m_BitmapData[i].Green, 8, BitCountGreen) << BitPosGreen) | + (CColor::Convert(m_BitmapData[i].Red, 8, BitCountRed) << BitPosRed) | + (CColor::Convert(m_BitmapData[i].Alpha, 8, BitCountAlpha) << BitPosAlpha); + + if (IncludePadding) { + j++; + if (j >= w) { + for (unsigned int k = 0; k < LineWidth - dataBytesPerLine; k++) { + BufferPtr += (BitCount >> 3); + } + j = 0; + } + } + + BufferPtr += (BitCount >> 3); + } + + Size -= sizeof(unsigned int); + + return Result; + } + + /* See GetBits(). + * It creates a corresponding color table (palette) which have to be destroyed by the user after usage. + * + * Supported Bit depths are: 4, 8 + * + * Todo: Optimize, use optimized palette, do ditehring (see my dithering class), support padding for 4 bit bitmaps + */ + + bool GetBitsWithPalette(void* Buffer, unsigned int &Size, unsigned int BitCount, BGRA* &Palette, unsigned int &PaletteSize, bool OptimalPalette = false, bool IncludePadding = true) { + bool Result = false; + + if (BitCount > 16) { + return false; + } + + unsigned int w = GetWidth(); + unsigned int dataBytesPerLine = (w * BitCount + 7) / 8; + unsigned int LineWidth = (dataBytesPerLine + 3) & ~3; + + if (Size == 0 || Buffer == 0) { + Size = (LineWidth * GetHeight() * BitCount) / 8; + return true; + } + + + if (OptimalPalette) { + PaletteSize = 0; + // Not implemented + } else { + if (BitCount == 1) { + PaletteSize = 2; + // Not implemented: Who need that? + } else if (BitCount == 4) { // 2:2:1 + PaletteSize = 16; + Palette = new BGRA[PaletteSize]; + for (int r = 0; r < 4; r++) { + for (int g = 0; g < 2; g++) { + for (int b = 0; b < 2; b++) { + Palette[r | g << 2 | b << 3].Red = r ? (r << 6) | 0x3f : 0; + Palette[r | g << 2 | b << 3].Green = g ? (g << 7) | 0x7f : 0; + Palette[r | g << 2 | b << 3].Blue = b ? (b << 7) | 0x7f : 0; + Palette[r | g << 2 | b << 3].Alpha = 0xff; + } + } + } + } else if (BitCount == 8) { // 3:3:2 + PaletteSize = 256; + Palette = new BGRA[PaletteSize]; + for (int r = 0; r < 8; r++) { + for (int g = 0; g < 8; g++) { + for (int b = 0; b < 4; b++) { + Palette[r | g << 3 | b << 6].Red = r ? (r << 5) | 0x1f : 0; + Palette[r | g << 3 | b << 6].Green = g ? (g << 5) | 0x1f : 0; + Palette[r | g << 3 | b << 6].Blue = b ? (b << 6) | 0x3f : 0; + Palette[r | g << 3 | b << 6].Alpha = 0xff; + } + } + } + } else if (BitCount == 16) { // 5:5:5 + // Not implemented + } + } + + unsigned int j = 0; + uint8_t* BufferPtr = (uint8_t*) Buffer; + + for (unsigned int i = 0; i < m_BitmapSize; i++) { + if (BitCount == 1) { + // Not implemented: Who needs that? + } else if (BitCount == 4) { + *BufferPtr = ((m_BitmapData[i].Red >> 6) | (m_BitmapData[i].Green >> 7) << 2 | (m_BitmapData[i].Blue >> 7) << 3) << 4; + i++; + *BufferPtr |= (m_BitmapData[i].Red >> 6) | (m_BitmapData[i].Green >> 7) << 2 | (m_BitmapData[i].Blue >> 7) << 3; + } else if (BitCount == 8) { + *BufferPtr = (m_BitmapData[i].Red >> 5) | (m_BitmapData[i].Green >> 5) << 3 | (m_BitmapData[i].Blue >> 5) << 6; + } else if (BitCount == 16) { + // Not implemented + } + + if (IncludePadding) { + j++; + if (j >= w) { + for (unsigned int k = 0; k < (LineWidth - dataBytesPerLine); k++) { + BufferPtr += BitCount / 8; + } + j = 0; + } + } + + BufferPtr++; + } + + Result = true; + + return Result; + } + + /* Set Bitmap Bits. Will be converted to RGBA internally */ + + bool SetBits(void* Buffer, unsigned int Width, unsigned int Height, unsigned int RedMask, unsigned int GreenMask, unsigned int BlueMask, unsigned int AlphaMask = 0) { + if (Buffer == 0) { + return false; + } + + uint8_t *BufferPtr = (uint8_t*) Buffer; + + Dispose(); + + m_BitmapHeader.Width = Width; + m_BitmapHeader.Height = Height; + m_BitmapHeader.BitCount = 32; + m_BitmapHeader.Compression = 3; + + m_BitmapSize = GetWidth() * GetHeight(); + m_BitmapData = new RGBA[m_BitmapSize]; + + /* Find bit count by masks (rounded to next 8 bit boundary) */ + + unsigned int BitCount = (CColor::BitCountByMask(RedMask | GreenMask | BlueMask | AlphaMask) + 7) & ~7; + + uint32_t BitCountRed = CColor::BitCountByMask(RedMask); + uint32_t BitCountGreen = CColor::BitCountByMask(GreenMask); + uint32_t BitCountBlue = CColor::BitCountByMask(BlueMask); + uint32_t BitCountAlpha = CColor::BitCountByMask(AlphaMask); + + for (unsigned int i = 0; i < m_BitmapSize; i++) { + unsigned int Color = 0; + if (BitCount <= 8) { + Color = *((uint8_t*) BufferPtr); + BufferPtr += 1; + } else if (BitCount <= 16) { + Color = *((uint16_t*) BufferPtr); + BufferPtr += 2; + } else if (BitCount <= 24) { + Color = *((uint32_t*) BufferPtr); + BufferPtr += 3; + } else if (BitCount <= 32) { + Color = *((uint32_t*) BufferPtr); + BufferPtr += 4; + } else { + /* unsupported */ + BufferPtr += 1; + } + m_BitmapData[i].Alpha = CColor::Convert(CColor::ComponentByMask(Color, AlphaMask), BitCountAlpha, 8); + m_BitmapData[i].Red = CColor::Convert(CColor::ComponentByMask(Color, RedMask), BitCountRed, 8); + m_BitmapData[i].Green = CColor::Convert(CColor::ComponentByMask(Color, GreenMask), BitCountGreen, 8); + m_BitmapData[i].Blue = CColor::Convert(CColor::ComponentByMask(Color, BlueMask), BitCountBlue, 8); + } + + return true; + } +}; + +//////////////////////////////////////////////////////////////////////////// +// +// Fonction pour charger une texture +// +//////////////////////////////////////////////////////////////////////////// + +#include +#include +//#include + +unsigned char *ChargerImage( std::string fichier, GLsizei &largeur, GLsizei &hauteur ) +{ + // vérifier la présence du fichier BMP en essayant de l'ouvrir + FILE *img = fopen( fichier.c_str(), "r" ); + if ( !img ) + { + std::cerr << "Fichier de texture manquant: " << fichier << std::endl; + return NULL; + } + fclose( img ); + + // lire le fichier dans un objet CBitmap + CBitmap image( fichier.c_str() ); + // obtenir la taille de l'image et les composantes RGBA + largeur = image.GetWidth(); + hauteur = image.GetHeight(); + unsigned int taille = largeur * hauteur * 4; // l * h * 4 composantes + unsigned char *pixels = new unsigned char[taille]; + image.GetBits( (void*) pixels, taille, 0x000000FF, 0x0000FF00, 0x00FF0000, 0xFF000000 ); + + //SDL_Surface* surface = SDL_LoadBMP( fichier.c_str() ); + // https://wiki.libsdl.org/SDL_LoadBMP + // largeur = surface->w; + // hauteur = surface->h; + // pixels = surface->pixels; + + return pixels; +} + +#endif + +#ifndef INF2705_FORME_H +#define INF2705_FORME_H + +//////////////////////////////////////////////////////////////////////////// +// +// Fonctions variées pour afficher des formes connues +// (INF2705, Benoît Ozell) +//////////////////////////////////////////////////////////////////////////// + +#include +#include + +#if !defined( __APPLE__ ) + +/* Copyright (c) Mark J. Kilgard, 1994, 1997. */ + +/** + (c) Copyright 1993, Silicon Graphics, Inc. + + ALL RIGHTS RESERVED + + Permission to use, copy, modify, and distribute this software + for any purpose and without fee is hereby granted, provided + that the above copyright notice appear in all copies and that + both the copyright notice and this permission notice appear in + supporting documentation, and that the name of Silicon + Graphics, Inc. not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. + + THE MATERIAL EMBODIED ON THIS SOFTWARE IS PROVIDED TO YOU + "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR + OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY WARRANTY OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. IN NO + EVENT SHALL SILICON GRAPHICS, INC. BE LIABLE TO YOU OR ANYONE + ELSE FOR ANY DIRECT, SPECIAL, INCIDENTAL, INDIRECT OR + CONSEQUENTIAL DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER, + INCLUDING WITHOUT LIMITATION, LOSS OF PROFIT, LOSS OF USE, + SAVINGS OR REVENUE, OR THE CLAIMS OF THIRD PARTIES, WHETHER OR + NOT SILICON GRAPHICS, INC. HAS BEEN ADVISED OF THE POSSIBILITY + OF SUCH LOSS, HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + ARISING OUT OF OR IN CONNECTION WITH THE POSSESSION, USE OR + PERFORMANCE OF THIS SOFTWARE. + + US Government Users Restricted Rights + + Use, duplication, or disclosure by the Government is subject to + restrictions set forth in FAR 52.227.19(c)(2) or subparagraph + (c)(1)(ii) of the Rights in Technical Data and Computer + Software clause at DFARS 252.227-7013 and/or in similar or + successor clauses in the FAR or the DOD or NASA FAR + Supplement. Unpublished-- rights reserved under the copyright + laws of the United States. Contractor/manufacturer is Silicon + Graphics, Inc., 2011 N. Shoreline Blvd., Mountain View, CA + 94039-7311. + + OpenGL(TM) is a trademark of Silicon Graphics, Inc. +*/ + +#include +#include + +static GLfloat dodec[20][3]; + +static void initDodecahedron(void) +{ + GLfloat alpha, beta; + + alpha = sqrt(2.0 / (3.0 + sqrt(5.0))); + beta = 1.0 + sqrt(6.0 / (3.0 + sqrt(5.0)) - 2.0 + 2.0 * sqrt(2.0 / (3.0 + sqrt(5.0)))); + dodec[0][0] = -alpha; dodec[0][1] = 0; dodec[0][2] = beta; + dodec[1][0] = alpha; dodec[1][1] = 0; dodec[1][2] = beta; + dodec[2][0] = -1; dodec[2][1] = -1; dodec[2][2] = -1; + dodec[3][0] = -1; dodec[3][1] = -1; dodec[3][2] = 1; + dodec[4][0] = -1; dodec[4][1] = 1; dodec[4][2] = -1; + dodec[5][0] = -1; dodec[5][1] = 1; dodec[5][2] = 1; + dodec[6][0] = 1; dodec[6][1] = -1; dodec[6][2] = -1; + dodec[7][0] = 1; dodec[7][1] = -1; dodec[7][2] = 1; + dodec[8][0] = 1; dodec[8][1] = 1; dodec[8][2] = -1; + dodec[9][0] = 1; dodec[9][1] = 1; dodec[9][2] = 1; + dodec[10][0] = beta; dodec[10][1] = alpha; dodec[10][2] = 0; + dodec[11][0] = beta; dodec[11][1] = -alpha; dodec[11][2] = 0; + dodec[12][0] = -beta; dodec[12][1] = alpha; dodec[12][2] = 0; + dodec[13][0] = -beta; dodec[13][1] = -alpha; dodec[13][2] = 0; + dodec[14][0] = -alpha; dodec[14][1] = 0; dodec[14][2] = -beta; + dodec[15][0] = alpha; dodec[15][1] = 0; dodec[15][2] = -beta; + dodec[16][0] = 0; dodec[16][1] = beta; dodec[16][2] = alpha; + dodec[17][0] = 0; dodec[17][1] = beta; dodec[17][2] = -alpha; + dodec[18][0] = 0; dodec[18][1] = -beta; dodec[18][2] = alpha; + dodec[19][0] = 0; dodec[19][1] = -beta; dodec[19][2] = -alpha; + +} + +#define DIFF3(_a,_b,_c) { \ + (_c)[0] = (_a)[0] - (_b)[0]; \ + (_c)[1] = (_a)[1] - (_b)[1]; \ + (_c)[2] = (_a)[2] - (_b)[2]; \ + } + +static void crossprod(GLfloat v1[3], GLfloat v2[3], GLfloat prod[3]) +{ + GLfloat p[3]; /* in case prod == v1 or v2 */ + + p[0] = v1[1] * v2[2] - v2[1] * v1[2]; + p[1] = v1[2] * v2[0] - v2[2] * v1[0]; + p[2] = v1[0] * v2[1] - v2[0] * v1[1]; + prod[0] = p[0]; + prod[1] = p[1]; + prod[2] = p[2]; +} + +static void normalize(GLfloat v[3]) +{ + GLfloat d; + + d = sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]); + if (d == 0.0) + { + printf("normalize: zero length vector"); + v[0] = d = 1.0; + } + d = 1 / d; + v[0] *= d; + v[1] *= d; + v[2] *= d; +} + +static void pentagon(int a, int b, int c, int d, int e, GLenum shadeType) +{ + GLfloat n0[3], d1[3], d2[3]; + + DIFF3(dodec[a], dodec[b], d1); + DIFF3(dodec[b], dodec[c], d2); + crossprod(d1, d2, n0); + normalize(n0); + + glBegin(shadeType); + glNormal3fv(n0); + glVertex3fv(&dodec[a][0]); + glVertex3fv(&dodec[b][0]); + glVertex3fv(&dodec[c][0]); + glVertex3fv(&dodec[d][0]); + glVertex3fv(&dodec[e][0]); + glEnd(); +} + +static void dodecahedron(GLenum type) +{ + static int inited = 0; + + if (inited == 0) + { + inited = 1; + initDodecahedron(); + } + pentagon(0, 1, 9, 16, 5, type); + pentagon(1, 0, 3, 18, 7, type); + pentagon(1, 7, 11, 10, 9, type); + pentagon(11, 7, 18, 19, 6, type); + pentagon(8, 17, 16, 9, 10, type); + pentagon(2, 14, 15, 6, 19, type); + pentagon(2, 13, 12, 4, 14, type); + pentagon(2, 19, 18, 3, 13, type); + pentagon(3, 0, 5, 12, 13, type); + pentagon(6, 15, 8, 10, 11, type); + pentagon(4, 17, 8, 15, 14, type); + pentagon(4, 12, 5, 16, 17, type); +} + +void shapesWireDodecahedron(void) +{ + dodecahedron(GL_LINE_LOOP); +} + +void shapesSolidDodecahedron(void) +{ + dodecahedron(GL_TRIANGLE_FAN); +} + +static void recorditem(GLfloat * n1, GLfloat * n2, GLfloat * n3, + GLenum shadeType) +{ + GLfloat q0[3], q1[3]; + + DIFF3(n1, n2, q0); + DIFF3(n2, n3, q1); + crossprod(q0, q1, q1); + normalize(q1); + + glBegin(shadeType); + glNormal3fv(q1); + glVertex3fv(n1); + glVertex3fv(n2); + glVertex3fv(n3); + glEnd(); +} + +static void subdivide(GLfloat * v0, GLfloat * v1, GLfloat * v2, + GLenum shadeType) +{ + int depth = 1; + for (int i = 0; i < depth; i++) + { + for (int j = 0; i + j < depth; j++) + { + GLfloat w0[3], w1[3], w2[3]; + int k = depth - i - j; + for (int n = 0; n < 3; n++) + { + w0[n] = (i * v0[n] + j * v1[n] + k * v2[n]) / depth; + w1[n] = ((i + 1) * v0[n] + j * v1[n] + (k - 1) * v2[n]) / depth; + w2[n] = (i * v0[n] + (j + 1) * v1[n] + (k - 1) * v2[n]) / depth; + } + GLfloat l; + l = sqrt(w0[0] * w0[0] + w0[1] * w0[1] + w0[2] * w0[2]); + w0[0] /= l; + w0[1] /= l; + w0[2] /= l; + l = sqrt(w1[0] * w1[0] + w1[1] * w1[1] + w1[2] * w1[2]); + w1[0] /= l; + w1[1] /= l; + w1[2] /= l; + l = sqrt(w2[0] * w2[0] + w2[1] * w2[1] + w2[2] * w2[2]); + w2[0] /= l; + w2[1] /= l; + w2[2] /= l; + recorditem(w1, w0, w2, shadeType); + } + } +} + +static void drawtriangle(int i, GLfloat data[][3], int ndx[][3], + GLenum shadeType) +{ + GLfloat *x0 = data[ndx[i][0]]; + GLfloat *x1 = data[ndx[i][1]]; + GLfloat *x2 = data[ndx[i][2]]; + subdivide(x0, x1, x2, shadeType); +} + +/* octahedron data: The octahedron produced is centered at the + origin and has radius 1.0 */ +static GLfloat odata[6][3] = +{ + {1.0, 0.0, 0.0}, + {-1.0, 0.0, 0.0}, + {0.0, 1.0, 0.0}, + {0.0, -1.0, 0.0}, + {0.0, 0.0, 1.0}, + {0.0, 0.0, -1.0} +}; + +static int ondex[8][3] = +{ + {0, 4, 2}, + {1, 2, 4}, + {0, 3, 4}, + {1, 4, 3}, + {0, 2, 5}, + {1, 5, 2}, + {0, 5, 3}, + {1, 3, 5} +}; + +static void octahedron(GLenum shadeType) +{ + for (int i = 7; i >= 0; i--) + { + drawtriangle(i, odata, ondex, shadeType); + } +} + +void shapesWireOctahedron(void) +{ + octahedron(GL_LINE_LOOP); +} + +void shapesSolidOctahedron(void) +{ + octahedron(GL_TRIANGLES); +} + + +/* icosahedron data: These numbers are rigged to make an + icosahedron of radius 1.0 */ + +#define X .525731112119133606 +#define Z .850650808352039932 + +static GLfloat idata[12][3] = +{ + {-X, 0, Z}, + {X, 0, Z}, + {-X, 0, -Z}, + {X, 0, -Z}, + {0, Z, X}, + {0, Z, -X}, + {0, -Z, X}, + {0, -Z, -X}, + {Z, X, 0}, + {-Z, X, 0}, + {Z, -X, 0}, + {-Z, -X, 0} +}; + +static int connectivity[20][3] = +{ + {0, 4, 1}, + {0, 9, 4}, + {9, 5, 4}, + {4, 5, 8}, + {4, 8, 1}, + {8, 10, 1}, + {8, 3, 10}, + {5, 3, 8}, + {5, 2, 3}, + {2, 7, 3}, + {7, 10, 3}, + {7, 6, 10}, + {7, 11, 6}, + {11, 0, 6}, + {0, 1, 6}, + {6, 1, 10}, + {9, 0, 11}, + {9, 11, 2}, + {9, 2, 5}, + {7, 2, 11}, +}; + +static void icosahedron(GLenum shadeType) +{ + for (int i = 19; i >= 0; i--) + { + drawtriangle(i, idata, connectivity, shadeType); + } +} + +void shapesWireIcosahedron(void) +{ + icosahedron(GL_LINE_LOOP); +} + +void shapesSolidIcosahedron(void) +{ + icosahedron(GL_TRIANGLES); +} + + +/* tetrahedron data: */ + +#define T 1.73205080756887729 + +static GLfloat tdata[4][3] = +{ + {T, T, T}, + {T, -T, -T}, + {-T, T, -T}, + {-T, -T, T} +}; +#undef T + +static int tndex[4][3] = +{ + {0, 1, 3}, + {2, 1, 0}, + {3, 2, 0}, + {1, 2, 3} +}; + +static void tetrahedron(GLenum shadeType) +{ + for (int i = 3; i >= 0; i--) + drawtriangle(i, tdata, tndex, shadeType); +} + +void shapesWireTetrahedron(void) +{ + tetrahedron(GL_LINE_LOOP); +} + +void shapesSolidTetrahedron(void) +{ + tetrahedron(GL_TRIANGLES); +} + + +/* Rim, body, lid, and bottom data must be reflected in x and + y; handle and spout data across the y axis only. */ + +static int patchdata[][16] = +{ + /* rim */ + {102, 103, 104, 105, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + /* body */ + {12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27}, + {24, 25, 26, 27, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40}, + /* lid */ + {96, 96, 96, 96, 97, 98, 99, 100, 101, 101, 101, 101, 0, 1, 2, 3,}, + {0, 1, 2, 3, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117}, + /* bottom */ + {118, 118, 118, 118, 124, 122, 119, 121, 123, 126, 125, 120, 40, 39, 38, 37}, + /* handle */ + {41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56}, + {53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 28, 65, 66, 67}, + /* spout */ + {68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83}, + {80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95} +}; + +static float cpdata[][3] = +{ + {0.2,0,2.7}, {0.2,-0.112,2.7}, {0.112,-0.2,2.7}, {0,-0.2,2.7}, + {1.3375,0,2.53125}, {1.3375,-0.749,2.53125}, {0.749,-1.3375,2.53125}, + {0,-1.3375,2.53125}, {1.4375,0,2.53125}, {1.4375,-0.805, 2.53125}, + {0.805,-1.4375,2.53125}, {0,-1.4375,2.53125}, {1.5,0,2.4}, {1.5,-0.84,2.4}, + {0.84,-1.5,2.4}, {0,-1.5,2.4}, {1.75,0, 1.875}, {1.75,-0.98,1.875}, + {0.98,-1.75,1.875}, {0,-1.75,1.875}, {2,0,1.35}, {2,-1.12,1.35}, + {1.12,-2,1.35}, {0,-2,1.35}, {2,0,0.9}, {2,-1.12,0.9}, {1.12,-2,0.9}, + {0,-2,0.9}, {-2,0,0.9}, {2, 0,0.45}, {2,-1.12,0.45}, {1.12,-2,0.45}, + {0,-2,0.45}, {1.5,0,0.225}, {1.5,-0.84,0.225}, {0.84,-1.5,0.225}, + {0,-1.5,0.225}, {1.5, 0,0.15}, {1.5,-0.84,0.15}, {0.84,-1.5,0.15}, + {0,-1.5,0.15}, {-1.6,0,2.025}, {-1.6,-0.3,2.025}, {-1.5,-0.3,2.25}, + {-1.5,0,2.25}, {-2.3,0,2.025}, {-2.3,-0.3,2.025}, {-2.5,-0.3,2.25}, + {-2.5,0,2.25}, {-2.7,0,2.025}, {-2.7,-0.3,2.025}, {-3,-0.3,2.25}, + {-3,0,2.25}, {-2.7,0,1.8}, {-2.7,-0.3,1.8}, {-3,-0.3,1.8}, {-3,0,1.8}, + {-2.7,0,1.575}, {-2.7,-0.3,1.575}, {-3,-0.3,1.35}, {-3,0,1.35}, + {-2.5,0,1.125}, {-2.5,-0.3,1.125}, {-2.65,-0.3,0.9375}, {-2.65,0, 0.9375}, + {-2,-0.3,0.9}, {-1.9,-0.3,0.6}, {-1.9,0,0.6}, {1.7,0, 1.425}, + {1.7,-0.66,1.425}, {1.7,-0.66,0.6}, {1.7,0,0.6}, {2.6,0, 1.425}, + {2.6,-0.66,1.425}, {3.1,-0.66,0.825}, {3.1,0,0.825}, {2.3, 0,2.1}, + {2.3,-0.25,2.1}, {2.4,-0.25,2.025}, {2.4,0,2.025}, {2.7, 0,2.4}, + {2.7,-0.25,2.4}, {3.3,-0.25,2.4}, {3.3,0,2.4}, {2.8,0, 2.475}, + {2.8,-0.25,2.475}, {3.525,-0.25,2.49375}, {3.525,0, 2.49375}, {2.9,0,2.475}, + {2.9,-0.15,2.475}, {3.45,-0.15,2.5125}, {3.45,0,2.5125}, {2.8,0,2.4}, + {2.8,-0.15,2.4}, {3.2,-0.15,2.4}, {3.2,0,2.4}, {0,0,3.15}, {0.8,0,3.15}, + {0.8,-0.45,3.15}, {0.45, -0.8,3.15}, {0,-0.8,3.15}, {0,0,2.85}, {1.4,0,2.4}, + {1.4,-0.784, 2.4}, {0.784,-1.4,2.4}, {0,-1.4,2.4}, {0.4,0,2.55}, + {0.4,-0.224, 2.55}, {0.224,-0.4,2.55}, {0,-0.4,2.55}, {1.3,0,2.55}, + {1.3,-0.728,2.55}, {0.728,-1.3,2.55}, {0,-1.3,2.55}, {1.3,0,2.4}, + {1.3,-0.728,2.4}, {0.728,-1.3,2.4}, {0,-1.3,2.4}, {0,0,0}, {1.425,-0.798,0}, + {1.5,0,0.075}, {1.425,0,0}, {0.798,-1.425,0}, {0,-1.5, 0.075}, {0,-1.425,0}, + {1.5,-0.84,0.075}, {0.84,-1.5,0.075} +}; + +static float tex[2][2][2] = +{ + { {0, 0}, + {1, 0}}, + { {0, 1}, + {1, 1}} +}; + + +static void teapot(GLint grid, GLenum type) +{ + float p[4][4][3], q[4][4][3], r[4][4][3], s[4][4][3]; + + glPushAttrib(GL_ENABLE_BIT | GL_EVAL_BIT); + glEnable(GL_AUTO_NORMAL); + glEnable(GL_MAP2_VERTEX_3); + glEnable(GL_MAP2_TEXTURE_COORD_2); + for (int i = 0; i < 10; i++) + { + for (int j = 0; j < 4; j++) + { + for (int k = 0; k < 4; k++) + { + for (int l = 0; l < 3; l++) + { + p[j][k][l] = cpdata[patchdata[i][j * 4 + k]][l]; + q[j][k][l] = cpdata[patchdata[i][j * 4 + (3 - k)]][l]; + if (l == 1) q[j][k][l] *= -1.0; + if (i < 6) + { + r[j][k][l] = cpdata[patchdata[i][j * 4 + (3 - k)]][l]; + if (l == 0) r[j][k][l] *= -1.0; + s[j][k][l] = cpdata[patchdata[i][j * 4 + k]][l]; + if (l == 0) s[j][k][l] *= -1.0; + if (l == 1) s[j][k][l] *= -1.0; + } + } + } + } + glMap2f(GL_MAP2_TEXTURE_COORD_2, 0, 1, 2, 2, 0, 1, 4, 2, &tex[0][0][0]); + glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &p[0][0][0]); + glMapGrid2f(grid, 0.0, 1.0, grid, 0.0, 1.0); + glEvalMesh2(type, 0, grid, 0, grid); + glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &q[0][0][0]); + glEvalMesh2(type, 0, grid, 0, grid); + if (i < 6) + { + glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &r[0][0][0]); + glEvalMesh2(type, 0, grid, 0, grid); + glMap2f(GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &s[0][0][0]); + glEvalMesh2(type, 0, grid, 0, grid); + } + } + glPopAttrib(); +} + +void shapesSolidTeapot() +{ + teapot(14, GL_FILL); +} + +void shapesWireTeapot() +{ + teapot(10, GL_LINE); +} +#endif + + +////////////////////////////////////////////////////////// + +class FormeBase2705 +{ +public: + FormeBase2705( bool plein = true ) + : plein_(plein), + vao(0), locVertex(-1), locNormal(-1), locTexCoord(-1) + { + glGenVertexArrays( 1, &vao ); + } + ~FormeBase2705() + { + glDeleteVertexArrays( 1, &vao ); + } +protected: + bool obtenirAttributs( ) + { + GLint prog = 0; glGetIntegerv( GL_CURRENT_PROGRAM, &prog ); + if ( prog <= 0 ) + { + std::cerr << "Pas de programme actif!" << std::endl; + return(false); + } + + locVertex = glGetAttribLocation( prog, "Vertex" ); + if ( locVertex == -1 ) + { + std::cerr << "Pas de nuanceur de sommets!" << std::endl; + return(false); + } + locNormal = glGetAttribLocation( prog, "Normal" ); + locTexCoord = glGetAttribLocation( prog, "TexCoord" ); + if ( locTexCoord < 0 ) locTexCoord = glGetAttribLocation( prog, "TexCoord0" ); + if ( locTexCoord < 0 ) locTexCoord = glGetAttribLocation( prog, "MultiTexCoord0" ); + return(true); + } + + bool plein_; + GLuint vao; + GLint locVertex, locNormal, locTexCoord; +}; + +////////// + +#define AJOUTE(X,Y,Z,NX,NY,NZ,S,T) \ +{ \ + if ( locTexCoord >= 0 ) \ + { texcoord[2*nsommets+0] = (S); texcoord[2*nsommets+1] = (T); } \ + if ( locNormal >= 0 ) \ + { normales[3*nsommets+0] = (NX); normales[3*nsommets+1] = (NY); normales[3*nsommets+2] = (NZ); } \ + { sommets[3*nsommets+0] = (X); sommets[3*nsommets+1] = (Y); sommets[3*nsommets+2] = (Z); ++nsommets; } \ +} + +class FormeCube : public FormeBase2705 +{ +public: + FormeCube( GLfloat taille = 1.0, + bool plein = true ) + : FormeBase2705( plein ) + { + /* +Y */ + /* 3+-----------+2 */ + /* |\ |\ */ + /* | \ | \ */ + /* | \ | \ */ + /* | 7+-----------+6 */ + /* | | | | */ + /* | | | | */ + /* 0+---|-------+1 | */ + /* \ | \ | +X */ + /* \ | \ | */ + /* \| \| */ + /* 4+-----------+5 */ + /* +Z */ + + static GLint faces[6][4] = + { + { 3, 2, 1, 0 }, + { 5, 4, 0, 1 }, + { 6, 5, 1, 2 }, + { 7, 6, 2, 3 }, + { 4, 7, 3, 0 }, + { 4, 5, 6, 7 } + }; + static GLfloat n[6][3] = + { + { 0.0, 0.0, -1.0 }, + { 0.0, -1.0, 0.0 }, + { 1.0, 0.0, 0.0 }, + { 0.0, 1.0, 0.0 }, + { -1.0, 0.0, 0.0 }, + { 0.0, 0.0, 1.0 } + }; + GLfloat v[8][3]; + v[4][0] = v[7][0] = v[3][0] = v[0][0] = -taille / 2.; + v[6][0] = v[5][0] = v[1][0] = v[2][0] = +taille / 2.; + v[5][1] = v[4][1] = v[0][1] = v[1][1] = -taille / 2.; + v[7][1] = v[6][1] = v[2][1] = v[3][1] = +taille / 2.; + v[3][2] = v[2][2] = v[1][2] = v[0][2] = -taille / 2.; + v[4][2] = v[5][2] = v[6][2] = v[7][2] = +taille / 2.; + GLfloat t[8][2]; + t[4][0] = t[7][0] = t[3][0] = t[0][0] = 0.; + t[6][0] = t[5][0] = t[1][0] = t[2][0] = 1.; + t[5][1] = t[4][1] = t[0][1] = t[1][1] = 0.; + t[7][1] = t[6][1] = t[2][1] = t[3][1] = 1.; + + if ( obtenirAttributs( ) ) + { + // initialisation + const int TAILLEMAX = 6*6; + GLfloat sommets[3*TAILLEMAX], normales[3*TAILLEMAX], texcoord[2*TAILLEMAX]; + int nsommets = 0; + + if ( plein_ ) + { + for ( int i = 0 ; i < 6 ; ++i ) + { + AJOUTE( v[faces[i][0]][0], v[faces[i][0]][1], v[faces[i][0]][2], n[i][0], n[i][1], n[i][2], t[faces[i][0]][0], t[faces[i][0]][1] ); + AJOUTE( v[faces[i][1]][0], v[faces[i][1]][1], v[faces[i][1]][2], n[i][0], n[i][1], n[i][2], t[faces[i][1]][0], t[faces[i][1]][1] ); + AJOUTE( v[faces[i][2]][0], v[faces[i][2]][1], v[faces[i][2]][2], n[i][0], n[i][1], n[i][2], t[faces[i][2]][0], t[faces[i][2]][1] ); + AJOUTE( v[faces[i][2]][0], v[faces[i][2]][1], v[faces[i][2]][2], n[i][0], n[i][1], n[i][2], t[faces[i][2]][0], t[faces[i][2]][1] ); + AJOUTE( v[faces[i][3]][0], v[faces[i][3]][1], v[faces[i][3]][2], n[i][0], n[i][1], n[i][2], t[faces[i][3]][0], t[faces[i][3]][1] ); + AJOUTE( v[faces[i][0]][0], v[faces[i][0]][1], v[faces[i][0]][2], n[i][0], n[i][1], n[i][2], t[faces[i][0]][0], t[faces[i][0]][1] ); + } + } + else + { + for ( int i = 0 ; i < 6 ; ++i ) + { + AJOUTE( v[faces[i][0]][0], v[faces[i][0]][1], v[faces[i][0]][2], n[i][0], n[i][1], n[i][2], t[faces[i][0]][0], t[faces[i][0]][1] ); + AJOUTE( v[faces[i][1]][0], v[faces[i][1]][1], v[faces[i][1]][2], n[i][0], n[i][1], n[i][2], t[faces[i][1]][0], t[faces[i][1]][1] ); + AJOUTE( v[faces[i][2]][0], v[faces[i][2]][1], v[faces[i][2]][2], n[i][0], n[i][1], n[i][2], t[faces[i][2]][0], t[faces[i][2]][1] ); + AJOUTE( v[faces[i][3]][0], v[faces[i][3]][1], v[faces[i][3]][2], n[i][0], n[i][1], n[i][2], t[faces[i][3]][0], t[faces[i][3]][1] ); + } + } + nsommets_ = nsommets; + + // remplir VBOs + glBindVertexArray( vao ); + glGenBuffers( 3, vbo ); + + glBindBuffer( GL_ARRAY_BUFFER, vbo[0] ); + glBufferData( GL_ARRAY_BUFFER, 3*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 3*nsommets*sizeof(GLfloat), sommets ); + glVertexAttribPointer( locVertex, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locVertex); + + if ( locNormal >= 0 ) + { + glBindBuffer( GL_ARRAY_BUFFER, vbo[1] ); + glBufferData( GL_ARRAY_BUFFER, 3*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 3*nsommets*sizeof(GLfloat), normales ); + glVertexAttribPointer( locNormal, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locNormal); + } + + if ( locTexCoord >= 0 ) + { + glBindBuffer( GL_ARRAY_BUFFER, vbo[2] ); + glBufferData( GL_ARRAY_BUFFER, 2*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 2*nsommets*sizeof(GLfloat), texcoord ); + glVertexAttribPointer( locTexCoord, 2, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locTexCoord); + } + + glBindVertexArray( 0 ); + } + } + ~FormeCube() + { + glDeleteBuffers( 3, vbo ); + } + void afficher() + { + glBindVertexArray( vao ); + if ( plein_ ) + glDrawArrays( GL_TRIANGLES, 0, nsommets_ ); + else + for ( int i = 0 ; i < 6 ; ++i ) glDrawArrays( GL_LINE_LOOP, 4*i, 4 ); + glBindVertexArray( 0 ); + } +private: + GLint nsommets_; + GLuint vbo[3]; +}; + +////////// + +class FormeSphere : public FormeBase2705 +{ +public: + FormeSphere( GLdouble radius, GLint slices, GLint stacks, + bool plein = true, bool entiere = true ) + : FormeBase2705( plein ) + { + if ( obtenirAttributs( ) ) + { + // initialisation + const int TAILLEMAX = 2*(slices+1)*(stacks+2); + GLfloat sommets[3*TAILLEMAX], normales[3*TAILLEMAX], texcoord[2*TAILLEMAX]; + int nsommets = 0; + + // variables locales + const GLfloat drho = M_PI / (GLfloat) stacks; + const GLfloat dtheta = 2.0 * M_PI / (GLfloat) slices; + + GLint imin, imax; + if ( locTexCoord >= 0 ) { imin = 0; imax = stacks; } else { imin = 1; imax = stacks - 1; } + if ( !entiere ) imax = imax/2 + 1; // pour se rendre seulement à la moitié supérieure + + /* texturing: s goes from 0.0/0.25/0.5/0.75/1.0 at +y/+x/-y/-x/+y axis */ + /* t goes from -1.0/+1.0 at z = -radius/+radius (linear along longitudes) */ + /* cannot use triangle fan on texturing (s coord. at top/bottom tip varies) */ + + nsommets = 0; + { + GLfloat t = 1.0; + GLfloat ds = 1.0 / slices; + GLfloat dt = 1.0 / stacks; + for ( GLint i = imin; i < imax; i++ ) + { + GLfloat rho = i * drho; + GLfloat s = 0.0; + for ( GLint j = 0; j <= slices; j++ ) + { + GLfloat x, y, z; + GLfloat theta = (j == slices) ? 0.0 : j * dtheta; + x = -sin(theta) * sin(rho); + y = cos(theta) * sin(rho); + z = cos(rho); + AJOUTE( x * radius, y * radius, z * radius, x, y, z, s, t ); + + x = -sin(theta) * sin(rho + drho); + y = cos(theta) * sin(rho + drho); + z = cos(rho + drho); + AJOUTE( x * radius, y * radius, z * radius, x, y, z, s, t-dt ); + s += ds; + } + t -= dt; + } + } + nsommetsStrip_ = nsommets; + + if ( !(locTexCoord >= 0) ) + { + AJOUTE( 0.0, 0.0, radius, 0.0, 0.0, 1.0, 0, 0 ); + for ( GLint j = 0; j <= slices; j++ ) + { + GLfloat x, y, z; + GLfloat theta = (j == slices) ? 0.0 : j * dtheta; + x = -sin(theta) * sin(drho); + y = cos(theta) * sin(drho); + z = cos(drho); + AJOUTE( x * radius, y * radius, z * radius, x, y, z, 0, 0 ); + } + } + nsommetsFan_ = nsommets - nsommetsStrip_; + + if ( !(locTexCoord >= 0) && entiere ) + { + AJOUTE( 0.0, 0.0, -radius, 0.0, 0.0, -1.0, 0, 0 ); + GLfloat rho = M_PI - drho; + for ( GLint j = slices; j >= 0; j-- ) + { + GLfloat x, y, z; + GLfloat theta = (j == slices) ? 0.0 : j * dtheta; + x = -sin(theta) * sin(rho); + y = cos(theta) * sin(rho); + z = cos(rho); + AJOUTE( x * radius, y * radius, z * radius, x, y, z, 0, 0 ); + } + } + + // remplir VBOs + glBindVertexArray( vao ); + glGenBuffers( 3, vbo ); + + glBindBuffer( GL_ARRAY_BUFFER, vbo[0] ); + glBufferData( GL_ARRAY_BUFFER, 3*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 3*nsommets*sizeof(GLfloat), sommets ); + glVertexAttribPointer( locVertex, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locVertex); + + if ( locNormal >= 0 ) + { + glBindBuffer( GL_ARRAY_BUFFER, vbo[1] ); + glBufferData( GL_ARRAY_BUFFER, 3*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 3*nsommets*sizeof(GLfloat), normales ); + glVertexAttribPointer( locNormal, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locNormal); + } + + if ( locTexCoord >= 0 ) + { + glBindBuffer( GL_ARRAY_BUFFER, vbo[2] ); + glBufferData( GL_ARRAY_BUFFER, 2*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 2*nsommets*sizeof(GLfloat), texcoord ); + glVertexAttribPointer( locTexCoord, 2, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locTexCoord); + } + + glBindVertexArray( 0 ); + } + } + ~FormeSphere() + { + glDeleteBuffers( 3, vbo ); + } + void afficher() + { + glBindVertexArray( vao ); + glDrawArrays( GL_TRIANGLE_STRIP, 0, nsommetsStrip_ ); + if ( !(locTexCoord >= 0) ) + { + glDrawArrays( GL_TRIANGLE_FAN, nsommetsStrip_, nsommetsFan_ ); + glDrawArrays( GL_TRIANGLE_FAN, nsommetsStrip_+nsommetsFan_, nsommetsFan_ ); + } + glBindVertexArray( 0 ); + } +private: + GLint nsommetsStrip_, nsommetsFan_; + GLuint vbo[3]; +}; + +////////// + +class FormeTore : public FormeBase2705 +{ +public: + FormeTore( GLdouble innerRadius, GLdouble outerRadius, GLint nsides, GLint rings, + bool plein = true ) + : FormeBase2705( plein ) + { + if ( obtenirAttributs( ) ) + { + // initialisation + const int TAILLEMAX = 2*(nsides+1)*(rings); + GLfloat sommets[3*TAILLEMAX], normales[3*TAILLEMAX], texcoord[2*TAILLEMAX]; + int nsommets = 0; + + // variables locales + const GLfloat ringDelta = 2.0 * M_PI / rings; + const GLfloat sideDelta = 2.0 * M_PI / nsides; + + GLfloat theta = 0.0; + GLfloat cosTheta = 1.0; + GLfloat sinTheta = 0.0; + for ( int i = rings - 1; i >= 0; i-- ) + { + GLfloat theta1 = theta + ringDelta; + GLfloat cosTheta1 = cos(theta1); + GLfloat sinTheta1 = sin(theta1); + GLfloat phi = 0.0; + for ( int j = nsides; j >= 0; j-- ) + { + phi += sideDelta; + GLfloat cosPhi = cos(phi); + GLfloat sinPhi = sin(phi); + GLfloat dist = outerRadius + innerRadius * cosPhi; + + AJOUTE( cosTheta1 * dist, -sinTheta1 * dist, innerRadius * sinPhi, + cosTheta1 * cosPhi, -sinTheta1 * cosPhi, sinPhi, + (4.0*(i+1))/rings, (4.0*j)/nsides ); + + AJOUTE( cosTheta * dist, -sinTheta * dist, innerRadius * sinPhi, + cosTheta * cosPhi, -sinTheta * cosPhi, sinPhi, + (4.0*i)/rings, (4.0*j)/nsides ); + } + theta = theta1; + cosTheta = cosTheta1; + sinTheta = sinTheta1; + } + nsommets_ = nsommets; + + // remplir VBOs + glBindVertexArray( vao ); + glGenBuffers( 3, vbo ); + + glBindBuffer( GL_ARRAY_BUFFER, vbo[0] ); + glBufferData( GL_ARRAY_BUFFER, 3*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 3*nsommets*sizeof(GLfloat), sommets ); + glVertexAttribPointer( locVertex, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locVertex); + + if ( locNormal >= 0 ) + { + glBindBuffer( GL_ARRAY_BUFFER, vbo[1] ); + glBufferData( GL_ARRAY_BUFFER, 3*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 3*nsommets*sizeof(GLfloat), normales ); + glVertexAttribPointer( locNormal, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locNormal); + } + + if ( locTexCoord >= 0 ) + { + glBindBuffer( GL_ARRAY_BUFFER, vbo[2] ); + glBufferData( GL_ARRAY_BUFFER, 2*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 2*nsommets*sizeof(GLfloat), texcoord ); + glVertexAttribPointer( locTexCoord, 2, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locTexCoord); + } + + glBindVertexArray( 0 ); + } + } + ~FormeTore() + { + glDeleteBuffers( 3, vbo ); + } + void afficher() + { + glBindVertexArray( vao ); + glDrawArrays( GL_TRIANGLE_STRIP, 0, nsommets_ ); + glBindVertexArray( 0 ); + } +private: + GLint nsommets_; + GLuint vbo[3]; +}; + +////////// + +class FormeCylindre : public FormeBase2705 +{ +public: + FormeCylindre( GLdouble base, GLdouble top, GLdouble height, GLint slices, GLint stacks, + bool plein = true ) + : FormeBase2705( plein ) + { + if ( obtenirAttributs( ) ) + { + // initialisation + const int TAILLEMAX = 2*(slices+1)*(stacks) + 2*(slices+1) + 2; + GLfloat sommets[3*TAILLEMAX], normales[3*TAILLEMAX], texcoord[2*TAILLEMAX]; + int nsommets = 0; + + // variables locales + const GLdouble da = 2.0 * M_PI / slices; + { + // le cylindre + const GLdouble dr = (top - base) / stacks; + const GLdouble nz = (base - top) / height; + const GLdouble dz = height / stacks; + const GLfloat ds = 1.0 / slices; + const GLfloat dt = 1.0 / stacks; + GLfloat t = 0.0; + GLfloat z = 0.0; + GLfloat r = base; + for ( int j = 0; j < stacks; j++ ) + { + GLfloat s = 0.0; + for ( int i = 0; i <= slices; i++ ) + { + GLfloat a = ( i == slices ) ? 0.0 : i * da; + GLfloat x = sin( a ); + GLfloat y = cos( a ); + AJOUTE( x * r, y * r, z, x, y, nz, s, t ); + AJOUTE( x * (r + dr), y * (r + dr), z + dz, x, y, nz, s, t + dt ); + s += ds; + } + r += dr; + t += dt; + z += dz; + } + nsommetsCyl_ = nsommets; + } + { + // les deux bouts avec des disques + /* texture of a shapesDisk is a cut out of the texture unit square + * x, y in [-outerRadius, +outerRadius]; s, t in [0, 1] (linear mapping) */ + AJOUTE( 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, 0.5, 0.5 ); + for ( int i = slices; i >= 0; i-- ) + { + GLfloat a = ( i == slices ) ? 0.0 : i * -da; + GLfloat x = sin( a ); + GLfloat y = cos( a ); + AJOUTE( base*x, base*y, 0.0, 0.0, 0.0, -1.0, 0.5*(1.0-x), 0.5*(1.0+y) ); + } + nsommetsBout_ = nsommets - nsommetsCyl_; + AJOUTE( 0.0, 0.0, height, 0.0, 0.0, +1.0, 0.5, 0.5 ); + for ( int i = slices; i >= 0; i-- ) + { + GLfloat a = ( i == slices ) ? 0.0 : i * da; + GLfloat x = sin( a ); + GLfloat y = cos( a ); + AJOUTE( top*x, top*y, height, 0.0, 0.0, +1.0, 0.5*(1.0-x), 0.5*(1.0+y) ); + } + } + + // remplir VBOs + glBindVertexArray( vao ); + glGenBuffers( 3, vbo ); + + glBindBuffer( GL_ARRAY_BUFFER, vbo[0] ); + glBufferData( GL_ARRAY_BUFFER, 3*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 3*nsommets*sizeof(GLfloat), sommets ); + glVertexAttribPointer( locVertex, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locVertex); + + if ( locNormal >= 0 ) + { + glBindBuffer( GL_ARRAY_BUFFER, vbo[1] ); + glBufferData( GL_ARRAY_BUFFER, 3*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 3*nsommets*sizeof(GLfloat), normales ); + glVertexAttribPointer( locNormal, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locNormal); + } + + if ( locTexCoord >= 0 ) + { + glBindBuffer( GL_ARRAY_BUFFER, vbo[2] ); + glBufferData( GL_ARRAY_BUFFER, 2*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 2*nsommets*sizeof(GLfloat), texcoord ); + glVertexAttribPointer( locTexCoord, 2, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locTexCoord); + } + + glBindVertexArray( 0 ); + } + } + ~FormeCylindre() + { + glDeleteBuffers( 3, vbo ); + } + void afficher() + { + glBindVertexArray( vao ); + glDrawArrays( GL_TRIANGLE_STRIP, 0, nsommetsCyl_ ); + glDrawArrays( GL_TRIANGLE_FAN, nsommetsCyl_, nsommetsBout_ ); + glDrawArrays( GL_TRIANGLE_FAN, nsommetsCyl_+nsommetsBout_, nsommetsBout_ ); + glBindVertexArray( 0 ); + } +private: + GLint nsommetsCyl_, nsommetsBout_; + GLuint vbo[3]; +}; + +////////// + +class FormeDisque : public FormeBase2705 +{ +public: + FormeDisque( GLdouble inner, GLdouble outer, GLint slices, GLint loops, + bool plein = true ) + : FormeBase2705( plein ) + { + if ( obtenirAttributs( ) ) + { + // initialisation + const int TAILLEMAX = 2*(slices+1)*(loops); + GLfloat sommets[3*TAILLEMAX], normales[3*TAILLEMAX], texcoord[2*TAILLEMAX]; + int nsommets = 0; + + // variables locales + const GLfloat da = 2.0 * M_PI / slices; + const GLfloat dr = (outer - inner) / (GLfloat) loops; + + /* texture of a shapesDisk is a cut out of the texture unit square + * x, y in [-outer, +outer]; s, t in [0, 1] * (linear mapping) */ + GLfloat r1 = inner; + for ( int l = 0; l < loops; l++ ) + { + GLfloat r2 = r1 + dr; + for ( int i = slices; i >= 0; i-- ) + { + GLfloat a = ( i == slices ) ? 0.0 : i * da; + GLfloat x = sin( a ); + GLfloat y = cos( a ); + AJOUTE( r2*x, r2*y, 0.0, 0.0, 0.0, +1.0, 0.5*( 1.0 - x*r2/outer ), 0.5*( 1.0 + y*r2/outer ) ); + AJOUTE( r1*x, r1*y, 0.0, 0.0, 0.0, +1.0, 0.5*( 1.0 - x*r1/outer ), 0.5*( 1.0 + y*r1/outer ) ); + } + r1 = r2; + } + nsommets_ = nsommets; + + // remplir VBOs + glBindVertexArray( vao ); + glGenBuffers( 3, vbo ); + + glBindBuffer( GL_ARRAY_BUFFER, vbo[0] ); + glBufferData( GL_ARRAY_BUFFER, 3*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 3*nsommets*sizeof(GLfloat), sommets ); + glVertexAttribPointer( locVertex, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locVertex); + + if ( locNormal >= 0 ) + { + glBindBuffer( GL_ARRAY_BUFFER, vbo[1] ); + glBufferData( GL_ARRAY_BUFFER, 3*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 3*nsommets*sizeof(GLfloat), normales ); + glVertexAttribPointer( locNormal, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locNormal); + } + + if ( locTexCoord >= 0 ) + { + glBindBuffer( GL_ARRAY_BUFFER, vbo[2] ); + glBufferData( GL_ARRAY_BUFFER, 2*nsommets*sizeof(GLfloat), NULL, GL_STATIC_DRAW ); + glBufferSubData( GL_ARRAY_BUFFER, 0, 2*nsommets*sizeof(GLfloat), texcoord ); + glVertexAttribPointer( locTexCoord, 2, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locTexCoord); + } + + glBindVertexArray( 0 ); + } + } + ~FormeDisque() + { + glDeleteBuffers( 3, vbo ); + } + void afficher() + { + glBindVertexArray( vao ); + glDrawArrays( GL_TRIANGLE_STRIP, 0, nsommets_ ); + glBindVertexArray( 0 ); + } +private: + GLint nsommets_; + GLuint vbo[3]; +}; + +////////// + +class FormeIcosaedre : public FormeBase2705 +{ +public: + FormeIcosaedre( bool plein = true ) + : FormeBase2705( plein ) + { + } + ~FormeIcosaedre() + { + } + void afficher() + { +#if !defined( __APPLE__ ) + shapesSolidIcosahedron( ); +#endif + } +private: +}; + +////////// + +class FormeDodecaedre : public FormeBase2705 +{ +public: + FormeDodecaedre( bool plein = true ) + : FormeBase2705( plein ) + { + } + ~FormeDodecaedre() + { + } + void afficher() + { +#if !defined( __APPLE__ ) + shapesSolidDodecahedron( ); +#endif + } +private: +}; + +////////// + +// teapot(14, true); +// teapot(10, false); +class FormeTheiere : public FormeBase2705 +{ +public: + FormeTheiere( GLint npts = 14, + bool plein = true ) + : FormeBase2705( plein ) //, npts_(npts) + { +#if 0 + if ( obtenirAttributs( ) ) + { + // initialisation + const int TAILLEMAX = 10*(npts_+1)*(npts_); + GLfloat sommets[3*TAILLEMAX], normales[3*TAILLEMAX], texcoord[2*TAILLEMAX]; + int nsommets = 0; + + } +#endif + } + ~FormeTheiere() + { +#if 0 + glDeleteBuffers( 3, vbo ); +#endif + } + void afficher() + { +#if 1 +#if !defined( __APPLE__ ) + shapesSolidTeapot(); +#endif +#else + glBindVertexArray( vao ); + //glDrawArrays( GL_TRIANGLE_STRIP, 0, nsommets_ ); + /* Rim, body, lid, and bottom data must be reflected in x and + y; handle and spout data across the y axis only. */ + int patchdata[][16] = + { + /* rim */ + {102, 103, 104, 105, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + /* body */ + {12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27}, + {24, 25, 26, 27, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40}, + /* lid */ + {96, 96, 96, 96, 97, 98, 99, 100, 101, 101, 101, 101, 0, 1, 2, 3,}, + {0, 1, 2, 3, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117}, + /* bottom */ + {118, 118, 118, 118, 124, 122, 119, 121, 123, 126, 125, 120, 40, 39, 38, 37}, + /* handle */ + {41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56}, + {53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 28, 65, 66, 67}, + /* spout */ + {68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83}, + {80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95} + }; + float cpdata[][3] = + { + {0.2,0,2.7}, {0.2,-0.112,2.7}, {0.112,-0.2,2.7}, {0,-0.2,2.7}, + {1.3375,0,2.53125}, {1.3375,-0.749,2.53125}, {0.749,-1.3375,2.53125}, + {0,-1.3375,2.53125}, {1.4375,0,2.53125}, {1.4375,-0.805, 2.53125}, + {0.805,-1.4375,2.53125}, {0,-1.4375,2.53125}, {1.5,0,2.4}, {1.5,-0.84,2.4}, + {0.84,-1.5,2.4}, {0,-1.5,2.4}, {1.75,0, 1.875}, {1.75,-0.98,1.875}, + {0.98,-1.75,1.875}, {0,-1.75,1.875}, {2,0,1.35}, {2,-1.12,1.35}, + {1.12,-2,1.35}, {0,-2,1.35}, {2,0,0.9}, {2,-1.12,0.9}, {1.12,-2,0.9}, + {0,-2,0.9}, {-2,0,0.9}, {2, 0,0.45}, {2,-1.12,0.45}, {1.12,-2,0.45}, + {0,-2,0.45}, {1.5,0,0.225}, {1.5,-0.84,0.225}, {0.84,-1.5,0.225}, + {0,-1.5,0.225}, {1.5, 0,0.15}, {1.5,-0.84,0.15}, {0.84,-1.5,0.15}, + {0,-1.5,0.15}, {-1.6,0,2.025}, {-1.6,-0.3,2.025}, {-1.5,-0.3,2.25}, + {-1.5,0,2.25}, {-2.3,0,2.025}, {-2.3,-0.3,2.025}, {-2.5,-0.3,2.25}, + {-2.5,0,2.25}, {-2.7,0,2.025}, {-2.7,-0.3,2.025}, {-3,-0.3,2.25}, + {-3,0,2.25}, {-2.7,0,1.8}, {-2.7,-0.3,1.8}, {-3,-0.3,1.8}, {-3,0,1.8}, + {-2.7,0,1.575}, {-2.7,-0.3,1.575}, {-3,-0.3,1.35}, {-3,0,1.35}, + {-2.5,0,1.125}, {-2.5,-0.3,1.125}, {-2.65,-0.3,0.9375}, {-2.65,0, 0.9375}, + {-2,-0.3,0.9}, {-1.9,-0.3,0.6}, {-1.9,0,0.6}, {1.7,0, 1.425}, + {1.7,-0.66,1.425}, {1.7,-0.66,0.6}, {1.7,0,0.6}, {2.6,0, 1.425}, + {2.6,-0.66,1.425}, {3.1,-0.66,0.825}, {3.1,0,0.825}, {2.3, 0,2.1}, + {2.3,-0.25,2.1}, {2.4,-0.25,2.025}, {2.4,0,2.025}, {2.7, 0,2.4}, + {2.7,-0.25,2.4}, {3.3,-0.25,2.4}, {3.3,0,2.4}, {2.8,0, 2.475}, + {2.8,-0.25,2.475}, {3.525,-0.25,2.49375}, {3.525,0, 2.49375}, {2.9,0,2.475}, + {2.9,-0.15,2.475}, {3.45,-0.15,2.5125}, {3.45,0,2.5125}, {2.8,0,2.4}, + {2.8,-0.15,2.4}, {3.2,-0.15,2.4}, {3.2,0,2.4}, {0,0,3.15}, {0.8,0,3.15}, + {0.8,-0.45,3.15}, {0.45, -0.8,3.15}, {0,-0.8,3.15}, {0,0,2.85}, {1.4,0,2.4}, + {1.4,-0.784, 2.4}, {0.784,-1.4,2.4}, {0,-1.4,2.4}, {0.4,0,2.55}, + {0.4,-0.224, 2.55}, {0.224,-0.4,2.55}, {0,-0.4,2.55}, {1.3,0,2.55}, + {1.3,-0.728,2.55}, {0.728,-1.3,2.55}, {0,-1.3,2.55}, {1.3,0,2.4}, + {1.3,-0.728,2.4}, {0.728,-1.3,2.4}, {0,-1.3,2.4}, {0,0,0}, {1.425,-0.798,0}, + {1.5,0,0.075}, {1.425,0,0}, {0.798,-1.425,0}, {0,-1.5, 0.075}, {0,-1.425,0}, + {1.5,-0.84,0.075}, {0.84,-1.5,0.075} + }; + float tex[2][2][2] = + { + { {0, 0}, + {1, 0}}, + { {0, 1}, + {1, 1}} + }; + float p[4][4][3], q[4][4][3], r[4][4][3], s[4][4][3]; + for ( int i = 0; i < 10; i++ ) + { + for ( int j = 0; j < 4; j++ ) + { + for ( int k = 0; k < 4; k++ ) + { + for ( int l = 0; l < 3; l++ ) + { + p[j][k][l] = cpdata[patchdata[i][j * 4 + k]][l]; + q[j][k][l] = cpdata[patchdata[i][j * 4 + (3 - k)]][l]; + if (l == 1) q[j][k][l] *= -1.0; + if (i < 6) + { + r[j][k][l] = cpdata[patchdata[i][j * 4 + (3 - k)]][l]; + if (l == 0) r[j][k][l] *= -1.0; + s[j][k][l] = cpdata[patchdata[i][j * 4 + k]][l]; + if (l == 0) s[j][k][l] *= -1.0; + if (l == 1) s[j][k][l] *= -1.0; + } + } + } + } + glEnable( GL_AUTO_NORMAL ); + glEnable( GL_MAP2_TEXTURE_COORD_2 ); + glEnable( GL_MAP2_VERTEX_3 ); + glMap2f( GL_MAP2_TEXTURE_COORD_2, 0, 1, 2, 2, 0, 1, 4, 2, &tex[0][0][0] ); + glMap2f( GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &p[0][0][0] ); + glMapGrid2f( npts_, 0.0, 1.0, npts_, 0.0, 1.0 ); + glEvalMesh2( GL_FILL, 0, npts_, 0, npts_ ); + glMap2f( GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &q[0][0][0] ); + glEvalMesh2( GL_FILL, 0, npts_, 0, npts_ ); + if ( i < 6 ) + { + glMap2f( GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &r[0][0][0] ); + glEvalMesh2( GL_FILL, 0, npts_, 0, npts_ ); + glMap2f( GL_MAP2_VERTEX_3, 0, 1, 3, 4, 0, 1, 12, 4, &s[0][0][0] ); + glEvalMesh2( GL_FILL, 0, npts_, 0, npts_ ); + } + } + glBindVertexArray( 0 ); +#endif + } +private: +#if 0 + void Map2f( GLenum target, + GLfloat u1, GLfloat u2, GLint ustride, GLint uorder, + GLfloat v1, GLfloat v2, GLint vstride, GLint vorder, + const GLfloat *points ) + { + } + GLint un; GLfloat u1, u2, du; + GLint vn; GLfloat v1, v2, dv; + void MapGrid2f( GLint unArg, GLfloat u1Arg, GLfloat u2Arg, + GLint vnArg, GLfloat v1Arg, GLfloat v2Arg ) + { + un = unArg; u1 = u1Arg; u2 = u2Arg; du = u2 - u1; + vn = vnArg; v1 = v1Arg; v2 = v2Arg; dv = v2 - v1; + } + void EvalMesh2( GLint i1, GLint i2, GLint j1, GLint j2 ) + { + if ( plein_ ) + { + for ( int j = j1; j < j2; j += 1 ) + { + glBegin( GL_QUAD_STRIP ); + for ( int i = i1; i <= i2; i += 1 ) + { + glEvalCoord2f( i * du + u1, j * dv + v1 ); + glEvalCoord2f( i * du + u1, j + 1 * dv + v1 ); + } glEnd(); + } + } + else + { + for ( int j = j1; j <= j2; j += 1 ) + { + glBegin( GL_LINE_STRIP ); + for ( int i = i1; i <= i2; i += 1 ) + glEvalCoord2f( i * du + u1, j * dv + v1 ); + glEnd(); + } + for ( int i = i1; i <= i2; i += 1 ) + { + glBegin( GL_LINE_STRIP ); + for ( int j = j1; j <= j1; j += 1 ) + glEvalCoord2f( i * du + u1, j * dv + v1 ); + glEnd(); + } + } + } +#endif + +#if 0 + GLint npts_; + GLint nsommets_; + GLuint vbo[3]; +#endif +}; + +////////// + +#endif + diff --git a/makefile b/makefile new file mode 100644 index 0000000..eb17631 --- /dev/null +++ b/makefile @@ -0,0 +1,33 @@ +CONTEXT=sdl2 +ifeq "$(shell uname)" "Darwin" + CONTEXT=glfw3 + LDFLAGS += -lobjc -framework Foundation -framework OpenGL -framework Cocoa +endif + +CXXFLAGS += -g -W -Wall -Wno-unused-parameter -Wno-deprecated-declarations +CXXFLAGS += $(shell pkg-config --cflags glew) +CXXFLAGS += $(shell pkg-config --cflags $(CONTEXT)) + +LDFLAGS += -g +LDFLAGS += $(shell pkg-config --libs glew) +LDFLAGS += $(shell pkg-config --libs $(CONTEXT)) + +TP="tp3" +SRC=ripple + +exe : $(SRC).exe +run : exe + optirun ./$(SRC).exe +$(SRC).exe : $(SRC).cpp *.h + $(CXX) $(CXXFLAGS) -o$@ $(SRC).cpp $(LDFLAGS) + +sol : ; make SRC=$(SRC)Solution exe +runs : ; make SRC=$(SRC)Solution run + +clean : + rm -rf *.o *.exe *.exe.dSYM + +remise zip : + make clean + rm -f remise_$(TP).zip + zip -r remise_$(TP).zip *.cpp *.h *.glsl makefile *.txt textures diff --git a/nuanceurFragmentsSolution.glsl b/nuanceurFragmentsSolution.glsl new file mode 100644 index 0000000..3a4de24 --- /dev/null +++ b/nuanceurFragmentsSolution.glsl @@ -0,0 +1,141 @@ +#version 410 + +// Définition des paramètres des sources de lumière +layout (std140) uniform LightSourceParameters +{ + vec4 ambient; + vec4 diffuse; + vec4 specular; + vec4 position; + vec3 spotDirection; + float spotExponent; + float spotCutoff; // ([0.0,90.0] ou 180.0) + float constantAttenuation; + float linearAttenuation; + float quadraticAttenuation; +} LightSource[1]; + +// Définition des paramètres des matériaux +layout (std140) uniform MaterialParameters +{ + vec4 emission; + vec4 ambient; + vec4 diffuse; + vec4 specular; + float shininess; +} FrontMaterial; + +// Définition des paramètres globaux du modèle de lumière +layout (std140) uniform LightModelParameters +{ + vec4 ambient; // couleur ambiante + bool localViewer; // observateur local ou à l'infini? + bool twoSide; // éclairage sur les deux côtés ou un seul? +} LightModel; + +layout (std140) uniform varsUnif +{ + // partie 1: illumination + int typeIllumination; // 0:Lambert, 1:Gouraud, 2:Phong + bool utiliseBlinn; // indique si on veut utiliser modèle spéculaire de Blinn ou Phong + bool utiliseDirect; // indique si on utilise un spot style Direct3D ou OpenGL + bool afficheNormales; // indique si on utilise les normales comme couleurs (utile pour le débogage) + // partie 3: texture + int texnumero; // numéro de la texture appliquée + bool utiliseCouleur; // doit-on utiliser la couleur de base de l'objet en plus de celle de la texture? + int afficheTexelNoir; // un texel noir doit-il être affiché 0:noir, 1:mi-coloré, 2:transparent? +}; + +uniform sampler2D laTexture; + +///////////////////////////////////////////////////////////////// + +in Attribs { + vec3 lumiDir, spotDir; + vec3 normale, obsVec; + vec2 texCoord; + vec4 couleur; +} AttribsIn; + +out vec4 FragColor; + +float calculerSpot( in vec3 spotDir, in vec3 L ) +{ + float spotFacteur; + float spotDot = dot( L, normalize( spotDir ) ); + if ( utiliseDirect ) // modèle Direct3D + { + float cosAngleInterne = cos(radians(LightSource[0].spotCutoff)); + float exposant = 1.01 + LightSource[0].spotExponent / 2.0; + float cosAngleExterne = pow( cos(radians(LightSource[0].spotCutoff)), exposant ); + // calculer le facteur spot avec la fonction smoothstep() + spotFacteur = smoothstep( cosAngleExterne, cosAngleInterne, spotDot ); + } + else // modèle OpenGL + { + spotFacteur = ( spotDot > cos(radians(LightSource[0].spotCutoff)) ) ? pow( spotDot, LightSource[0].spotExponent ) : 0.0; + } + return( spotFacteur ); +} + +vec4 calculerReflexion( in vec3 L, in vec3 N, in vec3 O ) +{ + vec4 coul = FrontMaterial.emission + FrontMaterial.ambient * LightModel.ambient; + + // calcul de la composante ambiante + coul += FrontMaterial.ambient * LightSource[0].ambient; + + // calcul de l'éclairage seulement si le produit scalaire est positif + float NdotL = max( 0.0, dot( N, L ) ); + if ( NdotL > 0.0 ) + { + // calcul de la composante diffuse + //coul += ( utiliseCouleur ? FrontMaterial.diffuse : vec4(1.0) ) * LightSource[0].diffuse * NdotL; + coul += FrontMaterial.diffuse * LightSource[0].diffuse * NdotL; + + // calcul de la composante spéculaire (Blinn ou Phong) + float NdotHV = max( 0.0, ( utiliseBlinn ) ? dot( normalize( L + O ), N ) : dot( reflect( -L, N ), O ) ); + coul += FrontMaterial.specular * LightSource[0].specular * ( ( NdotHV == 0.0 ) ? 0.0 : pow( NdotHV, FrontMaterial.shininess ) ); + } + return( coul ); +} + +void main( void ) +{ + vec3 L = normalize( AttribsIn.lumiDir ); // vecteur vers la source lumineuse + vec3 N = normalize( AttribsIn.normale ); // vecteur normal + //vec3 N = normalize( gl_FrontFacing ? AttribsIn.normale : -AttribsIn.normale ); + vec3 O = normalize( AttribsIn.obsVec ); // position de l'observateur + + // calculer la réflexion: + // si illumination de 1:Gouraud, prendre la couleur interpolée qui a été reçue + // si illumination de 2:Phong, le faire! + // si illumination de 0:Lambert, faire comme Phong, même si les normales sont les mêmes pour tous les fragments + vec4 coul = ( typeIllumination == 1 ) ? AttribsIn.couleur : calculerReflexion( L, N, O ); + + // calculer l'influence du spot + float spotFacteur = calculerSpot( AttribsIn.spotDir, L ); + coul *= spotFacteur; + //if ( spotFacteur <= 0.0 ) discard; // pour éliminer tout ce qui n'est pas dans le cône + // calcul de la composante ambiante + //coul += FrontMaterial.ambient * LightSource[0].ambient; + + // appliquer la texture s'il y a lieu + if ( texnumero != 0 ) + { + vec4 couleurTexture = texture( laTexture, AttribsIn.texCoord ); + // comment afficher un texel noir? + if ( couleurTexture.r < 0.1 && couleurTexture.g < 0.1 && couleurTexture.b < 0.1 && + spotFacteur > 0.0 ) + if ( afficheTexelNoir == 1 ) + couleurTexture = coul / 2.0; + else if ( afficheTexelNoir == 2 ) + discard; + coul *= couleurTexture; + } + + // assigner la couleur finale + FragColor = clamp( coul, 0.0, 1.0 ); + + if ( afficheNormales ) FragColor = vec4(N,1.0); +} diff --git a/nuanceurGeometrieSolution.glsl b/nuanceurGeometrieSolution.glsl new file mode 100644 index 0000000..3341035 --- /dev/null +++ b/nuanceurGeometrieSolution.glsl @@ -0,0 +1,73 @@ +#version 410 + +layout(triangles) in; +layout(triangle_strip, max_vertices = 3) out; + +uniform mat4 matrModel; +uniform mat4 matrVisu; +uniform mat4 matrProj; +uniform mat3 matrNormale; + +layout (std140) uniform varsUnif +{ + // partie 1: illumination + int typeIllumination; // 0:Lambert, 1:Gouraud, 2:Phong + bool utiliseBlinn; // indique si on veut utiliser modèle spéculaire de Blinn ou Phong + bool utiliseDirect; // indique si on utilise un spot style Direct3D ou OpenGL + bool afficheNormales; // indique si on utilise les normales comme couleurs (utile pour le débogage) + // partie 3: texture + int texnumero; // numéro de la texture appliquée + bool utiliseCouleur; // doit-on utiliser la couleur de base de l'objet en plus de celle de la texture? + int afficheTexelNoir; // un texel noir doit-il être affiché 0:noir, 1:mi-coloré, 2:transparent? +}; + +in Attribs { + vec3 lumiDir, spotDir; + vec3 normale, obsVec; + vec2 texCoord; + vec4 couleur; +} AttribsIn[]; + +out Attribs { + vec3 lumiDir, spotDir; + vec3 normale, obsVec; + vec2 texCoord; + vec4 couleur; +} AttribsOut; + +void main() +{ + // si illumination est Lambert, calculer une nouvelle normale + vec3 n = vec3(0.0); + if ( typeIllumination == 0 ) + { + vec3 p0 = gl_in[0].gl_Position.xyz; + vec3 p1 = gl_in[1].gl_Position.xyz; + vec3 p2 = gl_in[2].gl_Position.xyz; + n = cross( p1-p0, p2-p0 ); // cette nouvelle normale est déjà dans le repère de la caméra + // il n'est pas nécessaire de la multiplier par matrNormale + } + // ou faire une moyenne, MAIS CE N'EST PAS CE QU'ON VEUT! + // if ( typeIllumination == 0 ) + // { + // // calculer le centre + // for ( int i = 0 ; i < gl_in.length() ; ++i ) + // { + // n += AttribsIn[i].normale; + // } + // n /= gl_in.length(); + // } + + // émettre les sommets + for ( int i = 0 ; i < gl_in.length() ; ++i ) + { + gl_Position = matrProj * gl_in[i].gl_Position; // on termine la transformation débutée dans le nuanceur de sommets + AttribsOut.lumiDir = AttribsIn[i].lumiDir; + AttribsOut.spotDir = AttribsIn[i].spotDir; + AttribsOut.normale = ( typeIllumination == 0 ) ? n : AttribsIn[i].normale; + AttribsOut.obsVec = AttribsIn[i].obsVec; + AttribsOut.texCoord = AttribsIn[i].texCoord; + AttribsOut.couleur = AttribsIn[i].couleur; + EmitVertex(); + } +} diff --git a/nuanceurSommetsSolution.glsl b/nuanceurSommetsSolution.glsl new file mode 100644 index 0000000..80bff7d --- /dev/null +++ b/nuanceurSommetsSolution.glsl @@ -0,0 +1,130 @@ +#version 410 + +// Définition des paramètres des sources de lumière +layout (std140) uniform LightSourceParameters +{ + vec4 ambient; + vec4 diffuse; + vec4 specular; + vec4 position; + vec3 spotDirection; + float spotExponent; + float spotCutoff; // ([0.0,90.0] ou 180.0) + float constantAttenuation; + float linearAttenuation; + float quadraticAttenuation; +} LightSource[1]; + +// Définition des paramètres des matériaux +layout (std140) uniform MaterialParameters +{ + vec4 emission; + vec4 ambient; + vec4 diffuse; + vec4 specular; + float shininess; +} FrontMaterial; + +// Définition des paramètres globaux du modèle de lumière +layout (std140) uniform LightModelParameters +{ + vec4 ambient; // couleur ambiante + bool localViewer; // observateur local ou à l'infini? + bool twoSide; // éclairage sur les deux côtés ou un seul? +} LightModel; + +layout (std140) uniform varsUnif +{ + // partie 1: illumination + int typeIllumination; // 0:Lambert, 1:Gouraud, 2:Phong + bool utiliseBlinn; // indique si on veut utiliser modèle spéculaire de Blinn ou Phong + bool utiliseDirect; // indique si on utilise un spot style Direct3D ou OpenGL + bool afficheNormales; // indique si on utilise les normales comme couleurs (utile pour le débogage) + // partie 3: texture + int texnumero; // numéro de la texture appliquée + bool utiliseCouleur; // doit-on utiliser la couleur de base de l'objet en plus de celle de la texture? + int afficheTexelNoir; // un texel noir doit-il être affiché 0:noir, 1:mi-coloré, 2:transparent? +}; + +uniform mat4 matrModel; +uniform mat4 matrVisu; +uniform mat4 matrProj; +uniform mat3 matrNormale; + +///////////////////////////////////////////////////////////////// + +layout(location=0) in vec4 Vertex; +layout(location=2) in vec3 Normal; +layout(location=3) in vec4 Color; +layout(location=8) in vec4 TexCoord; + +out Attribs { + vec3 lumiDir, spotDir; + vec3 normale, obsVec; + vec2 texCoord; + vec4 couleur; +} AttribsOut; + +vec4 calculerReflexion( in vec3 L, in vec3 N, in vec3 O ) +{ + vec4 coul = FrontMaterial.emission + FrontMaterial.ambient * LightModel.ambient; + + // calcul de la composante ambiante + coul += FrontMaterial.ambient * LightSource[0].ambient; + + // calcul de l'éclairage seulement si le produit scalaire est positif + float NdotL = max( 0.0, dot( N, L ) ); + if ( NdotL > 0.0 ) + { + // calcul de la composante diffuse + //coul += ( utiliseCouleur ? FrontMaterial.diffuse : vec4(1.0) ) * LightSource[0].diffuse * NdotL; + coul += FrontMaterial.diffuse * LightSource[0].diffuse * NdotL; + + // calcul de la composante spéculaire (Blinn ou Phong) + float NdotHV = max( 0.0, ( utiliseBlinn ) ? dot( normalize( L + O ), N ) : dot( reflect( -L, N ), O ) ); + coul += FrontMaterial.specular * LightSource[0].specular * ( ( NdotHV == 0.0 ) ? 0.0 : pow( NdotHV, FrontMaterial.shininess ) ); + } + return( coul ); +} + +void main( void ) +{ + // transformation standard du sommet, ** sans la projection ** + gl_Position = matrVisu * matrModel * Vertex; + + // calculer la normale qui sera interpolée pour le nuanceur de fragment + AttribsOut.normale = matrNormale * Normal; + + // calculer la position du sommet (dans le repère de la caméra) + vec3 pos = vec3( matrVisu * matrModel * Vertex ); + + // vecteur de la direction de la lumière (dans le repère de la caméra) + AttribsOut.lumiDir = vec3( ( matrVisu * LightSource[0].position ).xyz - pos ); + + // vecteur de la direction vers l'observateur (dans le repère de la caméra) + AttribsOut.obsVec = ( LightModel.localViewer ? + normalize(-pos) : // =(0-pos) un vecteur qui pointe vers le (0,0,0), c'est-à-dire vers la caméra + vec3( 0.0, 0.0, 1.0 ) ); // on considère que l'observateur (la caméra) est à l'infini dans la direction (0,0,1) + // vecteur de la direction du spot (en tenant compte seulement des rotations de la caméra) + AttribsOut.spotDir = inverse(mat3(matrVisu)) * -LightSource[0].spotDirection; + // On accepte aussi: (si on suppose que .spotDirection est déjà dans le repère de la caméra) + //AttribsOut.spotDir = -LightSource[0].spotDirection; + // On accepte aussi: (car matrVisu a seulement une translation et pas de rotation => "mat3(matrVisu) == I" ) + //AttribsOut.spotDir = -LightSource[0].spotDirection; + // On accepte aussi: (car c'était le calcul qui était dans la solution précédente présentée dans le lab!) + //AttribsOut.spotDir = -( matrVisu * vec4(LightSource[0].spotDirection,1.0) ).xyz; + + // si illumination est 1:Gouraud, calculer la réflexion ici, sinon ne rien faire de plus + if ( typeIllumination == 1 ) + { + vec3 L = normalize( AttribsOut.lumiDir ); // calcul du vecteur de la surface vers la source lumineuse + vec3 N = normalize( AttribsOut.normale ); // vecteur normal + vec3 O = normalize( AttribsOut.obsVec ); // position de l'observateur + AttribsOut.couleur = calculerReflexion( L, N, O ); + } + //else + // couleur = vec4(0.0); // inutile + + // assigner les coordonnées de texture + AttribsOut.texCoord = TexCoord.st; +} diff --git a/ripple.cpp b/ripple.cpp new file mode 100644 index 0000000..46e1dc0 --- /dev/null +++ b/ripple.cpp @@ -0,0 +1,954 @@ +// Prénoms, noms et matricule des membres de l'équipe: +// - Prénom1 NOM1 (matricule1) +// - Prénom2 NOM2 (matricule2) + +#include +#include +#include "inf2705.h" + +#define SOL 1 + +// variables pour l'utilisation des nuanceurs +GLuint prog; // votre programme de nuanceurs +GLint locVertex = -1; +GLint locNormal = -1; +GLint locTexCoord = -1; +GLint locmatrModel = -1; +GLint locmatrVisu = -1; +GLint locmatrProj = -1; +GLint locmatrNormale = -1; +GLint loclaTexture = -1; +GLuint indLightSource; +GLuint indFrontMaterial; +GLuint indLightModel; +GLuint indvarsUnif; +GLuint progBase; // le programme de nuanceurs de base +GLint locVertexBase = -1; +GLint locColorBase = -1; +GLint locmatrModelBase = -1; +GLint locmatrVisuBase = -1; +GLint locmatrProjBase = -1; + +GLuint vao[2]; +GLuint vbo[5]; +GLuint ubo[4]; + +// matrices de du pipeline graphique +MatricePipeline matrModel; +MatricePipeline matrVisu; +MatricePipeline matrProj; + +// les formes +FormeSphere *sphere = NULL, *sphereLumi = NULL; +FormeTheiere *theiere = NULL; +FormeTore *tore = NULL; +FormeCylindre *cylindre = NULL; +FormeCylindre *cone = NULL; + +// variables pour définir le point de vue +double thetaCam = 0.0; // angle de rotation de la caméra (coord. sphériques) +double phiCam = 0.0; // angle de rotation de la caméra (coord. sphériques) +double distCam = 0.0; // distance (coord. sphériques) + +// variables d'état +bool enPerspective = false; // indique si on est en mode Perspective (true) ou Ortho (false) +bool enmouvement = false; // le modèle est en mouvement/rotation automatique ou non +bool afficheAxes = true; // indique si on affiche les axes +GLenum modePolygone = GL_FILL; // comment afficher les polygones + +//////////////////////////////////////// +// déclaration des variables globales // +//////////////////////////////////////// + +// partie 1: illumination +int modele = 1; // le modèle à afficher + +// partie 3: texture +GLuint textureDE = 0; +GLuint textureECHIQUIER = 0; + +// définition des lumières +struct LightSourceParameters +{ + glm::vec4 ambient; + glm::vec4 diffuse; + glm::vec4 specular; + glm::vec4 position; + glm::vec3 spotDirection; + float spotExposant; + float spotAngle; // ([0.0,90.0] ou 180.0) + float constantAttenuation; + float linearAttenuation; + float quadraticAttenuation; +} LightSource[1] = { { glm::vec4( 1.0, 1.0, 1.0, 1.0 ), + glm::vec4( 1.0, 1.0, 1.0, 1.0 ), + glm::vec4( 1.0, 1.0, 1.0, 1.0 ), + glm::vec4( 4, 1, 15, 1.0 ), + glm::vec3( -5.0, -2.0, -10.0 ), + 1.0, // l'exposant du cône + 15.0, // l'angle du cône du spot + 1., 0., 0. } }; + +// définition du matériau +struct MaterialParameters +{ + glm::vec4 emission; + glm::vec4 ambient; + glm::vec4 diffuse; + glm::vec4 specular; + float shininess; +} FrontMaterial = { glm::vec4( 0.0, 0.0, 0.0, 1.0 ), + glm::vec4( 0.1, 0.1, 0.1, 1.0 ), + glm::vec4( 1.0, 0.1, 1.0, 1.0 ), + glm::vec4( 1.0, 1.0, 1.0, 1.0 ), + 100.0 }; + +struct LightModelParameters +{ + glm::vec4 ambient; // couleur ambiante + int localViewer; // doit-on prendre en compte la position de l'observateur? (local ou à l'infini) + int twoSide; // éclairage sur les deux côtés ou un seul? +} LightModel = { glm::vec4(0,0,0,1), false, false }; + +struct +{ + // partie 1: illumination + int typeIllumination; // 0:Lambert, 1:Gouraud, 2:Phong + int utiliseBlinn; // indique si on veut utiliser modèle spéculaire de Blinn ou Phong + int utiliseDirect; // indique si on utilise un spot style Direct3D ou OpenGL + int afficheNormales; // indique si on utilise les normales comme couleurs (utile pour le débogage) + // partie 3: texture + int texnumero; // numéro de la texture appliquée + int utiliseCouleur; // doit-on utiliser la couleur de base de l'objet en plus de celle de la texture? + int afficheTexelNoir; // un texel noir doit-il être affiché 0:noir, 1:mi-coloré, 2:transparent? +} varsUnif = { 2, false, false, false, + 0, true, 0 }; +// ( En glsl, les types 'bool' et 'int' sont de la même taille, ce qui n'est pas le cas en C++. +// Ci-dessus, on triche donc un peu en déclarant les 'bool' comme des 'int', mais ça facilite la +// copie directe vers le nuanceur où les variables seront bien de type 'bool'. ) + + +void verifierAngles() +{ + if ( thetaCam > 360.0 ) + thetaCam -= 360.0; + else if ( thetaCam < 0.0 ) + thetaCam += 360.0; + + const GLdouble MINPHI = -90.0, MAXPHI = 90.0; + if ( phiCam > MAXPHI ) + phiCam = MAXPHI; + else if ( phiCam < MINPHI ) + phiCam = MINPHI; +} + +void calculerPhysique( ) +{ + if ( enmouvement ) + { + static int sensTheta = 1; + static int sensPhi = 1; + thetaCam += 0.3 * sensTheta; + phiCam += 0.5 * sensPhi; + //if ( thetaCam <= 0. || thetaCam >= 360.0 ) sensTheta = -sensTheta; + if ( phiCam < -90.0 || phiCam > 90.0 ) sensPhi = -sensPhi; + + static int sensAngle = 1; + LightSource[0].spotAngle += sensAngle * 0.3; + if ( LightSource[0].spotAngle < 5.0 ) sensAngle = -sensAngle; + if ( LightSource[0].spotAngle > 60.0 ) sensAngle = -sensAngle; + +#if 0 + static int sensExposant = 1; + LightSource[0].spotExposant += sensExposant * 0.3; + if ( LightSource[0].spotExposant < 1.0 ) sensExposant = -sensExposant; + if ( LightSource[0].spotExposant > 10.0 ) sensExposant = -sensExposant; +#endif + + // De temps à autre, alterner entre le modèle d'illumination: Lambert, Gouraud, Phong + static float type = 0; + type += 0.005; + varsUnif.typeIllumination = fmod(type,3); + } + + verifierAngles(); +} + +void chargerTextures() +{ + unsigned char *pixels; + GLsizei largeur, hauteur; + if ( ( pixels = ChargerImage( "textures/de.bmp", largeur, hauteur ) ) != NULL ) + { + glGenTextures( 1, &textureDE ); + glBindTexture( GL_TEXTURE_2D, textureDE ); + glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, largeur, hauteur, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); + glBindTexture( GL_TEXTURE_2D, 0 ); + delete[] pixels; + } + if ( ( pixels = ChargerImage( "textures/echiquier.bmp", largeur, hauteur ) ) != NULL ) + { + glGenTextures( 1, &textureECHIQUIER ); + glBindTexture( GL_TEXTURE_2D, textureECHIQUIER ); + glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, largeur, hauteur, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); + glBindTexture( GL_TEXTURE_2D, 0 ); + delete[] pixels; + } + + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT ); + glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT ); +} + +void chargerNuanceurs() +{ + // charger le nuanceur de base + { + // créer le programme + progBase = glCreateProgram(); + + // attacher le nuanceur de sommets + { + GLuint nuanceurObj = glCreateShader( GL_VERTEX_SHADER ); + glShaderSource( nuanceurObj, 1, &ProgNuanceur::chainesSommetsMinimal, NULL ); + glCompileShader( nuanceurObj ); + glAttachShader( progBase, nuanceurObj ); + ProgNuanceur::afficherLogCompile( nuanceurObj ); + } + // attacher le nuanceur de fragments + { + GLuint nuanceurObj = glCreateShader( GL_FRAGMENT_SHADER ); + glShaderSource( nuanceurObj, 1, &ProgNuanceur::chainesFragmentsMinimal, NULL ); + glCompileShader( nuanceurObj ); + glAttachShader( progBase, nuanceurObj ); + ProgNuanceur::afficherLogCompile( nuanceurObj ); + } + + // faire l'édition des liens du programme + glLinkProgram( progBase ); + + ProgNuanceur::afficherLogLink( progBase ); + // demander la "Location" des variables + if ( ( locVertexBase = glGetAttribLocation( progBase, "Vertex" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de Vertex" << std::endl; + if ( ( locColorBase = glGetAttribLocation( progBase, "Color" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de Color" << std::endl; + if ( ( locmatrModelBase = glGetUniformLocation( progBase, "matrModel" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de matrModel" << std::endl; + if ( ( locmatrVisuBase = glGetUniformLocation( progBase, "matrVisu" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de matrVisu" << std::endl; + if ( ( locmatrProjBase = glGetUniformLocation( progBase, "matrProj" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de matrProj" << std::endl; + } + + // charger le nuanceur de ce TP + { + // créer le programme + prog = glCreateProgram(); + + // attacher le nuanceur de sommets +#if !defined(SOL) + const GLchar *chainesSommets = ProgNuanceur::lireNuanceur( "nuanceurSommets.glsl" ); +#else + const GLchar *chainesSommets = ProgNuanceur::lireNuanceur( "nuanceurSommetsSolution.glsl" ); +#endif + if ( chainesSommets != NULL ) + { + GLuint nuanceurObj = glCreateShader( GL_VERTEX_SHADER ); + glShaderSource( nuanceurObj, 1, &chainesSommets, NULL ); + glCompileShader( nuanceurObj ); + glAttachShader( prog, nuanceurObj ); + ProgNuanceur::afficherLogCompile( nuanceurObj ); + delete [] chainesSommets; + } +#if !defined(SOL) + const GLchar *chainesGeometrie = ProgNuanceur::lireNuanceur( "nuanceurGeometrie.glsl" ); +#else + const GLchar *chainesGeometrie = ProgNuanceur::lireNuanceur( "nuanceurGeometrieSolution.glsl" ); +#endif + if ( chainesGeometrie != NULL ) + { + GLuint nuanceurObj = glCreateShader( GL_GEOMETRY_SHADER ); + glShaderSource( nuanceurObj, 1, &chainesGeometrie, NULL ); + glCompileShader( nuanceurObj ); + glAttachShader( prog, nuanceurObj ); + ProgNuanceur::afficherLogCompile( nuanceurObj ); + delete [] chainesGeometrie; + } + // attacher le nuanceur de fragments +#if !defined(SOL) + const GLchar *chainesFragments = ProgNuanceur::lireNuanceur( "nuanceurFragments.glsl" ); +#else + const GLchar *chainesFragments = ProgNuanceur::lireNuanceur( "nuanceurFragmentsSolution.glsl" ); +#endif + if ( chainesFragments != NULL ) + { + GLuint nuanceurObj = glCreateShader( GL_FRAGMENT_SHADER ); + glShaderSource( nuanceurObj, 1, &chainesFragments, NULL ); + glCompileShader( nuanceurObj ); + glAttachShader( prog, nuanceurObj ); + ProgNuanceur::afficherLogCompile( nuanceurObj ); + delete [] chainesFragments; + } + + // faire l'édition des liens du programme + glLinkProgram( prog ); + + ProgNuanceur::afficherLogLink( prog ); + // demander la "Location" des variables + if ( ( locVertex = glGetAttribLocation( prog, "Vertex" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de Vertex" << std::endl; + if ( ( locNormal = glGetAttribLocation( prog, "Normal" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de Normal (partie 1)" << std::endl; + if ( ( locTexCoord = glGetAttribLocation( prog, "TexCoord" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de TexCoord (partie 3)" << std::endl; + if ( ( locmatrModel = glGetUniformLocation( prog, "matrModel" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de matrModel" << std::endl; + if ( ( locmatrVisu = glGetUniformLocation( prog, "matrVisu" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de matrVisu" << std::endl; + if ( ( locmatrProj = glGetUniformLocation( prog, "matrProj" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de matrProj" << std::endl; + if ( ( locmatrNormale = glGetUniformLocation( prog, "matrNormale" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de matrNormale (partie 1)" << std::endl; + if ( ( loclaTexture = glGetUniformLocation( prog, "laTexture" ) ) == -1 ) std::cerr << "!!! pas trouvé la \"Location\" de laTexture (partie 3)" << std::endl; + if ( ( indLightSource = glGetUniformBlockIndex( prog, "LightSourceParameters" ) ) == GL_INVALID_INDEX ) std::cerr << "!!! pas trouvé l'\"index\" de LightSource" << std::endl; + if ( ( indFrontMaterial = glGetUniformBlockIndex( prog, "MaterialParameters" ) ) == GL_INVALID_INDEX ) std::cerr << "!!! pas trouvé l'\"index\" de FrontMaterial" << std::endl; + if ( ( indLightModel = glGetUniformBlockIndex( prog, "LightModelParameters" ) ) == GL_INVALID_INDEX ) std::cerr << "!!! pas trouvé l'\"index\" de LightModel" << std::endl; + if ( ( indvarsUnif = glGetUniformBlockIndex( prog, "varsUnif" ) ) == GL_INVALID_INDEX ) std::cerr << "!!! pas trouvé l'\"index\" de varsUnif" << std::endl; + + // charger les ubo + { + glBindBuffer( GL_UNIFORM_BUFFER, ubo[0] ); + glBufferData( GL_UNIFORM_BUFFER, sizeof(LightSource), &LightSource, GL_DYNAMIC_COPY ); + glBindBuffer( GL_UNIFORM_BUFFER, 0 ); + const GLuint bindingIndex = 0; + glBindBufferBase( GL_UNIFORM_BUFFER, bindingIndex, ubo[0] ); + glUniformBlockBinding( prog, indLightSource, bindingIndex ); + } + { + glBindBuffer( GL_UNIFORM_BUFFER, ubo[1] ); + glBufferData( GL_UNIFORM_BUFFER, sizeof(FrontMaterial), &FrontMaterial, GL_DYNAMIC_COPY ); + glBindBuffer( GL_UNIFORM_BUFFER, 0 ); + const GLuint bindingIndex = 1; + glBindBufferBase( GL_UNIFORM_BUFFER, bindingIndex, ubo[1] ); + glUniformBlockBinding( prog, indFrontMaterial, bindingIndex ); + } + { + glBindBuffer( GL_UNIFORM_BUFFER, ubo[2] ); + glBufferData( GL_UNIFORM_BUFFER, sizeof(LightModel), &LightModel, GL_DYNAMIC_COPY ); + glBindBuffer( GL_UNIFORM_BUFFER, 0 ); + const GLuint bindingIndex = 2; + glBindBufferBase( GL_UNIFORM_BUFFER, bindingIndex, ubo[2] ); + glUniformBlockBinding( prog, indLightModel, bindingIndex ); + } + { + glBindBuffer( GL_UNIFORM_BUFFER, ubo[3] ); + glBufferData( GL_UNIFORM_BUFFER, sizeof(varsUnif), &varsUnif, GL_DYNAMIC_COPY ); + glBindBuffer( GL_UNIFORM_BUFFER, 0 ); + const GLuint bindingIndex = 3; + glBindBufferBase( GL_UNIFORM_BUFFER, bindingIndex, ubo[3] ); + glUniformBlockBinding( prog, indvarsUnif, bindingIndex ); + } + } +} + +// initialisation d'openGL +void initialiser() +{ + // donner l'orientation du modèle + thetaCam = 0.0; + phiCam = 0.0; + distCam = 30.0; + + // couleur de l'arrière-plan + glClearColor( 0.4, 0.2, 0.0, 1.0 ); + + // activer les etats openGL + glEnable( GL_DEPTH_TEST ); + + // charger les textures + chargerTextures(); + + // allouer les UBO pour les variables uniformes + glGenBuffers( 4, ubo ); + + // charger les nuanceurs + chargerNuanceurs(); + glUseProgram( prog ); + + // (partie 1) créer le cube + /* +Y */ + /* 3+-----------+2 */ + /* |\ |\ */ + /* | \ | \ */ + /* | \ | \ */ + /* | 7+-----------+6 */ + /* | | | | */ + /* | | | | */ + /* 0+---|-------+1 | */ + /* \ | \ | +X */ + /* \ | \ | */ + /* \| \| */ + /* 4+-----------+5 */ + /* +Z */ + + GLfloat sommets[3*4*6] = + { + -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, -1.0, // P3,P2,P0,P1 + 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, // P5,P4,P1,P0 + 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, // P6,P5,P2,P1 + -1.0, 1.0, 1.0, 1.0, 1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, // P7,P6,P3,P2 + -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, // P4,P7,P0,P3 + -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, -1.0, 1.0, 1.0, 1.0, 1.0, 1.0 // P4,P5,P7,P6 + }; + GLfloat normales[3*4*6] = + { + 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, + 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, 0.0,-1.0, 0.0, + 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, + -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, + }; + GLfloat texcoordsDe[2*4*6] = + { + 1.000000,0.000000, 0.666666,0.000000, 1.000000,0.333333, 0.666666,0.333333, + 0.000000,0.666666, 0.333333,0.666666, 0.000000,0.333333, 0.333333,0.333333, + 0.666666,1.000000, 0.666666,0.666666, 0.333333,1.000000, 0.333333,0.666666, + 1.000000,0.333333, 0.666666,0.333333, 1.000000,0.666666, 0.666666,0.666666, + 0.333333,0.000000, 0.333333,0.333333, 0.666666,0.000000, 0.666666,0.333333, + 0.666666,0.333333, 0.333333,0.333333, 0.666666,0.666666, 0.333333,0.666666 + }; + GLfloat texcoordsEchiquier[2*4*6] = + { + -1.0, -1.0, -1.0, 2.0, 2.0, -1.0, 2.0, 2.0, + 2.0, -1.0, -1.0, -1.0, 2.0, 2.0, -1.0, 2.0, + -1.0, -1.0, -1.0, 2.0, 2.0, -1.0, 2.0, 2.0, + -1.0, 2.0, 2.0, 2.0, -1.0, -1.0, 2.0, -1.0, + 2.0, 2.0, 2.0, -1.0, -1.0, 2.0, -1.0, -1.0, + -1.0, -1.0, -1.0, 2.0, 2.0, -1.0, 2.0, 2.0 + }; + + // allouer les objets OpenGL + glGenVertexArrays( 2, vao ); + glGenBuffers( 5, vbo ); + // initialiser le VAO + glBindVertexArray( vao[0] ); + + // charger le VBO pour les sommets + glBindBuffer( GL_ARRAY_BUFFER, vbo[0] ); + glBufferData( GL_ARRAY_BUFFER, sizeof(sommets), sommets, GL_STATIC_DRAW ); + glVertexAttribPointer( locVertex, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locVertex); + // (partie 1) charger le VBO pour les normales + glBindBuffer( GL_ARRAY_BUFFER, vbo[1] ); + glBufferData( GL_ARRAY_BUFFER, sizeof(normales), normales, GL_STATIC_DRAW ); + glVertexAttribPointer( locNormal, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locNormal); + // (partie 3) charger le VBO pour les coordonnées de texture du dé + glBindBuffer( GL_ARRAY_BUFFER, vbo[2] ); + glBufferData( GL_ARRAY_BUFFER, sizeof(texcoordsDe), texcoordsDe, GL_STATIC_DRAW ); + glVertexAttribPointer( locTexCoord, 2, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locTexCoord); + // (partie 3) charger le VBO pour les coordonnées de texture de l'échiquier + glBindBuffer( GL_ARRAY_BUFFER, vbo[3] ); + glBufferData( GL_ARRAY_BUFFER, sizeof(texcoordsEchiquier), texcoordsEchiquier, GL_STATIC_DRAW ); + glVertexAttribPointer( locTexCoord, 2, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locTexCoord); + + glBindVertexArray(0); + + // initialiser le VAO pour une ligne (montrant la direction du spot) + glBindVertexArray( vao[1] ); + GLfloat coords[] = { 0., 0., 0., 0., 0., 1. }; + glBindBuffer( GL_ARRAY_BUFFER, vbo[4] ); + glBufferData( GL_ARRAY_BUFFER, sizeof(coords), coords, GL_STATIC_DRAW ); + glVertexAttribPointer( locVertexBase, 3, GL_FLOAT, GL_FALSE, 0, 0 ); + glEnableVertexAttribArray(locVertexBase); + glBindVertexArray(0); + + // créer quelques autres formes + sphere = new FormeSphere( 1.0, 32, 32 ); + sphereLumi = new FormeSphere( 0.5, 10, 10 ); + theiere = new FormeTheiere( ); + tore = new FormeTore( 0.4, 0.8, 32, 32 ); + cylindre = new FormeCylindre( 0.3, 0.3, 3.0, 32, 32 ); + cone = new FormeCylindre( 0.0, 0.5, 3.0, 32, 32 ); +} + +void conclure() +{ + glUseProgram( 0 ); + glDeleteVertexArrays( 2, vao ); + glDeleteBuffers( 4, vbo ); + glDeleteBuffers( 4, ubo ); + delete sphere; + delete sphereLumi; + delete theiere; + delete tore; + delete cylindre; + delete cone; +} + +void afficherModele() +{ + // partie 3: paramètres de texture + switch ( varsUnif.texnumero ) + { + default: + //std::cout << "Sans texture" << std::endl; + glBindTexture( GL_TEXTURE_2D, 0 ); + break; + case 1: + //std::cout << "Texture DE" << std::endl; + glBindTexture( GL_TEXTURE_2D, textureDE ); + break; + case 2: + //std::cout << "Texture ECHIQUIER" << std::endl; + glBindTexture( GL_TEXTURE_2D, textureECHIQUIER ); + break; + } + + // Dessiner le modèle + matrModel.PushMatrix(); { + + // appliquer les rotations + matrModel.Rotate( phiCam, -1.0, 0.0, 0.0 ); + matrModel.Rotate( thetaCam, 0.0, -1.0, 0.0 ); + + // mise à l'échelle + matrModel.Scale( 5.0, 5.0, 5.0 ); + + glUniformMatrix4fv( locmatrModel, 1, GL_FALSE, matrModel ); + // (partie 1: ne pas oublier de calculer et donner une matrice pour les transformations des normales) + glUniformMatrix3fv( locmatrNormale, 1, GL_TRUE, glm::value_ptr( glm::inverse( glm::mat3( matrVisu.getMatr() * matrModel.getMatr() ) ) ) ); + + switch ( modele ) + { + default: + case 1: + // afficher le cube + glBindVertexArray( vao[0] ); + glBindBuffer( GL_ARRAY_BUFFER, varsUnif.texnumero == 1 ? vbo[2] : vbo[3] ); + glVertexAttribPointer( locTexCoord, 2, GL_FLOAT, GL_FALSE, 0, 0 ); + glDrawArrays( GL_TRIANGLE_STRIP, 0, 4 ); + glDrawArrays( GL_TRIANGLE_STRIP, 4, 4 ); + glDrawArrays( GL_TRIANGLE_STRIP, 8, 4 ); + glDrawArrays( GL_TRIANGLE_STRIP, 12, 4 ); + glDrawArrays( GL_TRIANGLE_STRIP, 16, 4 ); + glDrawArrays( GL_TRIANGLE_STRIP, 20, 4 ); + glBindVertexArray( 0 ); + break; + case 2: + tore->afficher(); + break; + case 3: + sphere->afficher(); + break; + case 4: + matrModel.Rotate( -90.0, 1.0, 0.0, 0.0 ); + matrModel.Translate( 0.0, 0.0, -0.5 ); + matrModel.Scale( 0.5, 0.5, 0.5 ); + glUniformMatrix4fv( locmatrModel, 1, GL_FALSE, matrModel ); + glUniformMatrix3fv( locmatrNormale, 1, GL_TRUE, glm::value_ptr( glm::inverse( glm::mat3( matrVisu.getMatr() * matrModel.getMatr() ) ) ) ); + theiere->afficher( ); + break; + case 5: + matrModel.PushMatrix(); { + matrModel.Translate( 0.0, 0.0, -1.5 ); + glUniformMatrix4fv( locmatrModel, 1, GL_FALSE, matrModel ); + glUniformMatrix3fv( locmatrNormale, 1, GL_TRUE, glm::value_ptr( glm::inverse( glm::mat3( matrVisu.getMatr() * matrModel.getMatr() ) ) ) ); + cylindre->afficher(); + } matrModel.PopMatrix(); + break; + case 6: + matrModel.PushMatrix(); { + matrModel.Translate( 0.0, 0.0, -1.5 ); + glUniformMatrix4fv( locmatrModel, 1, GL_FALSE, matrModel ); + glUniformMatrix3fv( locmatrNormale, 1, GL_TRUE, glm::value_ptr( glm::inverse( glm::mat3( matrVisu.getMatr() * matrModel.getMatr() ) ) ) ); + cone->afficher(); + } matrModel.PopMatrix(); + break; + } + } matrModel.PopMatrix(); glUniformMatrix4fv( locmatrModel, 1, GL_FALSE, matrModel ); +} + +void afficherLumiere() +{ + // Dessiner la lumiere + + // tracer une ligne vers la source lumineuse + const GLfloat fact = 5.; + GLfloat coords[] = + { + LightSource[0].position.x , LightSource[0].position.y , LightSource[0].position.z, + LightSource[0].position.x+LightSource[0].spotDirection.x/fact, LightSource[0].position.y+LightSource[0].spotDirection.y/fact, LightSource[0].position.z+LightSource[0].spotDirection.z/fact + }; + glLineWidth( 3.0 ); + glVertexAttrib3f( locColorBase, 1.0, 1.0, 0.5 ); // jaune + glBindVertexArray( vao[1] ); + matrModel.PushMatrix(); { + glBindBuffer( GL_ARRAY_BUFFER, vbo[4] ); + glBufferSubData( GL_ARRAY_BUFFER, 0, sizeof(coords), coords ); + glDrawArrays( GL_LINES, 0, 2 ); + } matrModel.PopMatrix(); glUniformMatrix4fv( locmatrModelBase, 1, GL_FALSE, matrModel ); + glBindVertexArray( 0 ); + glLineWidth( 1.0 ); + + // tracer la source lumineuse + matrModel.PushMatrix(); { + matrModel.Translate( LightSource[0].position.x, LightSource[0].position.y, LightSource[0].position.z ); + glUniformMatrix4fv( locmatrModelBase, 1, GL_FALSE, matrModel ); + sphereLumi->afficher(); + } matrModel.PopMatrix(); glUniformMatrix4fv( locmatrModelBase, 1, GL_FALSE, matrModel ); +} + +// fonction d'affichage +void FenetreTP::afficherScene() +{ + // effacer l'ecran et le tampon de profondeur + glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT ); + + glUseProgram( progBase ); + + // définir le pipeline graphique + if ( enPerspective ) + { + matrProj.Perspective( 35.0, (GLdouble)largeur_ / (GLdouble)hauteur_, + 0.1, 60.0 ); + } + else + { + const GLfloat d = 8.0; + if ( largeur_ <= hauteur_ ) + { + matrProj.Ortho( -d, d, + -d*(GLdouble)hauteur_ / (GLdouble)largeur_, + d*(GLdouble)hauteur_ / (GLdouble)largeur_, + 0.1, 60.0 ); + } + else + { + matrProj.Ortho( -d*(GLdouble)largeur_ / (GLdouble)hauteur_, + d*(GLdouble)largeur_ / (GLdouble)hauteur_, + -d, d, + 0.1, 60.0 ); + } + } + glUniformMatrix4fv( locmatrProjBase, 1, GL_FALSE, matrProj ); + + matrVisu.LookAt( 0.0, 0.0, distCam, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0 ); + glUniformMatrix4fv( locmatrVisuBase, 1, GL_FALSE, matrVisu ); + + matrModel.LoadIdentity(); + glUniformMatrix4fv( locmatrModelBase, 1, GL_FALSE, matrModel ); + + // afficher les axes + if ( afficheAxes ) FenetreTP::afficherAxes( 8.0 ); + + // dessiner la scène + afficherLumiere(); + + glUseProgram( prog ); + + // mettre à jour les blocs de variables uniformes + { + glBindBuffer( GL_UNIFORM_BUFFER, ubo[0] ); + GLvoid *p = glMapBuffer( GL_UNIFORM_BUFFER, GL_WRITE_ONLY ); + memcpy( p, &LightSource, sizeof(LightSource) ); + glUnmapBuffer( GL_UNIFORM_BUFFER ); + } + { + glBindBuffer( GL_UNIFORM_BUFFER, ubo[1] ); + GLvoid *p = glMapBuffer( GL_UNIFORM_BUFFER, GL_WRITE_ONLY ); + memcpy( p, &FrontMaterial, sizeof(FrontMaterial) ); + glUnmapBuffer( GL_UNIFORM_BUFFER ); + } + { + glBindBuffer( GL_UNIFORM_BUFFER, ubo[2] ); + GLvoid *p = glMapBuffer( GL_UNIFORM_BUFFER, GL_WRITE_ONLY ); + memcpy( p, &LightModel, sizeof(LightModel) ); + glUnmapBuffer( GL_UNIFORM_BUFFER ); + } + { + glBindBuffer( GL_UNIFORM_BUFFER, ubo[3] ); + GLvoid *p = glMapBuffer( GL_UNIFORM_BUFFER, GL_WRITE_ONLY ); + memcpy( p, &varsUnif, sizeof(varsUnif) ); + glUnmapBuffer( GL_UNIFORM_BUFFER ); + } + + // mettre à jour les matrices et autres uniformes + glUniformMatrix4fv( locmatrProj, 1, GL_FALSE, matrProj ); + glUniformMatrix4fv( locmatrVisu, 1, GL_FALSE, matrVisu ); + glUniformMatrix4fv( locmatrModel, 1, GL_FALSE, matrModel ); + //glActiveTexture( GL_TEXTURE0 ); // activer la texture '0' (valeur de défaut) + glUniform1i( loclaTexture, 0 ); // '0' => utilisation de GL_TEXTURE0 + + afficherModele(); +} + +// fonction de redimensionnement de la fenêtre graphique +void FenetreTP::redimensionner( GLsizei w, GLsizei h ) +{ + glViewport( 0, 0, w, h ); +} + +static void echoEtats( ) +{ + static std::string illuminationStr[] = { "0:Lambert", "1:Gouraud", "2:Phong" }; + static std::string reflexionStr[] = { "0:Phong", "1:Blinn" }; + static std::string spotStr[] = { "0:OpenGL", "1:Direct3D" }; + std::cout << " modèle d'illumination= " << illuminationStr[varsUnif.typeIllumination] + << ", refléxion spéculaire= " << reflexionStr[varsUnif.utiliseBlinn] + << ", spot= " << spotStr[varsUnif.utiliseDirect] + << std::endl; +} + +// fonction de gestion du clavier +void FenetreTP::clavier( TP_touche touche ) +{ + // traitement des touches q et echap + switch ( touche ) + { + case TP_ECHAP: + case TP_q: // Quitter l'application + quit(); + break; + + case TP_x: // Activer/désactiver l'affichage des axes + afficheAxes = !afficheAxes; + std::cout << "// Affichage des axes ? " << ( afficheAxes ? "OUI" : "NON" ) << std::endl; + break; + + case TP_v: // Recharger les fichiers des nuanceurs et recréer le programme + chargerNuanceurs(); + std::cout << "// Recharger nuanceurs" << std::endl; + break; + + case TP_p: // Permuter la projection: perspective ou orthogonale + enPerspective = !enPerspective; + break; + + case TP_i: // Alterner entre le modèle d'illumination: Lambert, Gouraud, Phong + if ( ++varsUnif.typeIllumination > 2 ) varsUnif.typeIllumination = 0; + echoEtats( ); + break; + + case TP_r: // Alterner entre le modèle de réflexion spéculaire: Phong, Blinn + varsUnif.utiliseBlinn = !varsUnif.utiliseBlinn; + echoEtats( ); + break; + + case TP_s: // Alterner entre le modèle de spot: OpenGL, Direct3D + varsUnif.utiliseDirect = !varsUnif.utiliseDirect; + echoEtats( ); + break; + + //case TP_l: // Alterner entre une caméra locale à la scène ou distante (localViewer) + // LightModel.localViewer = !LightModel.localViewer; + // std::cout << " localViewer=" << LightModel.localViewer << std::endl; + // break; + + case TP_a: // Incrémenter l'angle du cône du spot + case TP_EGAL: + case TP_PLUS: + LightSource[0].spotAngle += 2.0; + if ( LightSource[0].spotAngle > 90.0 ) LightSource[0].spotAngle = 90.0; + std::cout << " spotAngle=" << LightSource[0].spotAngle << std::endl; + break; + case TP_z: // Décrémenter l'angle du cône du spot + case TP_MOINS: + case TP_SOULIGNE: + LightSource[0].spotAngle -= 2.0; + if ( LightSource[0].spotAngle < 0.0 ) LightSource[0].spotAngle = 0.0; + std::cout << " spotAngle=" << LightSource[0].spotAngle << std::endl; + break; + + case TP_d: // Incrémenter l'exposant du spot + case TP_BARREOBLIQUE: + LightSource[0].spotExposant += 0.3; + if ( LightSource[0].spotExposant > 89.0 ) LightSource[0].spotExposant = 89.0; + std::cout << " spotExposant=" << LightSource[0].spotExposant << std::endl; + break; + case TP_e: // Décrémenter l'exposant du spot + case TP_POINT: + LightSource[0].spotExposant -= 0.3; + if ( LightSource[0].spotExposant < 0.0 ) LightSource[0].spotExposant = 0.0; + std::cout << " spotExposant=" << LightSource[0].spotExposant << std::endl; + break; + + case TP_j: // Incrémenter le coefficient de brillance + case TP_CROCHETDROIT: + FrontMaterial.shininess *= 1.1; + std::cout << " FrontMaterial.shininess=" << FrontMaterial.shininess << std::endl; + break; + case TP_u: // Décrémenter le coefficient de brillance + case TP_CROCHETGAUCHE: + FrontMaterial.shininess /= 1.1; if ( FrontMaterial.shininess < 0.0 ) FrontMaterial.shininess = 0.0; + std::cout << " FrontMaterial.shininess=" << FrontMaterial.shininess << std::endl; + break; + + case TP_DROITE: + LightSource[0].position.x += 0.3; + break; + case TP_GAUCHE: + LightSource[0].position.x -= 0.3; + break; + case TP_BAS: + LightSource[0].position.y += 0.3; + break; + case TP_HAUT: + LightSource[0].position.y -= 0.3; + break; + + case TP_FIN: + LightSource[0].spotDirection.x += 0.6; + break; + case TP_DEBUT: + LightSource[0].spotDirection.x -= 0.6; + break; + case TP_PAGEPREC: + LightSource[0].spotDirection.y += 0.6; + break; + case TP_PAGESUIV: + LightSource[0].spotDirection.y -= 0.6; + break; + + case TP_m: // Choisir le modèle affiché: cube, tore, sphère, théière, cylindre, cône + if ( ++modele > 6 ) modele = 1; + std::cout << " modele=" << modele << std::endl; + break; + + case TP_0: + thetaCam = 0.0; phiCam = 0.0; distCam = 30.0; // placer les choses afin d'avoir une belle vue + break; + + case TP_t: // Choisir la texture utilisée: aucune, dé, échiquier + varsUnif.texnumero++; + if ( varsUnif.texnumero > 2 ) varsUnif.texnumero = 0; + std::cout << " varsUnif.texnumero=" << varsUnif.texnumero << std::endl; + break; + + // case TP_c: // Changer l'affichage de l'objet texturé avec couleurs ou sans couleur + // varsUnif.utiliseCouleur = !varsUnif.utiliseCouleur; + // std::cout << " utiliseCouleur=" << varsUnif.utiliseCouleur << std::endl; + // break; + + case TP_o: // Changer l'affichage des texels noirs (noir, mi-coloré, transparent) + varsUnif.afficheTexelNoir++; + if ( varsUnif.afficheTexelNoir > 2 ) varsUnif.afficheTexelNoir = 0; + std::cout << " afficheTexelNoir=" << varsUnif.afficheTexelNoir << std::endl; + break; + + case TP_g: // Permuter l'affichage en fil de fer ou plein + modePolygone = ( modePolygone == GL_FILL ) ? GL_LINE : GL_FILL; + glPolygonMode( GL_FRONT_AND_BACK, modePolygone ); + break; + + case TP_n: // Utiliser ou non les normales calculées comme couleur (pour le débogage) + varsUnif.afficheNormales = !varsUnif.afficheNormales; + break; + + case TP_ESPACE: // Permuter la rotation automatique du modèle + enmouvement = !enmouvement; + break; + + default: + std::cout << " touche inconnue : " << (char) touche << std::endl; + imprimerTouches(); + break; + } + +} + +// fonction callback pour un clic de souris +int dernierX = 0; // la dernière valeur en X de position de la souris +int dernierY = 0; // la derniere valeur en Y de position de la souris +static enum { deplaceCam, deplaceSpotDirection, deplaceSpotPosition } deplace = deplaceCam; +static bool pressed = false; +void FenetreTP::sourisClic( int button, int state, int x, int y ) +{ + pressed = ( state == TP_PRESSE ); + if ( pressed ) + { + // on vient de presser la souris + dernierX = x; + dernierY = y; + switch ( button ) + { + case TP_BOUTON_GAUCHE: // Tourner l'objet + deplace = deplaceCam; + break; + case TP_BOUTON_MILIEU: // Modifier l'orientation du spot + deplace = deplaceSpotDirection; + break; + case TP_BOUTON_DROIT: // Déplacer la lumière + deplace = deplaceSpotPosition; + break; + } + } + else + { + // on vient de relâcher la souris + } +} + +void FenetreTP::sourisWheel( int x, int y ) // Changer la taille du spot +{ + const int sens = +1; + LightSource[0].spotAngle += sens*y; + if ( LightSource[0].spotAngle > 90.0 ) LightSource[0].spotAngle = 90.0; + if ( LightSource[0].spotAngle < 0.0 ) LightSource[0].spotAngle = 0.0; + std::cout << " spotAngle=" << LightSource[0].spotAngle << std::endl; +} + +// fonction de mouvement de la souris +void FenetreTP::sourisMouvement( int x, int y ) +{ + if ( pressed ) + { + int dx = x - dernierX; + int dy = y - dernierY; + switch ( deplace ) + { + case deplaceCam: + thetaCam -= dx / 3.0; + phiCam -= dy / 3.0; + break; + case deplaceSpotDirection: + LightSource[0].spotDirection.x += 0.06 * dx; + LightSource[0].spotDirection.y -= 0.06 * dy; + // std::cout << " LightSource[0].spotDirection=" << glm::to_string(LightSource[0].spotDirection) << std::endl; + break; + case deplaceSpotPosition: + LightSource[0].position.x += 0.03 * dx; + LightSource[0].position.y -= 0.03 * dy; + // std::cout << " LightSource[0].position=" << glm::to_string(LightSource[0].position) << std::endl; + //glm::vec3 ecranPos( x, hauteur_-y, ecranLumi[2] ); + //LightSource[0].position = glm::vec4(glm::unProject( ecranPos, VM, P, cloture ), 1.0); + break; + } + + dernierX = x; + dernierY = y; + + verifierAngles(); + } +} + +int main( int argc, char *argv[] ) +{ + // créer une fenêtre + FenetreTP fenetre( "INF2705 TP" ); + + // allouer des ressources et définir le contexte OpenGL + initialiser(); + + bool boucler = true; + while ( boucler ) + { + // mettre à jour la physique + calculerPhysique( ); + + // affichage + fenetre.afficherScene(); + fenetre.swap(); + + // récupérer les événements et appeler la fonction de rappel + boucler = fenetre.gererEvenement(); + } + + // détruire les ressources OpenGL allouées + conclure(); + + return 0; +} diff --git a/textures/TestIsland.bmp b/textures/TestIsland.bmp new file mode 100644 index 0000000000000000000000000000000000000000..f824b6ac566f18ba8d7536b3381907c8d298e106 GIT binary patch literal 3145784 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тMqanw~zj=j|F@) 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

odEDnbN4~AJlK;xq&flL}1m~;s6 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 literal 0 HcmV?d00001